Browse Source

GPG: don't prompt for a passphrase for unprotected keys

BouncyCastle supports reading GPG keys without passphrase since 1.62.
Handle this in JGit, too, and don't prompt for a passphrase unless
it's necessary.

Make two passes over the private key files, a first pass without
passphrase provider. If that succeeds it has managed to read a
matching key without passphrase. Otherwise, ask the user for
the passphrase and make a second pass over the key files.

BouncyCastle 1.65 still has no method to get the GPG "key grip" from
a given public key, so JGit still cannot determine the correct file
to read up front. (The file name is the key grip as 40 hex digits,
upper case, with extension ".key").

Bug: 548763
Change-Id: I448181276548c08716d913c7ba1b4bc64c62f952
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
master
Thomas Wolf 5 years ago
parent
commit
e3f7a06764
  1. 61
      org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java
  2. 11
      org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyPassphrasePrompt.java
  3. 31
      org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java

61
org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2018, Salesforce. and others * Copyright (C) 2018, 2020 Salesforce and others
* *
* This program and the accompanying materials are made available under the * This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at * terms of the Eclipse Distribution License v. 1.0 which is available at
@ -25,7 +25,9 @@ import java.nio.file.Paths;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException; import java.security.NoSuchProviderException;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -76,6 +78,13 @@ public class BouncyCastleGpgKeyLocator {
} }
/** Thrown if we try to read an encrypted private key without password. */
private static class EncryptedPgpKeyException extends RuntimeException {
private static final long serialVersionUID = 1L;
}
private static final Logger log = LoggerFactory private static final Logger log = LoggerFactory
.getLogger(BouncyCastleGpgKeyLocator.class); .getLogger(BouncyCastleGpgKeyLocator.class);
@ -433,22 +442,46 @@ public class BouncyCastleGpgKeyLocator {
PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder() PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder()
.build(); .build();
PBEProtectionRemoverFactory passphraseProvider = new JcePBEProtectionRemoverFactory(
passphrasePrompt.getPassphrase(publicKey.getFingerprint(),
userKeyboxPath));
try (Stream<Path> keyFiles = Files.walk(USER_SECRET_KEY_DIR)) { try (Stream<Path> keyFiles = Files.walk(USER_SECRET_KEY_DIR)) {
for (Path keyFile : keyFiles.filter(Files::isRegularFile) List<Path> allPaths = keyFiles.filter(Files::isRegularFile)
.collect(Collectors.toList())) { .collect(Collectors.toCollection(ArrayList::new));
PGPSecretKey secretKey = attemptParseSecretKey(keyFile, if (allPaths.isEmpty()) {
calculatorProvider, passphraseProvider, publicKey); return null;
if (secretKey != null) { }
if (!secretKey.isSigningKey()) { PBEProtectionRemoverFactory passphraseProvider = p -> {
throw new PGPException(MessageFormat.format( throw new EncryptedPgpKeyException();
BCText.get().gpgNotASigningKey, signingKey)); };
for (int attempts = 0; attempts < 2; attempts++) {
// Second pass will traverse only the encrypted keys with a real
// passphrase provider.
Iterator<Path> pathIterator = allPaths.iterator();
while (pathIterator.hasNext()) {
Path keyFile = pathIterator.next();
try {
PGPSecretKey secretKey = attemptParseSecretKey(keyFile,
calculatorProvider, passphraseProvider,
publicKey);
pathIterator.remove();
if (secretKey != null) {
if (!secretKey.isSigningKey()) {
throw new PGPException(MessageFormat.format(
BCText.get().gpgNotASigningKey,
signingKey));
}
return new BouncyCastleGpgKey(secretKey,
userKeyboxPath);
}
} catch (EncryptedPgpKeyException e) {
// Ignore; we'll try again.
} }
return new BouncyCastleGpgKey(secretKey, userKeyboxPath);
} }
if (attempts > 0 || allPaths.isEmpty()) {
break;
}
// allPaths contains only the encrypted keys now.
passphraseProvider = new JcePBEProtectionRemoverFactory(
passphrasePrompt.getPassphrase(
publicKey.getFingerprint(), userKeyboxPath));
} }
passphrasePrompt.clear(); passphrasePrompt.clear();

11
org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyPassphrasePrompt.java

@ -1,5 +1,5 @@
/*- /*-
* Copyright (C) 2019, Salesforce. and others * Copyright (C) 2019, 2020 Salesforce and others
* *
* This program and the accompanying materials are made available under the * This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at * terms of the Eclipse Distribution License v. 1.0 which is available at
@ -97,4 +97,13 @@ class BouncyCastleGpgKeyPassphrasePrompt implements AutoCloseable {
return passphrase.getValue(); return passphrase.getValue();
} }
/**
* Determines whether a passphrase was already obtained.
*
* @return {@code true} if a passphrase is already set, {@code false}
* otherwise
*/
public boolean hasPassphrase() {
return passphrase != null && passphrase.getValue() != null;
}
} }

31
org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2018, Salesforce. and others * Copyright (C) 2018, 2020, Salesforce and others
* *
* This program and the accompanying materials are made available under the * This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at * terms of the Eclipse Distribution License v. 1.0 which is available at
@ -104,13 +104,28 @@ public class BouncyCastleGpgSigner extends GpgSigner {
throw new JGitInternalException( throw new JGitInternalException(
BCText.get().unableToSignCommitNoSecretKey); BCText.get().unableToSignCommitNoSecretKey);
} }
char[] passphrase = passphrasePrompt.getPassphrase( JcePBESecretKeyDecryptorBuilder decryptorBuilder = new JcePBESecretKeyDecryptorBuilder()
secretKey.getPublicKey().getFingerprint(), .setProvider(BouncyCastleProvider.PROVIDER_NAME);
gpgKey.getOrigin()); PGPPrivateKey privateKey = null;
PGPPrivateKey privateKey = secretKey if (!passphrasePrompt.hasPassphrase()) {
.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder() // Either the key is not encrypted, or it was read from the
.setProvider(BouncyCastleProvider.PROVIDER_NAME) // legacy secring.gpg. Try getting the private key without
.build(passphrase)); // passphrase first.
try {
privateKey = secretKey.extractPrivateKey(
decryptorBuilder.build(new char[0]));
} catch (PGPException e) {
// Ignore and try again with passphrase below
}
}
if (privateKey == null) {
// Try using a passphrase
char[] passphrase = passphrasePrompt.getPassphrase(
secretKey.getPublicKey().getFingerprint(),
gpgKey.getOrigin());
privateKey = secretKey
.extractPrivateKey(decryptorBuilder.build(passphrase));
}
PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator( PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
new JcaPGPContentSignerBuilder( new JcaPGPContentSignerBuilder(
secretKey.getPublicKey().getAlgorithm(), secretKey.getPublicKey().getAlgorithm(),

Loading…
Cancel
Save