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.PGPPublicKey;
76 import org.bouncycastle.openpgp.PGPPublicKeyRing;
77 import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
78 import org.bouncycastle.openpgp.PGPSecretKey;
79 import org.bouncycastle.openpgp.PGPSecretKeyRing;
80 import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
81 import org.bouncycastle.openpgp.PGPUtil;
82 import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
83 import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
84 import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
85 import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
86 import org.bouncycastle.openpgp.operator.jcajce.JcePBEProtectionRemoverFactory;
87 import org.bouncycastle.util.encoders.Hex;
88 import org.eclipse.jgit.annotations.NonNull;
89 import org.eclipse.jgit.api.errors.CanceledException;
90 import org.eclipse.jgit.errors.UnsupportedCredentialItem;
91 import org.eclipse.jgit.internal.JGitText;
92 import org.eclipse.jgit.util.FS;
93 import org.eclipse.jgit.util.SystemReader;
94 import org.slf4j.Logger;
95 import org.slf4j.LoggerFactory;
96
97
98
99
100
101 class BouncyCastleGpgKeyLocator {
102
103
104 private static class NoOpenPgpKeyException extends Exception {
105
106 private static final long serialVersionUID = 1L;
107
108 }
109
110 private static final Logger log = LoggerFactory
111 .getLogger(BouncyCastleGpgKeyLocator.class);
112
113 private static final Path GPG_DIRECTORY = findGpgDirectory();
114
115 private static final Path USER_KEYBOX_PATH = GPG_DIRECTORY
116 .resolve("pubring.kbx");
117
118 private static final Path USER_SECRET_KEY_DIR = GPG_DIRECTORY
119 .resolve("private-keys-v1.d");
120
121 private static final Path USER_PGP_PUBRING_FILE = GPG_DIRECTORY
122 .resolve("pubring.gpg");
123
124 private static final Path USER_PGP_LEGACY_SECRING_FILE = GPG_DIRECTORY
125 .resolve("secring.gpg");
126
127 private final String signingKey;
128
129 private BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt;
130
131 private static Path findGpgDirectory() {
132 SystemReader system = SystemReader.getInstance();
133 if (system.isWindows()) {
134
135
136 String appData = system.getenv("APPDATA");
137 if (appData != null && !appData.isEmpty()) {
138 try {
139 Path directory = Paths.get(appData).resolve("gnupg");
140 if (Files.isDirectory(directory)) {
141 return directory;
142 }
143 } catch (SecurityException | InvalidPathException e) {
144
145 }
146 }
147 }
148
149
150 File home = FS.DETECTED.userHome();
151 if (home == null) {
152
153 home = new File(".").getAbsoluteFile();
154 }
155 return home.toPath().resolve(".gnupg");
156 }
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172 public BouncyCastleGpgKeyLocator(String signingKey,
173 @NonNull BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt) {
174 this.signingKey = signingKey;
175 this.passphrasePrompt = passphrasePrompt;
176 }
177
178 private PGPSecretKey attemptParseSecretKey(Path keyFile,
179 PGPDigestCalculatorProvider calculatorProvider,
180 PBEProtectionRemoverFactory passphraseProvider,
181 PGPPublicKey publicKey) {
182 try (InputStream in = newInputStream(keyFile)) {
183 return new SExprParser(calculatorProvider).parseSecretKey(
184 new BufferedInputStream(in), passphraseProvider, publicKey);
185 } catch (IOException | PGPException | ClassCastException e) {
186 if (log.isDebugEnabled())
187 log.debug("Ignoring unreadable file '{}': {}", keyFile,
188 e.getMessage(), e);
189 return null;
190 }
191 }
192
193 private boolean containsSigningKey(String userId) {
194 return userId.toLowerCase(Locale.ROOT)
195 .contains(signingKey.toLowerCase(Locale.ROOT));
196 }
197
198 private PGPPublicKey findPublicKeyByKeyId(KeyBlob keyBlob)
199 throws IOException {
200 String keyId = signingKey.toLowerCase(Locale.ROOT);
201 for (KeyInformation keyInfo : keyBlob.getKeyInformation()) {
202 String fingerprint = Hex.toHexString(keyInfo.getFingerprint())
203 .toLowerCase(Locale.ROOT);
204 if (fingerprint.endsWith(keyId)) {
205 return getFirstPublicKey(keyBlob);
206 }
207 }
208 return null;
209 }
210
211 private PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob)
212 throws IOException {
213 for (UserID userID : keyBlob.getUserIds()) {
214 if (containsSigningKey(userID.getUserIDAsString())) {
215 return getFirstPublicKey(keyBlob);
216 }
217 }
218 return null;
219 }
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234 private PGPPublicKey findPublicKeyInKeyBox(Path keyboxFile)
235 throws IOException, NoSuchAlgorithmException,
236 NoSuchProviderException, NoOpenPgpKeyException {
237 KeyBox keyBox = readKeyBoxFile(keyboxFile);
238 boolean hasOpenPgpKey = false;
239 for (KeyBlob keyBlob : keyBox.getKeyBlobs()) {
240 if (keyBlob.getType() == BlobType.OPEN_PGP_BLOB) {
241 hasOpenPgpKey = true;
242 PGPPublicKey key = findPublicKeyByKeyId(keyBlob);
243 if (key != null) {
244 return key;
245 }
246 key = findPublicKeyByUserId(keyBlob);
247 if (key != null) {
248 return key;
249 }
250 }
251 }
252 if (!hasOpenPgpKey) {
253 throw new NoOpenPgpKeyException();
254 }
255 return null;
256 }
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278 @NonNull
279 public BouncyCastleGpgKey findSecretKey() throws IOException,
280 NoSuchAlgorithmException, NoSuchProviderException, PGPException,
281 CanceledException, UnsupportedCredentialItem, URISyntaxException {
282 BouncyCastleGpgKey key;
283 PGPPublicKey publicKey = null;
284 if (hasKeyFiles(USER_SECRET_KEY_DIR)) {
285
286
287
288 if (exists(USER_KEYBOX_PATH)) {
289 try {
290 publicKey = findPublicKeyInKeyBox(USER_KEYBOX_PATH);
291 if (publicKey != null) {
292 key = findSecretKeyForKeyBoxPublicKey(publicKey,
293 USER_KEYBOX_PATH);
294 if (key != null) {
295 return key;
296 }
297 throw new PGPException(MessageFormat.format(
298 JGitText.get().gpgNoSecretKeyForPublicKey,
299 Long.toHexString(publicKey.getKeyID())));
300 }
301 throw new PGPException(MessageFormat.format(
302 JGitText.get().gpgNoPublicKeyFound, signingKey));
303 } catch (NoOpenPgpKeyException e) {
304
305
306 if (log.isDebugEnabled()) {
307 log.debug("{} does not contain any OpenPGP keys",
308 USER_KEYBOX_PATH);
309 }
310 }
311 }
312 if (exists(USER_PGP_PUBRING_FILE)) {
313 publicKey = findPublicKeyInPubring(USER_PGP_PUBRING_FILE);
314 if (publicKey != null) {
315
316
317
318
319
320
321 key = findSecretKeyForKeyBoxPublicKey(publicKey,
322 USER_PGP_PUBRING_FILE);
323 if (key != null) {
324 return key;
325 }
326 }
327 }
328 if (publicKey == null) {
329 throw new PGPException(MessageFormat.format(
330 JGitText.get().gpgNoPublicKeyFound, signingKey));
331 }
332
333
334 }
335 boolean hasSecring = false;
336 if (exists(USER_PGP_LEGACY_SECRING_FILE)) {
337 hasSecring = true;
338 key = loadKeyFromSecring(USER_PGP_LEGACY_SECRING_FILE);
339 if (key != null) {
340 return key;
341 }
342 }
343 if (publicKey != null) {
344 throw new PGPException(MessageFormat.format(
345 JGitText.get().gpgNoSecretKeyForPublicKey,
346 Long.toHexString(publicKey.getKeyID())));
347 } else if (hasSecring) {
348
349 throw new PGPException(MessageFormat.format(
350 JGitText.get().gpgNoKeyInLegacySecring, signingKey));
351 } else {
352 throw new PGPException(JGitText.get().gpgNoKeyring);
353 }
354 }
355
356 private boolean hasKeyFiles(Path dir) {
357 try (DirectoryStream<Path> contents = Files.newDirectoryStream(dir,
358 "*.key")) {
359 return contents.iterator().hasNext();
360 } catch (IOException e) {
361
362 return false;
363 }
364 }
365
366 private BouncyCastleGpgKey loadKeyFromSecring(Path secring)
367 throws IOException, PGPException {
368 PGPSecretKey secretKey = findSecretKeyInLegacySecring(signingKey,
369 secring);
370
371 if (secretKey != null) {
372 if (!secretKey.isSigningKey()) {
373 throw new PGPException(MessageFormat
374 .format(JGitText.get().gpgNotASigningKey, signingKey));
375 }
376 return new BouncyCastleGpgKey(secretKey, secring);
377 }
378 return null;
379 }
380
381 private BouncyCastleGpgKey findSecretKeyForKeyBoxPublicKey(
382 PGPPublicKey publicKey, Path userKeyboxPath)
383 throws PGPException, CanceledException, UnsupportedCredentialItem,
384 URISyntaxException {
385
386
387
388
389
390
391 PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder()
392 .build();
393
394 PBEProtectionRemoverFactory passphraseProvider = new JcePBEProtectionRemoverFactory(
395 passphrasePrompt.getPassphrase(publicKey.getFingerprint(),
396 userKeyboxPath));
397
398 try (Stream<Path> keyFiles = Files.walk(USER_SECRET_KEY_DIR)) {
399 for (Path keyFile : keyFiles.filter(Files::isRegularFile)
400 .collect(Collectors.toList())) {
401 PGPSecretKey secretKey = attemptParseSecretKey(keyFile,
402 calculatorProvider, passphraseProvider, publicKey);
403 if (secretKey != null) {
404 if (!secretKey.isSigningKey()) {
405 throw new PGPException(MessageFormat.format(
406 JGitText.get().gpgNotASigningKey, signingKey));
407 }
408 return new BouncyCastleGpgKey(secretKey, userKeyboxPath);
409 }
410 }
411
412 passphrasePrompt.clear();
413 return null;
414 } catch (RuntimeException e) {
415 passphrasePrompt.clear();
416 throw e;
417 } catch (IOException e) {
418 passphrasePrompt.clear();
419 throw new PGPException(MessageFormat.format(
420 JGitText.get().gpgFailedToParseSecretKey,
421 USER_SECRET_KEY_DIR.toAbsolutePath()), e);
422 }
423 }
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439 private PGPSecretKey findSecretKeyInLegacySecring(String signingkey,
440 Path secringFile) throws IOException, PGPException {
441
442 try (InputStream in = newInputStream(secringFile)) {
443 PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(
444 PGPUtil.getDecoderStream(new BufferedInputStream(in)),
445 new JcaKeyFingerprintCalculator());
446
447 String keyId = signingkey.toLowerCase(Locale.ROOT);
448 Iterator<PGPSecretKeyRing> keyrings = pgpSec.getKeyRings();
449 while (keyrings.hasNext()) {
450 PGPSecretKeyRing keyRing = keyrings.next();
451 Iterator<PGPSecretKey> keys = keyRing.getSecretKeys();
452 while (keys.hasNext()) {
453 PGPSecretKey key = keys.next();
454
455 String fingerprint = Hex
456 .toHexString(key.getPublicKey().getFingerprint())
457 .toLowerCase(Locale.ROOT);
458 if (fingerprint.endsWith(keyId)) {
459 return key;
460 }
461
462 Iterator<String> userIDs = key.getUserIDs();
463 while (userIDs.hasNext()) {
464 String userId = userIDs.next();
465 if (containsSigningKey(userId)) {
466 return key;
467 }
468 }
469 }
470 }
471 }
472 return null;
473 }
474
475
476
477
478
479
480
481
482
483
484
485
486 private PGPPublicKey findPublicKeyInPubring(Path pubringFile)
487 throws IOException, PGPException {
488 try (InputStream in = newInputStream(pubringFile)) {
489 PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(
490 new BufferedInputStream(in),
491 new JcaKeyFingerprintCalculator());
492
493 String keyId = signingKey.toLowerCase(Locale.ROOT);
494 Iterator<PGPPublicKeyRing> keyrings = pgpPub.getKeyRings();
495 while (keyrings.hasNext()) {
496 PGPPublicKeyRing keyRing = keyrings.next();
497 Iterator<PGPPublicKey> keys = keyRing.getPublicKeys();
498 while (keys.hasNext()) {
499 PGPPublicKey key = keys.next();
500
501 String fingerprint = Hex.toHexString(key.getFingerprint())
502 .toLowerCase(Locale.ROOT);
503 if (fingerprint.endsWith(keyId)) {
504 return key;
505 }
506
507 Iterator<String> userIDs = key.getUserIDs();
508 while (userIDs.hasNext()) {
509 String userId = userIDs.next();
510 if (containsSigningKey(userId)) {
511 return key;
512 }
513 }
514 }
515 }
516 }
517 return null;
518 }
519
520 private PGPPublicKey getFirstPublicKey(KeyBlob keyBlob) throws IOException {
521 return ((PublicKeyRingBlob) keyBlob).getPGPPublicKeyRing()
522 .getPublicKey();
523 }
524
525 private KeyBox readKeyBoxFile(Path keyboxFile) throws IOException,
526 NoSuchAlgorithmException, NoSuchProviderException,
527 NoOpenPgpKeyException {
528 if (keyboxFile.toFile().length() == 0) {
529 throw new NoOpenPgpKeyException();
530 }
531 KeyBox keyBox;
532 try (InputStream in = new BufferedInputStream(
533 newInputStream(keyboxFile))) {
534 keyBox = new JcaKeyBoxBuilder().build(in);
535 }
536 return keyBox;
537 }
538 }