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 22KB

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