diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java new file mode 100644 index 00000000..e9f6c916 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -0,0 +1,188 @@ +package org.pgpainless.decryption_verification; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.Passphrase; + +/** + * Options for decryption and signature verification. + */ +public class ConsumerOptions { + + private Date verifyNotBefore; + private Date verifyNotAfter; + + // Set of verification keys + private Set certificates = new HashSet<>(); + private Set detachedSignatures = new HashSet<>(); + private MissingPublicKeyCallback missingCertificateCallback = null; + + // Session key for decryption without passphrase/key + private byte[] sessionKey = null; + + private final Map decryptionKeys = new HashMap<>(); + private final Set decryptionPassphrases = new HashSet<>(); + + + /** + * Consider signatures made before the given timestamp invalid. + * + * @param timestamp timestamp + * @return options + */ + public ConsumerOptions verifyNotBefore(Date timestamp) { + this.verifyNotBefore = timestamp; + return this; + } + + /** + * Consider signatures made after the given timestamp invalid. + * + * @param timestamp timestamp + * @return options + */ + public ConsumerOptions verifyNotAfter(Date timestamp) { + this.verifyNotAfter = timestamp; + return this; + } + + /** + * Add a certificate (public key ring) for signature verification. + * + * @param verificationCert certificate for signature verification + * @return options + */ + public ConsumerOptions addVerificationCert(PGPPublicKeyRing verificationCert) { + this.certificates.add(verificationCert); + return this; + } + + /** + * Add a set of certificates (public key rings) for signature verification. + * + * @param verificationCerts certificates for signature verification + * @return options + */ + public ConsumerOptions addVerificationCerts(PGPPublicKeyRingCollection verificationCerts) { + for (PGPPublicKeyRing certificate : verificationCerts) { + addVerificationCert(certificate); + } + return this; + } + + /** + * Add a detached signature for the signature verification process. + * + * @param detachedSignature detached signature + * @return options + */ + public ConsumerOptions addVerificationOfDetachedSignature(PGPSignature detachedSignature) { + detachedSignatures.add(detachedSignature); + return this; + } + + /** + * Set a callback that's used when a certificate (public key) is missing for signature verification. + * + * @param callback callback + * @return options + */ + public ConsumerOptions setMissingCertificateCallback(MissingPublicKeyCallback callback) { + this.missingCertificateCallback = callback; + return this; + } + + + /** + * Attempt decryption using a session key. + * + * Note: PGPainless does not yet support decryption with session keys. + * TODO: Implement + * + * @see RFC4880 on Session Keys + * + * @param sessionKey session key + * @return options + */ + public ConsumerOptions setSessionKey(@Nonnull byte[] sessionKey) { + this.sessionKey = sessionKey; + return this; + } + + /** + * Add a key for message decryption. + * The key is expected to be unencrypted. + * + * @param key unencrypted key + * @return options + */ + public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing key) { + return addDecryptionKey(key, SecretKeyRingProtector.unprotectedKeys()); + } + + /** + * Add a key for message decryption. If the key is encrypted, the {@link SecretKeyRingProtector} is used to decrypt it + * when needed. + * + * @param key key + * @param keyRingProtector protector for the secret key + * @return options + */ + public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing key, @Nonnull SecretKeyRingProtector keyRingProtector) { + decryptionKeys.put(key, keyRingProtector); + return this; + } + + /** + * Add a passphrase for message decryption. + * + * @param passphrase passphrase + * @return options + */ + public ConsumerOptions addDecryptionPassphrase(@Nonnull Passphrase passphrase) { + decryptionPassphrases.add(passphrase); + return this; + } + + public Set getDecryptionKeys() { + return Collections.unmodifiableSet(decryptionKeys.keySet()); + } + + public Set getDecryptionPassphrases() { + return Collections.unmodifiableSet(decryptionPassphrases); + } + + public Set getCertificates() { + return Collections.unmodifiableSet(certificates); + } + + public MissingPublicKeyCallback getMissingCertificateCallback() { + return missingCertificateCallback; + } + + public SecretKeyRingProtector getSecretKeyProtector(PGPSecretKeyRing decryptionKeyRing) { + return decryptionKeys.get(decryptionKeyRing); + } + + public Set getDetachedSignatures() { + return Collections.unmodifiableSet(detachedSignatures); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java index 8bafe6ff..25e76057 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java @@ -63,6 +63,15 @@ public class DecryptionBuilder implements DecryptionBuilderInterface { class DecryptWithImpl implements DecryptWith { + @Override + public DecryptionStream withOptions(ConsumerOptions consumerOptions) throws PGPException, IOException { + if (consumerOptions == null) { + throw new IllegalArgumentException("Consumer options cannot be null."); + } + + return DecryptionStreamFactory.create(inputStream, consumerOptions); + } + @Override public Verify decryptWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRingCollection secretKeyRings) { DecryptionBuilder.this.decryptionKeys = secretKeyRings; @@ -219,8 +228,27 @@ public class DecryptionBuilder implements DecryptionBuilderInterface { @Override public DecryptionStream build() throws IOException, PGPException { - return DecryptionStreamFactory.create(inputStream, decryptionKeys, decryptionKeyDecryptor, - decryptionPassphrase, detachedSignatures, verificationKeys, missingPublicKeyCallback); + ConsumerOptions options = new ConsumerOptions(); + + for (PGPSecretKeyRing decryptionKey : (decryptionKeys != null ? decryptionKeys : Collections.emptyList())) { + options.addDecryptionKey(decryptionKey, decryptionKeyDecryptor); + } + + for (PGPPublicKeyRing certificate : (verificationKeys != null ? verificationKeys : Collections.emptyList())) { + options.addVerificationCert(certificate); + } + + for (PGPSignature detachedSignature : (detachedSignatures != null ? detachedSignatures : Collections.emptyList())) { + options.addVerificationOfDetachedSignature(detachedSignature); + } + + options.setMissingCertificateCallback(missingPublicKeyCallback); + + if (decryptionPassphrase != null) { + options.addDecryptionPassphrase(decryptionPassphrase); + } + + return DecryptionStreamFactory.create(inputStream, options); } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java index c890ff0f..45717753 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilderInterface.java @@ -46,6 +46,8 @@ public interface DecryptionBuilderInterface { interface DecryptWith { + DecryptionStream withOptions(ConsumerOptions consumerOptions) throws PGPException, IOException; + /** * Decrypt the encrypted data using the secret keys found in the provided {@link PGPSecretKeyRingCollection}. * Here it is assumed that the secret keys are not password protected. diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java index c9c42034..686876a9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java @@ -54,6 +54,7 @@ import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.EncryptionPurpose; import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.exception.MessageNotIntegrityProtectedException; @@ -62,6 +63,7 @@ import org.pgpainless.exception.UnacceptableAlgorithmException; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.signature.DetachedSignature; @@ -75,11 +77,7 @@ public final class DecryptionStreamFactory { private static final Level LEVEL = Level.FINE; private static final int MAX_RECURSION_DEPTH = 16; - private final PGPSecretKeyRingCollection decryptionKeys; - private final SecretKeyRingProtector decryptionKeyDecryptor; - private final Passphrase decryptionPassphrase; - private final Set verificationKeys = new HashSet<>(); - private final MissingPublicKeyCallback missingPublicKeyCallback; + private final ConsumerOptions options; private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); private static final PGPContentVerifierBuilderProvider verifierBuilderProvider = ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(); @@ -87,47 +85,30 @@ public final class DecryptionStreamFactory { private final Map verifiableOnePassSignatures = new HashMap<>(); private final List integrityProtectedStreams = new ArrayList<>(); - private DecryptionStreamFactory(@Nullable PGPSecretKeyRingCollection decryptionKeys, - @Nullable SecretKeyRingProtector decryptor, - @Nullable Passphrase decryptionPassphrase, - @Nullable Set verificationKeys, - @Nullable MissingPublicKeyCallback missingPublicKeyCallback) { - this.decryptionKeys = decryptionKeys; - this.decryptionKeyDecryptor = decryptor; - this.decryptionPassphrase = decryptionPassphrase; - this.verificationKeys.addAll(verificationKeys != null ? verificationKeys : Collections.emptyList()); - this.missingPublicKeyCallback = missingPublicKeyCallback; + public DecryptionStreamFactory(ConsumerOptions options) { + this.options = options; } public static DecryptionStream create(@Nonnull InputStream inputStream, - @Nullable PGPSecretKeyRingCollection decryptionKeys, - @Nullable SecretKeyRingProtector decryptor, - @Nullable Passphrase decryptionPassphrase, - @Nullable List detachedSignatures, - @Nullable Set verificationKeys, - @Nullable MissingPublicKeyCallback missingPublicKeyCallback) - throws IOException, PGPException { - InputStream pgpInputStream; - DecryptionStreamFactory factory = new DecryptionStreamFactory(decryptionKeys, decryptor, - decryptionPassphrase, verificationKeys, missingPublicKeyCallback); + @Nonnull ConsumerOptions options) throws PGPException, IOException { + InputStream pgpInputStream = inputStream; + DecryptionStreamFactory factory = new DecryptionStreamFactory(options); - if (detachedSignatures != null) { - pgpInputStream = inputStream; - for (PGPSignature signature : detachedSignatures) { - PGPPublicKeyRing signingKeyRing = factory.findSignatureVerificationKeyRing(signature.getKeyID()); - if (signingKeyRing == null) { - continue; - } - PGPPublicKey signingKey = signingKeyRing.getPublicKey(signature.getKeyID()); - signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), signingKey); - factory.resultBuilder.addDetachedSignature( - new DetachedSignature(signature, signingKeyRing, new SubkeyIdentifier(signingKeyRing, signature.getKeyID()))); + for (PGPSignature signature : options.getDetachedSignatures()) { + PGPPublicKeyRing signingKeyRing = factory.findSignatureVerificationKeyRing(signature.getKeyID()); + if (signingKeyRing == null) { + continue; } - } else { - PGPObjectFactory objectFactory = new PGPObjectFactory( - PGPUtil.getDecoderStream(inputStream), keyFingerprintCalculator); - pgpInputStream = factory.processPGPPackets(objectFactory, 1); + PGPPublicKey signingKey = signingKeyRing.getPublicKey(signature.getKeyID()); + signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), signingKey); + factory.resultBuilder.addDetachedSignature( + new DetachedSignature(signature, signingKeyRing, new SubkeyIdentifier(signingKeyRing, signature.getKeyID()))); } + + PGPObjectFactory objectFactory = new PGPObjectFactory( + PGPUtil.getDecoderStream(inputStream), keyFingerprintCalculator); + pgpInputStream = factory.processPGPPackets(objectFactory, 1); + return new DecryptionStream(pgpInputStream, factory.resultBuilder, factory.integrityProtectedStreams); } @@ -210,50 +191,68 @@ public final class DecryptionStreamFactory { while (encryptedDataIterator.hasNext()) { PGPEncryptedData encryptedData = encryptedDataIterator.next(); + // TODO: Can we just skip non-integrity-protected packages? if (!encryptedData.isIntegrityProtected()) { throw new MessageNotIntegrityProtectedException(); } + // Data is passphrase encrypted if (encryptedData instanceof PGPPBEEncryptedData) { PGPPBEEncryptedData pbeEncryptedData = (PGPPBEEncryptedData) encryptedData; - if (decryptionPassphrase != null) { + for (Passphrase passphrase : options.getDecryptionPassphrases()) { PBEDataDecryptorFactory passphraseDecryptor = ImplementationFactory.getInstance() - .getPBEDataDecryptorFactory(decryptionPassphrase); - SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.fromId( - pbeEncryptedData.getSymmetricAlgorithm(passphraseDecryptor)); - throwIfAlgorithmIsRejected(symmetricKeyAlgorithm); - resultBuilder.setSymmetricKeyAlgorithm(symmetricKeyAlgorithm); - + .getPBEDataDecryptorFactory(passphrase); try { - return pbeEncryptedData.getDataStream(passphraseDecryptor); + InputStream decryptedDataStream = pbeEncryptedData.getDataStream(passphraseDecryptor); + + SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.fromId( + pbeEncryptedData.getSymmetricAlgorithm(passphraseDecryptor)); + throwIfAlgorithmIsRejected(symmetricKeyAlgorithm); + resultBuilder.setSymmetricKeyAlgorithm(symmetricKeyAlgorithm); + + return decryptedDataStream; } catch (PGPException e) { LOGGER.log(LEVEL, "Probable passphrase mismatch, skip PBE encrypted data block", e); } } } + // data is public key encrypted else if (encryptedData instanceof PGPPublicKeyEncryptedData) { + if (options.getDecryptionKeys().isEmpty()) { + + } PGPPublicKeyEncryptedData publicKeyEncryptedData = (PGPPublicKeyEncryptedData) encryptedData; long keyId = publicKeyEncryptedData.getKeyID(); - if (decryptionKeys != null) { + if (!options.getDecryptionKeys().isEmpty()) { // Known key id if (keyId != 0) { LOGGER.log(LEVEL, "PGPEncryptedData is encrypted for key " + Long.toHexString(keyId)); resultBuilder.addRecipientKeyId(keyId); - PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); - if (secretKey != null) { + PGPSecretKeyRing decryptionKeyRing = findDecryptionKeyRing(keyId); + if (decryptionKeyRing != null) { + PGPSecretKey secretKey = decryptionKeyRing.getSecretKey(keyId); LOGGER.log(LEVEL, "Found respective secret key " + Long.toHexString(keyId)); // Watch out! This assignment is possibly done multiple times. encryptedSessionKey = publicKeyEncryptedData; - decryptionKey = UnlockSecretKey.unlockSecretKey(secretKey, decryptionKeyDecryptor); + decryptionKey = UnlockSecretKey.unlockSecretKey(secretKey, options.getSecretKeyProtector(decryptionKeyRing)); resultBuilder.setDecryptionFingerprint(new OpenPgpV4Fingerprint(secretKey)); } - } else { - // Hidden recipient + } + + // Hidden recipient + else { LOGGER.log(LEVEL, "Hidden recipient detected. Try to decrypt with all available secret keys."); - outerloop: for (PGPSecretKeyRing ring : decryptionKeys) { - for (PGPSecretKey key : ring) { - PGPPrivateKey privateKey = key.extractPrivateKey(decryptionKeyDecryptor.getDecryptor(key.getKeyID())); + outerloop: for (PGPSecretKeyRing ring : options.getDecryptionKeys()) { + KeyRingInfo info = new KeyRingInfo(ring); + List encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.STORAGE_AND_COMMUNICATIONS); + for (PGPPublicKey pubkey : encryptionSubkeys) { + PGPSecretKey key = ring.getSecretKey(pubkey.getKeyID()); + if (key == null) { + continue; + } + + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(key, options.getSecretKeyProtector(ring).getDecryptor(key.getKeyID())); PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance().getPublicKeyDataDecryptorFactory(privateKey); try { publicKeyEncryptedData.getSymmetricAlgorithm(decryptorFactory); // will only succeed if we have the right secret key @@ -271,7 +270,11 @@ public final class DecryptionStreamFactory { } } } + return decryptWith(encryptedSessionKey, decryptionKey); + } + private InputStream decryptWith(PGPPublicKeyEncryptedData encryptedSessionKey, PGPPrivateKey decryptionKey) + throws PGPException { if (decryptionKey == null) { throw new MissingDecryptionMethodException("Decryption failed - No suitable decryption key or passphrase found"); } @@ -339,9 +342,18 @@ public final class DecryptionStreamFactory { verifiableOnePassSignatures.put(fingerprint, onePassSignature); } + private PGPSecretKeyRing findDecryptionKeyRing(long keyId) { + for (PGPSecretKeyRing key : options.getDecryptionKeys()) { + if (key.getSecretKey(keyId) != null) { + return key; + } + } + return null; + } + private PGPPublicKeyRing findSignatureVerificationKeyRing(long keyId) { PGPPublicKeyRing verificationKeyRing = null; - for (PGPPublicKeyRing publicKeyRing : verificationKeys) { + for (PGPPublicKeyRing publicKeyRing : options.getCertificates()) { PGPPublicKey verificationKey = publicKeyRing.getPublicKey(keyId); if (verificationKey != null) { LOGGER.log(LEVEL, "Found public key " + Long.toHexString(keyId) + " for signature verification"); @@ -350,8 +362,8 @@ public final class DecryptionStreamFactory { } } - if (verificationKeyRing == null && missingPublicKeyCallback != null) { - verificationKeyRing = missingPublicKeyCallback.onMissingPublicKeyEncountered(keyId); + if (verificationKeyRing == null && options.getMissingCertificateCallback() != null) { + verificationKeyRing = options.getMissingCertificateCallback().onMissingPublicKeyEncountered(keyId); } return verificationKeyRing; diff --git a/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/SymmetricEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/SymmetricEncryptionTest.java index 1dc95319..9c7e6f8b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/SymmetricEncryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/symmetric_encryption/SymmetricEncryptionTest.java @@ -109,7 +109,7 @@ public class SymmetricEncryptionTest { @ParameterizedTest @MethodSource("org.pgpainless.util.TestUtil#provideImplementationFactories") - public void testMissmatchPassphraseFails(ImplementationFactory implementationFactory) throws IOException, PGPException { + public void testMismatchPassphraseFails(ImplementationFactory implementationFactory) throws IOException, PGPException { ImplementationFactory.setFactoryImplementation(implementationFactory); byte[] bytes = new byte[5000];