From ce645fc4297396aecc12ff2ce1e4f40ca4170f89 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Sep 2021 16:33:03 +0200 Subject: [PATCH] Postpone decryption of PKESK if secret key passphrase is missing and try next PKESK first before passphrase retrieval using callback Fixes #186 --- .../ConsumerOptions.java | 2 +- .../DecryptionStreamFactory.java | 218 +++++++++++------ .../CachingSecretKeyRingProtector.java | 10 + .../PasswordBasedSecretKeyRingProtector.java | 15 ++ .../protection/SecretKeyRingProtector.java | 2 + .../protection/UnprotectedKeysProtector.java | 5 + .../MapBasedPassphraseProvider.java | 5 + .../SecretKeyPassphraseProvider.java | 2 + .../SolitaryPassphraseProvider.java | 5 + .../main/java/org/pgpainless/util/Tuple.java | 35 +++ ...tionUsingKeyWithMissingPassphraseTest.java | 221 ++++++++++++++++++ .../CachingSecretKeyRingProtectorTest.java | 5 + .../PassphraseProtectedKeyTest.java | 5 + .../SecretKeyRingProtectorTest.java | 5 + 14 files changed, 466 insertions(+), 69 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java 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 5e07e380..9ddecba0 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 @@ -257,7 +257,7 @@ public class ConsumerOptions { return missingCertificateCallback; } - public @Nullable SecretKeyRingProtector getSecretKeyProtector(PGPSecretKeyRing decryptionKeyRing) { + public @Nonnull SecretKeyRingProtector getSecretKeyProtector(PGPSecretKeyRing decryptionKeyRing) { return decryptionKeys.get(decryptionKeyRing); } 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 1a6c27a3..84f94819 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 @@ -61,6 +61,7 @@ import org.pgpainless.exception.WrongConsumingMethodException; 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.UnlockSecretKey; import org.pgpainless.signature.DetachedSignature; import org.pgpainless.signature.OnePassSignatureCheck; @@ -68,6 +69,7 @@ import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.CRCingArmoredInputStreamWrapper; import org.pgpainless.util.IntegrityProtectedInputStream; import org.pgpainless.util.Passphrase; +import org.pgpainless.util.Tuple; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -276,93 +278,173 @@ public final class DecryptionStreamFactory { PGPPrivateKey decryptionKey = null; PGPPublicKeyEncryptedData encryptedSessionKey = null; + + List passphraseProtected = new ArrayList<>(); + List publicKeyProtected = new ArrayList<>(); + List> postponedDueToMissingPassphrase = new ArrayList<>(); + + // Sort PKESK and SKESK packets while (encryptedDataIterator.hasNext()) { PGPEncryptedData encryptedData = encryptedDataIterator.next(); - - // TODO: Can we just skip non-integrity-protected packages? + // TODO: Maybe just skip non-integrity-protected packages? if (!encryptedData.isIntegrityProtected()) { throw new MessageNotIntegrityProtectedException(); } - // Data is passphrase encrypted + // SKESK if (encryptedData instanceof PGPPBEEncryptedData) { - PGPPBEEncryptedData pbeEncryptedData = (PGPPBEEncryptedData) encryptedData; - for (Passphrase passphrase : options.getDecryptionPassphrases()) { - PBEDataDecryptorFactory passphraseDecryptor = ImplementationFactory.getInstance() - .getPBEDataDecryptorFactory(passphrase); - try { - InputStream decryptedDataStream = pbeEncryptedData.getDataStream(passphraseDecryptor); - - SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.fromId( - pbeEncryptedData.getSymmetricAlgorithm(passphraseDecryptor)); - throwIfAlgorithmIsRejected(symmetricKeyAlgorithm); - resultBuilder.setSymmetricKeyAlgorithm(symmetricKeyAlgorithm); - - integrityProtectedEncryptedInputStream = new IntegrityProtectedInputStream(decryptedDataStream, pbeEncryptedData); - - return integrityProtectedEncryptedInputStream; - } catch (PGPException e) { - LOGGER.debug("Probable passphrase mismatch, skip PBE encrypted data block", e); - } - } + passphraseProtected.add((PGPPBEEncryptedData) encryptedData); } - - // data is public key encrypted + // PKESK else if (encryptedData instanceof PGPPublicKeyEncryptedData) { - PGPPublicKeyEncryptedData publicKeyEncryptedData = (PGPPublicKeyEncryptedData) encryptedData; - long keyId = publicKeyEncryptedData.getKeyID(); - if (!options.getDecryptionKeys().isEmpty()) { - // Known key id - if (keyId != 0) { - LOGGER.debug("PGPEncryptedData is encrypted for key {}", Long.toHexString(keyId)); - resultBuilder.addRecipientKeyId(keyId); - PGPSecretKeyRing decryptionKeyRing = findDecryptionKeyRing(keyId); - if (decryptionKeyRing != null) { - PGPSecretKey secretKey = decryptionKeyRing.getSecretKey(keyId); - LOGGER.debug("Found respective secret key {}", Long.toHexString(keyId)); - // Watch out! This assignment is possibly done multiple times. - encryptedSessionKey = publicKeyEncryptedData; - decryptionKey = UnlockSecretKey.unlockSecretKey(secretKey, options.getSecretKeyProtector(decryptionKeyRing)); - resultBuilder.setDecryptionKey(new SubkeyIdentifier(decryptionKeyRing, decryptionKey.getKeyID())); - } - } + publicKeyProtected.add((PGPPublicKeyEncryptedData) encryptedData); + } + } - // Hidden recipient - else { - LOGGER.debug("Hidden recipient detected. Try to decrypt with all available secret keys."); - 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; - } + // Try decryption with passphrases first + for (PGPPBEEncryptedData pbeEncryptedData : passphraseProtected) { + for (Passphrase passphrase : options.getDecryptionPassphrases()) { + PBEDataDecryptorFactory passphraseDecryptor = ImplementationFactory.getInstance() + .getPBEDataDecryptorFactory(passphrase); + try { + InputStream decryptedDataStream = pbeEncryptedData.getDataStream(passphraseDecryptor); - 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 - LOGGER.debug("Found correct key {} for hidden recipient decryption.", Long.toHexString(key.getKeyID())); - decryptionKey = privateKey; - resultBuilder.setDecryptionKey(new SubkeyIdentifier(ring, decryptionKey.getKeyID())); - encryptedSessionKey = publicKeyEncryptedData; - break outerloop; - } catch (PGPException | ClassCastException e) { - LOGGER.debug("Skipping wrong key {} for hidden recipient decryption.", Long.toHexString(key.getKeyID()), e); - } - } - } - } + SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.fromId( + pbeEncryptedData.getSymmetricAlgorithm(passphraseDecryptor)); + throwIfAlgorithmIsRejected(symmetricKeyAlgorithm); + resultBuilder.setSymmetricKeyAlgorithm(symmetricKeyAlgorithm); + + integrityProtectedEncryptedInputStream = new IntegrityProtectedInputStream(decryptedDataStream, pbeEncryptedData); + + return integrityProtectedEncryptedInputStream; + } catch (PGPException e) { + LOGGER.debug("Probable passphrase mismatch, skip PBE encrypted data block", e); } } } + + // Then try decryption with public key encryption + for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) { + PGPPrivateKey privateKey = null; + if (options.getDecryptionKeys().isEmpty()) { + break; + } + + long keyId = publicKeyEncryptedData.getKeyID(); + // Wildcard KeyID + if (keyId == 0L) { + LOGGER.debug("Hidden recipient detected. Try to decrypt with all available secret keys."); + for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) { + if (privateKey != null) { + break; + } + KeyRingInfo info = new KeyRingInfo(secretKeys); + List encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.STORAGE_AND_COMMUNICATIONS); + for (PGPPublicKey pubkey : encryptionSubkeys) { + PGPSecretKey secretKey = secretKeys.getSecretKey(pubkey.getKeyID()); + // Skip missing secret key + if (secretKey == null) { + continue; + } + + privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData, postponedDueToMissingPassphrase, true); + } + } + } + // Non-wildcard key-id + else { + LOGGER.debug("PGPEncryptedData is encrypted for key {}", Long.toHexString(keyId)); + resultBuilder.addRecipientKeyId(keyId); + + PGPSecretKeyRing secretKeys = findDecryptionKeyRing(keyId); + if (secretKeys == null) { + LOGGER.debug("Missing certificate of {}. Skip.", Long.toHexString(keyId)); + continue; + } + + PGPSecretKey secretKey = secretKeys.getSecretKey(keyId); + privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData, postponedDueToMissingPassphrase, true); + } + if (privateKey == null) { + continue; + } + decryptionKey = privateKey; + encryptedSessionKey = publicKeyEncryptedData; + } + + // Try postponed keys with missing passphrases (will cause missing passphrase callbacks to fire) + if (encryptedSessionKey == null) { + for (Tuple missingPassphrases : postponedDueToMissingPassphrase) { + SubkeyIdentifier keyId = missingPassphrases.getA(); + PGPPublicKeyEncryptedData publicKeyEncryptedData = missingPassphrases.getB(); + PGPSecretKeyRing secretKeys = findDecryptionKeyRing(keyId.getKeyId()); + PGPSecretKey secretKey = secretKeys.getSecretKey(keyId.getSubkeyId()); + + PGPPrivateKey privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData, postponedDueToMissingPassphrase, false); + if (privateKey == null) { + continue; + } + + decryptionKey = privateKey; + encryptedSessionKey = publicKeyEncryptedData; + break; + } + } + return decryptWith(encryptedSessionKey, decryptionKey); } + /** + * Try decryption of the provided public-key-encrypted-data using the given secret key. + * 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 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( + PGPSecretKeyRing secretKeys, + PGPSecretKey secretKey, + PGPPublicKeyEncryptedData publicKeyEncryptedData, + List> postponed, + boolean postponeIfMissingPassphrase) throws PGPException { + SecretKeyRingProtector protector = options.getSecretKeyProtector(secretKeys); + + if (postponeIfMissingPassphrase && !protector.hasPassphraseFor(secretKey.getKeyID())) { + // Postpone decryption with key with missing passphrase + SubkeyIdentifier identifier = new SubkeyIdentifier(secretKeys, secretKey.getKeyID()); + postponed.add(new Tuple<>(identifier, publicKeyEncryptedData)); + return null; + } + + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey( + secretKey, protector.getDecryptor(secretKey.getKeyID())); + + // test if we have the right private key + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + try { + publicKeyEncryptedData.getSymmetricAlgorithm(decryptorFactory); // will only succeed if we have the right secret key + LOGGER.debug("Found correct decryption key {}.", Long.toHexString(secretKey.getKeyID())); + resultBuilder.setDecryptionKey(new SubkeyIdentifier(secretKeys, privateKey.getKeyID())); + return privateKey; + } catch (PGPException | ClassCastException e) { + return null; + } + } + private InputStream decryptWith(PGPPublicKeyEncryptedData encryptedSessionKey, PGPPrivateKey decryptionKey) throws PGPException { - if (decryptionKey == null) { + if (decryptionKey == null || encryptedSessionKey == null) { throw new MissingDecryptionMethodException("Decryption failed - No suitable decryption key or passphrase found"); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/CachingSecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/CachingSecretKeyRingProtector.java index f1678230..e00a9f34 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/CachingSecretKeyRingProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/CachingSecretKeyRingProtector.java @@ -149,6 +149,16 @@ public class CachingSecretKeyRingProtector implements SecretKeyRingProtector, Se return passphrase; } + @Override + public boolean hasPassphrase(Long keyId) { + return cache.containsKey(keyId); + } + + @Override + public boolean hasPassphraseFor(Long keyId) { + return cache.containsKey(keyId); + } + @Override @Nullable public PBESecretKeyDecryptor getDecryptor(@Nonnull Long keyId) throws PGPException { diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java index e5678fb5..ace1739e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/PasswordBasedSecretKeyRingProtector.java @@ -65,6 +65,11 @@ public class PasswordBasedSecretKeyRingProtector implements SecretKeyRingProtect } return null; } + + @Override + public boolean hasPassphrase(Long keyId) { + return keyRing.getPublicKey(keyId) != null; + } }; return new PasswordBasedSecretKeyRingProtector(protectionSettings, passphraseProvider); } @@ -80,10 +85,20 @@ public class PasswordBasedSecretKeyRingProtector implements SecretKeyRingProtect } return null; } + + @Override + public boolean hasPassphrase(Long keyId) { + return keyId == key.getKeyID(); + } }; return new PasswordBasedSecretKeyRingProtector(protectionSettings, passphraseProvider); } + @Override + public boolean hasPassphraseFor(Long keyId) { + return passphraseProvider.hasPassphrase(keyId); + } + @Override @Nullable public PBESecretKeyDecryptor getDecryptor(Long keyId) throws PGPException { diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java index d3881ea0..d57ab6fb 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java @@ -38,6 +38,8 @@ import org.pgpainless.util.Passphrase; */ public interface SecretKeyRingProtector { + boolean hasPassphraseFor(Long keyId); + /** * Return a decryptor for the key of id {@code keyId}. * This method returns null if the key is unprotected. diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnprotectedKeysProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnprotectedKeysProtector.java index fdac82da..0092b473 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnprotectedKeysProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnprotectedKeysProtector.java @@ -25,6 +25,11 @@ import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; */ public class UnprotectedKeysProtector implements SecretKeyRingProtector { + @Override + public boolean hasPassphraseFor(Long keyId) { + return true; + } + @Override @Nullable public PBESecretKeyDecryptor getDecryptor(Long keyId) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java index 24ea569d..9bf7a1fa 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/MapBasedPassphraseProvider.java @@ -45,4 +45,9 @@ public class MapBasedPassphraseProvider implements SecretKeyPassphraseProvider { public Passphrase getPassphraseFor(Long keyId) { return map.get(keyId); } + + @Override + public boolean hasPassphrase(Long keyId) { + return map.containsKey(keyId); + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java index 342a4154..f2bf4fc8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SecretKeyPassphraseProvider.java @@ -46,4 +46,6 @@ public interface SecretKeyPassphraseProvider { * @return passphrase or null, if no passphrase record has been found. */ @Nullable Passphrase getPassphraseFor(Long keyId); + + boolean hasPassphrase(Long keyId); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.java index e251111f..c0983d15 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/passphrase_provider/SolitaryPassphraseProvider.java @@ -36,4 +36,9 @@ public class SolitaryPassphraseProvider implements SecretKeyPassphraseProvider { // always return the same passphrase. return passphrase; } + + @Override + public boolean hasPassphrase(Long keyId) { + return true; + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java b/pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java new file mode 100644 index 00000000..2f9407ea --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pgpainless.util; + +public class Tuple { + + private final A a; + private final B b; + + public Tuple(A a, B b) { + this.a = a; + this.b = b; + } + + public A getA() { + return a; + } + + public B getB() { + return b; + } +} 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 new file mode 100644 index 00000000..fbaff4ad --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PostponeDecryptionUsingKeyWithMissingPassphraseTest.java @@ -0,0 +1,221 @@ +/* + * Copyright 2021 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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 javax.annotation.Nullable; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +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.key.protection.CachingSecretKeyRingProtector; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider; +import org.pgpainless.util.Passphrase; + +public class PostponeDecryptionUsingKeyWithMissingPassphraseTest { + + private static PGPSecretKeyRing k1; + private static PGPSecretKeyRing k2; + private static final Passphrase p1 = Passphrase.fromPassword("P1"); + private static final Passphrase p2 = Passphrase.fromPassword("P2"); + + private static final String PLAINTEXT = "Hello, World!\n"; + + // message is encrypted for both k1 and k2. + // The first PKESK is for k1, the second for k2 + private static final String ENCRYPTED_FOR_K1_K2 = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4Dp8eMx2kPzEYSAQdAk0P2LL3pqZdq46eAGFkkESamDoPTn0EOLuPP+iA8lx8w\n" + + "RAMb6mUxPDVGqoXt05h2ps4BOTpy+Utsli0+BUzXTvtGM6RDTkaCuZvHQwPsggnN\n" + + "hF4DENqQkAsc7GgSAQdAJHwMR6+P5+HxwF8RqBEfrMCr0ZXWaLbekXf+FGTf/HYw\n" + + "+Et5NgaJazx0BdCf+D11Q4Vvem4Z9UEFL7x89B4mnv1dkJWRNwH6CkCNYVyIVrHi\n" + + "0kABB8V6DKCC1PNYlwCbSARz6X+xS9NsTFjGyROXajVEQ3x3ecLyKnyKxpcCJ2cb\n" + + "lfnLZ5ezQoyoukRkcdul1CWf\n" + + "=pvaA\n" + + "-----END PGP MESSAGE-----"; + + private static final String ENCRYPTED_FOR_K2_PASS_K1 = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4DENqQkAsc7GgSAQdAqtuQIjsRLypFfT8UykXqOv0dnrZWcrZBEiek4DNufmsw\n" + + "zRgbpnyKme7LaM+Lu0yJk9wUsvdpypB5GrKY9cD1Hg5nx4bGyujC6olowa/8o6Xe\n" + + "jC4ECQMCrjrXCpFawJtgj0y9PpciV6TpHJtI+lGbMed1+c5u3+U/HpRjLl3wBv9C\n" + + "hF4Dp8eMx2kPzEYSAQdA+Qrv5R4hOnOuVHDJpCCW72ONcdnzEhw45MxT/7mp3nQw\n" + + "8xs3dyVjMwmvqhbce9LIRdEM5YBWj3nBQM5ZQURAaQHPTTFuqCd8AgbeUz5FOFrA\n" + + "0kABJpvij5utFmhTVDqm3TrWOAmZ/eba0GMg0g/vFh7HoEGr1gRHLpc+vaIMs+fF\n" + + "uXVb1J9NX60PiBqxnM2iIBtD\n" + + "=p8Ye\n" + + "-----END PGP MESSAGE-----"; + private static final Passphrase PASSPHRASE = Passphrase.fromPassword("Wow!"); + + @BeforeAll + public static void prepareKeys() throws IOException { + k1 = PGPainless.readKeyRing().secretKeyRing("-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 9D0D 40DD 5B0A B5C7 9E73 E847 71BD 67EB 18A5 1034\n" + + "Comment: K1\n" + + "\n" + + "lIYEYUH25xYJKwYBBAHaRw8BAQdAfnFMBZxpsZiJ0yheGIzWEQixVWexv3oxBpUS\n" + + "kboJPIP+CQMCBjB6dP815OFgNjItEDhZVpGgZfHd9eqdNGj7RRiz7QN6Egk4kpGF\n" + + "Mqd0wZ8Ey4vmiYaeSP7QT+Wf9EccHOR4D8XD+y//Pu5aJx1X7gmVvLQCSzGIeAQT\n" + + "FgoAIAUCYUH25wIbAQUWAgMBAAQLCQgHBRUKCQgLAh4BAhkBAAoJEHG9Z+sYpRA0\n" + + "OqkA/il0Tw+95YKXK5oPgqoHTzR5zaRyjmZ3r8Pjp5S+gCZYAP0RMmleSMVxkf+o\n" + + "4FBKwE+Vv41GPYKBUQ2op/mCOyc3CpyLBGFB9ucSCisGAQQBl1UBBQEBB0C2DoDx\n" + + "UjTXA/vFikJr64fB8qcCMyBx5ODBwBG9woSvLAMBCAf+CQMCBjB6dP815OFg8JuS\n" + + "6Z6j+M+7X8QNhZtHohmGbbWntREzAAVlN+UEmLljpcKdZKqlPgGoacw2ta/928FR\n" + + "6GD7tjyAPzSRTqPo6+pwBjU4/4h1BBgWCgAdBQJhQfbnAhsMBRYCAwEABAsJCAcF\n" + + "FQoJCAsCHgEACgkQcb1n6xilEDReNgD+L+B9YfbIPGd4NnOvt+9qrrzmRPXbhTu7\n" + + "9Vw0VmW7YfcBANH+0tH7HYbL5NOzGI888E28V0VhHqhhvtlctI574qAInIYEYUH2\n" + + "5xYJKwYBBAHaRw8BAQdARWEfFJZIKcsMrb/A3/AwFgwTLqMmoK6XTuTUfuqxZCb+\n" + + "CQMCBjB6dP815OFgHXyV5OYmG3BDr8xnw8boGZEdZRQARrbmYLCBEblH8X7X/jJy\n" + + "/SdBnsKed/dVItHAENVBjkbFXx7V8z7jqmZAEDSFR7R6o4jVBBgWCgB9BQJhQfbn\n" + + "AhsCBRYCAwEABAsJCAcFFQoJCAsCHgFfIAQZFgoABgUCYUH25wAKCRD9CYoy8Jjb\n" + + "qYXCAP487XkaTSeqHiygM9x5jKJQzBTNoa8LP5kmhk/qiRMKWAEAnEOvrTRMS9OL\n" + + "qLPQDJ5Zl4fwjXDC4MDEstxkwEkUXQEACgkQcb1n6xilEDTK+QD6AwFVz+NguD9k\n" + + "MElK0o9VDLUWheP9tXE/sHcCVXKrm4kA/2TK8puF9FKpBb3pJhsvLfFuklVlXEBv\n" + + "/lv8PRbqIHsN\n" + + "=PETI\n" + + "-----END PGP PRIVATE KEY BLOCK-----"); + k2 = PGPainless.readKeyRing().secretKeyRing("-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 1458 6C84 3082 7226 5F4A 1EAC 15C9 772F 51A3 F48A\n" + + "Comment: K2\n" + + "\n" + + "lIYEYUH25xYJKwYBBAHaRw8BAQdAQHpL7nSEOpOdEVcmNxTsjJmqPYI7ObVGZqCi\n" + + "snlK8XP+CQMCySs/5txmbAtgB6fPvXfs7I0bYIEcGNZqSPMqVU04EjLyvmeP2EZL\n" + + "L5ezq3U4Z835xEILFN5ngBxajMEu1A0pksiabHTR28RspoBDph+4/bQCSzKIeAQT\n" + + "FgoAIAUCYUH25wIbAQUWAgMBAAQLCQgHBRUKCQgLAh4BAhkBAAoJEBXJdy9Ro/SK\n" + + "vyQBALXaK7xt/JbIE4jqhWbliIHm8bskX3WG+jME5XjfDjBGAQC8hcKiWbOAF1tK\n" + + "8KH2mzeHsh0yhybUvlq6wq7GZ3aZCZyLBGFB9ucSCisGAQQBl1UBBQEBB0CDCUwj\n" + + "XBrIL5xf7TDKNOCyXgepXp+Ca3q2q0qmWm1nYwMBCAf+CQMCySs/5txmbAtg0/tL\n" + + "Rw8WbfHVGS3u+aEuookij7swVMTspPY/s1W3Mt1TP85lM1Bkn5fDr4UP9prEQNc7\n" + + "/fWsqvc1b9ZRBBqmwPOsKDfd3oh1BBgWCgAdBQJhQfbnAhsMBRYCAwEABAsJCAcF\n" + + "FQoJCAsCHgEACgkQFcl3L1Gj9IoDBAD/YNTgbTvgM6UsqJ1DFiaihR1kV3nv2fuc\n" + + "EAJfu7guvbsA/0gnPBxywJd4cK7spoAZjyjdgN8RPcZcUo6vXbMnT4YHnIYEYUH2\n" + + "5xYJKwYBBAHaRw8BAQdApegYPw86Q19XMX1M5YykP51E27ZvwBIMc1bORa7xAFv+\n" + + "CQMCySs/5txmbAtgFrYkIkpujELJEpD1hJlFSZzIxiA193PXfdo9CbFHBkjwIBh7\n" + + "idT7l1gA+eHhiC0QyEPt3un3P4gj4UMeBPtCwqTUxo887IjVBBgWCgB9BQJhQfbn\n" + + "AhsCBRYCAwEABAsJCAcFFQoJCAsCHgFfIAQZFgoABgUCYUH25wAKCRBQkr/WCypZ\n" + + "Xp2zAP43zOQPtlbM1cabvP8kaWEsYG/x9ka4GtT/vkFh2cg3NAD/aSi13QhFHIVq\n" + + "FI+3tH0vnxFAWmU9u7JnvM2+3ULHDA0ACgkQFcl3L1Gj9IqeqQEAx/2y5PMGl7t4\n" + + "oHOJ4zhtqzTo33qjbu05eneS+zp4ElYA/RP/IGjIVz9wzraOKzBptB1BOaiqu3JG\n" + + "xnMR9GND5bgA\n" + + "=Sknt\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"); + } + + @Test + public void missingPassphraseFirst() throws PGPException, IOException { + SecretKeyRingProtector protector1 = new CachingSecretKeyRingProtector(new SecretKeyPassphraseProvider() { + @Nullable + @Override + public Passphrase getPassphraseFor(Long keyId) { + fail("Although the first PKESK is for k1, we should have skipped it and tried k2 first, which has passphrase available."); + return null; + } + + @Override + public boolean hasPassphrase(Long keyId) { + return false; + } + }); + SecretKeyRingProtector protector2 = SecretKeyRingProtector.unlockAllKeysWith(p2, k2); + + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(ENCRYPTED_FOR_K1_K2.getBytes(StandardCharsets.UTF_8))) + .withOptions(new ConsumerOptions() + .addDecryptionKey(k1, protector1) + .addDecryptionKey(k2, protector2)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + + assertEquals(PLAINTEXT, out.toString()); + } + + @Test + public void missingPassphraseSecond() throws PGPException, IOException { + SecretKeyRingProtector protector1 = SecretKeyRingProtector.unlockAllKeysWith(p1, k1); + SecretKeyRingProtector protector2 = new CachingSecretKeyRingProtector(new SecretKeyPassphraseProvider() { + @Nullable + @Override + public Passphrase getPassphraseFor(Long keyId) { + fail("This callback should not get called, since the first PKESK is for k1, which has a passphrase available."); + return null; + } + + @Override + public boolean hasPassphrase(Long keyId) { + return false; + } + }); + + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(ENCRYPTED_FOR_K1_K2.getBytes(StandardCharsets.UTF_8))) + .withOptions(new ConsumerOptions() + .addDecryptionKey(k1, protector1) + .addDecryptionKey(k2, protector2)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + + assertEquals(PLAINTEXT, out.toString()); + } + + @Test + public void messagePassphraseFirst() throws PGPException, IOException { + SecretKeyPassphraseProvider provider = new SecretKeyPassphraseProvider() { + @Nullable + @Override + public Passphrase getPassphraseFor(Long keyId) { + fail("Since we provide a decryption passphrase, we should not try to decrypt any key."); + return null; + } + + @Override + public boolean hasPassphrase(Long keyId) { + return false; + } + }; + SecretKeyRingProtector protector = new CachingSecretKeyRingProtector(provider); + + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(ENCRYPTED_FOR_K2_PASS_K1.getBytes(StandardCharsets.UTF_8))) + .withOptions(new ConsumerOptions() + .addDecryptionPassphrase(PASSPHRASE) + .addDecryptionKey(k1, protector) + .addDecryptionKey(k2, protector)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + + assertEquals(PLAINTEXT, out.toString()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/CachingSecretKeyRingProtectorTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/CachingSecretKeyRingProtectorTest.java index 598a4c0e..6ba16525 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/CachingSecretKeyRingProtectorTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/CachingSecretKeyRingProtectorTest.java @@ -46,6 +46,11 @@ public class CachingSecretKeyRingProtectorTest { long doubled = keyId * 2; return Passphrase.fromPassword(Long.toString(doubled)); } + + @Override + public boolean hasPassphrase(Long keyId) { + return true; + } }; private CachingSecretKeyRingProtector protector; diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java index 8dd23bf9..f86b0815 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/PassphraseProtectedKeyTest.java @@ -49,6 +49,11 @@ public class PassphraseProtectedKeyTest { return null; } } + + @Override + public boolean hasPassphrase(Long keyId) { + return keyId.equals(TestKeys.CRYPTIE_KEY_ID); + } }); @Test diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java index dbbb398d..f7010a3f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java @@ -121,6 +121,11 @@ public class SecretKeyRingProtectorTest { public Passphrase getPassphraseFor(Long keyId) { return Passphrase.fromPassword("missingP455w0rd"); } + + @Override + public boolean hasPassphrase(Long keyId) { + return true; + } }); assertEquals(Passphrase.emptyPassphrase(), protector.getPassphraseFor(1L));