diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPMessageGenerator.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPMessageGenerator.java index 09f751bb05..b57a752d67 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPMessageGenerator.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPMessageGenerator.java @@ -13,6 +13,7 @@ import org.bouncycastle.bcpg.AEADAlgorithmTags; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.bcpg.KeyIdentifier; import org.bouncycastle.bcpg.S2K; import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; import org.bouncycastle.bcpg.sig.PreferredAEADCiphersuites; @@ -53,6 +54,7 @@ public class OpenPGPMessageGenerator private boolean isArmored = true; public boolean isAllowPadding = true; private final List encryptionKeys = new ArrayList(); + private final List anonymousRecipients = new ArrayList(); private final List messagePassphrases = new ArrayList(); // Literal Data metadata @@ -88,7 +90,25 @@ public OpenPGPMessageGenerator(OpenPGPImplementation implementation, OpenPGPPoli public OpenPGPMessageGenerator addEncryptionCertificate(OpenPGPCertificate recipientCertificate) throws InvalidEncryptionKeyException { - return addEncryptionCertificate(recipientCertificate, encryptionKeySelector); + return addEncryptionCertificate(recipientCertificate, false); + } + + /** + * Add a recipients certificate to the set of encryption keys. + * Subkeys will be selected using the default {@link SubkeySelector}, which can be replaced by calling + * {@link #setEncryptionKeySelector(SubkeySelector)}. + * The recipient will be able to decrypt the message using their corresponding secret key. + * If anonymousRecipient is true, the recipients key-identifier will be obfuscated via a wildcard key-identifier. + * + * @param recipientCertificate recipient certificate (public key) + * @param anonymousRecipient whether to obfuscate the recipients identity within the message + * @return this + */ + public OpenPGPMessageGenerator addEncryptionCertificate(OpenPGPCertificate recipientCertificate, + boolean anonymousRecipient) + throws InvalidEncryptionKeyException + { + return addEncryptionCertificate(recipientCertificate, encryptionKeySelector, anonymousRecipient); } /** @@ -103,15 +123,39 @@ public OpenPGPMessageGenerator addEncryptionCertificate(OpenPGPCertificate recip */ public OpenPGPMessageGenerator addEncryptionCertificate(OpenPGPCertificate recipientCertificate, SubkeySelector subkeySelector) - throws InvalidEncryptionKeyException + throws InvalidEncryptionKeyException + { + return addEncryptionCertificate(recipientCertificate, subkeySelector, false); + } + + /** + * Add a recipients certificate to the set of encryption keys. + * Subkeys will be selected using the provided {@link SubkeySelector}. + * The recipient will be able to decrypt the message using their corresponding secret key. + * If anonymous recipient is true, the identity of the recipient will be obfuscated via a wildcard key-identifier. + * + * @param recipientCertificate recipient certificate (public key) + * @param subkeySelector selector for encryption subkeys + * @param anonymousRecipient whether to obfuscate the recipients identity within the message + * @return this + * @throws InvalidEncryptionKeyException if the certificate is not capable of encryption + */ + public OpenPGPMessageGenerator addEncryptionCertificate(OpenPGPCertificate recipientCertificate, + SubkeySelector subkeySelector, + boolean anonymousRecipient) + throws InvalidEncryptionKeyException { List subkeys = - subkeySelector.select(recipientCertificate, policy); + subkeySelector.select(recipientCertificate, policy); if (subkeys.isEmpty()) { throw new InvalidEncryptionKeyException(recipientCertificate); } - this.encryptionKeys.addAll(subkeys); + + for (OpenPGPCertificate.OpenPGPComponentKey subkey : subkeys) + { + addEncryptionCertificate(subkey, anonymousRecipient); + } return this; } @@ -124,13 +168,34 @@ public OpenPGPMessageGenerator addEncryptionCertificate(OpenPGPCertificate recip * @throws InvalidEncryptionKeyException if the key is not capable of encryption */ public OpenPGPMessageGenerator addEncryptionCertificate(OpenPGPCertificate.OpenPGPComponentKey encryptionKey) - throws InvalidEncryptionKeyException + throws InvalidEncryptionKeyException + { + return addEncryptionCertificate(encryptionKey, false); + } + + /** + * Add a (sub-)key to the set of recipient encryption keys. + * The recipient will be able to decrypt the message using their corresponding secret key. + * If anonymous recipient is true, the identity of the recipient will be obfuscated via a wildcard key-identifier. + * + * @param encryptionKey encryption capable subkey + * @param anonymousRecipient whether to obfuscate the recipients identity within the message + * @return this + * @throws InvalidEncryptionKeyException if the key is not capable of encryption + */ + public OpenPGPMessageGenerator addEncryptionCertificate(OpenPGPCertificate.OpenPGPComponentKey encryptionKey, + boolean anonymousRecipient) + throws InvalidEncryptionKeyException { if (!encryptionKey.isEncryptionKey()) { throw new InvalidEncryptionKeyException(encryptionKey); } encryptionKeys.add(encryptionKey); + if (anonymousRecipient) + { + anonymousRecipients.add(encryptionKey.getKeyIdentifier()); + } return this; } @@ -282,6 +347,8 @@ private void applyOptionalEncryption( { PublicKeyKeyEncryptionMethodGenerator method = implementation.publicKeyKeyEncryptionMethodGenerator( encryptionSubkey.getPGPPublicKey()); + // optionally mark recipient as anonymous + method.setUseWildcardRecipient(anonymousRecipients.contains(encryptionSubkey.getKeyIdentifier())); encGen.addMethod(method); } diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPMessageProcessor.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPMessageProcessor.java index 546d70a1bd..9d167d9575 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPMessageProcessor.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPMessageProcessor.java @@ -294,6 +294,7 @@ Decrypted decrypt(PGPEncryptedDataList encDataList) List skesks = skesks(encDataList); List pkesks = pkesks(encDataList); + List anonPkesks = anonPkesks(pkesks); PGPException exception = null; @@ -384,6 +385,57 @@ Decrypted decrypt(PGPEncryptedDataList encDataList) } } + // Edge-case: There are anonymous (wildcard) key identifiers + if (!anonPkesks.isEmpty()) + { + for (PGPPublicKeyEncryptedData pkesk : anonPkesks) + { + for (OpenPGPKey key : configuration.keyPool.getAllItems()) + { + for (OpenPGPCertificate.OpenPGPComponentKey encryptionKey : key.getEncryptionKeys()) + { + OpenPGPKey.OpenPGPSecretKey decryptionKey = key.getSecretKey(encryptionKey); + if (decryptionKey == null) + { + // Missing (potentially stripped) secret key + continue; + } + + try + { + char[] keyPassphrase = configuration.keyPassphraseProvider.getKeyPassword(decryptionKey); + PGPKeyPair unlockedKey = decryptionKey.unlock(keyPassphrase).getKeyPair(); + if (unlockedKey == null) + { + throw new KeyPassphraseException(decryptionKey, new PGPException("Cannot unlock secret key.")); + } + + // Decrypt the message session key using the private key + PublicKeyDataDecryptorFactory pkDecryptorFactory = + implementation.publicKeyDataDecryptorFactory(unlockedKey.getPrivateKey()); + PGPSessionKey decryptedSessionKey = pkesk.getSessionKey(pkDecryptorFactory); + + // Decrypt the message using the decrypted session key + SessionKeyDataDecryptorFactory skDecryptorFactory = + implementation.sessionKeyDataDecryptorFactory(decryptedSessionKey); + PGPSessionKeyEncryptedData encData = encDataList.extractSessionKeyEncryptedData(); + InputStream decryptedIn = encData.getDataStream(skDecryptorFactory); + IntegrityProtectedInputStream verifyingIn = new IntegrityProtectedInputStream(decryptedIn, encData); + Decrypted decrypted = new Decrypted(encData, decryptedSessionKey, verifyingIn); + decrypted.decryptionKey = decryptionKey; + return decrypted; + } + catch (PGPException e) + { + onException(e); + // cache first exception, then continue to try next skesk if present + exception = exception != null ? exception : e; + } + } + } + } + } + // And lastly, we'll prompt the user dynamically for a message passphrase if (!skesks.isEmpty() && configuration.missingMessagePassphraseCallback != null) { @@ -463,6 +515,19 @@ private List pkesks(PGPEncryptedDataList encDataList) return list; } + private List anonPkesks(List pkesks) + { + List list = new ArrayList(); + for (PGPPublicKeyEncryptedData pkesk : pkesks) + { + if (pkesk.getKeyIdentifier().isWildcard()) + { + list.add(pkesk); + } + } + return list; + } + OpenPGPCertificate provideCertificate(KeyIdentifier identifier) { return configuration.certificatePool.provide(identifier); diff --git a/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPMessageGeneratorTest.java b/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPMessageGeneratorTest.java index bd709d7355..7447d11849 100644 --- a/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPMessageGeneratorTest.java +++ b/pg/src/test/java/org/bouncycastle/openpgp/api/test/OpenPGPMessageGeneratorTest.java @@ -1,17 +1,26 @@ package org.bouncycastle.openpgp.api.test; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.BCPGInputStream; import org.bouncycastle.bcpg.CompressionAlgorithmTags; import org.bouncycastle.openpgp.OpenPGPTestKeys; +import org.bouncycastle.openpgp.PGPEncryptedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; import org.bouncycastle.openpgp.api.OpenPGPApi; import org.bouncycastle.openpgp.api.OpenPGPCertificate; import org.bouncycastle.openpgp.api.OpenPGPKey; import org.bouncycastle.openpgp.api.OpenPGPMessageGenerator; +import org.bouncycastle.openpgp.api.OpenPGPMessageInputStream; import org.bouncycastle.openpgp.api.OpenPGPMessageOutputStream; +import org.bouncycastle.openpgp.api.OpenPGPMessageProcessor; import org.bouncycastle.openpgp.api.OpenPGPPolicy; import org.bouncycastle.util.Strings; import org.bouncycastle.util.encoders.Hex; @@ -38,6 +47,7 @@ protected void performTestWith(OpenPGPApi api) seipd2EncryptedMessage(api); seipd2EncryptedSignedMessage(api); + encryptWithWildcardKeyIdentifier(api); } private void armoredLiteralDataPacket(OpenPGPApi api) @@ -186,6 +196,45 @@ private void seipd2EncryptedSignedMessage(OpenPGPApi api) System.out.println(bOut); } + public void encryptWithWildcardKeyIdentifier(OpenPGPApi api) + throws IOException, PGPException { + OpenPGPKey key = api.readKeyOrCertificate().parseKey(OpenPGPTestKeys.V6_KEY); + + OpenPGPMessageGenerator gen = api.signAndOrEncryptMessage() + .setAllowPadding(true) + .setArmored(true) + .addSigningKey(key) + .addEncryptionCertificate(key, true); + + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + OutputStream encOut = gen.open(bOut); + encOut.write("Hello, World!\n".getBytes()); + encOut.close(); + + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); + ArmoredInputStream aIn = ArmoredInputStream.builder() + .build(bIn); + BCPGInputStream pIn = BCPGInputStream.wrap(aIn); + PGPObjectFactory objFac = api.getImplementation().pgpObjectFactory(pIn); + PGPEncryptedDataList encList = (PGPEncryptedDataList) objFac.nextObject(); + for (PGPEncryptedData encData : encList) + { + isTrue(encData instanceof PGPPublicKeyEncryptedData); + PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) encData; + isTrue(pkesk.getKeyIdentifier().isWildcard()); + } + + bIn = new ByteArrayInputStream(bOut.toByteArray()); + OpenPGPMessageProcessor processor = api.decryptAndOrVerifyMessage() + .addDecryptionKey(key); + + OpenPGPMessageInputStream mIn = processor.process(bIn); + bOut = new ByteArrayOutputStream(); + org.bouncycastle.util.io.Streams.pipeAll(mIn, bOut); + mIn.close(); + isEncodingEqual("Hello, World!\n".getBytes(), bOut.toByteArray()); + } + public static void main(String[] args) { runTest(new OpenPGPMessageGeneratorTest());