Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

BouncyCastleGpgKeyLocator.java 22KB

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