From 676bbb54c8358e7a69e5d6eda53113452729332e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 28 Oct 2022 14:56:06 +0200 Subject: [PATCH] Fix CachingBcPublicKeyDataDecryptorFactory --- ...chingBcPublicKeyDataDecryptorFactory.java} | 44 ++++++--- ...ngBcPublicKeyDataDecryptorFactoryTest.java | 96 +++++++++++++++++++ 2 files changed, 126 insertions(+), 14 deletions(-) rename pgpainless-core/src/main/java/org/bouncycastle/{CachingPublicKeyDataDecryptorFactory.java => CachingBcPublicKeyDataDecryptorFactory.java} (52%) create mode 100644 pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java diff --git a/pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java similarity index 52% rename from pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java rename to pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java index 1498b6f2..3c967224 100644 --- a/pgpainless-core/src/main/java/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.java +++ b/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java @@ -5,9 +5,15 @@ package org.bouncycastle; import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.operator.PGPDataDecryptor; +import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; import org.bouncycastle.util.encoders.Base64; +import org.bouncycastle.util.encoders.Hex; +import org.pgpainless.decryption_verification.CustomPublicKeyDataDecryptorFactory; +import org.pgpainless.key.SubkeyIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; @@ -20,36 +26,46 @@ import java.util.Map; * cache hits. * If no hit is found, the method call is delegated to the underlying {@link PublicKeyDataDecryptorFactory}. * The result of that is then placed in the cache and returned. - * - * TODO: Do we also cache invalid session keys? */ -public class CachingPublicKeyDataDecryptorFactory implements PublicKeyDataDecryptorFactory { +public class CachingBcPublicKeyDataDecryptorFactory + extends BcPublicKeyDataDecryptorFactory + implements CustomPublicKeyDataDecryptorFactory { + + private static final Logger LOGGER = LoggerFactory.getLogger(CachingBcPublicKeyDataDecryptorFactory.class); private final Map cachedSessionKeys = new HashMap<>(); - private final PublicKeyDataDecryptorFactory factory; + private final SubkeyIdentifier decryptionKey; - public CachingPublicKeyDataDecryptorFactory(PublicKeyDataDecryptorFactory factory) { - this.factory = factory; + public CachingBcPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey, SubkeyIdentifier decryptionKey) { + super(privateKey); + this.decryptionKey = decryptionKey; } @Override public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { - byte[] sessionKey = lookup(secKeyData); + byte[] sessionKey = lookupSessionKeyData(secKeyData); if (sessionKey == null) { - sessionKey = factory.recoverSessionData(keyAlgorithm, secKeyData); - cache(secKeyData, sessionKey); + LOGGER.debug("Cache miss for encrypted session key " + Hex.toHexString(secKeyData[0])); + sessionKey = costlyRecoverSessionData(keyAlgorithm, secKeyData); + cacheSessionKeyData(secKeyData, sessionKey); + } else { + LOGGER.debug("Cache hit for encrypted session key " + Hex.toHexString(secKeyData[0])); } return sessionKey; } - private byte[] lookup(byte[][] secKeyData) { + public byte[] costlyRecoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { + return super.recoverSessionData(keyAlgorithm, secKeyData); + } + + private byte[] lookupSessionKeyData(byte[][] secKeyData) { byte[] sk = secKeyData[0]; String key = Base64.toBase64String(sk); byte[] sessionKey = cachedSessionKeys.get(key); return copy(sessionKey); } - private void cache(byte[][] secKeyData, byte[] sessionKey) { + private void cacheSessionKeyData(byte[][] secKeyData, byte[] sessionKey) { byte[] sk = secKeyData[0]; String key = Base64.toBase64String(sk); cachedSessionKeys.put(key, copy(sessionKey)); @@ -69,7 +85,7 @@ public class CachingPublicKeyDataDecryptorFactory implements PublicKeyDataDecryp } @Override - public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) throws PGPException { - return null; + public SubkeyIdentifier getSubkeyIdentifier() { + return decryptionKey; } } diff --git a/pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java new file mode 100644 index 00000000..186bcbc2 --- /dev/null +++ b/pgpainless-core/src/test/java/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package bouncycastle; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.CachingBcPublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnlockSecretKey; + +public class CachingBcPublicKeyDataDecryptorFactoryTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: C8AE 4279 5958 5F46 86A9 8B5F EC69 7C29 2BE4 44E0\n" + + "Comment: Alice\n" + + "\n" + + "lFgEY1vEcxYJKwYBBAHaRw8BAQdAXOUK1uc1iBeM+mMt2nLCukXWoJd/SodrtN9S\n" + + "U/zzwu0AAP9eePPw91KLuq6PF9jQoTRz/cW4CyiALNJpsOJIZ1rp3xOBtAVBbGlj\n" + + "ZYiPBBMWCgBBBQJjW8RzCRDsaXwpK+RE4BYhBMiuQnlZWF9GhqmLX+xpfCkr5ETg\n" + + "Ap4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAGqWAQC8oz7l8izjUis5ji+sgI+q\n" + + "gML22VNybqmLBpzZwnNU5wEApe9fNTRbK5yAITGBscxH7o74Qe+CLI6Ni5MwzKxr\n" + + "5AucXQRjW8RzEgorBgEEAZdVAQUBAQdAm8xk0QSvpp2ZU1KQ31E7eEZYLKpbW4JE\n" + + "opmtMQx6AlIDAQgHAAD/XTb/qSosfkNvli3BQiUzVRAqKaU4PKAq7at6afxoYSgN\n" + + "4Yh1BBgWCgAdBQJjW8RzAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQ7Gl8KSvk\n" + + "ROB38QEA0MvDt0bjEXwFoM0E34z0MtPcG3VBYcQ+iFRIqFfEl5UA/2yZxFjoZqrs\n" + + "AQE8TaVpXYfbc2p/GEKA9LGd9l/g0QQLnFgEY1vEcxYJKwYBBAHaRw8BAQdAyCOv\n" + + "6hGUvHcCBSDKP3fRz+scyJ9zwMt7nFXK5A/k2YgAAQCn3Es+IhvePn3eBlcYMMr0\n" + + "xcktrY1NJAIZPfjlUJ0J1g6LiNUEGBYKAH0FAmNbxHMCngECmwIFFgIDAQAECwkI\n" + + "BwUVCgkIC18gBBkWCgAGBQJjW8RzAAoJECxLf7KoUc8wD18BANNpIr4E+RRVVztR\n" + + "OVwdxSe0SRWGjkW8nHrRyghHKTuMAP9p4ZKicOYA1uZbiNNjyuJuS8xBH6Hihurb\n" + + "gDypVgxdBQAKCRDsaXwpK+RE4EQjAP9ARZEPxKNLFkrvjoZ8nrts3qhv3VtMrU+9\n" + + "huZnYLe1FQEAtgO6V7wutHvVARHXqPJ6lcv+SueIu+BjLFYEKuBwggs=\n" + + "=ShJd\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + private static final String MSG = "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4DJmQMTBqw3G8SAQdALkHpO0UkS/CqkwxUz74MJU3PV72ZrIL8ZcrO8ofhblkw\n" + + "iDIhSwwGTG3tj+sG+ZVWKsmONKi7Om5seJDHQtQ8MfdCELAgwYHSt6MrgDBhuDIH\n" + + "0kABZhq2/8qk3EGXPpc+xxs4r4g8SgHOiiHSim5NGtounXXIaF6T/hUmlorkeYf/\n" + + "a9pCC0QXRUAr8NOcdsfbvb5V\n" + + "=dQa8\n" + + "-----END PGP MESSAGE-----"; + + @Test + public void test() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + SubkeyIdentifier decryptionKey = new SubkeyIdentifier(secretKeys, + info.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0).getKeyID()); + + PGPSecretKey secretKey = secretKeys.getSecretKey(decryptionKey.getSubkeyId()); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); + CachingBcPublicKeyDataDecryptorFactory cachingFactory = new CachingBcPublicKeyDataDecryptorFactory( + privateKey, decryptionKey); + + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(MSG.getBytes()); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get() + .addCustomDecryptorFactory(cachingFactory)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + + decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get() + .addCustomDecryptorFactory(cachingFactory)); + out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + } +}