package org.eclipse.jgit.lib.internal;
import static java.nio.file.Files.exists;
import static java.nio.file.Files.newInputStream;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.text.MessageFormat;
import java.util.Iterator;
import java.util.Locale;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.bouncycastle.gpg.SExprParser;
import org.bouncycastle.gpg.keybox.BlobType;
import org.bouncycastle.gpg.keybox.KeyBlob;
import org.bouncycastle.gpg.keybox.KeyBox;
import org.bouncycastle.gpg.keybox.KeyInformation;
import org.bouncycastle.gpg.keybox.PublicKeyRingBlob;
import org.bouncycastle.gpg.keybox.UserID;
import org.bouncycastle.gpg.keybox.jcajce.JcaKeyBoxBuilder;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcePBEProtectionRemoverFactory;
import org.bouncycastle.util.encoders.Hex;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.SystemReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class BouncyCastleGpgKeyLocator {
private static class NoOpenPgpKeyException extends Exception {
private static final long serialVersionUID = 1L;
}
private static final Logger log = LoggerFactory
.getLogger(BouncyCastleGpgKeyLocator.class);
private static final Path GPG_DIRECTORY = findGpgDirectory();
private static final Path USER_KEYBOX_PATH = GPG_DIRECTORY
.resolve("pubring.kbx");
private static final Path USER_SECRET_KEY_DIR = GPG_DIRECTORY
.resolve("private-keys-v1.d");
private static final Path USER_PGP_PUBRING_FILE = GPG_DIRECTORY
.resolve("pubring.gpg");
private static final Path USER_PGP_LEGACY_SECRING_FILE = GPG_DIRECTORY
.resolve("secring.gpg");
private final String signingKey;
private BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt;
private static Path findGpgDirectory() {
SystemReader system = SystemReader.getInstance();
if (system.isWindows()) {
String appData = system.getenv("APPDATA");
if (appData != null && !appData.isEmpty()) {
try {
Path directory = Paths.get(appData).resolve("gnupg");
if (Files.isDirectory(directory)) {
return directory;
}
} catch (SecurityException | InvalidPathException e) {
}
}
}
File home = FS.DETECTED.userHome();
if (home == null) {
home = new File(".").getAbsoluteFile();
}
return home.toPath().resolve(".gnupg");
}
public BouncyCastleGpgKeyLocator(String signingKey,
@NonNull BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt) {
this.signingKey = signingKey;
this.passphrasePrompt = passphrasePrompt;
}
private PGPSecretKey attemptParseSecretKey(Path keyFile,
PGPDigestCalculatorProvider calculatorProvider,
PBEProtectionRemoverFactory passphraseProvider,
PGPPublicKey publicKey) {
try (InputStream in = newInputStream(keyFile)) {
return new SExprParser(calculatorProvider).parseSecretKey(
new BufferedInputStream(in), passphraseProvider, publicKey);
} catch (IOException | PGPException | ClassCastException e) {
if (log.isDebugEnabled())
log.debug("Ignoring unreadable file '{}': {}", keyFile,
e.getMessage(), e);
return null;
}
}
private boolean containsSigningKey(String userId) {
return userId.toLowerCase(Locale.ROOT)
.contains(signingKey.toLowerCase(Locale.ROOT));
}
private PGPPublicKey findPublicKeyByKeyId(KeyBlob keyBlob)
throws IOException {
String keyId = signingKey.toLowerCase(Locale.ROOT);
for (KeyInformation keyInfo : keyBlob.getKeyInformation()) {
String fingerprint = Hex.toHexString(keyInfo.getFingerprint())
.toLowerCase(Locale.ROOT);
if (fingerprint.endsWith(keyId)) {
return getFirstPublicKey(keyBlob);
}
}
return null;
}
private PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob)
throws IOException {
for (UserID userID : keyBlob.getUserIds()) {
if (containsSigningKey(userID.getUserIDAsString())) {
return getFirstPublicKey(keyBlob);
}
}
return null;
}
private PGPPublicKey findPublicKeyInKeyBox(Path keyboxFile)
throws IOException, NoSuchAlgorithmException,
NoSuchProviderException, NoOpenPgpKeyException {
KeyBox keyBox = readKeyBoxFile(keyboxFile);
boolean hasOpenPgpKey = false;
for (KeyBlob keyBlob : keyBox.getKeyBlobs()) {
if (keyBlob.getType() == BlobType.OPEN_PGP_BLOB) {
hasOpenPgpKey = true;
PGPPublicKey key = findPublicKeyByKeyId(keyBlob);
if (key != null) {
return key;
}
key = findPublicKeyByUserId(keyBlob);
if (key != null) {
return key;
}
}
}
if (!hasOpenPgpKey) {
throw new NoOpenPgpKeyException();
}
return null;
}
@NonNull
public BouncyCastleGpgKey findSecretKey() throws IOException,
NoSuchAlgorithmException, NoSuchProviderException, PGPException,
CanceledException, UnsupportedCredentialItem, URISyntaxException {
BouncyCastleGpgKey key;
PGPPublicKey publicKey = null;
if (hasKeyFiles(USER_SECRET_KEY_DIR)) {
if (exists(USER_KEYBOX_PATH)) {
try {
publicKey = findPublicKeyInKeyBox(USER_KEYBOX_PATH);
if (publicKey != null) {
key = findSecretKeyForKeyBoxPublicKey(publicKey,
USER_KEYBOX_PATH);
if (key != null) {
return key;
}
throw new PGPException(MessageFormat.format(
JGitText.get().gpgNoSecretKeyForPublicKey,
Long.toHexString(publicKey.getKeyID())));
}
throw new PGPException(MessageFormat.format(
JGitText.get().gpgNoPublicKeyFound, signingKey));
} catch (NoOpenPgpKeyException e) {
if (log.isDebugEnabled()) {
log.debug("{} does not contain any OpenPGP keys",
USER_KEYBOX_PATH);
}
}
}
if (exists(USER_PGP_PUBRING_FILE)) {
publicKey = findPublicKeyInPubring(USER_PGP_PUBRING_FILE);
if (publicKey != null) {
key = findSecretKeyForKeyBoxPublicKey(publicKey,
USER_PGP_PUBRING_FILE);
if (key != null) {
return key;
}
}
}
if (publicKey == null) {
throw new PGPException(MessageFormat.format(
JGitText.get().gpgNoPublicKeyFound, signingKey));
}
}
boolean hasSecring = false;
if (exists(USER_PGP_LEGACY_SECRING_FILE)) {
hasSecring = true;
key = loadKeyFromSecring(USER_PGP_LEGACY_SECRING_FILE);
if (key != null) {
return key;
}
}
if (publicKey != null) {
throw new PGPException(MessageFormat.format(
JGitText.get().gpgNoSecretKeyForPublicKey,
Long.toHexString(publicKey.getKeyID())));
} else if (hasSecring) {
throw new PGPException(MessageFormat.format(
JGitText.get().gpgNoKeyInLegacySecring, signingKey));
} else {
throw new PGPException(JGitText.get().gpgNoKeyring);
}
}
private boolean hasKeyFiles(Path dir) {
try (DirectoryStream<Path> contents = Files.newDirectoryStream(dir,
"*.key")) {
return contents.iterator().hasNext();
} catch (IOException e) {
return false;
}
}
private BouncyCastleGpgKey loadKeyFromSecring(Path secring)
throws IOException, PGPException {
PGPSecretKey secretKey = findSecretKeyInLegacySecring(signingKey,
secring);
if (secretKey != null) {
if (!secretKey.isSigningKey()) {
throw new PGPException(MessageFormat
.format(JGitText.get().gpgNotASigningKey, signingKey));
}
return new BouncyCastleGpgKey(secretKey, secring);
}
return null;
}
private BouncyCastleGpgKey findSecretKeyForKeyBoxPublicKey(
PGPPublicKey publicKey, Path userKeyboxPath)
throws PGPException, CanceledException, UnsupportedCredentialItem,
URISyntaxException {
PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder()
.build();
PBEProtectionRemoverFactory passphraseProvider = new JcePBEProtectionRemoverFactory(
passphrasePrompt.getPassphrase(publicKey.getFingerprint(),
userKeyboxPath));
try (Stream<Path> keyFiles = Files.walk(USER_SECRET_KEY_DIR)) {
for (Path keyFile : keyFiles.filter(Files::isRegularFile)
.collect(Collectors.toList())) {
PGPSecretKey secretKey = attemptParseSecretKey(keyFile,
calculatorProvider, passphraseProvider, publicKey);
if (secretKey != null) {
if (!secretKey.isSigningKey()) {
throw new PGPException(MessageFormat.format(
JGitText.get().gpgNotASigningKey, signingKey));
}
return new BouncyCastleGpgKey(secretKey, userKeyboxPath);
}
}
passphrasePrompt.clear();
return null;
} catch (RuntimeException e) {
passphrasePrompt.clear();
throw e;
} catch (IOException e) {
passphrasePrompt.clear();
throw new PGPException(MessageFormat.format(
JGitText.get().gpgFailedToParseSecretKey,
USER_SECRET_KEY_DIR.toAbsolutePath()), e);
}
}
private PGPSecretKey findSecretKeyInLegacySecring(String signingkey,
Path secringFile) throws IOException, PGPException {
try (InputStream in = newInputStream(secringFile)) {
PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(
PGPUtil.getDecoderStream(new BufferedInputStream(in)),
new JcaKeyFingerprintCalculator());
String keyId = signingkey.toLowerCase(Locale.ROOT);
Iterator<PGPSecretKeyRing> keyrings = pgpSec.getKeyRings();
while (keyrings.hasNext()) {
PGPSecretKeyRing keyRing = keyrings.next();
Iterator<PGPSecretKey> keys = keyRing.getSecretKeys();
while (keys.hasNext()) {
PGPSecretKey key = keys.next();
String fingerprint = Hex
.toHexString(key.getPublicKey().getFingerprint())
.toLowerCase(Locale.ROOT);
if (fingerprint.endsWith(keyId)) {
return key;
}
Iterator<String> userIDs = key.getUserIDs();
while (userIDs.hasNext()) {
String userId = userIDs.next();
if (containsSigningKey(userId)) {
return key;
}
}
}
}
}
return null;
}
private PGPPublicKey findPublicKeyInPubring(Path pubringFile)
throws IOException, PGPException {
try (InputStream in = newInputStream(pubringFile)) {
PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(
new BufferedInputStream(in),
new JcaKeyFingerprintCalculator());
String keyId = signingKey.toLowerCase(Locale.ROOT);
Iterator<PGPPublicKeyRing> keyrings = pgpPub.getKeyRings();
while (keyrings.hasNext()) {
PGPPublicKeyRing keyRing = keyrings.next();
Iterator<PGPPublicKey> keys = keyRing.getPublicKeys();
while (keys.hasNext()) {
PGPPublicKey key = keys.next();
String fingerprint = Hex.toHexString(key.getFingerprint())
.toLowerCase(Locale.ROOT);
if (fingerprint.endsWith(keyId)) {
return key;
}
Iterator<String> userIDs = key.getUserIDs();
while (userIDs.hasNext()) {
String userId = userIDs.next();
if (containsSigningKey(userId)) {
return key;
}
}
}
}
}
return null;
}
private PGPPublicKey getFirstPublicKey(KeyBlob keyBlob) throws IOException {
return ((PublicKeyRingBlob) keyBlob).getPGPPublicKeyRing()
.getPublicKey();
}
private KeyBox readKeyBoxFile(Path keyboxFile) throws IOException,
NoSuchAlgorithmException, NoSuchProviderException,
NoOpenPgpKeyException {
if (keyboxFile.toFile().length() == 0) {
throw new NoOpenPgpKeyException();
}
KeyBox keyBox;
try (InputStream in = new BufferedInputStream(
newInputStream(keyboxFile))) {
keyBox = new JcaKeyBoxBuilder().build(in);
}
return keyBox;
}
}