From 63bf5a8e69bb2252ca7b7c59e4265f2786f5205f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 9 Jan 2021 16:16:17 +0100 Subject: [PATCH] Add support for decryption with hidden recipients --- .../DecryptionStreamFactory.java | 44 +++++++++---- .../DecryptHiddenRecipientMessage.java | 64 +++++++++++++++++++ 2 files changed, 97 insertions(+), 11 deletions(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessage.java 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 92b0527c..4fbdea3c 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 @@ -33,6 +33,7 @@ import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPEncryptedDataList; import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyValidationException; import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPOnePassSignature; @@ -43,6 +44,7 @@ import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPUtil; @@ -208,18 +210,38 @@ public final class DecryptionStreamFactory { } else if (encryptedData instanceof PGPPublicKeyEncryptedData) { PGPPublicKeyEncryptedData publicKeyEncryptedData = (PGPPublicKeyEncryptedData) encryptedData; long keyId = publicKeyEncryptedData.getKeyID(); - - resultBuilder.addRecipientKeyId(keyId); - LOGGER.log(LEVEL, "PGPEncryptedData is encrypted for key " + Long.toHexString(keyId)); - if (decryptionKeys != null) { - PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); - if (secretKey != null) { - LOGGER.log(LEVEL, "Found respective secret key " + Long.toHexString(keyId)); - // Watch out! This assignment is possibly done multiple times. - encryptedSessionKey = publicKeyEncryptedData; - decryptionKey = secretKey.extractPrivateKey(decryptionKeyDecryptor.getDecryptor(keyId)); - resultBuilder.setDecryptionFingerprint(new OpenPgpV4Fingerprint(secretKey)); + // 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) { + LOGGER.log(LEVEL, "Found respective secret key " + Long.toHexString(keyId)); + // Watch out! This assignment is possibly done multiple times. + encryptedSessionKey = publicKeyEncryptedData; + decryptionKey = secretKey.extractPrivateKey(decryptionKeyDecryptor.getDecryptor(keyId)); + resultBuilder.setDecryptionFingerprint(new OpenPgpV4Fingerprint(secretKey)); + } + } else { + // Hidden recipient + 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())); + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance().getPublicKeyDataDecryptorFactory(privateKey); + try { + publicKeyEncryptedData.getSymmetricAlgorithm(decryptorFactory); // will only succeed if we have the right secret key + LOGGER.log(LEVEL, "Found correct key " + Long.toHexString(key.getKeyID()) + " for hidden recipient decryption."); + decryptionKey = privateKey; + resultBuilder.setDecryptionFingerprint(new OpenPgpV4Fingerprint(key)); + encryptedSessionKey = publicKeyEncryptedData; + break outerloop; + } catch (ClassCastException | PGPKeyValidationException e) { + LOGGER.log(LEVEL, "Skipping wrong key " + Long.toHexString(key.getKeyID()) + " for hidden recipient decryption.", e); + } + } + } } } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessage.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessage.java new file mode 100644 index 00000000..b103c2d8 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/DecryptHiddenRecipientMessage.java @@ -0,0 +1,64 @@ +/* + * 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 org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.TestKeys; +import org.pgpainless.key.protection.UnprotectedKeysProtector; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class DecryptHiddenRecipientMessage { + + private static final String PLAINTEXT = "Hello WOrld"; + + /** + * Message was created using + * gpg --no-default-keyring --keyring ~/.pgpainless --no-default-recipient --armor --output ~/message.asc --encrypt --hidden-recipient emil@email.user ~/message.txt + * where ~/.pgpainless was a gnupg keyring containing all the test keys and message.asc contained the plaintext message. + */ + private static final String CHIPERHTEXT = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "hG4DAAAAAAAAAAASAgMEUif9JYa7qZedAFs0AUUJ3cxdc7tpsREZxhzzFpgWzKbH\n" + + "YB1i7rp3PM9zO88v3GRQY6TH5tR8CJkHRTWGu03c0SBTUbrzWngh0i2uR9DJnjLk\n" + + "B8HD4fgn9VP+Qj5Igg6KJdJSAVZlFqVh0H6PCEUaSre/KlyVVqikZKTm0hnWb6XZ\n" + + "/TrefNuUR6PDNyh/oMbfAQD46UZjfRrbNUdKjOyfwtBYkkcGKTF+HilQbjk1nKud\n" + + "pojSOQ==\n" + + "=nyvJ\n" + + "-----END PGP MESSAGE-----"; + + @Test + public void testDecryptionWithHiddenRecipient() throws IOException, PGPException { + PGPSecretKeyRingCollection emilSecret = TestKeys.getEmilSecretKeyRingCollection(); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify().onInputStream(new ByteArrayInputStream(CHIPERHTEXT.getBytes(StandardCharsets.UTF_8))) + .decryptWith(new UnprotectedKeysProtector(), emilSecret) + .doNotVerify() + .build(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + + OpenPgpMetadata metadata = decryptionStream.getResult(); + } +}