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