~/.gnupg/private-keys-v1.d
or
* ~/.gnupg/secring.gpg
*/
public class BouncyCastleGpgKeyLocator {
/** 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);
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();
Function* 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. *
* * @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, SecretKeys.PassphraseSupplier passphraseSupplier, PGPPublicKey publicKey) throws IOException, PGPException, CanceledException, UnsupportedCredentialItem, URISyntaxException { try (InputStream in = newInputStream(keyFile)) { return SecretKeys.readSecretKey(in, calculatorProvider, passphraseSupplier, publicKey); } } /** * Checks whether a given OpenPGP {@code userId} matches a given * {@code signingKeySpec}, which is supposed to have one of the formats * defined by GPG. ** Not all formats are supported; only formats starting with '=', '<', * '@', and '*' are handled. Any other format results in a case-insensitive * substring match. *
* * @param userId * of a key * @param signingKeySpec * GPG key identification * @return whether the {@code userId} matches * @see GPG * Documentation: How to Specify a User ID */ static boolean containsSigningKey(String userId, String signingKeySpec) { if (StringUtils.isEmptyOrNull(userId) || StringUtils.isEmptyOrNull(signingKeySpec)) { return false; } String toMatch = signingKeySpec; if (toMatch.startsWith("0x") && toMatch.trim().length() > 2) { //$NON-NLS-1$ return false; // Explicit fingerprint } int command = toMatch.charAt(0); switch (command) { case '=': case '<': case '@': case '*': toMatch = toMatch.substring(1); if (toMatch.isEmpty()) { return false; } break; default: break; } switch (command) { case '=': return userId.equals(toMatch); case '<': { int begin = userId.indexOf('<'); int end = userId.indexOf('>', begin + 1); int stop = toMatch.indexOf('>'); return begin >= 0 && end > begin + 1 && stop > 0 && userId.substring(begin + 1, end) .equalsIgnoreCase(toMatch.substring(0, stop)); } case '@': { int begin = userId.indexOf('<'); int end = userId.indexOf('>', begin + 1); return begin >= 0 && end > begin + 1 && containsIgnoreCase(userId.substring(begin + 1, end), toMatch); } default: if (toMatch.trim().isEmpty()) { return false; } return containsIgnoreCase(userId, toMatch); } } private static boolean containsIgnoreCase(String a, String b) { int alength = a.length(); int blength = b.length(); for (int i = 0; i + blength <= alength; i++) { if (a.regionMatches(true, i, b, 0, blength)) { return true; } } return false; } private static String toFingerprint(String keyId) { if (keyId.startsWith("0x")) { //$NON-NLS-1$ return keyId.substring(2); } return keyId; } static BouncyCastleGpgPublicKey findPublicKey(String fingerprint, String keySpec) throws IOException, PGPException { BouncyCastleGpgPublicKey result = findPublicKeyInPubring( USER_PGP_PUBRING_FILE, fingerprint, keySpec); if (result == null && exists(USER_KEYBOX_PATH)) { try { result = findPublicKeyInKeyBox(USER_KEYBOX_PATH, fingerprint, keySpec); } catch (NoSuchAlgorithmException | NoSuchProviderException | IOException | NoOpenPgpKeyException e) { log.error(e.getMessage(), e); } } return result; } private static PGPPublicKey findPublicKeyByKeyId(KeyBlob keyBlob, String keyId) throws IOException { if (keyId.isEmpty()) { return null; } for (KeyInformation keyInfo : keyBlob.getKeyInformation()) { String fingerprint = Hex.toHexString(keyInfo.getFingerprint()) .toLowerCase(Locale.ROOT); if (fingerprint.endsWith(keyId)) { return getPublicKey(keyBlob, keyInfo.getFingerprint()); } } return null; } private static PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob, String keySpec) throws IOException { for (UserID userID : keyBlob.getUserIds()) { if (containsSigningKey(userID.getUserIDAsString(), keySpec)) { return getSigningPublicKey(keyBlob); } } return null; } /** * Finds a public key associated with the signing key. * * @param keyboxFile * the KeyBox file * @param keyId * to look for, may be null * @param keySpec * to look for * @return publicKey the public key (maybenull
)
* @throws IOException
* in case of problems reading the file
* @throws NoSuchAlgorithmException
* if an algorithm isn't available
* @throws NoSuchProviderException
* if a provider isn't available
* @throws NoOpenPgpKeyException
* if the file does not contain any OpenPGP key
*/
private static BouncyCastleGpgPublicKey findPublicKeyInKeyBox(
Path keyboxFile,
String keyId, String keySpec)
throws IOException, NoSuchAlgorithmException,
NoSuchProviderException, NoOpenPgpKeyException {
KeyBox keyBox = readKeyBoxFile(keyboxFile);
String id = keyId != null ? keyId
: toFingerprint(keySpec).toLowerCase(Locale.ROOT);
boolean hasOpenPgpKey = false;
for (KeyBlob keyBlob : keyBox.getKeyBlobs()) {
if (keyBlob.getType() == BlobType.OPEN_PGP_BLOB) {
hasOpenPgpKey = true;
PGPPublicKey key = findPublicKeyByKeyId(keyBlob, id);
if (key != null) {
if (!isSigningKey(key)) {
return null;
}
return new BouncyCastleGpgPublicKey(key, true,
toStrings(keyBlob.getUserIds()));
}
key = findPublicKeyByUserId(keyBlob, keySpec);
if (key != null) {
return new BouncyCastleGpgPublicKey(key, true,
toStrings(keyBlob.getUserIds()));
}
}
}
if (!hasOpenPgpKey) {
throw new NoOpenPgpKeyException();
}
return null;
}
private static List* If there is no private key directory (or it doesn't contain any keys), * try to find the key in secring.gpg directly. *
* * @return the secret key * @throws IOException * in case of issues reading key files * @throws NoSuchAlgorithmException * algorithm is not available * @throws NoSuchProviderException * provider is not available * @throws PGPException * in case of issues finding a key, including no key found * @throws CanceledException * operation was cancelled * @throws URISyntaxException * URI is invalid * @throws UnsupportedCredentialItem * credential item is not supported */ @NonNull public BouncyCastleGpgKey findSecretKey() throws IOException, NoSuchAlgorithmException, NoSuchProviderException, PGPException, CanceledException, UnsupportedCredentialItem, URISyntaxException { BouncyCastleGpgKey key; BouncyCastleGpgPublicKey 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, null, signingKey); if (publicKey != null) { key = findSecretKeyForKeyBoxPublicKey( publicKey.getPublicKey(), USER_KEYBOX_PATH); if (key != null) { return key; } throw new PGPException(MessageFormat.format( BCText.get().gpgNoSecretKeyForPublicKey, Long.toHexString( publicKey.getPublicKey().getKeyID()))); } throw new PGPException(MessageFormat.format( BCText.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, null, signingKey); 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.getPublicKey(), USER_PGP_PUBRING_FILE); if (key != null) { return key; } } } if (publicKey == null) { throw new PGPException(MessageFormat.format( BCText.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( BCText.get().gpgNoSecretKeyForPublicKey, Long.toHexString(publicKey.getPublicKey().getKeyID()))); } else if (hasSecring) { // publicKey == null: user has _only_ pubring.gpg/secring.gpg. throw new PGPException(MessageFormat.format( BCText.get().gpgNoKeyInLegacySecring, signingKey)); } else { throw new PGPException(BCText.get().gpgNoKeyring); } } private boolean hasKeyFiles(Path dir) { try (DirectoryStream