/*
 * Copyright (C) 2018, Salesforce.
 * and other copyright owners as documented in the project's IP log.
 *
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Distribution License v1.0 which
 * accompanies this distribution, is reproduced below, and is
 * available at http://www.eclipse.org/org/documents/edl-v10.php
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials provided
 *   with the distribution.
 *
 * - Neither the name of the Eclipse Foundation, Inc. nor the
 *   names of its contributors may be used to endorse or promote
 *   products derived from this software without specific prior
 *   written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
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;

Locates GPG keys from either ~/.gnupg/private-keys-v1.d or ~/.gnupg/secring.gpg
/** * Locates GPG keys from either <code>~/.gnupg/private-keys-v1.d</code> or * <code>~/.gnupg/secring.gpg</code> */
class BouncyCastleGpgKeyLocator {
Thrown if a keybox file exists but doesn't contain an OpenPGP key.
/** Thrown if a keybox file exists but doesn't contain an OpenPGP key. */
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"); //$NON-NLS-1$ private static final Path USER_SECRET_KEY_DIR = GPG_DIRECTORY .resolve("private-keys-v1.d"); //$NON-NLS-1$ private static final Path USER_PGP_PUBRING_FILE = GPG_DIRECTORY .resolve("pubring.gpg"); //$NON-NLS-1$ private static final Path USER_PGP_LEGACY_SECRING_FILE = GPG_DIRECTORY .resolve("secring.gpg"); //$NON-NLS-1$ private final String signingKey; private BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt; private static Path findGpgDirectory() { SystemReader system = SystemReader.getInstance(); if (system.isWindows()) { // On Windows prefer %APPDATA%\gnupg if it exists, even if Cygwin is // used. String appData = system.getenv("APPDATA"); //$NON-NLS-1$ if (appData != null && !appData.isEmpty()) { try { Path directory = Paths.get(appData).resolve("gnupg"); //$NON-NLS-1$ if (Files.isDirectory(directory)) { return directory; } } catch (SecurityException | InvalidPathException e) { // Ignore and return the default location below. } } } // All systems, including Cygwin and even Windows if // %APPDATA%\gnupg doesn't exist: ~/.gnupg File home = FS.DETECTED.userHome(); if (home == null) { // Oops. What now? home = new File(".").getAbsoluteFile(); //$NON-NLS-1$ } return home.toPath().resolve(".gnupg"); //$NON-NLS-1$ }
Create a new key locator for the specified signing key.

The signing key must either be a hex representation of a specific key or a user identity substring (eg., email address). All keys in the KeyBox will be looked up in the order as returned by the KeyBox. A key id will be searched before attempting to find a key by user id.

Params:
  • signingKey – the signing key to search for
  • passphrasePrompt – the provider to use when asking for key passphrase
/** * Create a new key locator for the specified signing key. * <p> * The signing key must either be a hex representation of a specific key or * a user identity substring (eg., email address). All keys in the KeyBox * will be looked up in the order as returned by the KeyBox. A key id will * be searched before attempting to find a key by user id. * </p> * * @param signingKey * the signing key to search for * @param passphrasePrompt * the provider to use when asking for key passphrase */
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, //$NON-NLS-1$ 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; }
Finds a public key associated with the signing key.
Params:
  • keyboxFile – the KeyBox file
Throws:
Returns:publicKey the public key (maybe null)
/** * Finds a public key associated with the signing key. * * @param keyboxFile * the KeyBox file * @return publicKey the public key (maybe <code>null</code>) * @throws IOException * in case of problems reading the file * @throws NoSuchAlgorithmException * @throws NoSuchProviderException * @throws NoOpenPgpKeyException * if the file does not contain any OpenPGP key */
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; }
If there is a private key directory containing keys, use pubring.kbx or pubring.gpg to find the public key; then try to find the secret key in the directory.

If there is no private key directory (or it doesn't contain any keys), try to find the key in secring.gpg directly.

Throws:
Returns:the secret key
/** * If there is a private key directory containing keys, use pubring.kbx or * pubring.gpg to find the public key; then try to find the secret key in * the directory. * <p> * If there is no private key directory (or it doesn't contain any keys), * try to find the key in secring.gpg directly. * </p> * * @return the secret key * @throws IOException * in case of issues reading key files * @throws NoSuchAlgorithmException * @throws NoSuchProviderException * @throws PGPException * in case of issues finding a key, including no key found * @throws CanceledException * @throws URISyntaxException * @throws UnsupportedCredentialItem */
@NonNull public BouncyCastleGpgKey findSecretKey() throws IOException, NoSuchAlgorithmException, NoSuchProviderException, PGPException, CanceledException, UnsupportedCredentialItem, URISyntaxException { BouncyCastleGpgKey key; PGPPublicKey publicKey = null; if (hasKeyFiles(USER_SECRET_KEY_DIR)) { // Use pubring.kbx or pubring.gpg to find the public key, then try // the key files in the directory. If the public key was found in // pubring.gpg also try secring.gpg to find the secret key. 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) { // There are no OpenPGP keys in the keybox at all: try the // pubring.gpg, if it exists. if (log.isDebugEnabled()) { log.debug("{} does not contain any OpenPGP keys", //$NON-NLS-1$ USER_KEYBOX_PATH); } } } if (exists(USER_PGP_PUBRING_FILE)) { publicKey = findPublicKeyInPubring(USER_PGP_PUBRING_FILE); if (publicKey != null) { // GPG < 2.1 may have both; the agent using the directory // and gpg using secring.gpg. GPG >= 2.1 delegates all // secret key handling to the agent and doesn't use // secring.gpg at all, even if it exists. Which means for us // we have to try both since we don't know which GPG version // the user has. key = findSecretKeyForKeyBoxPublicKey(publicKey, USER_PGP_PUBRING_FILE); if (key != null) { return key; } } } if (publicKey == null) { throw new PGPException(MessageFormat.format( JGitText.get().gpgNoPublicKeyFound, signingKey)); } // We found a public key, but didn't find the secret key in the // private key directory. Go try the secring.gpg. } 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) { // publicKey == null: user has _only_ pubring.gpg/secring.gpg. 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")) { //$NON-NLS-1$ return contents.iterator().hasNext(); } catch (IOException e) { // Not a directory, or something else 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 { /* * this is somewhat brute-force but there doesn't seem to be another * way; we have to walk all private key files we find and try to open * them */ 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); } }
Return the first suitable key for signing in the key ring collection. For this case we only expect there to be one key available for signing.

Params:
  • signingkey –
  • secringFile –
Throws:
Returns:the first suitable PGP secret key found for signing
/** * Return the first suitable key for signing in the key ring collection. For * this case we only expect there to be one key available for signing. * </p> * * @param signingkey * @param secringFile * * @return the first suitable PGP secret key found for signing * @throws IOException * on I/O related errors * @throws PGPException * on BouncyCastle errors */
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(); // try key id String fingerprint = Hex .toHexString(key.getPublicKey().getFingerprint()) .toLowerCase(Locale.ROOT); if (fingerprint.endsWith(keyId)) { return key; } // try user id Iterator<String> userIDs = key.getUserIDs(); while (userIDs.hasNext()) { String userId = userIDs.next(); if (containsSigningKey(userId)) { return key; } } } } } return null; }
Return the first public key matching the key id (signingKey.
Params:
  • pubringFile –
Throws:
Returns:the PGP public key, or null if none found
/** * Return the first public key matching the key id ({@link #signingKey}. * * @param pubringFile * * @return the PGP public key, or {@code null} if none found * @throws IOException * on I/O related errors * @throws PGPException * on BouncyCastle errors */
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(); // try key id String fingerprint = Hex.toHexString(key.getFingerprint()) .toLowerCase(Locale.ROOT); if (fingerprint.endsWith(keyId)) { return key; } // try user id 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; } }