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.Files;
54 import java.nio.file.InvalidPathException;
55 import java.nio.file.Path;
56 import java.nio.file.Paths;
57 import java.security.NoSuchAlgorithmException;
58 import java.security.NoSuchProviderException;
59 import java.text.MessageFormat;
60 import java.util.Iterator;
61 import java.util.Locale;
62 import java.util.stream.Collectors;
63 import java.util.stream.Stream;
64
65 import org.bouncycastle.gpg.SExprParser;
66 import org.bouncycastle.gpg.keybox.BlobType;
67 import org.bouncycastle.gpg.keybox.KeyBlob;
68 import org.bouncycastle.gpg.keybox.KeyBox;
69 import org.bouncycastle.gpg.keybox.KeyInformation;
70 import org.bouncycastle.gpg.keybox.PublicKeyRingBlob;
71 import org.bouncycastle.gpg.keybox.UserID;
72 import org.bouncycastle.gpg.keybox.jcajce.JcaKeyBoxBuilder;
73 import org.bouncycastle.openpgp.PGPException;
74 import org.bouncycastle.openpgp.PGPPublicKey;
75 import org.bouncycastle.openpgp.PGPSecretKey;
76 import org.bouncycastle.openpgp.PGPSecretKeyRing;
77 import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
78 import org.bouncycastle.openpgp.PGPUtil;
79 import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
80 import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
81 import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
82 import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
83 import org.bouncycastle.openpgp.operator.jcajce.JcePBEProtectionRemoverFactory;
84 import org.bouncycastle.util.encoders.Hex;
85 import org.eclipse.jgit.annotations.NonNull;
86 import org.eclipse.jgit.api.errors.CanceledException;
87 import org.eclipse.jgit.errors.UnsupportedCredentialItem;
88 import org.eclipse.jgit.internal.JGitText;
89 import org.eclipse.jgit.util.FS;
90 import org.eclipse.jgit.util.SystemReader;
91 import org.slf4j.Logger;
92 import org.slf4j.LoggerFactory;
93
94
95
96
97
98 class BouncyCastleGpgKeyLocator {
99
100 private static final Logger log = LoggerFactory
101 .getLogger(BouncyCastleGpgKeyLocator.class);
102
103 private static final Path GPG_DIRECTORY = findGpgDirectory();
104
105 private static final Path USER_KEYBOX_PATH = GPG_DIRECTORY
106 .resolve("pubring.kbx");
107
108 private static final Path USER_SECRET_KEY_DIR = GPG_DIRECTORY
109 .resolve("private-keys-v1.d");
110
111 private static final Path USER_PGP_LEGACY_SECRING_FILE = GPG_DIRECTORY
112 .resolve("secring.gpg");
113
114 private final String signingKey;
115
116 private BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt;
117
118 private static Path findGpgDirectory() {
119 SystemReader system = SystemReader.getInstance();
120 if (system.isWindows()) {
121
122
123 String appData = system.getenv("APPDATA");
124 if (appData != null && !appData.isEmpty()) {
125 try {
126 Path directory = Paths.get(appData).resolve("gnupg");
127 if (Files.isDirectory(directory)) {
128 return directory;
129 }
130 } catch (SecurityException | InvalidPathException e) {
131
132 }
133 }
134 }
135
136
137 File home = FS.DETECTED.userHome();
138 if (home == null) {
139
140 home = new File(".").getAbsoluteFile();
141 }
142 return home.toPath().resolve(".gnupg");
143 }
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159 public BouncyCastleGpgKeyLocator(String signingKey,
160 @NonNull BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt) {
161 this.signingKey = signingKey;
162 this.passphrasePrompt = passphrasePrompt;
163 }
164
165 private PGPSecretKey attemptParseSecretKey(Path keyFile,
166 PGPDigestCalculatorProvider calculatorProvider,
167 PBEProtectionRemoverFactory passphraseProvider,
168 PGPPublicKey publicKey) {
169 try (InputStream in = newInputStream(keyFile)) {
170 return new SExprParser(calculatorProvider).parseSecretKey(
171 new BufferedInputStream(in), passphraseProvider, publicKey);
172 } catch (IOException | PGPException | ClassCastException e) {
173 if (log.isDebugEnabled())
174 log.debug("Ignoring unreadable file '{}': {}", keyFile,
175 e.getMessage(), e);
176 return null;
177 }
178 }
179
180 private boolean containsSigningKey(String userId) {
181 return userId.toLowerCase(Locale.ROOT)
182 .contains(signingKey.toLowerCase(Locale.ROOT));
183 }
184
185 private PGPPublicKey findPublicKeyByKeyId(KeyBlob keyBlob)
186 throws IOException {
187 String keyId = signingKey.toLowerCase(Locale.ROOT);
188 for (KeyInformation keyInfo : keyBlob.getKeyInformation()) {
189 String fingerprint = Hex.toHexString(keyInfo.getFingerprint())
190 .toLowerCase(Locale.ROOT);
191 if (fingerprint.endsWith(keyId)) {
192 return getFirstPublicKey(keyBlob);
193 }
194 }
195 return null;
196 }
197
198 private PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob)
199 throws IOException {
200 for (UserID userID : keyBlob.getUserIds()) {
201 if (containsSigningKey(userID.getUserIDAsString())) {
202 return getFirstPublicKey(keyBlob);
203 }
204 }
205 return null;
206 }
207
208
209
210
211
212
213
214
215
216
217
218
219 private PGPPublicKey findPublicKeyInKeyBox(Path keyboxFile)
220 throws IOException, NoSuchAlgorithmException,
221 NoSuchProviderException {
222 KeyBox keyBox = readKeyBoxFile(keyboxFile);
223 for (KeyBlob keyBlob : keyBox.getKeyBlobs()) {
224 if (keyBlob.getType() == BlobType.OPEN_PGP_BLOB) {
225 PGPPublicKey key = findPublicKeyByKeyId(keyBlob);
226 if (key != null) {
227 return key;
228 }
229 key = findPublicKeyByUserId(keyBlob);
230 if (key != null) {
231 return key;
232 }
233 }
234 }
235 return null;
236 }
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253 public BouncyCastleGpgKey findSecretKey() throws IOException,
254 NoSuchAlgorithmException, NoSuchProviderException, PGPException,
255 CanceledException, UnsupportedCredentialItem, URISyntaxException {
256 if (exists(USER_KEYBOX_PATH)) {
257 PGPPublicKey publicKey =
258 findPublicKeyInKeyBox(USER_KEYBOX_PATH);
259
260 if (publicKey != null) {
261 return findSecretKeyForKeyBoxPublicKey(publicKey,
262 USER_KEYBOX_PATH);
263 }
264
265 throw new PGPException(MessageFormat
266 .format(JGitText.get().gpgNoPublicKeyFound, signingKey));
267 } else if (exists(USER_PGP_LEGACY_SECRING_FILE)) {
268 PGPSecretKey secretKey = findSecretKeyInLegacySecring(signingKey,
269 USER_PGP_LEGACY_SECRING_FILE);
270
271 if (secretKey != null) {
272 if (!secretKey.isSigningKey()) {
273 throw new PGPException(MessageFormat.format(
274 JGitText.get().gpgNotASigningKey, signingKey));
275 }
276 return new BouncyCastleGpgKey(secretKey, USER_PGP_LEGACY_SECRING_FILE);
277 }
278
279 throw new PGPException(MessageFormat.format(
280 JGitText.get().gpgNoKeyInLegacySecring, signingKey));
281 }
282
283 throw new PGPException(JGitText.get().gpgNoKeyring);
284 }
285
286 private BouncyCastleGpgKey findSecretKeyForKeyBoxPublicKey(
287 PGPPublicKey publicKey, Path userKeyboxPath)
288 throws PGPException, CanceledException, UnsupportedCredentialItem,
289 URISyntaxException {
290
291
292
293
294
295
296 PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder()
297 .build();
298
299 PBEProtectionRemoverFactory passphraseProvider = new JcePBEProtectionRemoverFactory(
300 passphrasePrompt.getPassphrase(publicKey.getFingerprint(),
301 userKeyboxPath));
302
303 try (Stream<Path> keyFiles = Files.walk(USER_SECRET_KEY_DIR)) {
304 for (Path keyFile : keyFiles.filter(Files::isRegularFile)
305 .collect(Collectors.toList())) {
306 PGPSecretKey secretKey = attemptParseSecretKey(keyFile,
307 calculatorProvider, passphraseProvider, publicKey);
308 if (secretKey != null) {
309 if (!secretKey.isSigningKey()) {
310 throw new PGPException(MessageFormat.format(
311 JGitText.get().gpgNotASigningKey, signingKey));
312 }
313 return new BouncyCastleGpgKey(secretKey, userKeyboxPath);
314 }
315 }
316
317 passphrasePrompt.clear();
318 throw new PGPException(MessageFormat.format(
319 JGitText.get().gpgNoSecretKeyForPublicKey,
320 Long.toHexString(publicKey.getKeyID())));
321 } catch (RuntimeException e) {
322 passphrasePrompt.clear();
323 throw e;
324 } catch (IOException e) {
325 passphrasePrompt.clear();
326 throw new PGPException(MessageFormat.format(
327 JGitText.get().gpgFailedToParseSecretKey,
328 USER_SECRET_KEY_DIR.toAbsolutePath()), e);
329 }
330 }
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346 private PGPSecretKey findSecretKeyInLegacySecring(String signingkey,
347 Path secringFile) throws IOException, PGPException {
348
349 try (InputStream in = newInputStream(secringFile)) {
350 PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(
351 PGPUtil.getDecoderStream(new BufferedInputStream(in)),
352 new JcaKeyFingerprintCalculator());
353
354 String keyId = signingkey.toLowerCase(Locale.ROOT);
355 Iterator<PGPSecretKeyRing> keyrings = pgpSec.getKeyRings();
356 while (keyrings.hasNext()) {
357 PGPSecretKeyRing keyRing = keyrings.next();
358 Iterator<PGPSecretKey> keys = keyRing.getSecretKeys();
359 while (keys.hasNext()) {
360 PGPSecretKey key = keys.next();
361
362 String fingerprint = Hex
363 .toHexString(key.getPublicKey().getFingerprint())
364 .toLowerCase(Locale.ROOT);
365 if (fingerprint.endsWith(keyId)) {
366 return key;
367 }
368
369 Iterator<String> userIDs = key.getUserIDs();
370 while (userIDs.hasNext()) {
371 String userId = userIDs.next();
372 if (containsSigningKey(userId)) {
373 return key;
374 }
375 }
376 }
377 }
378 }
379 return null;
380 }
381
382 private PGPPublicKey getFirstPublicKey(KeyBlob keyBlob) throws IOException {
383 return ((PublicKeyRingBlob) keyBlob).getPGPPublicKeyRing()
384 .getPublicKey();
385 }
386
387 private KeyBox readKeyBoxFile(Path keyboxFile) throws IOException,
388 NoSuchAlgorithmException, NoSuchProviderException {
389 KeyBox keyBox;
390 try (InputStream in = new BufferedInputStream(
391 newInputStream(keyboxFile))) {
392 keyBox = new JcaKeyBoxBuilder().build(in);
393 }
394 return keyBox;
395 }
396 }