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 index f4bc7134..9c8418e0 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java @@ -24,6 +24,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.pgpainless.exception.NotYetImplementedException; import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.SingleSecretKeyRingProtector; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.Passphrase; @@ -195,15 +196,22 @@ public class ConsumerOptions { } /** - * Add a key for message decryption. If the key is encrypted, the {@link SecretKeyRingProtector} is used to decrypt it + * Add a key ring for message decryption. If the key ring is encrypted, the {@link SecretKeyRingProtector} is used to decrypt it * when needed. * - * @param key key + * @param keyRing key ring * @param keyRingProtector protector for the secret key * @return options */ - public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing key, @Nonnull SecretKeyRingProtector keyRingProtector) { - decryptionKeys.put(key, keyRingProtector); + public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing keyRing, @Nonnull SecretKeyRingProtector keyRingProtector) { + decryptionKeys.forEach((key, value) -> { + if (value instanceof SingleSecretKeyRingProtector && value != keyRingProtector){ + throw new IllegalStateException("SingleSecretKeyRingProtector is found. " + + "Please specify the same SecretKeyRingProtector for all keys."); + } + }); + + decryptionKeys.put(keyRing, keyRingProtector); return this; } 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 6e9627de..c25cfb34 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 @@ -50,10 +50,12 @@ import org.pgpainless.exception.MissingLiteralDataException; import org.pgpainless.exception.SignatureValidationException; import org.pgpainless.exception.UnacceptableAlgorithmException; import org.pgpainless.exception.WrongConsumingMethodException; +import org.pgpainless.exception.WrongPassphraseException; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.SingleSecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.signature.DetachedSignatureCheck; import org.pgpainless.signature.OnePassSignatureCheck; @@ -262,7 +264,7 @@ public final class DecryptionStreamFactory { private PGPSignatureList parseSignatures(PGPObjectFactory objectFactory) throws IOException { PGPSignatureList signatureList = null; Object pgpObject = objectFactory.nextObject(); - while (pgpObject != null && signatureList == null) { + while (pgpObject != null && signatureList == null) { if (pgpObject instanceof PGPSignatureList) { signatureList = (PGPSignatureList) pgpObject; } else { @@ -397,6 +399,14 @@ public final class DecryptionStreamFactory { encryptedSessionKey = publicKeyEncryptedData; break; } + + if (!options.getDecryptionKeys().isEmpty()) { + SecretKeyRingProtector firstPeeked = options.getSecretKeyProtector(options.getDecryptionKeys().iterator().next()); + + if (decryptionKey == null && firstPeeked instanceof SingleSecretKeyRingProtector) { + throw new WrongPassphraseException(((SingleSecretKeyRingProtector) firstPeeked).getFailedKeyIds()); + } + } } return decryptWith(encryptedSessionKey, decryptionKey); @@ -407,16 +417,15 @@ public final class DecryptionStreamFactory { * If the secret key is encrypted and the secret key protector does not have a passphrase available and the boolean * postponeIfMissingPassphrase is true, data decryption is postponed by pushing a tuple of the encrypted data decryption key * identifier to the postponed list. - * + *

* This method only returns a non-null private key, if the private key is able to decrypt the message successfully. * - * @param secretKeys secret key ring - * @param secretKey secret key - * @param publicKeyEncryptedData encrypted data which is tried to decrypt using the secret key - * @param postponed list of postponed decryptions due to missing secret key passphrases + * @param secretKeys secret key ring + * @param secretKey secret key + * @param publicKeyEncryptedData encrypted data which is tried to decrypt using the secret key + * @param postponed list of postponed decryptions due to missing secret key passphrases * @param postponeIfMissingPassphrase flag to specify whether missing secret key passphrases should result in postponed decryption * @return private key if decryption is successful, null if decryption is unsuccessful or postponed - * * @throws PGPException in case of an OpenPGP error */ private PGPPrivateKey tryPublicKeyDecryption( @@ -434,8 +443,16 @@ public final class DecryptionStreamFactory { return null; } - PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey( - secretKey, protector.getDecryptor(secretKey.getKeyID())); + PGPPrivateKey privateKey; + try { + privateKey = UnlockSecretKey.unlockSecretKey( + secretKey, protector.getDecryptor(secretKey.getKeyID())); + } catch (Exception e) { + if (protector instanceof SingleSecretKeyRingProtector) { + ((SingleSecretKeyRingProtector) protector).addFailedKeyId(secretKey.getKeyID(), e); + return null; + } else throw e; + } // test if we have the right private key PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/WrongPassphraseException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/WrongPassphraseException.java index 409db3e2..b3d2af97 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/WrongPassphraseException.java +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/WrongPassphraseException.java @@ -4,9 +4,15 @@ package org.pgpainless.exception; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + import org.bouncycastle.openpgp.PGPException; public class WrongPassphraseException extends PGPException { + private Map keyIds = Collections.emptyMap(); public WrongPassphraseException(String message) { super(message); @@ -14,9 +20,21 @@ public class WrongPassphraseException extends PGPException { public WrongPassphraseException(long keyId, PGPException cause) { this("Wrong passphrase provided for key " + Long.toHexString(keyId), cause); + this.keyIds = new HashMap<>(); + this.keyIds.put(keyId, cause); + } + + public WrongPassphraseException(Map keyIds) { + this("Wrong passphrase provided for keys: " + + keyIds.keySet().stream().map(Long::toHexString).collect(Collectors.joining(", "))); + this.keyIds = keyIds; } public WrongPassphraseException(String message, PGPException cause) { super(message, cause); } + + public Map getKeyIds() { + return keyIds; + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SingleSecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SingleSecretKeyRingProtector.java new file mode 100644 index 00000000..6a1d0379 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SingleSecretKeyRingProtector.java @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2018 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 +package org.pgpainless.key.protection; + +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nonnull; + +/** + * This interface assumes that all handled keys will use a single + * realization of {@link SecretKeyRingProtector}. + */ +public interface SingleSecretKeyRingProtector { + Map failedKeyIds = new HashMap<>(); + + /** + * Return a map that contains a key id of each key that was not unlocked. + * + * @return a map of key ids. + */ + @Nonnull + default Map getFailedKeyIds() { + return failedKeyIds; + } + + /** + * Add a key id of some key that was not unlocked due to {@code e} + * + * @param keyId the key id + * @param e an instance of {@link Exception} + */ + default void addFailedKeyId(long keyId, Exception e) { + failedKeyIds.put(keyId, e); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java index 4aad6c9e..ce4f21b2 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java @@ -5,22 +5,27 @@ package org.pgpainless.decryption_verification; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import javax.annotation.Nullable; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; +import org.pgpainless.exception.WrongPassphraseException; import org.pgpainless.key.protection.CachingSecretKeyRingProtector; import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.SingleSecretKeyRingProtector; import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider; import org.pgpainless.util.Passphrase; @@ -177,6 +182,21 @@ public class PostponeDecryptionUsingKeyWithMissingPassphraseTest { assertEquals(PLAINTEXT, out.toString()); } + @Test + public void missingPassphraseFirstAndSecond() throws PGPException, IOException { + PGPSecretKeyRingCollection keyRings = new PGPSecretKeyRingCollection(Arrays.asList(k1, k2)); + assertThrows(WrongPassphraseException.class, () -> { + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(ENCRYPTED_FOR_K1_K2.getBytes(StandardCharsets.UTF_8))) + .withOptions(new ConsumerOptions() + .addDecryptionKeys(keyRings, new SingleCachingSecretKeyRingProtector())); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + }, "Wrong passphrase provided for keys: a7c78cc7690fcc46, 10da90900b1cec68"); + } + @Test public void messagePassphraseFirst() throws PGPException, IOException { SecretKeyPassphraseProvider provider = new SecretKeyPassphraseProvider() { @@ -207,4 +227,8 @@ public class PostponeDecryptionUsingKeyWithMissingPassphraseTest { assertEquals(PLAINTEXT, out.toString()); } + + private static class SingleCachingSecretKeyRingProtector extends CachingSecretKeyRingProtector + implements SingleSecretKeyRingProtector { + } }