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