1
2
3
4
5
6
7
8
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
69
70
71 public class BouncyCastleGpgKeyLocator {
72
73
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");
87
88 private static final Path USER_SECRET_KEY_DIR = GPG_DIRECTORY
89 .resolve("private-keys-v1.d");
90
91 private static final Path USER_PGP_PUBRING_FILE = GPG_DIRECTORY
92 .resolve("pubring.gpg");
93
94 private static final Path USER_PGP_LEGACY_SECRING_FILE = GPG_DIRECTORY
95 .resolve("secring.gpg");
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)) {
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"),
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,
117 s -> log.warn(BCText.get().logWarnGnuPGHome, s));
118 if (path != null) {
119 return path;
120 }
121 if (system.isWindows()) {
122
123
124 path = checkDirectory(system.getenv("APPDATA"),
125 s -> Paths.get(s).resolve("gnupg"), null);
126 if (path != null) {
127 return path;
128 }
129 }
130
131
132 return resolveTilde.apply("~/.gnupg");
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
145 }
146 if (warn != null) {
147 warn.accept(dir);
148 }
149 }
150 return null;
151 }
152
153
154
155
156
157
158
159
160
161
162
163
164
165
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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
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) {
211 return false;
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")) {
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
316
317
318
319
320
321
322
323
324
325
326
327
328
329
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
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
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
386
387
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
406
407 if (log.isDebugEnabled()) {
408 log.debug("{} does not contain any OpenPGP keys",
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
418
419
420
421
422
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
435
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
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")) {
461 return contents.iterator().hasNext();
462 } catch (IOException e) {
463
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";
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
545
546
547
548
549
550
551
552
553
554
555
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
573 String fingerprint = Hex
574 .toHexString(key.getPublicKey().getFingerprint())
575 .toLowerCase(Locale.ROOT);
576 if (fingerprint.endsWith(keyId)) {
577 return key;
578 }
579
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
595
596
597
598
599
600
601
602
603
604
605
606
607
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
626 String fingerprint = Hex.toHexString(key.getFingerprint())
627 .toLowerCase(Locale.ROOT);
628 if (fingerprint.endsWith(id)) {
629 return key;
630 }
631
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
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
661 if (isSigningKey(key)) {
662 if (key.isMasterKey()) {
663 masterKey = key;
664 } else {
665 return key;
666 }
667 }
668 }
669
670
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 }