View Javadoc
1   /*
2    * Copyright (C) 2018, 2021 Salesforce and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  package org.eclipse.jgit.gpg.bc.internal;
11  
12  import static java.nio.file.Files.exists;
13  import static java.nio.file.Files.newInputStream;
14  
15  import java.io.BufferedInputStream;
16  import java.io.File;
17  import java.io.FileNotFoundException;
18  import java.io.IOException;
19  import java.io.InputStream;
20  import java.net.URISyntaxException;
21  import java.nio.file.DirectoryStream;
22  import java.nio.file.Files;
23  import java.nio.file.InvalidPathException;
24  import java.nio.file.NoSuchFileException;
25  import java.nio.file.Path;
26  import java.nio.file.Paths;
27  import java.security.NoSuchAlgorithmException;
28  import java.security.NoSuchProviderException;
29  import java.text.MessageFormat;
30  import java.util.Iterator;
31  import java.util.Locale;
32  
33  import org.bouncycastle.gpg.keybox.BlobType;
34  import org.bouncycastle.gpg.keybox.KeyBlob;
35  import org.bouncycastle.gpg.keybox.KeyBox;
36  import org.bouncycastle.gpg.keybox.KeyInformation;
37  import org.bouncycastle.gpg.keybox.PublicKeyRingBlob;
38  import org.bouncycastle.gpg.keybox.UserID;
39  import org.bouncycastle.gpg.keybox.jcajce.JcaKeyBoxBuilder;
40  import org.bouncycastle.openpgp.PGPException;
41  import org.bouncycastle.openpgp.PGPKeyFlags;
42  import org.bouncycastle.openpgp.PGPPublicKey;
43  import org.bouncycastle.openpgp.PGPPublicKeyRing;
44  import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
45  import org.bouncycastle.openpgp.PGPSecretKey;
46  import org.bouncycastle.openpgp.PGPSecretKeyRing;
47  import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
48  import org.bouncycastle.openpgp.PGPSignature;
49  import org.bouncycastle.openpgp.PGPUtil;
50  import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
51  import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
52  import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
53  import org.bouncycastle.util.encoders.Hex;
54  import org.eclipse.jgit.annotations.NonNull;
55  import org.eclipse.jgit.api.errors.CanceledException;
56  import org.eclipse.jgit.errors.UnsupportedCredentialItem;
57  import org.eclipse.jgit.gpg.bc.internal.keys.KeyGrip;
58  import org.eclipse.jgit.gpg.bc.internal.keys.SecretKeys;
59  import org.eclipse.jgit.util.FS;
60  import org.eclipse.jgit.util.StringUtils;
61  import org.eclipse.jgit.util.SystemReader;
62  import org.slf4j.Logger;
63  import org.slf4j.LoggerFactory;
64  
65  /**
66   * Locates GPG keys from either <code>~/.gnupg/private-keys-v1.d</code> or
67   * <code>~/.gnupg/secring.gpg</code>
68   */
69  public class BouncyCastleGpgKeyLocator {
70  
71  	/** Thrown if a keybox file exists but doesn't contain an OpenPGP key. */
72  	private static class NoOpenPgpKeyException extends Exception {
73  
74  		private static final long serialVersionUID = 1L;
75  
76  	}
77  
78  	private static final Logger log = LoggerFactory
79  			.getLogger(BouncyCastleGpgKeyLocator.class);
80  
81  	static final Path GPG_DIRECTORY = findGpgDirectory();
82  
83  	private static final Path USER_KEYBOX_PATH = GPG_DIRECTORY
84  			.resolve("pubring.kbx"); //$NON-NLS-1$
85  
86  	private static final Path USER_SECRET_KEY_DIR = GPG_DIRECTORY
87  			.resolve("private-keys-v1.d"); //$NON-NLS-1$
88  
89  	private static final Path USER_PGP_PUBRING_FILE = GPG_DIRECTORY
90  			.resolve("pubring.gpg"); //$NON-NLS-1$
91  
92  	private static final Path USER_PGP_LEGACY_SECRING_FILE = GPG_DIRECTORY
93  			.resolve("secring.gpg"); //$NON-NLS-1$
94  
95  	private final String signingKey;
96  
97  	private BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt;
98  
99  	private static Path findGpgDirectory() {
100 		SystemReader system = SystemReader.getInstance();
101 		if (system.isWindows()) {
102 			// On Windows prefer %APPDATA%\gnupg if it exists, even if Cygwin is
103 			// used.
104 			String appData = system.getenv("APPDATA"); //$NON-NLS-1$
105 			if (appData != null && !appData.isEmpty()) {
106 				try {
107 					Path directory = Paths.get(appData).resolve("gnupg"); //$NON-NLS-1$
108 					if (Files.isDirectory(directory)) {
109 						return directory;
110 					}
111 				} catch (SecurityException | InvalidPathException e) {
112 					// Ignore and return the default location below.
113 				}
114 			}
115 		}
116 		// All systems, including Cygwin and even Windows if
117 		// %APPDATA%\gnupg doesn't exist: ~/.gnupg
118 		File home = FS.DETECTED.userHome();
119 		if (home == null) {
120 			// Oops. What now?
121 			home = new File(".").getAbsoluteFile(); //$NON-NLS-1$
122 		}
123 		return home.toPath().resolve(".gnupg"); //$NON-NLS-1$
124 	}
125 
126 	/**
127 	 * Create a new key locator for the specified signing key.
128 	 * <p>
129 	 * The signing key must either be a hex representation of a specific key or
130 	 * a user identity substring (eg., email address). All keys in the KeyBox
131 	 * will be looked up in the order as returned by the KeyBox. A key id will
132 	 * be searched before attempting to find a key by user id.
133 	 * </p>
134 	 *
135 	 * @param signingKey
136 	 *            the signing key to search for
137 	 * @param passphrasePrompt
138 	 *            the provider to use when asking for key passphrase
139 	 */
140 	public BouncyCastleGpgKeyLocator(String signingKey,
141 			@NonNull BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt) {
142 		this.signingKey = signingKey;
143 		this.passphrasePrompt = passphrasePrompt;
144 	}
145 
146 	private PGPSecretKey attemptParseSecretKey(Path keyFile,
147 			PGPDigestCalculatorProvider calculatorProvider,
148 			SecretKeys.PassphraseSupplier passphraseSupplier,
149 			PGPPublicKey publicKey)
150 			throws IOException, PGPException, CanceledException,
151 			UnsupportedCredentialItem, URISyntaxException {
152 		try (InputStream in = newInputStream(keyFile)) {
153 			return SecretKeys.readSecretKey(in, calculatorProvider,
154 					passphraseSupplier, publicKey);
155 		}
156 	}
157 
158 	/**
159 	 * Checks whether a given OpenPGP {@code userId} matches a given
160 	 * {@code signingKeySpec}, which is supposed to have one of the formats
161 	 * defined by GPG.
162 	 * <p>
163 	 * Not all formats are supported; only formats starting with '=', '&lt;',
164 	 * '@', and '*' are handled. Any other format results in a case-insensitive
165 	 * substring match.
166 	 * </p>
167 	 *
168 	 * @param userId
169 	 *            of a key
170 	 * @param signingKeySpec
171 	 *            GPG key identification
172 	 * @return whether the {@code userId} matches
173 	 * @see <a href=
174 	 *      "https://www.gnupg.org/documentation/manuals/gnupg/Specify-a-User-ID.html">GPG
175 	 *      Documentation: How to Specify a User ID</a>
176 	 */
177 	static boolean containsSigningKey(String userId, String signingKeySpec) {
178 		if (StringUtils.isEmptyOrNull(userId)
179 				|| StringUtils.isEmptyOrNull(signingKeySpec)) {
180 			return false;
181 		}
182 		String toMatch = signingKeySpec;
183 		if (toMatch.startsWith("0x") && toMatch.trim().length() > 2) { //$NON-NLS-1$
184 			return false; // Explicit fingerprint
185 		}
186 		int command = toMatch.charAt(0);
187 		switch (command) {
188 		case '=':
189 		case '<':
190 		case '@':
191 		case '*':
192 			toMatch = toMatch.substring(1);
193 			if (toMatch.isEmpty()) {
194 				return false;
195 			}
196 			break;
197 		default:
198 			break;
199 		}
200 		switch (command) {
201 		case '=':
202 			return userId.equals(toMatch);
203 		case '<': {
204 			int begin = userId.indexOf('<');
205 			int end = userId.indexOf('>', begin + 1);
206 			int stop = toMatch.indexOf('>');
207 			return begin >= 0 && end > begin + 1 && stop > 0
208 					&& userId.substring(begin + 1, end)
209 							.equalsIgnoreCase(toMatch.substring(0, stop));
210 		}
211 		case '@': {
212 			int begin = userId.indexOf('<');
213 			int end = userId.indexOf('>', begin + 1);
214 			return begin >= 0 && end > begin + 1
215 					&& containsIgnoreCase(userId.substring(begin + 1, end),
216 							toMatch);
217 		}
218 		default:
219 			if (toMatch.trim().isEmpty()) {
220 				return false;
221 			}
222 			return containsIgnoreCase(userId, toMatch);
223 		}
224 	}
225 
226 	private static boolean containsIgnoreCase(String a, String b) {
227 		int alength = a.length();
228 		int blength = b.length();
229 		for (int i = 0; i + blength <= alength; i++) {
230 			if (a.regionMatches(true, i, b, 0, blength)) {
231 				return true;
232 			}
233 		}
234 		return false;
235 	}
236 
237 	private static String toFingerprint(String keyId) {
238 		if (keyId.startsWith("0x")) { //$NON-NLS-1$
239 			return keyId.substring(2);
240 		}
241 		return keyId;
242 	}
243 
244 	static PGPPublicKey findPublicKey(String fingerprint, String keySpec)
245 			throws IOException, PGPException {
246 		PGPPublicKey result = findPublicKeyInPubring(USER_PGP_PUBRING_FILE,
247 				fingerprint, keySpec);
248 		if (result == null && exists(USER_KEYBOX_PATH)) {
249 			try {
250 				result = findPublicKeyInKeyBox(USER_KEYBOX_PATH, fingerprint,
251 						keySpec);
252 			} catch (NoSuchAlgorithmException | NoSuchProviderException
253 					| IOException | NoOpenPgpKeyException e) {
254 				log.error(e.getMessage(), e);
255 			}
256 		}
257 		return result;
258 	}
259 
260 	private static PGPPublicKey findPublicKeyByKeyId(KeyBlob keyBlob,
261 			String keyId)
262 			throws IOException {
263 		if (keyId.isEmpty()) {
264 			return null;
265 		}
266 		for (KeyInformation keyInfo : keyBlob.getKeyInformation()) {
267 			String fingerprint = Hex.toHexString(keyInfo.getFingerprint())
268 					.toLowerCase(Locale.ROOT);
269 			if (fingerprint.endsWith(keyId)) {
270 				return getPublicKey(keyBlob, keyInfo.getFingerprint());
271 			}
272 		}
273 		return null;
274 	}
275 
276 	private static PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob,
277 			String keySpec)
278 			throws IOException {
279 		for (UserID userID : keyBlob.getUserIds()) {
280 			if (containsSigningKey(userID.getUserIDAsString(), keySpec)) {
281 				return getSigningPublicKey(keyBlob);
282 			}
283 		}
284 		return null;
285 	}
286 
287 	/**
288 	 * Finds a public key associated with the signing key.
289 	 *
290 	 * @param keyboxFile
291 	 *            the KeyBox file
292 	 * @param keyId
293 	 *            to look for, may be null
294 	 * @param keySpec
295 	 *            to look for
296 	 * @return publicKey the public key (maybe <code>null</code>)
297 	 * @throws IOException
298 	 *             in case of problems reading the file
299 	 * @throws NoSuchAlgorithmException
300 	 * @throws NoSuchProviderException
301 	 * @throws NoOpenPgpKeyException
302 	 *             if the file does not contain any OpenPGP key
303 	 */
304 	private static PGPPublicKey findPublicKeyInKeyBox(Path keyboxFile,
305 			String keyId, String keySpec)
306 			throws IOException, NoSuchAlgorithmException,
307 			NoSuchProviderException, NoOpenPgpKeyException {
308 		KeyBox keyBox = readKeyBoxFile(keyboxFile);
309 		String id = keyId != null ? keyId
310 				: toFingerprint(keySpec).toLowerCase(Locale.ROOT);
311 		boolean hasOpenPgpKey = false;
312 		for (KeyBlob keyBlob : keyBox.getKeyBlobs()) {
313 			if (keyBlob.getType() == BlobType.OPEN_PGP_BLOB) {
314 				hasOpenPgpKey = true;
315 				PGPPublicKey key = findPublicKeyByKeyId(keyBlob, id);
316 				if (key != null) {
317 					return key;
318 				}
319 				key = findPublicKeyByUserId(keyBlob, keySpec);
320 				if (key != null) {
321 					return key;
322 				}
323 			}
324 		}
325 		if (!hasOpenPgpKey) {
326 			throw new NoOpenPgpKeyException();
327 		}
328 		return null;
329 	}
330 
331 	/**
332 	 * If there is a private key directory containing keys, use pubring.kbx or
333 	 * pubring.gpg to find the public key; then try to find the secret key in
334 	 * the directory.
335 	 * <p>
336 	 * If there is no private key directory (or it doesn't contain any keys),
337 	 * try to find the key in secring.gpg directly.
338 	 * </p>
339 	 *
340 	 * @return the secret key
341 	 * @throws IOException
342 	 *             in case of issues reading key files
343 	 * @throws NoSuchAlgorithmException
344 	 * @throws NoSuchProviderException
345 	 * @throws PGPException
346 	 *             in case of issues finding a key, including no key found
347 	 * @throws CanceledException
348 	 * @throws URISyntaxException
349 	 * @throws UnsupportedCredentialItem
350 	 */
351 	@NonNull
352 	public BouncyCastleGpgKey findSecretKey() throws IOException,
353 			NoSuchAlgorithmException, NoSuchProviderException, PGPException,
354 			CanceledException, UnsupportedCredentialItem, URISyntaxException {
355 		BouncyCastleGpgKey key;
356 		PGPPublicKey publicKey = null;
357 		if (hasKeyFiles(USER_SECRET_KEY_DIR)) {
358 			// Use pubring.kbx or pubring.gpg to find the public key, then try
359 			// the key files in the directory. If the public key was found in
360 			// pubring.gpg also try secring.gpg to find the secret key.
361 			if (exists(USER_KEYBOX_PATH)) {
362 				try {
363 					publicKey = findPublicKeyInKeyBox(USER_KEYBOX_PATH, null,
364 							signingKey);
365 					if (publicKey != null) {
366 						key = findSecretKeyForKeyBoxPublicKey(publicKey,
367 								USER_KEYBOX_PATH);
368 						if (key != null) {
369 							return key;
370 						}
371 						throw new PGPException(MessageFormat.format(
372 								BCText.get().gpgNoSecretKeyForPublicKey,
373 								Long.toHexString(publicKey.getKeyID())));
374 					}
375 					throw new PGPException(MessageFormat.format(
376 							BCText.get().gpgNoPublicKeyFound, signingKey));
377 				} catch (NoOpenPgpKeyException e) {
378 					// There are no OpenPGP keys in the keybox at all: try the
379 					// pubring.gpg, if it exists.
380 					if (log.isDebugEnabled()) {
381 						log.debug("{} does not contain any OpenPGP keys", //$NON-NLS-1$
382 								USER_KEYBOX_PATH);
383 					}
384 				}
385 			}
386 			if (exists(USER_PGP_PUBRING_FILE)) {
387 				publicKey = findPublicKeyInPubring(USER_PGP_PUBRING_FILE, null,
388 						signingKey);
389 				if (publicKey != null) {
390 					// GPG < 2.1 may have both; the agent using the directory
391 					// and gpg using secring.gpg. GPG >= 2.1 delegates all
392 					// secret key handling to the agent and doesn't use
393 					// secring.gpg at all, even if it exists. Which means for us
394 					// we have to try both since we don't know which GPG version
395 					// the user has.
396 					key = findSecretKeyForKeyBoxPublicKey(publicKey,
397 							USER_PGP_PUBRING_FILE);
398 					if (key != null) {
399 						return key;
400 					}
401 				}
402 			}
403 			if (publicKey == null) {
404 				throw new PGPException(MessageFormat.format(
405 						BCText.get().gpgNoPublicKeyFound, signingKey));
406 			}
407 			// We found a public key, but didn't find the secret key in the
408 			// private key directory. Go try the secring.gpg.
409 		}
410 		boolean hasSecring = false;
411 		if (exists(USER_PGP_LEGACY_SECRING_FILE)) {
412 			hasSecring = true;
413 			key = loadKeyFromSecring(USER_PGP_LEGACY_SECRING_FILE);
414 			if (key != null) {
415 				return key;
416 			}
417 		}
418 		if (publicKey != null) {
419 			throw new PGPException(MessageFormat.format(
420 					BCText.get().gpgNoSecretKeyForPublicKey,
421 					Long.toHexString(publicKey.getKeyID())));
422 		} else if (hasSecring) {
423 			// publicKey == null: user has _only_ pubring.gpg/secring.gpg.
424 			throw new PGPException(MessageFormat.format(
425 					BCText.get().gpgNoKeyInLegacySecring, signingKey));
426 		} else {
427 			throw new PGPException(BCText.get().gpgNoKeyring);
428 		}
429 	}
430 
431 	private boolean hasKeyFiles(Path dir) {
432 		try (DirectoryStream<Path> contents = Files.newDirectoryStream(dir,
433 				"*.key")) { //$NON-NLS-1$
434 			return contents.iterator().hasNext();
435 		} catch (IOException e) {
436 			// Not a directory, or something else
437 			return false;
438 		}
439 	}
440 
441 	private BouncyCastleGpgKey loadKeyFromSecring(Path secring)
442 			throws IOException, PGPException {
443 		PGPSecretKey secretKey = findSecretKeyInLegacySecring(signingKey,
444 				secring);
445 
446 		if (secretKey != null) {
447 			if (!secretKey.isSigningKey()) {
448 				throw new PGPException(MessageFormat
449 						.format(BCText.get().gpgNotASigningKey, signingKey));
450 			}
451 			return new BouncyCastleGpgKey(secretKey, secring);
452 		}
453 		return null;
454 	}
455 
456 	private BouncyCastleGpgKey findSecretKeyForKeyBoxPublicKey(
457 			PGPPublicKey publicKey, Path userKeyboxPath)
458 			throws PGPException, CanceledException, UnsupportedCredentialItem,
459 			URISyntaxException {
460 		byte[] keyGrip = null;
461 		try {
462 			keyGrip = KeyGrip.getKeyGrip(publicKey);
463 		} catch (PGPException e) {
464 			throw new PGPException(
465 					MessageFormat.format(BCText.get().gpgNoKeygrip,
466 							Hex.toHexString(publicKey.getFingerprint())),
467 					e);
468 		}
469 		String filename = Hex.toHexString(keyGrip).toUpperCase(Locale.ROOT)
470 				+ ".key"; //$NON-NLS-1$
471 		Path keyFile = USER_SECRET_KEY_DIR.resolve(filename);
472 		if (!Files.exists(keyFile)) {
473 			return null;
474 		}
475 		boolean clearPrompt = false;
476 		try {
477 			PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder()
478 					.build();
479 			clearPrompt = true;
480 			PGPSecretKey secretKey = null;
481 			try {
482 				secretKey = attemptParseSecretKey(keyFile, calculatorProvider,
483 						() -> passphrasePrompt.getPassphrase(
484 								publicKey.getFingerprint(), userKeyboxPath),
485 						publicKey);
486 			} catch (PGPException e) {
487 				throw new PGPException(MessageFormat.format(
488 						BCText.get().gpgFailedToParseSecretKey,
489 						keyFile.toAbsolutePath()), e);
490 			}
491 			if (secretKey != null) {
492 				if (!secretKey.isSigningKey()) {
493 					throw new PGPException(MessageFormat.format(
494 							BCText.get().gpgNotASigningKey, signingKey));
495 				}
496 				clearPrompt = false;
497 				return new BouncyCastleGpgKey(secretKey, userKeyboxPath);
498 			}
499 			return null;
500 		} catch (RuntimeException e) {
501 			throw e;
502 		} catch (FileNotFoundException | NoSuchFileException e) {
503 			clearPrompt = false;
504 			return null;
505 		} catch (IOException e) {
506 			throw new PGPException(MessageFormat.format(
507 					BCText.get().gpgFailedToParseSecretKey,
508 					keyFile.toAbsolutePath()), e);
509 		} finally {
510 			if (clearPrompt) {
511 				passphrasePrompt.clear();
512 			}
513 		}
514 	}
515 
516 	/**
517 	 * Return the first suitable key for signing in the key ring collection. For
518 	 * this case we only expect there to be one key available for signing.
519 	 * </p>
520 	 *
521 	 * @param signingkey
522 	 * @param secringFile
523 	 *
524 	 * @return the first suitable PGP secret key found for signing
525 	 * @throws IOException
526 	 *             on I/O related errors
527 	 * @throws PGPException
528 	 *             on BouncyCastle errors
529 	 */
530 	private PGPSecretKey findSecretKeyInLegacySecring(String signingkey,
531 			Path secringFile) throws IOException, PGPException {
532 
533 		try (InputStream in = newInputStream(secringFile)) {
534 			PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(
535 					PGPUtil.getDecoderStream(new BufferedInputStream(in)),
536 					new JcaKeyFingerprintCalculator());
537 
538 			String keyId = toFingerprint(signingkey).toLowerCase(Locale.ROOT);
539 			Iterator<PGPSecretKeyRing> keyrings = pgpSec.getKeyRings();
540 			while (keyrings.hasNext()) {
541 				PGPSecretKeyRing keyRing = keyrings.next();
542 				Iterator<PGPSecretKey> keys = keyRing.getSecretKeys();
543 				while (keys.hasNext()) {
544 					PGPSecretKey key = keys.next();
545 					// try key id
546 					String fingerprint = Hex
547 							.toHexString(key.getPublicKey().getFingerprint())
548 							.toLowerCase(Locale.ROOT);
549 					if (fingerprint.endsWith(keyId)) {
550 						return key;
551 					}
552 					// try user id
553 					Iterator<String> userIDs = key.getUserIDs();
554 					while (userIDs.hasNext()) {
555 						String userId = userIDs.next();
556 						if (containsSigningKey(userId, signingKey)) {
557 							return key;
558 						}
559 					}
560 				}
561 			}
562 		}
563 		return null;
564 	}
565 
566 	/**
567 	 * Return the first public key matching the key id ({@link #signingKey}.
568 	 *
569 	 * @param pubringFile
570 	 *            to search
571 	 * @param keyId
572 	 *            to look for, may be null
573 	 * @param keySpec
574 	 *            to look for
575 	 *
576 	 * @return the PGP public key, or {@code null} if none found
577 	 * @throws IOException
578 	 *             on I/O related errors
579 	 * @throws PGPException
580 	 *             on BouncyCastle errors
581 	 */
582 	private static PGPPublicKey findPublicKeyInPubring(Path pubringFile,
583 			String keyId, String keySpec)
584 			throws IOException, PGPException {
585 		try (InputStream in = newInputStream(pubringFile)) {
586 			PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(
587 					new BufferedInputStream(in),
588 					new JcaKeyFingerprintCalculator());
589 
590 			String id = keyId != null ? keyId
591 					: toFingerprint(keySpec).toLowerCase(Locale.ROOT);
592 			Iterator<PGPPublicKeyRing> keyrings = pgpPub.getKeyRings();
593 			while (keyrings.hasNext()) {
594 				PGPPublicKeyRing keyRing = keyrings.next();
595 				Iterator<PGPPublicKey> keys = keyRing.getPublicKeys();
596 				while (keys.hasNext()) {
597 					PGPPublicKey key = keys.next();
598 					// try key id
599 					String fingerprint = Hex.toHexString(key.getFingerprint())
600 							.toLowerCase(Locale.ROOT);
601 					if (fingerprint.endsWith(id)) {
602 						return key;
603 					}
604 					// try user id
605 					Iterator<String> userIDs = key.getUserIDs();
606 					while (userIDs.hasNext()) {
607 						String userId = userIDs.next();
608 						if (containsSigningKey(userId, keySpec)) {
609 							return key;
610 						}
611 					}
612 				}
613 			}
614 		} catch (FileNotFoundException | NoSuchFileException e) {
615 			// Ignore and return null
616 		}
617 		return null;
618 	}
619 
620 	private static PGPPublicKey getPublicKey(KeyBlob blob, byte[] fingerprint)
621 			throws IOException {
622 		return ((PublicKeyRingBlob) blob).getPGPPublicKeyRing()
623 				.getPublicKey(fingerprint);
624 	}
625 
626 	private static PGPPublicKey getSigningPublicKey(KeyBlob blob)
627 			throws IOException {
628 		PGPPublicKey masterKey = null;
629 		Iterator<PGPPublicKey> keys = ((PublicKeyRingBlob) blob)
630 				.getPGPPublicKeyRing().getPublicKeys();
631 		while (keys.hasNext()) {
632 			PGPPublicKey key = keys.next();
633 			// only consider keys that have the [S] usage flag set
634 			if (isSigningKey(key)) {
635 				if (key.isMasterKey()) {
636 					masterKey = key;
637 				} else {
638 					return key;
639 				}
640 			}
641 		}
642 		// return the master key if no other signing key was found or null if
643 		// the master key did not have the signing flag set
644 		return masterKey;
645 	}
646 
647 	private static boolean isSigningKey(PGPPublicKey key) {
648 		Iterator signatures = key.getSignatures();
649 		while (signatures.hasNext()) {
650 			PGPSignature sig = (PGPSignature) signatures.next();
651 			if ((sig.getHashedSubPackets().getKeyFlags()
652 					& PGPKeyFlags.CAN_SIGN) > 0) {
653 				return true;
654 			}
655 		}
656 		return false;
657 	}
658 
659 	private static KeyBox readKeyBoxFile(Path keyboxFile) throws IOException,
660 			NoSuchAlgorithmException, NoSuchProviderException,
661 			NoOpenPgpKeyException {
662 		if (keyboxFile.toFile().length() == 0) {
663 			throw new NoOpenPgpKeyException();
664 		}
665 		KeyBox keyBox;
666 		try (InputStream in = new BufferedInputStream(
667 				newInputStream(keyboxFile))) {
668 			keyBox = new JcaKeyBoxBuilder().build(in);
669 		}
670 		return keyBox;
671 	}
672 }