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