package org.c02e.jpgpj; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.regex.Pattern; import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyFlags; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder; import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; import org.c02e.jpgpj.util.Util; /** * A single public-key pair from a full {@link Key}. It may consist of only * the public part of the pair, or it may include both the public * and private parts. Each subkey is designated for a specific * cryptographic purpose (or purposes), typically either certification * (ie signing other keys), encryption, or signing (ie signing messages). * The passphrase for a subkey must be provided in order to use its * private part (the private part is needed for signing and decryption). * <p> * The purpose of a subkey is indicated by these four methods: * <ul> * <li>{@link #isForSigning}: true if should be used for signing messages * <li>{@link #isForVerification}: true if should be used for verifying messages * <li>{@link #isForEncryption}: true if should be used for encrypting messages * <li>{@link #isForDecryption}: true if should be used for decrypting messages * </ul> * <p> * By default, when a subkey with a "sign data" flag is loaded, its * <code>forVerification</code> property will be set to true; and if the subkey * includes the private part of its public-key pair, its * <code>forSigning</code> property will also be set to true. * When a subkey with a "encrypt communications" or "encrypt storage" flag * is loaded, its <code>forEncryption</code> property will be set to true; * and if the subkey includes the private part of its public-key pair, its * <code>forDecryption</code> property will also be set to true. * <p> * However, before actually using a subkey for signing or decryption, * you must also set the subkey's passphrase, either via the * {@link #setPassphraseChars} method on the subkey, or the * {@link Key#setPassphraseChars} on its containing {@link Key}. * If the subkey does not have a passphrase, set the passphrase to the * {@link Key#NO_PASSPHRASE} constant (or use {@link #setNoPassphrase}). * <p> * When a subkey is used for signing or decryption, its private key material * is extracted and cached in memory. To release this memory, call the subkey's * {@link #clearSecrets} method. This method will zero-out the subkey's * passphrase (if the passphrase had been set as a char[] via * {@link #setPassphraseChars}) and release the cached private key material * (however, the private key material will not be zeroed-out; also, the * passphrase will not be zeroed-out if it was set via {@link #setPassphrase}). */ public class Subkey { private static final char[] NO_PASSPHRASE = Key.NO_PASSPHRASE.toCharArray(); private static final char[] EMPTY_PASSPHRASE = new char[0]; protected boolean forSigning; protected boolean forVerification; protected boolean forEncryption; protected boolean forDecryption; protected char[] passphraseChars; /** @deprecated Null unless explicitly set by user. */ @Deprecated protected String passphrase; protected PGPPublicKey publicKey; protected PGPSecretKey secretKey; /** Decrypted private key material. Null unless decrypted. */ protected PGPPrivateKey privateKey; /** Constructs a blank subkey. */ public Subkey() { setPassphraseChars(null); } /** * Display string for the subkey, including its usage flags, * short ID, and user IDs. */ @Override public String toString() { if (publicKey == null) return "nul"; StringBuilder b = new StringBuilder(); b.append(secretKey != null ? "sec" : "pub"); b.append(Util.isEmpty(passphraseChars) && privateKey == null ? ' ' : '+'); if (forVerification) b.append('v'); else if (forEncryption) b.append('e'); else b.append(' '); if (forSigning) b.append('s'); else if (forDecryption) b.append('d'); else b.append(' '); b.append(' '); b.append(getShortId()); int count = 0; for (String uid : getUids()) { if (count++ > 0) b.append(','); b.append(' '); b.append(uid); } return b.toString(); } /** True if the subkey should be used for signing messages. */ public boolean isForSigning() { return forSigning; } /** True if the subkey should be used for signing messages. */ public void setForSigning(boolean x) { forSigning = x; } /** True if the subkey should be used for verifying messages. */ public boolean isForVerification() { return forVerification; } /** True if the subkey should be used for verifying messages. */ public void setForVerification(boolean x) { forVerification = x; } /** True if the subkey should be used for encrypting messages. */ public boolean isForEncryption() { return forEncryption; } /** True if the subkey should be used for encrypting messages. */ public void setForEncryption(boolean x) { forEncryption = x; } /** True if the subkey should be used for decrypting messages. */ public boolean isForDecryption() { return forDecryption; } /** True if the subkey should be used for decrypting messages. */ public void setForDecryption(boolean x) { forDecryption = x; } /** True if technically usable for signing. */ public boolean isUsableForSigning() { return isUsableForVerification() && secretKey != null && !secretKey.isPrivateKeyEmpty(); } /** True if technically usable for verification. */ public boolean isUsableForVerification() { int algorithm = publicKey != null ? publicKey.getAlgorithm() : 0; return algorithm == PublicKeyAlgorithmTags.RSA_GENERAL || algorithm == PublicKeyAlgorithmTags.RSA_SIGN || algorithm == PublicKeyAlgorithmTags.DSA || algorithm == PublicKeyAlgorithmTags.ECDSA || algorithm == PublicKeyAlgorithmTags.EDDSA; } /** True if technically usable for encryption. */ public boolean isUsableForEncryption() { return publicKey != null && publicKey.isEncryptionKey(); } /** True if technically usable for decryption. */ public boolean isUsableForDecryption() { return isUsableForEncryption() && secretKey != null && !secretKey.isPrivateKeyEmpty(); } /** * Passphrase needed to unlock the private part * of the subkey's public key-pair; or empty char[]. * Use {@link Key#NO_PASSPHRASE} to signal the private part of the subkey * is not protected by a passphrase. * Note that this char[] itself (and not a copy) will be cached and used * by the subkey until {@link #clearSecrets} is called (or * {@link #setPassphraseChars} is called again with a different passphrase), * and then the char[] will be zeroed. */ public char[] getPassphraseChars() { return passphraseChars; } /** * Passphrase needed to unlock the private part * of the subkey's public key-pair; or empty char[]. * Use {@link Key#NO_PASSPHRASE} to signal the private part of the subkey * is not protected by a passphrase. * Note that this char[] itself (and not a copy) will be cached and used * by the subkey until {@link #clearSecrets} is called (or * {@link #setPassphraseChars} is called again with a different passphrase), * and then the char[] will be zeroed. */ public void setPassphraseChars(char[] x) { if (x == null) x = EMPTY_PASSPHRASE; if (!Arrays.equals(x, passphraseChars)) { clearSecrets(); passphraseChars = x; } } /** * Passphrase needed to unlock the private part * of the subkey's public key-pair; or empty string. * Prefer {@link #getPassphraseChars} to avoid creating extra copies * of the passphrase in memory that cannot be cleaned up. * Use {@link Key#NO_PASSPHRASE} to signal the private part of the subkey * is not protected by a passphrase. * @see #getPassphraseChars */ public String getPassphrase() { if (passphrase == null) passphrase = new String(passphraseChars); return passphrase; } /** * Passphrase needed to unlock the private part * of the subkey's public key-pair; or empty string. * Prefer {@link #setPassphraseChars} to avoid creating extra copies * of the passphrase in memory that cannot be cleaned up. * Use {@link Key#NO_PASSPHRASE} to signal the private part of the subkey * is not protected by a passphrase. * @see #setPassphraseChars */ public void setPassphrase(String x) { setPassphraseChars(x != null ? x.toCharArray() : null); passphrase = x; } /** * True if no passphrase is needed to unlock the private part * of the subkey's public key-pair. */ public boolean isNoPassphrase() { return Arrays.equals(passphraseChars, NO_PASSPHRASE); } /** * True if no passphrase is needed to unlock the private part * of the subkey's public key-pair. */ public void setNoPassphrase(boolean x) { if (x != isNoPassphrase()) setPassphrase(x ? Key.NO_PASSPHRASE : null); } /** * Bouncy castle public-key pair, * containing only the public part of the pair; or null. */ public PGPPublicKey getPublicKey() { return publicKey; } /** * Bouncy castle public-key pair, * containing only the public part of the pair; or null. */ public void setPublicKey(PGPPublicKey x) throws PGPException { publicKey = x; calculateUsage(); } /** * Bouncy castle public-key pair, * containing both the public and private parts of the pair; or null. */ public PGPSecretKey getSecretKey() { return secretKey; } /** * Bouncy castle public-key pair, * containing both the public and private parts of the pair; or null. */ public void setSecretKey(PGPSecretKey x) throws PGPException { secretKey = x; if (secretKey != null) setPublicKey(secretKey.getPublicKey()); } /** * Extracts the Bouncy castle private-key material * from this subkey's secret key, using the subkey's passphrase, * and caches it in memory until {@link #clearSecrets} is called. * @return null if this subkey does not have a secret key. * @throws PassphraseException if passphrase is incorrect. */ public PGPPrivateKey getPrivateKey() throws PGPException { if (privateKey == null) privateKey = extractPrivateKey(passphraseChars); return privateKey; } /** * Fingerprint of public key, * or empty string if no public key. */ public String getFingerprint() { if (publicKey == null) return ""; return Util.formatAsHex(publicKey.getFingerprint()); } /** * Full '0xlong' format of public key, * or empty string if no public key. */ public String getId() { if (publicKey == null) return ""; return "0x" + String.format("%016X", publicKey.getKeyID()); } /** * Abbreviated 'short' format of public key, * or empty string if no public key. */ public String getShortId() { if (publicKey == null) return ""; return String.format("%016X", publicKey.getKeyID()).substring(8); } /** * User ID strings of public key * (ex ["My Name (comment) <[email protected]>"]), or empty list. */ public List<String> getUids() { if (publicKey == null) return Collections.emptyList(); ArrayList<String> result = new ArrayList<String>(); Iterator<String> uids = publicKey.getUserIDs(); while (uids.hasNext()) result.add(uids.next()); return result; } /** * Usage flags as Bouncy castle {@link PGPKeyFlags} bits. */ public int getUsageFlags() throws PGPException { if (publicKey == null) return 0; int flags = 0; // actually only need POSITIVE_CERTIFICATION (for master key) // and SUBKEY_BINDING (for subkeys) @SuppressWarnings("unchecked") Iterator<PGPSignature> signatures = publicKey.getSignatures(); while (signatures.hasNext()) { PGPSignature signature = signatures.next(); PGPSignatureSubpacketVector hashedSubPackets = signature.getHashedSubPackets(); if (hashedSubPackets != null) flags |= hashedSubPackets.getKeyFlags(); } return flags; } /** * True if the string is a case-insensitive substring of either: * <ul> * <li>the subkey's full ID (eg "0x1234567890ABCDEF"); * <li>the subkey's fingerprint (eg "1234567890ABCDEF1234567890ABCDEF12345678"); * <li>any subkey's user IDs (eg "Alice (work) <[email protected]>") * </ul> * <p> * So for example, a string "0x1234" would match the above full ID; * a string "90ab" would match the above fingerprint; * and a string "alice (work)" would match the above user ID. */ public boolean matches(String id) { if (Util.isEmpty(id)) return false; Pattern regex = Pattern.compile(id, Pattern.CASE_INSENSITIVE | Pattern.LITERAL); return matches(regex); } /** * True if the specified pattern matches any part of either: * <ul> * <li>the subkey's full ID (eg "0x1234567890ABCDEF"); * <li>the subkey's fingerprint (eg "1234567890ABCDEF1234567890ABCDEF12345678"); * <li>any subkey's user IDs (eg "Alice (work) <[email protected]>") * </ul> * <p> * So for example, a pattern /0x1234/ would match the above full ID; * a pattern /(?i)90ab/ would match the above fingerprint; * and a pattern /Alice .work./ would match the above user ID. */ public boolean matches(Pattern id) { if (id == null || publicKey == null) return false; if (id.matcher(getFingerprint()).find()) return true; if (id.matcher(getId()).find()) return true; Iterator<String> uids = publicKey.getUserIDs(); while (uids.hasNext()) { String uid = uids.next(); if (id.matcher(uid).find()) return true; } return false; } /** * True if the private key material has been extracted from this subkey's * secret key and is currently cached in memory. */ public boolean isUnlocked() { return privateKey != null; } /** * Extracts the private key material from this subkey's secret key * using the specified passphrase, and caches it in memory * until {@link #clearSecrets} is called. Does not cache the passphrase. * Does nothing if this subkey does not have a secret key. * @throws PassphraseException if passphrase is incorrect. */ public void unlock(char[] passphraseChars) throws PGPException { privateKey = extractPrivateKey(passphraseChars); } /** * Zeroes-out the cached passphrase for this subkey, * and releases the extracted private key material for garbage collection. * Note that if {@link #getPassphrase} or {@link #setPassphrase} is * used to access the passphrase, the passphrase data cannot be zeroed * (so instead use {@link #getPassphraseChars} and * {@link #setPassphraseChars}). */ public void clearSecrets() { // zero-out passphrase data if (passphraseChars != null) Arrays.fill(passphraseChars, (char) 0); // flag as empty passphraseChars = EMPTY_PASSPHRASE; // cannot cleanup futher, release for GC passphrase = null; // cannot cleanup futher, release for GC privateKey = null; } /** * Extracts the private key from this subkey's secret key * using the specified passphrase. */ protected PGPPrivateKey extractPrivateKey(char[] passphraseChars) throws PGPException { if (secretKey == null) return null; try { return secretKey.extractPrivateKey(buildDecryptor(passphraseChars)); } catch (PGPException e) { throw new PassphraseException( "incorrect passphrase for subkey " + this, e); } } /** * Builds a secret key decryptor for the specified passphrase. */ protected PBESecretKeyDecryptor buildDecryptor(char[] passphraseChars) { char[] chars = passphraseChars != null && !Arrays.equals(passphraseChars, NO_PASSPHRASE) ? passphraseChars : EMPTY_PASSPHRASE; return new BcPBESecretKeyDecryptorBuilder( new BcPGPDigestCalculatorProvider()).build(chars); } /** * Sets forSigning etc flags based on key data. */ protected void calculateUsage() throws PGPException { int flags = getUsageFlags(); boolean canSign = (flags & PGPKeyFlags.CAN_SIGN) == PGPKeyFlags.CAN_SIGN; boolean canEncrypt = ((flags & PGPKeyFlags.CAN_ENCRYPT_COMMS) == PGPKeyFlags.CAN_ENCRYPT_COMMS) || ((flags & PGPKeyFlags.CAN_ENCRYPT_STORAGE) == PGPKeyFlags.CAN_ENCRYPT_STORAGE); forSigning = canSign && isUsableForSigning(); forVerification = canSign && isUsableForVerification(); forEncryption = canEncrypt && isUsableForEncryption(); forDecryption = canEncrypt && isUsableForDecryption(); } }