Begin introducing new Decryption API

This commit is contained in:
Paul Schaub 2021-06-15 17:08:40 +02:00
parent 8fffa3079a
commit 70c4dcd1d2
Signed by: vanitasvitae
GPG Key ID: 62BEE9264BF17311
5 changed files with 293 additions and 63 deletions

View File

@ -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<PGPPublicKeyRing> certificates = new HashSet<>();
private Set<PGPSignature> detachedSignatures = new HashSet<>();
private MissingPublicKeyCallback missingCertificateCallback = null;
// Session key for decryption without passphrase/key
private byte[] sessionKey = null;
private final Map<PGPSecretKeyRing, SecretKeyRingProtector> decryptionKeys = new HashMap<>();
private final Set<Passphrase> 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 <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-2.1">RFC4880 on Session Keys</a>
*
* @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<PGPSecretKeyRing> getDecryptionKeys() {
return Collections.unmodifiableSet(decryptionKeys.keySet());
}
public Set<Passphrase> getDecryptionPassphrases() {
return Collections.unmodifiableSet(decryptionPassphrases);
}
public Set<PGPPublicKeyRing> getCertificates() {
return Collections.unmodifiableSet(certificates);
}
public MissingPublicKeyCallback getMissingCertificateCallback() {
return missingCertificateCallback;
}
public SecretKeyRingProtector getSecretKeyProtector(PGPSecretKeyRing decryptionKeyRing) {
return decryptionKeys.get(decryptionKeyRing);
}
public Set<PGPSignature> getDetachedSignatures() {
return Collections.unmodifiableSet(detachedSignatures);
}
}

View File

@ -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.<PGPSecretKeyRing>emptyList())) {
options.addDecryptionKey(decryptionKey, decryptionKeyDecryptor);
}
for (PGPPublicKeyRing certificate : (verificationKeys != null ? verificationKeys : Collections.<PGPPublicKeyRing>emptyList())) {
options.addVerificationCert(certificate);
}
for (PGPSignature detachedSignature : (detachedSignatures != null ? detachedSignatures : Collections.<PGPSignature>emptyList())) {
options.addVerificationOfDetachedSignature(detachedSignature);
}
options.setMissingCertificateCallback(missingPublicKeyCallback);
if (decryptionPassphrase != null) {
options.addDecryptionPassphrase(decryptionPassphrase);
}
return DecryptionStreamFactory.create(inputStream, options);
}
}
}

View File

@ -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.

View File

@ -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<PGPPublicKeyRing> 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<OpenPgpV4Fingerprint, OnePassSignature> verifiableOnePassSignatures = new HashMap<>();
private final List<IntegrityProtectedInputStream> integrityProtectedStreams = new ArrayList<>();
private DecryptionStreamFactory(@Nullable PGPSecretKeyRingCollection decryptionKeys,
@Nullable SecretKeyRingProtector decryptor,
@Nullable Passphrase decryptionPassphrase,
@Nullable Set<PGPPublicKeyRing> 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<PGPSignature> detachedSignatures,
@Nullable Set<PGPPublicKeyRing> 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<PGPPublicKey> 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;

View File

@ -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];