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