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
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.PGPDigestCalculatorProvider;
51 import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
52 import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
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.gpg.bc.internal.keys.SecretKeys;
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
66
67
68
69 public class BouncyCastleGpgKeyLocator {
70
71
72 private static class NoOpenPgpKeyException extends Exception {
73
74 private static final long serialVersionUID = 1L;
75
76 }
77
78 private static final Logger log = LoggerFactory
79 .getLogger(BouncyCastleGpgKeyLocator.class);
80
81 static final Path GPG_DIRECTORY = findGpgDirectory();
82
83 private static final Path USER_KEYBOX_PATH = GPG_DIRECTORY
84 .resolve("pubring.kbx");
85
86 private static final Path USER_SECRET_KEY_DIR = GPG_DIRECTORY
87 .resolve("private-keys-v1.d");
88
89 private static final Path USER_PGP_PUBRING_FILE = GPG_DIRECTORY
90 .resolve("pubring.gpg");
91
92 private static final Path USER_PGP_LEGACY_SECRING_FILE = GPG_DIRECTORY
93 .resolve("secring.gpg");
94
95 private final String signingKey;
96
97 private BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt;
98
99 private static Path findGpgDirectory() {
100 SystemReader system = SystemReader.getInstance();
101 if (system.isWindows()) {
102
103
104 String appData = system.getenv("APPDATA");
105 if (appData != null && !appData.isEmpty()) {
106 try {
107 Path directory = Paths.get(appData).resolve("gnupg");
108 if (Files.isDirectory(directory)) {
109 return directory;
110 }
111 } catch (SecurityException | InvalidPathException e) {
112
113 }
114 }
115 }
116
117
118 File home = FS.DETECTED.userHome();
119 if (home == null) {
120
121 home = new File(".").getAbsoluteFile();
122 }
123 return home.toPath().resolve(".gnupg");
124 }
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140 public BouncyCastleGpgKeyLocator(String signingKey,
141 @NonNull BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt) {
142 this.signingKey = signingKey;
143 this.passphrasePrompt = passphrasePrompt;
144 }
145
146 private PGPSecretKey attemptParseSecretKey(Path keyFile,
147 PGPDigestCalculatorProvider calculatorProvider,
148 SecretKeys.PassphraseSupplier passphraseSupplier,
149 PGPPublicKey publicKey)
150 throws IOException, PGPException, CanceledException,
151 UnsupportedCredentialItem, URISyntaxException {
152 try (InputStream in = newInputStream(keyFile)) {
153 return SecretKeys.readSecretKey(in, calculatorProvider,
154 passphraseSupplier, publicKey);
155 }
156 }
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177 static boolean containsSigningKey(String userId, String signingKeySpec) {
178 if (StringUtils.isEmptyOrNull(userId)
179 || StringUtils.isEmptyOrNull(signingKeySpec)) {
180 return false;
181 }
182 String toMatch = signingKeySpec;
183 if (toMatch.startsWith("0x") && toMatch.trim().length() > 2) {
184 return false;
185 }
186 int command = toMatch.charAt(0);
187 switch (command) {
188 case '=':
189 case '<':
190 case '@':
191 case '*':
192 toMatch = toMatch.substring(1);
193 if (toMatch.isEmpty()) {
194 return false;
195 }
196 break;
197 default:
198 break;
199 }
200 switch (command) {
201 case '=':
202 return userId.equals(toMatch);
203 case '<': {
204 int begin = userId.indexOf('<');
205 int end = userId.indexOf('>', begin + 1);
206 int stop = toMatch.indexOf('>');
207 return begin >= 0 && end > begin + 1 && stop > 0
208 && userId.substring(begin + 1, end)
209 .equalsIgnoreCase(toMatch.substring(0, stop));
210 }
211 case '@': {
212 int begin = userId.indexOf('<');
213 int end = userId.indexOf('>', begin + 1);
214 return begin >= 0 && end > begin + 1
215 && containsIgnoreCase(userId.substring(begin + 1, end),
216 toMatch);
217 }
218 default:
219 if (toMatch.trim().isEmpty()) {
220 return false;
221 }
222 return containsIgnoreCase(userId, toMatch);
223 }
224 }
225
226 private static boolean containsIgnoreCase(String a, String b) {
227 int alength = a.length();
228 int blength = b.length();
229 for (int i = 0; i + blength <= alength; i++) {
230 if (a.regionMatches(true, i, b, 0, blength)) {
231 return true;
232 }
233 }
234 return false;
235 }
236
237 private static String toFingerprint(String keyId) {
238 if (keyId.startsWith("0x")) {
239 return keyId.substring(2);
240 }
241 return keyId;
242 }
243
244 static PGPPublicKey findPublicKey(String fingerprint, String keySpec)
245 throws IOException, PGPException {
246 PGPPublicKey result = findPublicKeyInPubring(USER_PGP_PUBRING_FILE,
247 fingerprint, keySpec);
248 if (result == null && exists(USER_KEYBOX_PATH)) {
249 try {
250 result = findPublicKeyInKeyBox(USER_KEYBOX_PATH, fingerprint,
251 keySpec);
252 } catch (NoSuchAlgorithmException | NoSuchProviderException
253 | IOException | NoOpenPgpKeyException e) {
254 log.error(e.getMessage(), e);
255 }
256 }
257 return result;
258 }
259
260 private static PGPPublicKey findPublicKeyByKeyId(KeyBlob keyBlob,
261 String keyId)
262 throws IOException {
263 if (keyId.isEmpty()) {
264 return null;
265 }
266 for (KeyInformation keyInfo : keyBlob.getKeyInformation()) {
267 String fingerprint = Hex.toHexString(keyInfo.getFingerprint())
268 .toLowerCase(Locale.ROOT);
269 if (fingerprint.endsWith(keyId)) {
270 return getPublicKey(keyBlob, keyInfo.getFingerprint());
271 }
272 }
273 return null;
274 }
275
276 private static PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob,
277 String keySpec)
278 throws IOException {
279 for (UserID userID : keyBlob.getUserIds()) {
280 if (containsSigningKey(userID.getUserIDAsString(), keySpec)) {
281 return getSigningPublicKey(keyBlob);
282 }
283 }
284 return null;
285 }
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304 private static PGPPublicKey findPublicKeyInKeyBox(Path keyboxFile,
305 String keyId, String keySpec)
306 throws IOException, NoSuchAlgorithmException,
307 NoSuchProviderException, NoOpenPgpKeyException {
308 KeyBox keyBox = readKeyBoxFile(keyboxFile);
309 String id = keyId != null ? keyId
310 : toFingerprint(keySpec).toLowerCase(Locale.ROOT);
311 boolean hasOpenPgpKey = false;
312 for (KeyBlob keyBlob : keyBox.getKeyBlobs()) {
313 if (keyBlob.getType() == BlobType.OPEN_PGP_BLOB) {
314 hasOpenPgpKey = true;
315 PGPPublicKey key = findPublicKeyByKeyId(keyBlob, id);
316 if (key != null) {
317 return key;
318 }
319 key = findPublicKeyByUserId(keyBlob, keySpec);
320 if (key != null) {
321 return key;
322 }
323 }
324 }
325 if (!hasOpenPgpKey) {
326 throw new NoOpenPgpKeyException();
327 }
328 return null;
329 }
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351 @NonNull
352 public BouncyCastleGpgKey findSecretKey() throws IOException,
353 NoSuchAlgorithmException, NoSuchProviderException, PGPException,
354 CanceledException, UnsupportedCredentialItem, URISyntaxException {
355 BouncyCastleGpgKey key;
356 PGPPublicKey publicKey = null;
357 if (hasKeyFiles(USER_SECRET_KEY_DIR)) {
358
359
360
361 if (exists(USER_KEYBOX_PATH)) {
362 try {
363 publicKey = findPublicKeyInKeyBox(USER_KEYBOX_PATH, null,
364 signingKey);
365 if (publicKey != null) {
366 key = findSecretKeyForKeyBoxPublicKey(publicKey,
367 USER_KEYBOX_PATH);
368 if (key != null) {
369 return key;
370 }
371 throw new PGPException(MessageFormat.format(
372 BCText.get().gpgNoSecretKeyForPublicKey,
373 Long.toHexString(publicKey.getKeyID())));
374 }
375 throw new PGPException(MessageFormat.format(
376 BCText.get().gpgNoPublicKeyFound, signingKey));
377 } catch (NoOpenPgpKeyException e) {
378
379
380 if (log.isDebugEnabled()) {
381 log.debug("{} does not contain any OpenPGP keys",
382 USER_KEYBOX_PATH);
383 }
384 }
385 }
386 if (exists(USER_PGP_PUBRING_FILE)) {
387 publicKey = findPublicKeyInPubring(USER_PGP_PUBRING_FILE, null,
388 signingKey);
389 if (publicKey != null) {
390
391
392
393
394
395
396 key = findSecretKeyForKeyBoxPublicKey(publicKey,
397 USER_PGP_PUBRING_FILE);
398 if (key != null) {
399 return key;
400 }
401 }
402 }
403 if (publicKey == null) {
404 throw new PGPException(MessageFormat.format(
405 BCText.get().gpgNoPublicKeyFound, signingKey));
406 }
407
408
409 }
410 boolean hasSecring = false;
411 if (exists(USER_PGP_LEGACY_SECRING_FILE)) {
412 hasSecring = true;
413 key = loadKeyFromSecring(USER_PGP_LEGACY_SECRING_FILE);
414 if (key != null) {
415 return key;
416 }
417 }
418 if (publicKey != null) {
419 throw new PGPException(MessageFormat.format(
420 BCText.get().gpgNoSecretKeyForPublicKey,
421 Long.toHexString(publicKey.getKeyID())));
422 } else if (hasSecring) {
423
424 throw new PGPException(MessageFormat.format(
425 BCText.get().gpgNoKeyInLegacySecring, signingKey));
426 } else {
427 throw new PGPException(BCText.get().gpgNoKeyring);
428 }
429 }
430
431 private boolean hasKeyFiles(Path dir) {
432 try (DirectoryStream<Path> contents = Files.newDirectoryStream(dir,
433 "*.key")) {
434 return contents.iterator().hasNext();
435 } catch (IOException e) {
436
437 return false;
438 }
439 }
440
441 private BouncyCastleGpgKey loadKeyFromSecring(Path secring)
442 throws IOException, PGPException {
443 PGPSecretKey secretKey = findSecretKeyInLegacySecring(signingKey,
444 secring);
445
446 if (secretKey != null) {
447 if (!secretKey.isSigningKey()) {
448 throw new PGPException(MessageFormat
449 .format(BCText.get().gpgNotASigningKey, signingKey));
450 }
451 return new BouncyCastleGpgKey(secretKey, secring);
452 }
453 return null;
454 }
455
456 private BouncyCastleGpgKey findSecretKeyForKeyBoxPublicKey(
457 PGPPublicKey publicKey, Path userKeyboxPath)
458 throws PGPException, CanceledException, UnsupportedCredentialItem,
459 URISyntaxException {
460 byte[] keyGrip = null;
461 try {
462 keyGrip = KeyGrip.getKeyGrip(publicKey);
463 } catch (PGPException e) {
464 throw new PGPException(
465 MessageFormat.format(BCText.get().gpgNoKeygrip,
466 Hex.toHexString(publicKey.getFingerprint())),
467 e);
468 }
469 String filename = Hex.toHexString(keyGrip).toUpperCase(Locale.ROOT)
470 + ".key";
471 Path keyFile = USER_SECRET_KEY_DIR.resolve(filename);
472 if (!Files.exists(keyFile)) {
473 return null;
474 }
475 boolean clearPrompt = false;
476 try {
477 PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder()
478 .build();
479 clearPrompt = true;
480 PGPSecretKey secretKey = null;
481 try {
482 secretKey = attemptParseSecretKey(keyFile, calculatorProvider,
483 () -> passphrasePrompt.getPassphrase(
484 publicKey.getFingerprint(), userKeyboxPath),
485 publicKey);
486 } catch (PGPException e) {
487 throw new PGPException(MessageFormat.format(
488 BCText.get().gpgFailedToParseSecretKey,
489 keyFile.toAbsolutePath()), e);
490 }
491 if (secretKey != null) {
492 if (!secretKey.isSigningKey()) {
493 throw new PGPException(MessageFormat.format(
494 BCText.get().gpgNotASigningKey, signingKey));
495 }
496 clearPrompt = false;
497 return new BouncyCastleGpgKey(secretKey, userKeyboxPath);
498 }
499 return null;
500 } catch (RuntimeException e) {
501 throw e;
502 } catch (FileNotFoundException | NoSuchFileException e) {
503 clearPrompt = false;
504 return null;
505 } catch (IOException e) {
506 throw new PGPException(MessageFormat.format(
507 BCText.get().gpgFailedToParseSecretKey,
508 keyFile.toAbsolutePath()), e);
509 } finally {
510 if (clearPrompt) {
511 passphrasePrompt.clear();
512 }
513 }
514 }
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530 private PGPSecretKey findSecretKeyInLegacySecring(String signingkey,
531 Path secringFile) throws IOException, PGPException {
532
533 try (InputStream in = newInputStream(secringFile)) {
534 PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(
535 PGPUtil.getDecoderStream(new BufferedInputStream(in)),
536 new JcaKeyFingerprintCalculator());
537
538 String keyId = toFingerprint(signingkey).toLowerCase(Locale.ROOT);
539 Iterator<PGPSecretKeyRing> keyrings = pgpSec.getKeyRings();
540 while (keyrings.hasNext()) {
541 PGPSecretKeyRing keyRing = keyrings.next();
542 Iterator<PGPSecretKey> keys = keyRing.getSecretKeys();
543 while (keys.hasNext()) {
544 PGPSecretKey key = keys.next();
545
546 String fingerprint = Hex
547 .toHexString(key.getPublicKey().getFingerprint())
548 .toLowerCase(Locale.ROOT);
549 if (fingerprint.endsWith(keyId)) {
550 return key;
551 }
552
553 Iterator<String> userIDs = key.getUserIDs();
554 while (userIDs.hasNext()) {
555 String userId = userIDs.next();
556 if (containsSigningKey(userId, signingKey)) {
557 return key;
558 }
559 }
560 }
561 }
562 }
563 return null;
564 }
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582 private static PGPPublicKey findPublicKeyInPubring(Path pubringFile,
583 String keyId, String keySpec)
584 throws IOException, PGPException {
585 try (InputStream in = newInputStream(pubringFile)) {
586 PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(
587 new BufferedInputStream(in),
588 new JcaKeyFingerprintCalculator());
589
590 String id = keyId != null ? keyId
591 : toFingerprint(keySpec).toLowerCase(Locale.ROOT);
592 Iterator<PGPPublicKeyRing> keyrings = pgpPub.getKeyRings();
593 while (keyrings.hasNext()) {
594 PGPPublicKeyRing keyRing = keyrings.next();
595 Iterator<PGPPublicKey> keys = keyRing.getPublicKeys();
596 while (keys.hasNext()) {
597 PGPPublicKey key = keys.next();
598
599 String fingerprint = Hex.toHexString(key.getFingerprint())
600 .toLowerCase(Locale.ROOT);
601 if (fingerprint.endsWith(id)) {
602 return key;
603 }
604
605 Iterator<String> userIDs = key.getUserIDs();
606 while (userIDs.hasNext()) {
607 String userId = userIDs.next();
608 if (containsSigningKey(userId, keySpec)) {
609 return key;
610 }
611 }
612 }
613 }
614 } catch (FileNotFoundException | NoSuchFileException e) {
615
616 }
617 return null;
618 }
619
620 private static PGPPublicKey getPublicKey(KeyBlob blob, byte[] fingerprint)
621 throws IOException {
622 return ((PublicKeyRingBlob) blob).getPGPPublicKeyRing()
623 .getPublicKey(fingerprint);
624 }
625
626 private static PGPPublicKey getSigningPublicKey(KeyBlob blob)
627 throws IOException {
628 PGPPublicKey masterKey = null;
629 Iterator<PGPPublicKey> keys = ((PublicKeyRingBlob) blob)
630 .getPGPPublicKeyRing().getPublicKeys();
631 while (keys.hasNext()) {
632 PGPPublicKey key = keys.next();
633
634 if (isSigningKey(key)) {
635 if (key.isMasterKey()) {
636 masterKey = key;
637 } else {
638 return key;
639 }
640 }
641 }
642
643
644 return masterKey;
645 }
646
647 private static boolean isSigningKey(PGPPublicKey key) {
648 Iterator signatures = key.getSignatures();
649 while (signatures.hasNext()) {
650 PGPSignature sig = (PGPSignature) signatures.next();
651 if ((sig.getHashedSubPackets().getKeyFlags()
652 & PGPKeyFlags.CAN_SIGN) > 0) {
653 return true;
654 }
655 }
656 return false;
657 }
658
659 private static KeyBox readKeyBoxFile(Path keyboxFile) throws IOException,
660 NoSuchAlgorithmException, NoSuchProviderException,
661 NoOpenPgpKeyException {
662 if (keyboxFile.toFile().length() == 0) {
663 throw new NoOpenPgpKeyException();
664 }
665 KeyBox keyBox;
666 try (InputStream in = new BufferedInputStream(
667 newInputStream(keyboxFile))) {
668 keyBox = new JcaKeyBoxBuilder().build(in);
669 }
670 return keyBox;
671 }
672 }