You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

BouncyCastleGpgKeyLocator.java 21KB

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