diff --git a/CHANGELOG.md b/CHANGELOG.md index 89adc4a7..7817f8a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,35 @@ SPDX-License-Identifier: CC0-1.0 # PGPainless Changelog +## 1.4.0-rc1 +- Reimplement message consumption via new `OpenPgpMessageInputStream` + - Fix validation of prepended signatures (#314) + - Fix validation of nested signatures (#319) + - Reject malformed messages (#237) + - Utilize new `PDA` syntax verifier class + - Allow for custom message syntax via `Syntax` class + - Gracefully handle `UnsupportedPacketVersionException` for signatures + - Allow plugin decryption code (e.g. to add support for hardware-backed keys (see #318)) + - Add `HardwareSecurity` utility class + - Add `GnuPGDummyKeyUtil` which can be used to mimic GnuPGs proprietary S2K extensions + for keys which were placed on hardware tokens + - Add `OpenPgpPacket` enum class to enumerate available packet tags + - Remove old decryption classes in favor of new implementation + - Removed `DecryptionStream` class and replaced with new abstract class + - Removed `DecryptionStreamFactory` + - Removed `FinalIOException` + - Removed `MissingLiteralDataException` (replaced by `MalformedOpenPgpMessageException`) + - Introduce `MessageMetadata` class as potential future replacement for `OpenPgpMetadata`. + - can be obtained via `((OpenPgpMessageInputStream) decryptionStream).getMetadata();` +- Add `CachingBcPublicKeyDataDecryptorFactory` which can be extended to prevent costly decryption + of session keys +- Fix: Only verify message integrity once +- Remove unnecessary `@throws` declarations on `KeyRingReader` methods +- Remove unnecessary `@throws` declarations on `KeyRingUtils` methods +- Add `KeyIdUtil.formatKeyId(long id)` to format hexadecimal key-ids. +- Add `KeyRingUtils.publicKeys(PGPKeyRing keys)` +- Remove `BCUtil` class + ## 1.3.8 - Bump `bcprov` to `1.72` - Bump `bcpg` to `1.72.1` diff --git a/README.md b/README.md index 305e5cea..1855f742 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ repositories { } dependencies { - implementation 'org.pgpainless:pgpainless-core:1.3.8' + implementation 'org.pgpainless:pgpainless-core:1.4.0-rc1' } ``` diff --git a/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java new file mode 100644 index 00000000..3c967224 --- /dev/null +++ b/pgpainless-core/src/main/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.java @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.bouncycastle; + +import org.bouncycastle.openpgp.PGPException; +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; + +/** + * Implementation of the {@link PublicKeyDataDecryptorFactory} which caches decrypted session keys. + * That way, if a message needs to be decrypted multiple times, expensive private key operations can be omitted. + * + * This implementation changes the behavior or {@link #recoverSessionData(int, byte[][])} to first return any + * 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. + */ +public class CachingBcPublicKeyDataDecryptorFactory + extends BcPublicKeyDataDecryptorFactory + implements CustomPublicKeyDataDecryptorFactory { + + private static final Logger LOGGER = LoggerFactory.getLogger(CachingBcPublicKeyDataDecryptorFactory.class); + + private final Map cachedSessionKeys = new HashMap<>(); + private final SubkeyIdentifier decryptionKey; + + public CachingBcPublicKeyDataDecryptorFactory(PGPPrivateKey privateKey, SubkeyIdentifier decryptionKey) { + super(privateKey); + this.decryptionKey = decryptionKey; + } + + @Override + public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) throws PGPException { + byte[] sessionKey = lookupSessionKeyData(secKeyData); + if (sessionKey == null) { + 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; + } + + 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 cacheSessionKeyData(byte[][] secKeyData, byte[] sessionKey) { + byte[] sk = secKeyData[0]; + String key = Base64.toBase64String(sk); + cachedSessionKeys.put(key, copy(sessionKey)); + } + + private static byte[] copy(byte[] bytes) { + if (bytes == null) { + return null; + } + byte[] copy = new byte[bytes.length]; + System.arraycopy(bytes, 0, copy, 0, copy.length); + return copy; + } + + public void clear() { + cachedSessionKeys.clear(); + } + + @Override + public SubkeyIdentifier getSubkeyIdentifier() { + return decryptionKey; + } +} diff --git a/pgpainless-core/src/main/java/org/bouncycastle/package-info.java b/pgpainless-core/src/main/java/org/bouncycastle/package-info.java new file mode 100644 index 00000000..565bb5f4 --- /dev/null +++ b/pgpainless-core/src/main/java/org/bouncycastle/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Classes which could be upstreamed to BC at some point. + */ +package org.bouncycastle; diff --git a/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java new file mode 100644 index 00000000..d744e222 --- /dev/null +++ b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.gnupg; + +import org.bouncycastle.bcpg.S2K; + +public enum GnuPGDummyExtension { + + /** + * Do not store the secret part at all. + */ + NO_PRIVATE_KEY(S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY), + + /** + * A stub to access smartcards. + */ + DIVERT_TO_CARD(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD), + ; + + private final int id; + + GnuPGDummyExtension(int id) { + this.id = id; + } + + public int getId() { + return id; + } +} diff --git a/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java new file mode 100644 index 00000000..983d8e68 --- /dev/null +++ b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java @@ -0,0 +1,209 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.gnupg; + +import org.bouncycastle.bcpg.PublicKeyPacket; +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.bcpg.SecretKeyPacket; +import org.bouncycastle.bcpg.SecretSubkeyPacket; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.pgpainless.key.SubkeyIdentifier; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * This class can be used to remove private keys from secret software-keys by replacing them with + * stub secret keys in the style of GnuPGs proprietary extensions. + * + * @see + * GnuPGs doc/DETAILS - GNU extensions to the S2K algorithm + */ +public final class GnuPGDummyKeyUtil { + + private GnuPGDummyKeyUtil() { + + } + + /** + * Return the key-ids of all keys which appear to be stored on a hardware token / smartcard by GnuPG. + * Note, that this functionality is based on GnuPGs proprietary S2K extensions, which are not strictly required + * for dealing with hardware-backed keys. + * + * @param secretKeys secret keys + * @return set of keys with S2K type GNU_DUMMY_S2K and protection mode DIVERT_TO_CARD + */ + public static Set getIdsOfKeysWithGnuPGS2KDivertedToCard(PGPSecretKeyRing secretKeys) { + Set hardwareBackedKeys = new HashSet<>(); + for (PGPSecretKey secretKey : secretKeys) { + S2K s2K = secretKey.getS2K(); + if (s2K == null) { + continue; + } + + int type = s2K.getType(); + int mode = s2K.getProtectionMode(); + // TODO: Is GNU_DUMMY_S2K appropriate? + if (type == S2K.GNU_DUMMY_S2K && mode == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) { + SubkeyIdentifier hardwareBackedKey = new SubkeyIdentifier(secretKeys, secretKey.getKeyID()); + hardwareBackedKeys.add(hardwareBackedKey); + } + } + return hardwareBackedKeys; + } + + /** + * Modify the given {@link PGPSecretKeyRing}. + * + * @param secretKeys secret keys + * @return builder + */ + public static Builder modify(PGPSecretKeyRing secretKeys) { + return new Builder(secretKeys); + } + + public static final class Builder { + + private final PGPSecretKeyRing keys; + + private Builder(PGPSecretKeyRing keys) { + this.keys = keys; + } + + /** + * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with + * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#NO_PRIVATE_KEY}. + * + * @param filter filter to select keys for removal + * @return modified key ring + */ + public PGPSecretKeyRing removePrivateKeys(KeyFilter filter) { + return replacePrivateKeys(GnuPGDummyExtension.NO_PRIVATE_KEY, null, filter); + } + + /** + * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with + * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}. + * This method will set the serial number of the card to 0x00000000000000000000000000000000. + * + * NOTE: This method does not actually move any keys to a card. + * + * @param filter filter to select keys for removal + * @return modified key ring + */ + public PGPSecretKeyRing divertPrivateKeysToCard(KeyFilter filter) { + return divertPrivateKeysToCard(filter, new byte[16]); + } + + /** + * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with + * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}. + * This method will include the card serial number into the encoded dummy key. + * + * NOTE: This method does not actually move any keys to a card. + * + * @param filter filter to select keys for removal + * @param cardSerialNumber serial number of the card (at most 16 bytes long) + * @return modified key ring + */ + public PGPSecretKeyRing divertPrivateKeysToCard(KeyFilter filter, byte[] cardSerialNumber) { + if (cardSerialNumber != null && cardSerialNumber.length > 16) { + throw new IllegalArgumentException("Card serial number length cannot exceed 16 bytes."); + } + return replacePrivateKeys(GnuPGDummyExtension.DIVERT_TO_CARD, cardSerialNumber, filter); + } + + private PGPSecretKeyRing replacePrivateKeys(GnuPGDummyExtension extension, byte[] serial, KeyFilter filter) { + byte[] encodedSerial = serial != null ? encodeSerial(serial) : null; + S2K s2k = extensionToS2K(extension); + + List secretKeyList = new ArrayList<>(); + for (PGPSecretKey secretKey : keys) { + if (!filter.filter(secretKey.getKeyID())) { + // No conversion, do not modify subkey + secretKeyList.add(secretKey); + continue; + } + + PublicKeyPacket publicKeyPacket = secretKey.getPublicKey().getPublicKeyPacket(); + if (secretKey.isMasterKey()) { + SecretKeyPacket keyPacket = new SecretKeyPacket(publicKeyPacket, + 0, SecretKeyPacket.USAGE_SHA1, s2k, null, encodedSerial); + PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey()); + secretKeyList.add(onCard); + } else { + SecretSubkeyPacket keyPacket = new SecretSubkeyPacket(publicKeyPacket, + 0, SecretKeyPacket.USAGE_SHA1, s2k, null, encodedSerial); + PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey()); + secretKeyList.add(onCard); + } + } + + return new PGPSecretKeyRing(secretKeyList); + } + + private byte[] encodeSerial(@Nonnull byte[] serial) { + byte[] encoded = new byte[serial.length + 1]; + encoded[0] = (byte) (serial.length & 0xff); + System.arraycopy(serial, 0, encoded, 1, serial.length); + return encoded; + } + + private S2K extensionToS2K(@Nonnull GnuPGDummyExtension extension) { + return S2K.gnuDummyS2K(extension == GnuPGDummyExtension.DIVERT_TO_CARD ? + S2K.GNUDummyParams.divertToCard() : S2K.GNUDummyParams.noPrivateKey()); + } + } + + /** + * Filter for selecting keys. + */ + @FunctionalInterface + public interface KeyFilter { + + /** + * Return true, if the given key should be selected, false otherwise. + * + * @param keyId id of the key + * @return select + */ + boolean filter(long keyId); + + /** + * Select any key. + * + * @return filter + */ + static KeyFilter any() { + return keyId -> true; + } + + /** + * Select only the given keyId. + * + * @param onlyKeyId only acceptable key id + * @return filter + */ + static KeyFilter only(long onlyKeyId) { + return keyId -> keyId == onlyKeyId; + } + + /** + * Select all keyIds which are contained in the given set of ids. + * + * @param ids set of acceptable keyIds + * @return filter + */ + static KeyFilter selected(Collection ids) { + // noinspection Convert2MethodRef + return keyId -> ids.contains(keyId); + } + } +} diff --git a/pgpainless-core/src/main/java/org/gnupg/package-info.java b/pgpainless-core/src/main/java/org/gnupg/package-info.java new file mode 100644 index 00000000..03268619 --- /dev/null +++ b/pgpainless-core/src/main/java/org/gnupg/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Utility classes related to creating keys with GNU DUMMY S2K values. + */ +package org.gnupg; diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java new file mode 100644 index 00000000..41e3fb08 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/OpenPgpPacket.java @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import org.bouncycastle.bcpg.PacketTags; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; + +public enum OpenPgpPacket { + PKESK(PacketTags.PUBLIC_KEY_ENC_SESSION), + SIG(PacketTags.SIGNATURE), + SKESK(PacketTags.SYMMETRIC_KEY_ENC_SESSION), + OPS(PacketTags.ONE_PASS_SIGNATURE), + SK(PacketTags.SECRET_KEY), + PK(PacketTags.PUBLIC_KEY), + SSK(PacketTags.SECRET_SUBKEY), + COMP(PacketTags.COMPRESSED_DATA), + SED(PacketTags.SYMMETRIC_KEY_ENC), + MARKER(PacketTags.MARKER), + LIT(PacketTags.LITERAL_DATA), + TRUST(PacketTags.TRUST), + UID(PacketTags.USER_ID), + PSK(PacketTags.PUBLIC_SUBKEY), + UATTR(PacketTags.USER_ATTRIBUTE), + SEIPD(PacketTags.SYM_ENC_INTEGRITY_PRO), + MDC(PacketTags.MOD_DETECTION_CODE), + + EXP_1(PacketTags.EXPERIMENTAL_1), + EXP_2(PacketTags.EXPERIMENTAL_2), + EXP_3(PacketTags.EXPERIMENTAL_3), + EXP_4(PacketTags.EXPERIMENTAL_4), + ; + + static final Map MAP = new HashMap<>(); + + static { + for (OpenPgpPacket p : OpenPgpPacket.values()) { + MAP.put(p.getTag(), p); + } + } + + final int tag; + + @Nullable + public static OpenPgpPacket fromTag(int tag) { + return MAP.get(tag); + } + + @Nonnull + public static OpenPgpPacket requireFromTag(int tag) { + OpenPgpPacket p = fromTag(tag); + if (p == null) { + throw new NoSuchElementException("No OpenPGP packet known for tag " + tag); + } + return p; + } + + OpenPgpPacket(int tag) { + this.tag = tag; + } + + int getTag() { + return tag; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java index baa8f92e..c7599db9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/PublicKeyAlgorithm.java @@ -28,7 +28,7 @@ public enum PublicKeyAlgorithm { /** * RSA with usage encryption. * - * @deprecated see https://tools.ietf.org/html/rfc4880#section-13.5 + * @deprecated see Deprecation notice */ @Deprecated RSA_ENCRYPT (PublicKeyAlgorithmTags.RSA_ENCRYPT, false, true), @@ -36,7 +36,7 @@ public enum PublicKeyAlgorithm { /** * RSA with usage of creating signatures. * - * @deprecated see https://tools.ietf.org/html/rfc4880#section-13.5 + * @deprecated see Deprecation notice */ @Deprecated RSA_SIGN (PublicKeyAlgorithmTags.RSA_SIGN, true, false), @@ -71,7 +71,7 @@ public enum PublicKeyAlgorithm { /** * ElGamal General. * - * @deprecated see https://tools.ietf.org/html/rfc4880#section-13.8 + * @deprecated see Deprecation notice */ @Deprecated ELGAMAL_GENERAL (PublicKeyAlgorithmTags.ELGAMAL_GENERAL, true, true), 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 b6117a60..f8f59d36 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 @@ -22,8 +22,10 @@ import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; +import org.pgpainless.key.SubkeyIdentifier; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.signature.SignatureUtils; import org.pgpainless.util.Passphrase; @@ -48,6 +50,7 @@ public class ConsumerOptions { // Session key for decryption without passphrase/key private SessionKey sessionKey = null; + private final Map customPublicKeyDataDecryptorFactories = new HashMap<>(); private final Map decryptionKeys = new HashMap<>(); private final Set decryptionPassphrases = new HashSet<>(); @@ -238,6 +241,23 @@ public class ConsumerOptions { return this; } + /** + * Add a custom {@link PublicKeyDataDecryptorFactory} which enable decryption of messages, e.g. using + * hardware-backed secret keys. + * (See e.g. {@link org.pgpainless.decryption_verification.HardwareSecurity.HardwareDataDecryptorFactory}). + * + * @param factory decryptor factory + * @return options + */ + public ConsumerOptions addCustomDecryptorFactory(@Nonnull CustomPublicKeyDataDecryptorFactory factory) { + this.customPublicKeyDataDecryptorFactories.put(factory.getSubkeyIdentifier(), factory); + return this; + } + + Map getCustomDecryptorFactories() { + return new HashMap<>(customPublicKeyDataDecryptorFactories); + } + public @Nonnull Set getDecryptionKeys() { return Collections.unmodifiableSet(decryptionKeys.keySet()); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java new file mode 100644 index 00000000..37dc10a9 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.java @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.pgpainless.key.SubkeyIdentifier; + +public interface CustomPublicKeyDataDecryptorFactory extends PublicKeyDataDecryptorFactory { + + SubkeyIdentifier getSubkeyIdentifier(); + +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java index 68a68847..0baf4124 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionBuilder.java @@ -31,7 +31,7 @@ public class DecryptionBuilder implements DecryptionBuilderInterface { throw new IllegalArgumentException("Consumer options cannot be null."); } - return DecryptionStreamFactory.create(inputStream, consumerOptions); + return OpenPgpMessageInputStream.create(inputStream, consumerOptions); } } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java index e3f1e720..0f221ad7 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStream.java @@ -1,65 +1,13 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub +// SPDX-FileCopyrightText: 2022 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 package org.pgpainless.decryption_verification; -import java.io.IOException; -import java.io.InputStream; import javax.annotation.Nonnull; -import org.bouncycastle.util.io.Streams; - -/** - * Decryption Stream that handles updating and verification of detached signatures, - * as well as verification of integrity-protected input streams once the stream gets closed. - */ -public class DecryptionStream extends CloseForResultInputStream { - - private final InputStream inputStream; - private final IntegrityProtectedInputStream integrityProtectedInputStream; - private final InputStream armorStream; - - /** - * Create an input stream that handles decryption and - if necessary - integrity protection verification. - * - * @param wrapped underlying input stream - * @param resultBuilder builder for decryption metadata like algorithms, recipients etc. - * @param integrityProtectedInputStream in case of data encrypted using SEIP packet close this stream to check integrity - * @param armorStream armor stream to verify CRC checksums - */ - DecryptionStream(@Nonnull InputStream wrapped, - @Nonnull OpenPgpMetadata.Builder resultBuilder, - IntegrityProtectedInputStream integrityProtectedInputStream, - InputStream armorStream) { +public abstract class DecryptionStream extends CloseForResultInputStream { + public DecryptionStream(@Nonnull OpenPgpMetadata.Builder resultBuilder) { super(resultBuilder); - this.inputStream = wrapped; - this.integrityProtectedInputStream = integrityProtectedInputStream; - this.armorStream = armorStream; } - - @Override - public void close() throws IOException { - if (armorStream != null) { - Streams.drain(armorStream); - } - inputStream.close(); - if (integrityProtectedInputStream != null) { - integrityProtectedInputStream.close(); - } - super.close(); - } - - @Override - public int read() throws IOException { - int r = inputStream.read(); - return r; - } - - @Override - public int read(@Nonnull byte[] bytes, int offset, int length) throws IOException { - int read = inputStream.read(bytes, offset, length); - return read; - } - } 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 deleted file mode 100644 index a787280a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/DecryptionStreamFactory.java +++ /dev/null @@ -1,650 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.ArmoredInputStream; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPEncryptedData; -import org.bouncycastle.openpgp.PGPEncryptedDataList; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPOnePassSignature; -import org.bouncycastle.openpgp.PGPOnePassSignatureList; -import org.bouncycastle.openpgp.PGPPBEEncryptedData; -import org.bouncycastle.openpgp.PGPPrivateKey; -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.PGPSessionKey; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.bouncycastle.openpgp.PGPUtil; -import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; -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.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; -import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; -import org.pgpainless.exception.FinalIOException; -import org.pgpainless.exception.MessageNotIntegrityProtectedException; -import org.pgpainless.exception.MissingDecryptionMethodException; -import org.pgpainless.exception.MissingLiteralDataException; -import org.pgpainless.exception.MissingPassphraseException; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.exception.UnacceptableAlgorithmException; -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.SignatureUtils; -import org.pgpainless.signature.consumer.DetachedSignatureCheck; -import org.pgpainless.signature.consumer.OnePassSignatureCheck; -import org.pgpainless.util.ArmoredInputStreamFactory; -import org.pgpainless.util.Passphrase; -import org.pgpainless.util.SessionKey; -import org.pgpainless.util.Tuple; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public final class DecryptionStreamFactory { - - - private static final Logger LOGGER = LoggerFactory.getLogger(DecryptionStreamFactory.class); - // Maximum nesting depth of packets (e.g. compression, encryption...) - private static final int MAX_PACKET_NESTING_DEPTH = 16; - - private final ConsumerOptions options; - private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder(); - private final List onePassSignatureChecks = new ArrayList<>(); - private final List detachedSignatureChecks = new ArrayList<>(); - private final Map onePassSignaturesWithMissingCert = new HashMap<>(); - - private static final PGPContentVerifierBuilderProvider verifierBuilderProvider = - ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(); - private IntegrityProtectedInputStream integrityProtectedEncryptedInputStream; - - - public static DecryptionStream create(@Nonnull InputStream inputStream, - @Nonnull ConsumerOptions options) - throws PGPException, IOException { - DecryptionStreamFactory factory = new DecryptionStreamFactory(options); - OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); - return factory.parseOpenPGPDataAndCreateDecryptionStream(openPgpIn); - } - - public DecryptionStreamFactory(ConsumerOptions options) { - this.options = options; - initializeDetachedSignatures(options.getDetachedSignatures()); - } - - private void initializeDetachedSignatures(Set signatures) { - for (PGPSignature signature : signatures) { - long issuerKeyId = SignatureUtils.determineIssuerKeyId(signature); - PGPPublicKeyRing signingKeyRing = findSignatureVerificationKeyRing(issuerKeyId); - if (signingKeyRing == null) { - SignatureValidationException ex = new SignatureValidationException( - "Missing verification certificate " + Long.toHexString(issuerKeyId)); - resultBuilder.addInvalidDetachedSignature(new SignatureVerification(signature, null), ex); - continue; - } - PGPPublicKey signingKey = signingKeyRing.getPublicKey(issuerKeyId); - SubkeyIdentifier signingKeyIdentifier = new SubkeyIdentifier(signingKeyRing, signingKey.getKeyID()); - try { - signature.init(verifierBuilderProvider, signingKey); - DetachedSignatureCheck detachedSignature = - new DetachedSignatureCheck(signature, signingKeyRing, signingKeyIdentifier); - detachedSignatureChecks.add(detachedSignature); - } catch (PGPException e) { - SignatureValidationException ex = new SignatureValidationException( - "Cannot verify detached signature made by " + signingKeyIdentifier + ".", e); - resultBuilder.addInvalidDetachedSignature(new SignatureVerification(signature, signingKeyIdentifier), ex); - } - } - } - - private DecryptionStream parseOpenPGPDataAndCreateDecryptionStream(OpenPgpInputStream openPgpIn) - throws IOException, PGPException { - - InputStream pgpInStream; - InputStream outerDecodingStream; - PGPObjectFactory objectFactory; - - // Non-OpenPGP data. We are probably verifying detached signatures - if (openPgpIn.isNonOpenPgp() || options.isForceNonOpenPgpData()) { - outerDecodingStream = openPgpIn; - pgpInStream = wrapInVerifySignatureStream(outerDecodingStream, null); - return new DecryptionStream(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, null); - } - - // Data appears to be OpenPGP message, - // or we handle it as such, since user provided a session-key for decryption - if (openPgpIn.isLikelyOpenPgpMessage() || - (openPgpIn.isBinaryOpenPgp() && options.getSessionKey() != null)) { - outerDecodingStream = openPgpIn; - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); - // Parse OpenPGP message - pgpInStream = processPGPPackets(objectFactory, 1); - return new DecryptionStream(pgpInStream, - resultBuilder, integrityProtectedEncryptedInputStream, null); - } - - if (openPgpIn.isAsciiArmored()) { - ArmoredInputStream armoredInputStream = ArmoredInputStreamFactory.get(openPgpIn); - if (armoredInputStream.isClearText()) { - resultBuilder.setCleartextSigned(); - return parseCleartextSignedMessage(armoredInputStream); - } else { - outerDecodingStream = armoredInputStream; - objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream); - // Parse OpenPGP message - pgpInStream = processPGPPackets(objectFactory, 1); - return new DecryptionStream(pgpInStream, - resultBuilder, integrityProtectedEncryptedInputStream, - outerDecodingStream); - } - } - - throw new PGPException("Not sure how to handle the input stream."); - } - - private DecryptionStream parseCleartextSignedMessage(ArmoredInputStream armorIn) - throws IOException, PGPException { - resultBuilder.setCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) - .setFileEncoding(StreamEncoding.TEXT); - - MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); - PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, multiPassStrategy.getMessageOutputStream()); - - for (PGPSignature signature : signatures) { - options.addVerificationOfDetachedSignature(signature); - } - - initializeDetachedSignatures(options.getDetachedSignatures()); - - InputStream verifyIn = wrapInVerifySignatureStream(multiPassStrategy.getMessageInputStream(), null); - return new DecryptionStream(verifyIn, resultBuilder, integrityProtectedEncryptedInputStream, - null); - } - - private InputStream wrapInVerifySignatureStream(InputStream bufferedIn, @Nullable PGPObjectFactory objectFactory) { - return new SignatureInputStream.VerifySignatures( - bufferedIn, objectFactory, onePassSignatureChecks, - onePassSignaturesWithMissingCert, detachedSignatureChecks, options, - resultBuilder); - } - - private InputStream processPGPPackets(@Nonnull PGPObjectFactory objectFactory, int depth) - throws IOException, PGPException { - if (depth >= MAX_PACKET_NESTING_DEPTH) { - throw new PGPException("Maximum depth of nested packages exceeded."); - } - Object nextPgpObject; - try { - while ((nextPgpObject = objectFactory.nextObject()) != null) { - if (nextPgpObject instanceof PGPEncryptedDataList) { - return processPGPEncryptedDataList((PGPEncryptedDataList) nextPgpObject, depth); - } - if (nextPgpObject instanceof PGPCompressedData) { - return processPGPCompressedData((PGPCompressedData) nextPgpObject, depth); - } - if (nextPgpObject instanceof PGPOnePassSignatureList) { - return processOnePassSignatureList(objectFactory, (PGPOnePassSignatureList) nextPgpObject, depth); - } - if (nextPgpObject instanceof PGPLiteralData) { - return processPGPLiteralData(objectFactory, (PGPLiteralData) nextPgpObject, depth); - } - } - } catch (FinalIOException e) { - throw e; - } catch (IOException e) { - if (depth == 1 && e.getMessage().contains("invalid armor")) { - throw e; - } - if (depth == 1 && e.getMessage().contains("unknown object in stream:")) { - throw new MissingLiteralDataException("No Literal Data Packet found."); - } else { - throw new FinalIOException(e); - } - } - - throw new MissingLiteralDataException("No Literal Data Packet found"); - } - - private InputStream processPGPEncryptedDataList(PGPEncryptedDataList pgpEncryptedDataList, int depth) - throws PGPException, IOException { - LOGGER.debug("Depth {}: Encountered PGPEncryptedDataList", depth); - - SessionKey sessionKey = options.getSessionKey(); - if (sessionKey != null) { - integrityProtectedEncryptedInputStream = decryptWithProvidedSessionKey(pgpEncryptedDataList, sessionKey); - InputStream decodedDataStream = PGPUtil.getDecoderStream(integrityProtectedEncryptedInputStream); - PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(decodedDataStream); - return processPGPPackets(factory, ++depth); - } - - InputStream decryptedDataStream = decryptSessionKey(pgpEncryptedDataList); - InputStream decodedDataStream = PGPUtil.getDecoderStream(decryptedDataStream); - PGPObjectFactory factory = ImplementationFactory.getInstance().getPGPObjectFactory(decodedDataStream); - return processPGPPackets(factory, ++depth); - } - - private IntegrityProtectedInputStream decryptWithProvidedSessionKey( - PGPEncryptedDataList pgpEncryptedDataList, - SessionKey sessionKey) - throws PGPException { - SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() - .getSessionKeyDataDecryptorFactory(sessionKey); - InputStream decryptedDataStream = null; - PGPEncryptedData encryptedData = null; - for (PGPEncryptedData pgpEncryptedData : pgpEncryptedDataList) { - encryptedData = pgpEncryptedData; - if (!options.isIgnoreMDCErrors() && !encryptedData.isIntegrityProtected()) { - throw new MessageNotIntegrityProtectedException(); - } - - if (encryptedData instanceof PGPPBEEncryptedData) { - PGPPBEEncryptedData pbeEncrypted = (PGPPBEEncryptedData) encryptedData; - decryptedDataStream = pbeEncrypted.getDataStream(decryptorFactory); - break; - } else if (encryptedData instanceof PGPPublicKeyEncryptedData) { - PGPPublicKeyEncryptedData pkEncrypted = (PGPPublicKeyEncryptedData) encryptedData; - decryptedDataStream = pkEncrypted.getDataStream(decryptorFactory); - break; - } - } - - if (decryptedDataStream == null) { - throw new PGPException("No valid PGP data encountered."); - } - - resultBuilder.setSessionKey(sessionKey); - throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); - integrityProtectedEncryptedInputStream = - new IntegrityProtectedInputStream(decryptedDataStream, encryptedData, options); - return integrityProtectedEncryptedInputStream; - } - - private InputStream processPGPCompressedData(PGPCompressedData pgpCompressedData, int depth) - throws PGPException, IOException { - try { - CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.requireFromId(pgpCompressedData.getAlgorithm()); - LOGGER.debug("Depth {}: Encountered PGPCompressedData: {}", depth, compressionAlgorithm); - resultBuilder.setCompressionAlgorithm(compressionAlgorithm); - } catch (NoSuchElementException e) { - throw new PGPException("Unknown compression algorithm encountered.", e); - } - - InputStream inflatedDataStream = pgpCompressedData.getDataStream(); - InputStream decodedDataStream = PGPUtil.getDecoderStream(inflatedDataStream); - PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decodedDataStream); - - return processPGPPackets(objectFactory, ++depth); - } - - private InputStream processOnePassSignatureList( - @Nonnull PGPObjectFactory objectFactory, - PGPOnePassSignatureList onePassSignatures, - int depth) - throws PGPException, IOException { - LOGGER.debug("Depth {}: Encountered PGPOnePassSignatureList of size {}", depth, onePassSignatures.size()); - initOnePassSignatures(onePassSignatures); - return processPGPPackets(objectFactory, depth); - } - - private InputStream processPGPLiteralData( - @Nonnull PGPObjectFactory objectFactory, - PGPLiteralData pgpLiteralData, - int depth) { - LOGGER.debug("Depth {}: Found PGPLiteralData", depth); - InputStream literalDataInputStream = pgpLiteralData.getInputStream(); - - resultBuilder.setFileName(pgpLiteralData.getFileName()) - .setModificationDate(pgpLiteralData.getModificationTime()) - .setFileEncoding(StreamEncoding.requireFromCode(pgpLiteralData.getFormat())); - - if (onePassSignatureChecks.isEmpty() && onePassSignaturesWithMissingCert.isEmpty()) { - LOGGER.debug("No OnePassSignatures found -> We are done"); - return literalDataInputStream; - } - - return new SignatureInputStream.VerifySignatures(literalDataInputStream, objectFactory, - onePassSignatureChecks, onePassSignaturesWithMissingCert, detachedSignatureChecks, options, resultBuilder) { - }; - } - - private InputStream decryptSessionKey(@Nonnull PGPEncryptedDataList encryptedDataList) - throws PGPException { - Iterator encryptedDataIterator = encryptedDataList.getEncryptedDataObjects(); - if (!encryptedDataIterator.hasNext()) { - throw new PGPException("Decryption failed - EncryptedDataList has no items"); - } - - 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(); - - if (!encryptedData.isIntegrityProtected() && !options.isIgnoreMDCErrors()) { - throw new MessageNotIntegrityProtectedException(); - } - - // SKESK - if (encryptedData instanceof PGPPBEEncryptedData) { - passphraseProtected.add((PGPPBEEncryptedData) encryptedData); - } - // PKESK - else if (encryptedData instanceof PGPPublicKeyEncryptedData) { - publicKeyProtected.add((PGPPublicKeyEncryptedData) encryptedData); - } - } - - // 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); - - PGPSessionKey pgpSessionKey = pbeEncryptedData.getSessionKey(passphraseDecryptor); - SessionKey sessionKey = new SessionKey(pgpSessionKey); - resultBuilder.setSessionKey(sessionKey); - - throwIfAlgorithmIsRejected(sessionKey.getAlgorithm()); - - integrityProtectedEncryptedInputStream = - new IntegrityProtectedInputStream(decryptedDataStream, pbeEncryptedData, options); - - 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.ANY); - 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; - } - - // Make sure that the recipient key is encryption capable and non-expired - KeyRingInfo info = new KeyRingInfo(secretKeys); - List encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); - - PGPSecretKey secretKey = null; - for (PGPPublicKey pubkey : encryptionSubkeys) { - if (pubkey.getKeyID() == keyId) { - secretKey = secretKeys.getSecretKey(keyId); - break; - } - } - - if (secretKey == null) { - LOGGER.debug("Key " + Long.toHexString(keyId) + " is not valid or not capable for decryption."); - } else { - privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData, - postponedDueToMissingPassphrase, true); - } - } - if (privateKey == null) { - continue; - } - decryptionKey = privateKey; - encryptedSessionKey = publicKeyEncryptedData; - break; - } - - // Try postponed keys with missing passphrases (will cause missing passphrase callbacks to fire) - if (encryptedSessionKey == null) { - - if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.THROW_EXCEPTION) { - // Non-interactive mode: Throw an exception with all locked decryption keys - Set keyIds = new HashSet<>(); - for (Tuple k : postponedDueToMissingPassphrase) { - keyIds.add(k.getA()); - } - if (!keyIds.isEmpty()) { - throw new MissingPassphraseException(keyIds); - } - } - else if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.INTERACTIVE) { - // Interactive mode: Fire protector callbacks to get passphrases interactively - 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; - } - } else { - throw new IllegalStateException("Invalid PostponedKeysStrategy set in consumer options."); - } - - } - - 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 || encryptedSessionKey == null) { - throw new MissingDecryptionMethodException("Decryption failed - No suitable decryption key or passphrase found"); - } - - PublicKeyDataDecryptorFactory dataDecryptor = ImplementationFactory.getInstance() - .getPublicKeyDataDecryptorFactory(decryptionKey); - - PGPSessionKey pgpSessionKey = encryptedSessionKey.getSessionKey(dataDecryptor); - SessionKey sessionKey = new SessionKey(pgpSessionKey); - resultBuilder.setSessionKey(sessionKey); - - SymmetricKeyAlgorithm symmetricKeyAlgorithm = sessionKey.getAlgorithm(); - if (symmetricKeyAlgorithm == SymmetricKeyAlgorithm.NULL) { - LOGGER.debug("Message is unencrypted"); - } else { - LOGGER.debug("Message is encrypted using {}", symmetricKeyAlgorithm); - } - throwIfAlgorithmIsRejected(symmetricKeyAlgorithm); - - integrityProtectedEncryptedInputStream = new IntegrityProtectedInputStream( - encryptedSessionKey.getDataStream(dataDecryptor), encryptedSessionKey, options); - return integrityProtectedEncryptedInputStream; - } - - private void throwIfAlgorithmIsRejected(SymmetricKeyAlgorithm algorithm) - throws UnacceptableAlgorithmException { - if (!PGPainless.getPolicy().getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(algorithm)) { - throw new UnacceptableAlgorithmException("Data is " - + (algorithm == SymmetricKeyAlgorithm.NULL ? - "unencrypted" : - "encrypted with symmetric algorithm " + algorithm) + " which is not acceptable as per PGPainless' policy.\n" + - "To mark this algorithm as acceptable, use PGPainless.getPolicy().setSymmetricKeyDecryptionAlgorithmPolicy()."); - } - } - - private void initOnePassSignatures(@Nonnull PGPOnePassSignatureList onePassSignatureList) - throws PGPException { - Iterator iterator = onePassSignatureList.iterator(); - if (!iterator.hasNext()) { - throw new PGPException("Verification failed - No OnePassSignatures found"); - } - - processOnePassSignatures(iterator); - } - - private void processOnePassSignatures(Iterator signatures) - throws PGPException { - while (signatures.hasNext()) { - PGPOnePassSignature signature = signatures.next(); - processOnePassSignature(signature); - } - } - - private void processOnePassSignature(PGPOnePassSignature signature) - throws PGPException { - final long keyId = signature.getKeyID(); - - LOGGER.debug("Encountered OnePassSignature from {}", Long.toHexString(keyId)); - - // Find public key - PGPPublicKeyRing verificationKeyRing = findSignatureVerificationKeyRing(keyId); - if (verificationKeyRing == null) { - onePassSignaturesWithMissingCert.put(keyId, new OnePassSignatureCheck(signature, null)); - return; - } - PGPPublicKey verificationKey = verificationKeyRing.getPublicKey(keyId); - - signature.init(verifierBuilderProvider, verificationKey); - OnePassSignatureCheck onePassSignature = new OnePassSignatureCheck(signature, verificationKeyRing); - onePassSignatureChecks.add(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 : options.getCertificates()) { - PGPPublicKey verificationKey = publicKeyRing.getPublicKey(keyId); - if (verificationKey != null) { - LOGGER.debug("Found public key {} for signature verification", Long.toHexString(keyId)); - verificationKeyRing = publicKeyRing; - break; - } - } - - if (verificationKeyRing == null && options.getMissingCertificateCallback() != null) { - verificationKeyRing = options.getMissingCertificateCallback().onMissingPublicKeyEncountered(keyId); - } - - return verificationKeyRing; - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java new file mode 100644 index 00000000..daf902d4 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/HardwareSecurity.java @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.operator.PGPDataDecryptor; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; +import org.pgpainless.key.SubkeyIdentifier; + +/** + * Enable integration of hardware-backed OpenPGP keys. + */ +public class HardwareSecurity { + + public interface DecryptionCallback { + + /** + * Delegate decryption of a Public-Key-Encrypted-Session-Key (PKESK) to an external API for dealing with + * hardware security modules such as smartcards or TPMs. + * + * If decryption fails for some reason, a subclass of the {@link HardwareSecurityException} is thrown. + * + * @param keyId id of the key + * @param keyAlgorithm algorithm + * @param sessionKeyData encrypted session key + * + * @return decrypted session key + * @throws HardwareSecurityException exception + */ + byte[] decryptSessionKey(long keyId, int keyAlgorithm, byte[] sessionKeyData) + throws HardwareSecurityException; + + } + + /** + * Implementation of {@link PublicKeyDataDecryptorFactory} which delegates decryption of encrypted session keys + * to a {@link DecryptionCallback}. + * Users can provide such a callback to delegate decryption of messages to hardware security SDKs. + */ + public static class HardwareDataDecryptorFactory implements CustomPublicKeyDataDecryptorFactory { + + private final DecryptionCallback callback; + // luckily we can instantiate the BcPublicKeyDataDecryptorFactory with null as argument. + private final PublicKeyDataDecryptorFactory factory = + new BcPublicKeyDataDecryptorFactory(null); + private final SubkeyIdentifier subkey; + + /** + * Create a new {@link HardwareDataDecryptorFactory}. + * + * @param callback decryption callback + */ + public HardwareDataDecryptorFactory(SubkeyIdentifier subkeyIdentifier, DecryptionCallback callback) { + this.callback = callback; + this.subkey = subkeyIdentifier; + } + + @Override + public byte[] recoverSessionData(int keyAlgorithm, byte[][] secKeyData) + throws PGPException { + try { + // delegate decryption to the callback + return callback.decryptSessionKey(subkey.getSubkeyId(), keyAlgorithm, secKeyData[0]); + } catch (HardwareSecurityException e) { + throw new PGPException("Hardware-backed decryption failed.", e); + } + } + + @Override + public PGPDataDecryptor createDataDecryptor(boolean withIntegrityPacket, int encAlgorithm, byte[] key) + throws PGPException { + return factory.createDataDecryptor(withIntegrityPacket, encAlgorithm, key); + } + + @Override + public PGPDataDecryptor createDataDecryptor(int aeadAlgorithm, byte[] iv, int chunkSize, int encAlgorithm, byte[] key) + throws PGPException { + return factory.createDataDecryptor(aeadAlgorithm, iv, chunkSize, encAlgorithm, key); + } + + @Override + public SubkeyIdentifier getSubkeyIdentifier() { + return subkey; + } + } + + public static class HardwareSecurityException + extends Exception { + + } + +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java index 4da52d0f..286160e8 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/IntegrityProtectedInputStream.java @@ -11,12 +11,17 @@ import javax.annotation.Nonnull; import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPException; import org.pgpainless.exception.ModificationDetectionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class IntegrityProtectedInputStream extends InputStream { + private static final Logger LOGGER = LoggerFactory.getLogger(IntegrityProtectedInputStream.class); + private final InputStream inputStream; private final PGPEncryptedData encryptedData; private final ConsumerOptions options; + private boolean closed = false; public IntegrityProtectedInputStream(InputStream inputStream, PGPEncryptedData encryptedData, ConsumerOptions options) { this.inputStream = inputStream; @@ -36,11 +41,17 @@ public class IntegrityProtectedInputStream extends InputStream { @Override public void close() throws IOException { + if (closed) { + return; + } + closed = true; + if (encryptedData.isIntegrityProtected() && !options.isIgnoreMDCErrors()) { try { if (!encryptedData.verify()) { throw new ModificationDetectionException(); } + LOGGER.debug("Integrity Protection check passed"); } catch (PGPException e) { throw new IOException("Failed to verify integrity protection", e); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java new file mode 100644 index 00000000..7811578e --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MessageMetadata.java @@ -0,0 +1,456 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.StreamEncoding; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.util.SessionKey; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +public class MessageMetadata { + + protected Message message; + + public MessageMetadata(@Nonnull Message message) { + this.message = message; + } + + public @Nullable SymmetricKeyAlgorithm getEncryptionAlgorithm() { + Iterator algorithms = getEncryptionAlgorithms(); + if (algorithms.hasNext()) { + return algorithms.next(); + } + return null; + } + + public @Nonnull Iterator getEncryptionAlgorithms() { + return new LayerIterator(message) { + @Override + public boolean matches(Nested layer) { + return layer instanceof EncryptedData; + } + + @Override + public SymmetricKeyAlgorithm getProperty(Layer last) { + return ((EncryptedData) last).algorithm; + } + }; + } + + public @Nullable CompressionAlgorithm getCompressionAlgorithm() { + Iterator algorithms = getCompressionAlgorithms(); + if (algorithms.hasNext()) { + return algorithms.next(); + } + return null; + } + + public @Nonnull Iterator getCompressionAlgorithms() { + return new LayerIterator(message) { + @Override + public boolean matches(Nested layer) { + return layer instanceof CompressedData; + } + + @Override + public CompressionAlgorithm getProperty(Layer last) { + return ((CompressedData) last).algorithm; + } + }; + } + + public @Nullable SessionKey getSessionKey() { + Iterator sessionKeys = getSessionKeys(); + if (sessionKeys.hasNext()) { + return sessionKeys.next(); + } + return null; + } + + public @Nonnull Iterator getSessionKeys() { + return new LayerIterator(message) { + @Override + boolean matches(Nested layer) { + return layer instanceof EncryptedData; + } + + @Override + SessionKey getProperty(Layer last) { + return ((EncryptedData) last).getSessionKey(); + } + }; + } + + public List getVerifiedDetachedSignatures() { + return new ArrayList<>(message.verifiedDetachedSignatures); + } + + public List getRejectedDetachedSignatures() { + return new ArrayList<>(message.rejectedDetachedSignatures); + } + + public @Nonnull List getVerifiedInlineSignatures() { + List verifications = new ArrayList<>(); + Iterator> verificationsByLayer = getVerifiedInlineSignaturesByLayer(); + while (verificationsByLayer.hasNext()) { + verifications.addAll(verificationsByLayer.next()); + } + return verifications; + } + + public @Nonnull Iterator> getVerifiedInlineSignaturesByLayer() { + return new LayerIterator>(message) { + @Override + boolean matches(Nested layer) { + return layer instanceof Layer; + } + + @Override + boolean matches(Layer layer) { + return true; + } + + @Override + List getProperty(Layer last) { + List list = new ArrayList<>(); + list.addAll(last.getVerifiedOnePassSignatures()); + list.addAll(last.getVerifiedPrependedSignatures()); + return list; + } + }; + } + + public @Nonnull List getRejectedInlineSignatures() { + List rejected = new ArrayList<>(); + Iterator> rejectedByLayer = getRejectedInlineSignaturesByLayer(); + while (rejectedByLayer.hasNext()) { + rejected.addAll(rejectedByLayer.next()); + } + return rejected; + } + + public @Nonnull Iterator> getRejectedInlineSignaturesByLayer() { + return new LayerIterator>(message) { + @Override + boolean matches(Nested layer) { + return layer instanceof Layer; + } + + @Override + boolean matches(Layer layer) { + return true; + } + + @Override + List getProperty(Layer last) { + List list = new ArrayList<>(); + list.addAll(last.getRejectedOnePassSignatures()); + list.addAll(last.getRejectedPrependedSignatures()); + return list; + } + }; + } + + public String getFilename() { + LiteralData literalData = findLiteralData(); + if (literalData == null) { + return null; + } + return literalData.getFileName(); + } + + public Date getModificationDate() { + LiteralData literalData = findLiteralData(); + if (literalData == null) { + return null; + } + return literalData.getModificationDate(); + } + + public StreamEncoding getFormat() { + LiteralData literalData = findLiteralData(); + if (literalData == null) { + return null; + } + return literalData.getFormat(); + } + + private LiteralData findLiteralData() { + Nested nested = message.child; + if (nested == null) { + return null; + } + + while (nested.hasNestedChild()) { + Layer layer = (Layer) nested; + nested = layer.child; + } + return (LiteralData) nested; + } + + public SubkeyIdentifier getDecryptionKey() { + Iterator iterator = new LayerIterator(message) { + @Override + public boolean matches(Nested layer) { + return layer instanceof EncryptedData; + } + + @Override + public SubkeyIdentifier getProperty(Layer last) { + return ((EncryptedData) last).decryptionKey; + } + }; + if (iterator.hasNext()) { + return iterator.next(); + } + return null; + } + + public abstract static class Layer { + public static final int MAX_LAYER_DEPTH = 16; + protected final int depth; + protected final List verifiedDetachedSignatures = new ArrayList<>(); + protected final List rejectedDetachedSignatures = new ArrayList<>(); + protected final List verifiedOnePassSignatures = new ArrayList<>(); + protected final List rejectedOnePassSignatures = new ArrayList<>(); + protected final List verifiedPrependedSignatures = new ArrayList<>(); + protected final List rejectedPrependedSignatures = new ArrayList<>(); + protected Nested child; + + public Layer(int depth) { + this.depth = depth; + if (depth > MAX_LAYER_DEPTH) { + throw new MalformedOpenPgpMessageException("Maximum nesting depth exceeded."); + } + } + + public Nested getChild() { + return child; + } + + public void setChild(Nested child) { + this.child = child; + } + + public List getVerifiedDetachedSignatures() { + return new ArrayList<>(verifiedDetachedSignatures); + } + + public List getRejectedDetachedSignatures() { + return new ArrayList<>(rejectedDetachedSignatures); + } + + void addVerifiedDetachedSignature(SignatureVerification signatureVerification) { + verifiedDetachedSignatures.add(signatureVerification); + } + + void addRejectedDetachedSignature(SignatureVerification.Failure failure) { + rejectedDetachedSignatures.add(failure); + } + + public List getVerifiedOnePassSignatures() { + return new ArrayList<>(verifiedOnePassSignatures); + } + + public List getRejectedOnePassSignatures() { + return new ArrayList<>(rejectedOnePassSignatures); + } + + void addVerifiedOnePassSignature(SignatureVerification verifiedOnePassSignature) { + this.verifiedOnePassSignatures.add(verifiedOnePassSignature); + } + + void addRejectedOnePassSignature(SignatureVerification.Failure rejected) { + this.rejectedOnePassSignatures.add(rejected); + } + + public List getVerifiedPrependedSignatures() { + return new ArrayList<>(verifiedPrependedSignatures); + } + + public List getRejectedPrependedSignatures() { + return new ArrayList<>(rejectedPrependedSignatures); + } + + void addVerifiedPrependedSignature(SignatureVerification verified) { + this.verifiedPrependedSignatures.add(verified); + } + + void addRejectedPrependedSignature(SignatureVerification.Failure rejected) { + this.rejectedPrependedSignatures.add(rejected); + } + + } + + public interface Nested { + boolean hasNestedChild(); + } + + public static class Message extends Layer { + + public Message() { + super(0); + } + } + + public static class LiteralData implements Nested { + protected String fileName; + protected Date modificationDate; + protected StreamEncoding format; + + public LiteralData() { + this("", new Date(0L), StreamEncoding.BINARY); + } + + public LiteralData(String fileName, Date modificationDate, StreamEncoding format) { + this.fileName = fileName; + this.modificationDate = modificationDate; + this.format = format; + } + + public String getFileName() { + return fileName; + } + + public Date getModificationDate() { + return modificationDate; + } + + public StreamEncoding getFormat() { + return format; + } + + @Override + public boolean hasNestedChild() { + return false; + } + } + + public static class CompressedData extends Layer implements Nested { + protected final CompressionAlgorithm algorithm; + + public CompressedData(CompressionAlgorithm zip, int depth) { + super(depth); + this.algorithm = zip; + } + + public CompressionAlgorithm getAlgorithm() { + return algorithm; + } + + @Override + public boolean hasNestedChild() { + return true; + } + } + + public static class EncryptedData extends Layer implements Nested { + protected final SymmetricKeyAlgorithm algorithm; + protected SubkeyIdentifier decryptionKey; + protected SessionKey sessionKey; + protected List recipients; + + public EncryptedData(SymmetricKeyAlgorithm algorithm, int depth) { + super(depth); + this.algorithm = algorithm; + } + + public SymmetricKeyAlgorithm getAlgorithm() { + return algorithm; + } + + public SessionKey getSessionKey() { + return sessionKey; + } + + public List getRecipients() { + return new ArrayList<>(recipients); + } + + @Override + public boolean hasNestedChild() { + return true; + } + } + + + private abstract static class LayerIterator implements Iterator { + private Nested current; + Layer last = null; + Message parent; + + LayerIterator(Message message) { + super(); + this.parent = message; + this.current = message.child; + if (matches(current)) { + last = (Layer) current; + } + } + + @Override + public boolean hasNext() { + if (parent != null && matches(parent)) { + return true; + } + if (last == null) { + findNext(); + } + return last != null; + } + + @Override + public O next() { + if (parent != null && matches(parent)) { + O property = getProperty(parent); + parent = null; + return property; + } + if (last == null) { + findNext(); + } + if (last != null) { + O property = getProperty(last); + last = null; + return property; + } + throw new NoSuchElementException(); + } + + private void findNext() { + while (current instanceof Layer) { + current = ((Layer) current).child; + if (matches(current)) { + last = (Layer) current; + break; + } + } + } + + boolean matches(Nested layer) { + return false; + } + + boolean matches(Layer layer) { + if (layer instanceof Nested) { + return matches((Nested) layer); + } + return false; + } + + abstract O getProperty(Layer last); + } + +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java index f3eb949b..9b6f4a1e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/MissingPublicKeyCallback.java @@ -20,11 +20,13 @@ public interface MissingPublicKeyCallback { * you may not only search for the key-id on the key rings primary key! * * It would be super cool to provide the OpenPgp fingerprint here, but unfortunately one-pass-signatures - * only contain the key id (see https://datatracker.ietf.org/doc/html/rfc4880#section-5.4) + * only contain the key id. * * @param keyId ID of the missing signing (sub)key * * @return keyring containing the key or null + * + * @see RFC */ @Nullable PGPPublicKeyRing onMissingPublicKeyEncountered(@Nonnull Long keyId); diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java index 669c2dec..028fa666 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java @@ -47,6 +47,7 @@ import org.pgpainless.algorithm.SymmetricKeyAlgorithm; public class OpenPgpInputStream extends BufferedInputStream { + @SuppressWarnings("CharsetObjectCanBeUsed") private static final byte[] ARMOR_HEADER = "-----BEGIN PGP ".getBytes(Charset.forName("UTF8")); // Buffer beginning bytes of the data diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java new file mode 100644 index 00000000..f2c30ced --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.java @@ -0,0 +1,1162 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.Stack; +import javax.annotation.Nonnull; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.S2K; +import org.bouncycastle.bcpg.UnsupportedPacketVersionException; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPOnePassSignature; +import org.bouncycastle.openpgp.PGPPBEEncryptedData; +import org.bouncycastle.openpgp.PGPPrivateKey; +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.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory; +import org.bouncycastle.util.io.TeeInputStream; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.algorithm.OpenPgpPacket; +import org.pgpainless.algorithm.StreamEncoding; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.decryption_verification.syntax_check.InputSymbol; +import org.pgpainless.decryption_verification.syntax_check.PDA; +import org.pgpainless.decryption_verification.syntax_check.StackSymbol; +import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil; +import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; +import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.exception.MessageNotIntegrityProtectedException; +import org.pgpainless.exception.MissingDecryptionMethodException; +import org.pgpainless.exception.MissingPassphraseException; +import org.pgpainless.exception.SignatureValidationException; +import org.pgpainless.exception.UnacceptableAlgorithmException; +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.key.util.KeyIdUtil; +import org.pgpainless.key.util.KeyRingUtils; +import org.pgpainless.policy.Policy; +import org.pgpainless.signature.SignatureUtils; +import org.pgpainless.signature.consumer.CertificateValidator; +import org.pgpainless.signature.consumer.OnePassSignatureCheck; +import org.pgpainless.signature.consumer.SignatureCheck; +import org.pgpainless.signature.consumer.SignatureValidator; +import org.pgpainless.util.ArmoredInputStreamFactory; +import org.pgpainless.util.Passphrase; +import org.pgpainless.util.SessionKey; +import org.pgpainless.util.Tuple; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class OpenPgpMessageInputStream extends DecryptionStream { + + private static final Logger LOGGER = LoggerFactory.getLogger(OpenPgpMessageInputStream.class); + + // Options to consume the data + protected final ConsumerOptions options; + + private final Policy policy; + // Pushdown Automaton to verify validity of OpenPGP packet sequence in an OpenPGP message + protected final PDA syntaxVerifier = new PDA(); + // InputStream of OpenPGP packets + protected TeeBCPGInputStream packetInputStream; + // InputStream of a data packet containing nested data + protected InputStream nestedInputStream; + + private boolean closed = false; + + private final Signatures signatures; + private final MessageMetadata.Layer metadata; + + /** + * Create an {@link OpenPgpMessageInputStream} suitable for decryption and verification of + * OpenPGP messages and signatures. + * This constructor will use the global PGPainless {@link Policy}. + * + * @param inputStream underlying input stream + * @param options options for consuming the stream + * + * @throws IOException in case of an IO error + * @throws PGPException in case of an OpenPGP error + */ + public static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options) + throws IOException, PGPException { + return create(inputStream, options, PGPainless.getPolicy()); + } + + /** + * Create an {@link OpenPgpMessageInputStream} suitable for decryption and verification of + * OpenPGP messages and signatures. + * This factory method takes a custom {@link Policy} instead of using the global policy object. + * + * @param inputStream underlying input stream containing the OpenPGP message + * @param options options for consuming the message + * @param policy policy for acceptable algorithms etc. + * + * @throws PGPException in case of an OpenPGP error + * @throws IOException in case of an IO error + */ + public static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options, + @Nonnull Policy policy) + throws PGPException, IOException { + return create(inputStream, options, new MessageMetadata.Message(), policy); + } + + protected static OpenPgpMessageInputStream create(@Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options, + @Nonnull MessageMetadata.Layer metadata, + @Nonnull Policy policy) + throws IOException, PGPException { + OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream); + openPgpIn.reset(); + + if (openPgpIn.isNonOpenPgp() || options.isForceNonOpenPgpData()) { + return new OpenPgpMessageInputStream(Type.non_openpgp, + openPgpIn, options, metadata, policy); + } + + if (openPgpIn.isBinaryOpenPgp()) { + // Simply consume OpenPGP message + return new OpenPgpMessageInputStream(Type.standard, + openPgpIn, options, metadata, policy); + } + + if (openPgpIn.isAsciiArmored()) { + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn); + if (armorIn.isClearText()) { + return new OpenPgpMessageInputStream(Type.cleartext_signed, + armorIn, options, metadata, policy); + } else { + // Simply consume dearmored OpenPGP message + return new OpenPgpMessageInputStream(Type.standard, + armorIn, options, metadata, policy); + } + } else { + throw new AssertionError("Cannot deduce type of data."); + } + } + + protected OpenPgpMessageInputStream(@Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options, + @Nonnull MessageMetadata.Layer metadata, + @Nonnull Policy policy) + throws PGPException, IOException { + super(OpenPgpMetadata.getBuilder()); + + this.policy = policy; + this.options = options; + this.metadata = metadata; + this.signatures = new Signatures(options); + + // Add detached signatures only on the outermost OpenPgpMessageInputStream + if (metadata instanceof MessageMetadata.Message) { + this.signatures.addDetachedSignatures(options.getDetachedSignatures()); + } + + // tee out packet bytes for signature verification + packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), signatures); + + // *omnomnom* + consumePackets(); + } + + enum Type { + standard, + cleartext_signed, + non_openpgp + } + + protected OpenPgpMessageInputStream(@Nonnull Type type, + @Nonnull InputStream inputStream, + @Nonnull ConsumerOptions options, + @Nonnull MessageMetadata.Layer metadata, + @Nonnull Policy policy) throws PGPException, IOException { + super(OpenPgpMetadata.getBuilder()); + this.policy = policy; + this.options = options; + this.metadata = metadata; + this.signatures = new Signatures(options); + + if (metadata instanceof MessageMetadata.Message) { + this.signatures.addDetachedSignatures(options.getDetachedSignatures()); + } + + switch (type) { + + // Binary OpenPGP Message + case standard: + // tee out packet bytes for signature verification + packetInputStream = new TeeBCPGInputStream(BCPGInputStream.wrap(inputStream), this.signatures); + + // *omnomnom* + consumePackets(); + break; + + // Cleartext Signature Framework (probably signed message) + case cleartext_signed: + resultBuilder.setCleartextSigned(); + MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy(); + PGPSignatureList detachedSignatures = ClearsignedMessageUtil + .detachSignaturesFromInbandClearsignedMessage( + inputStream, multiPassStrategy.getMessageOutputStream()); + + for (PGPSignature signature : detachedSignatures) { + signatures.addDetachedSignature(signature); + } + + options.forceNonOpenPgpData(); + nestedInputStream = new TeeInputStream(multiPassStrategy.getMessageInputStream(), this.signatures); + break; + + // Non-OpenPGP Data (e.g. detached signature verification) + case non_openpgp: + packetInputStream = null; + nestedInputStream = new TeeInputStream(inputStream, this.signatures); + break; + } + } + + /** + * Consume OpenPGP packets from the current {@link BCPGInputStream}. + * Once an OpenPGP packet with nested data (Literal Data, Compressed Data, Encrypted Data) is reached, + * set
nestedInputStream
to the nested stream and breaks the loop. + * The nested stream is either a simple {@link InputStream} (in case of Literal Data), or another + * {@link OpenPgpMessageInputStream} in case of Compressed and Encrypted Data. + * Once the nested data is processed, this method is called again to consume the remainder + * of packets following the nested data packet. + * + * @throws IOException in case of an IO error + * @throws PGPException in case of an OpenPGP error + * @throws MissingDecryptionMethodException if there is an encrypted data packet which cannot be decrypted + * due to missing decryption methods (no key, no password, no sessionkey) + * @throws MalformedOpenPgpMessageException if the message is made of an invalid packet sequence which + * does not follow the packet syntax of RFC4880. + */ + private void consumePackets() + throws IOException, PGPException { + OpenPgpPacket nextPacket; + if (packetInputStream == null) { + return; + } + + loop: // we break this when we enter nested packets and later resume + while ((nextPacket = packetInputStream.nextPacketTag()) != null) { + signatures.nextPacket(nextPacket); + switch (nextPacket) { + + // Literal Data - the literal data content is the new input stream + case LIT: + processLiteralData(); + break loop; + + // Compressed Data - the content contains another OpenPGP message + case COMP: + processCompressedData(); + break loop; + + // One Pass Signature + case OPS: + processOnePassSignature(); + break; + + // Signature - either prepended to the message, or corresponding to a One Pass Signature + case SIG: + processSignature(); + break; + + // Encrypted Data (ESKs and SED/SEIPD are parsed the same by BC) + case PKESK: + case SKESK: + case SED: + case SEIPD: + if (processEncryptedData()) { + // Successfully decrypted, enter nested content + break loop; + } + + throw new MissingDecryptionMethodException("No working decryption method found."); + + // Marker Packets need to be skipped and ignored + case MARKER: + LOGGER.debug("Skipping Marker Packet"); + packetInputStream.readMarker(); + break; + + // Key Packets are illegal in this context + case SK: + case PK: + case SSK: + case PSK: + case TRUST: + case UID: + case UATTR: + throw new MalformedOpenPgpMessageException("Illegal Packet in Stream: " + nextPacket); + + // MDC packet is usually processed by PGPEncryptedDataList, so it is very likely we encounter this + // packet out of order + case MDC: + throw new MalformedOpenPgpMessageException("Unexpected Packet in Stream: " + nextPacket); + + // Experimental Packets are not supported + case EXP_1: + case EXP_2: + case EXP_3: + case EXP_4: + throw new MalformedOpenPgpMessageException("Unsupported Packet in Stream: " + nextPacket); + } + } + } + + private void processLiteralData() throws IOException { + LOGGER.debug("Literal Data Packet at depth " + metadata.depth + " encountered"); + syntaxVerifier.next(InputSymbol.LiteralData); + PGPLiteralData literalData = packetInputStream.readLiteralData(); + // Extract Metadata + this.metadata.setChild(new MessageMetadata.LiteralData( + literalData.getFileName(), + literalData.getModificationTime(), + StreamEncoding.requireFromCode(literalData.getFormat()))); + + nestedInputStream = literalData.getDataStream(); + } + + private void processCompressedData() throws IOException, PGPException { + syntaxVerifier.next(InputSymbol.CompressedData); + signatures.enterNesting(); + PGPCompressedData compressedData = packetInputStream.readCompressedData(); + // Extract Metadata + MessageMetadata.CompressedData compressionLayer = new MessageMetadata.CompressedData( + CompressionAlgorithm.fromId(compressedData.getAlgorithm()), + metadata.depth + 1); + + LOGGER.debug("Compressed Data Packet (" + compressionLayer.algorithm + ") at depth " + metadata.depth + " encountered"); + InputStream decompressed = compressedData.getDataStream(); + nestedInputStream = new OpenPgpMessageInputStream(decompressed, options, compressionLayer, policy); + } + + private void processOnePassSignature() throws PGPException, IOException { + syntaxVerifier.next(InputSymbol.OnePassSignature); + PGPOnePassSignature onePassSignature = packetInputStream.readOnePassSignature(); + LOGGER.debug("One-Pass-Signature Packet by key " + KeyIdUtil.formatKeyId(onePassSignature.getKeyID()) + + " at depth " + metadata.depth + " encountered"); + signatures.addOnePassSignature(onePassSignature); + } + + private void processSignature() throws PGPException, IOException { + // true if Signature corresponds to OnePassSignature + boolean isSigForOPS = syntaxVerifier.peekStack() == StackSymbol.ops; + syntaxVerifier.next(InputSymbol.Signature); + PGPSignature signature; + try { + signature = packetInputStream.readSignature(); + } catch (UnsupportedPacketVersionException e) { + LOGGER.debug("Unsupported Signature at depth " + metadata.depth + " encountered.", e); + return; + } + + long keyId = SignatureUtils.determineIssuerKeyId(signature); + if (isSigForOPS) { + LOGGER.debug("Signature Packet corresponding to One-Pass-Signature by key " + + KeyIdUtil.formatKeyId(keyId) + + " at depth " + metadata.depth + " encountered"); + signatures.leaveNesting(); // TODO: Only leave nesting if all OPSs of the nesting layer are dealt with + signatures.addCorrespondingOnePassSignature(signature, metadata, policy); + } else { + LOGGER.debug("Prepended Signature Packet by key " + + KeyIdUtil.formatKeyId(keyId) + + " at depth " + metadata.depth + " encountered"); + signatures.addPrependedSignature(signature); + } + } + + private boolean processEncryptedData() throws IOException, PGPException { + LOGGER.debug("Symmetrically Encrypted Data Packet at depth " + metadata.depth + " encountered"); + syntaxVerifier.next(InputSymbol.EncryptedData); + PGPEncryptedDataList encDataList = packetInputStream.readEncryptedDataList(); + + // TODO: Replace with !encDataList.isIntegrityProtected() + // once BC ships it + if (!encDataList.get(0).isIntegrityProtected()) { + LOGGER.debug("Symmetrically Encrypted Data Packet is not integrity-protected and is therefore rejected."); + throw new MessageNotIntegrityProtectedException(); + } + + SortedESKs esks = new SortedESKs(encDataList); + LOGGER.debug("Symmetrically Encrypted Integrity-Protected Data has " + + esks.skesks.size() + " SKESK(s) and " + + (esks.pkesks.size() + esks.anonPkesks.size()) + " PKESK(s) from which " + + esks.anonPkesks.size() + " PKESK(s) have an anonymous recipient"); + + // Try custom decryptor factories + for (SubkeyIdentifier subkeyIdentifier : options.getCustomDecryptorFactories().keySet()) { + LOGGER.debug("Attempt decryption with custom decryptor factory with key " + subkeyIdentifier); + PublicKeyDataDecryptorFactory decryptorFactory = options.getCustomDecryptorFactories().get(subkeyIdentifier); + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + // find matching PKESK + if (pkesk.getKeyID() != subkeyIdentifier.getSubkeyId()) { + continue; + } + + // attempt decryption + if (decryptPKESKAndStream(subkeyIdentifier, decryptorFactory, pkesk)) { + return true; + } + } + } + + // Try provided session key + if (options.getSessionKey() != null) { + LOGGER.debug("Attempt decryption with provided session key"); + SessionKey sessionKey = options.getSessionKey(); + throwIfUnacceptable(sessionKey.getAlgorithm()); + + SessionKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getSessionKeyDataDecryptorFactory(sessionKey); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + sessionKey.getAlgorithm(), metadata.depth + 1); + + try { + // TODO: Use BCs new API once shipped + // see https://github.com/bcgit/bc-java/pull/1228 (discussion) + PGPEncryptedData esk = esks.all().get(0); + if (esk instanceof PGPPBEEncryptedData) { + PGPPBEEncryptedData skesk = (PGPPBEEncryptedData) esk; + InputStream decrypted = skesk.getDataStream(decryptorFactory); + encryptedData.sessionKey = sessionKey; + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); + nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); + LOGGER.debug("Successfully decrypted data with provided session key"); + return true; + } else if (esk instanceof PGPPublicKeyEncryptedData) { + PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + encryptedData.sessionKey = sessionKey; + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); + nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); + LOGGER.debug("Successfully decrypted data with provided session key"); + return true; + } else { + throw new RuntimeException("Unknown ESK class type: " + esk.getClass().getName()); + } + } catch (PGPException e) { + // Session key mismatch? + LOGGER.debug("Decryption using provided session key failed. Mismatched session key and message?", e); + } + } + + // Try passwords + for (Passphrase passphrase : options.getDecryptionPassphrases()) { + for (PGPPBEEncryptedData skesk : esks.skesks) { + LOGGER.debug("Attempt decryption with provided passphrase"); + SymmetricKeyAlgorithm encapsulationAlgorithm = SymmetricKeyAlgorithm.requireFromId(skesk.getAlgorithm()); + try { + throwIfUnacceptable(encapsulationAlgorithm); + } catch (UnacceptableAlgorithmException e) { + LOGGER.debug("Skipping SKESK with unacceptable encapsulation algorithm", e); + continue; + } + + PBEDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPBEDataDecryptorFactory(passphrase); + if (decryptSKESKAndStream(skesk, decryptorFactory)) { + return true; + } + } + } + + List> postponedDueToMissingPassphrase = new ArrayList<>(); + + // Try (known) secret keys + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + long keyId = pkesk.getKeyID(); + LOGGER.debug("Encountered PKESK for recipient " + KeyIdUtil.formatKeyId(keyId)); + PGPSecretKeyRing decryptionKeys = getDecryptionKey(keyId); + if (decryptionKeys == null) { + LOGGER.debug("Skipping PKESK because no matching key " + KeyIdUtil.formatKeyId(keyId) + " was provided"); + continue; + } + PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId); + SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); + if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + continue; + } + LOGGER.debug("Attempt decryption using secret key " + decryptionKeyId); + + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeys); + // Postpone keys with missing passphrase + if (!protector.hasPassphraseFor(keyId)) { + LOGGER.debug("Missing passphrase for key " + decryptionKeyId + ". Postponing decryption until all other keys were tried"); + postponedDueToMissingPassphrase.add(new Tuple<>(secretKey, pkesk)); + continue; + } + + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); + if (decryptWithPrivateKey(privateKey, decryptionKeyId, pkesk)) { + return true; + } + } + + // try anonymous secret keys + for (PGPPublicKeyEncryptedData pkesk : esks.anonPkesks) { + for (Tuple decryptionKeyCandidate : findPotentialDecryptionKeys(pkesk)) { + PGPSecretKeyRing decryptionKeys = decryptionKeyCandidate.getA(); + PGPSecretKey secretKey = decryptionKeyCandidate.getB(); + SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKeys, secretKey.getKeyID()); + if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + continue; + } + LOGGER.debug("Attempt decryption of anonymous PKESK with key " + decryptionKeyId); + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKeyCandidate.getA()); + if (!protector.hasPassphraseFor(secretKey.getKeyID())) { + LOGGER.debug("Missing passphrase for key " + decryptionKeyId + ". Postponing decryption until all other keys were tried."); + postponedDueToMissingPassphrase.add(new Tuple<>(secretKey, pkesk)); + continue; + } + + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); + if (decryptWithPrivateKey(privateKey, decryptionKeyId, pkesk)) { + return true; + } + } + } + + if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.THROW_EXCEPTION) { + // Non-interactive mode: Throw an exception with all locked decryption keys + Set keyIds = new HashSet<>(); + for (Tuple k : postponedDueToMissingPassphrase) { + PGPSecretKey key = k.getA(); + PGPSecretKeyRing keys = getDecryptionKey(key.getKeyID()); + keyIds.add(new SubkeyIdentifier(keys, key.getKeyID())); + } + if (!keyIds.isEmpty()) { + throw new MissingPassphraseException(keyIds); + } + } else if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.INTERACTIVE) { + for (PGPPublicKeyEncryptedData pkesk : esks.pkesks) { + // Interactive mode: Fire protector callbacks to get passphrases interactively + for (Tuple missingPassphrases : postponedDueToMissingPassphrase) { + PGPSecretKey secretKey = missingPassphrases.getA(); + long keyId = secretKey.getKeyID(); + PGPSecretKeyRing decryptionKey = getDecryptionKey(keyId); + SubkeyIdentifier decryptionKeyId = new SubkeyIdentifier(decryptionKey, keyId); + if (hasUnsupportedS2KSpecifier(secretKey, decryptionKeyId)) { + continue; + } + + LOGGER.debug("Attempt decryption with key " + decryptionKeyId + " while interactively requesting its passphrase"); + SecretKeyRingProtector protector = options.getSecretKeyProtector(decryptionKey); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKey, protector); + if (decryptWithPrivateKey(privateKey, decryptionKeyId, pkesk)) { + return true; + } + } + } + } else { + throw new IllegalStateException("Invalid PostponedKeysStrategy set in consumer options."); + } + + // we did not yet succeed in decrypting any session key :/ + + LOGGER.debug("Failed to decrypt encrypted data packet"); + return false; + } + + private boolean decryptWithPrivateKey(PGPPrivateKey privateKey, + SubkeyIdentifier decryptionKeyId, + PGPPublicKeyEncryptedData pkesk) + throws PGPException, IOException { + PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance() + .getPublicKeyDataDecryptorFactory(privateKey); + return decryptPKESKAndStream(decryptionKeyId, decryptorFactory, pkesk); + } + + private static boolean hasUnsupportedS2KSpecifier(PGPSecretKey secretKey, SubkeyIdentifier decryptionKeyId) { + S2K s2K = secretKey.getS2K(); + if (s2K != null) { + int s2kType = s2K.getType(); + if (s2kType >= 100 && s2kType <= 110) { + LOGGER.debug("Skipping PKESK because key " + decryptionKeyId + " has unsupported private S2K specifier " + s2kType); + return true; + } + } + return false; + } + + private boolean decryptSKESKAndStream(PGPPBEEncryptedData skesk, PBEDataDecryptorFactory decryptorFactory) + throws IOException, UnacceptableAlgorithmException { + try { + InputStream decrypted = skesk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(skesk.getSessionKey(decryptorFactory)); + throwIfUnacceptable(sessionKey.getAlgorithm()); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + sessionKey.getAlgorithm(), metadata.depth + 1); + encryptedData.sessionKey = sessionKey; + LOGGER.debug("Successfully decrypted data with passphrase"); + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, skesk, options); + nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); + return true; + } catch (UnacceptableAlgorithmException e) { + throw e; + } catch (PGPException e) { + LOGGER.debug("Decryption of encrypted data packet using password failed. Password mismatch?", e); + } + return false; + } + + private boolean decryptPKESKAndStream(SubkeyIdentifier decryptionKeyId, + PublicKeyDataDecryptorFactory decryptorFactory, + PGPPublicKeyEncryptedData pkesk) + throws IOException, UnacceptableAlgorithmException { + try { + InputStream decrypted = pkesk.getDataStream(decryptorFactory); + SessionKey sessionKey = new SessionKey(pkesk.getSessionKey(decryptorFactory)); + throwIfUnacceptable(sessionKey.getAlgorithm()); + + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData( + SymmetricKeyAlgorithm.requireFromId(pkesk.getSymmetricAlgorithm(decryptorFactory)), + metadata.depth + 1); + encryptedData.decryptionKey = decryptionKeyId; + encryptedData.sessionKey = sessionKey; + + LOGGER.debug("Successfully decrypted data with key " + decryptionKeyId); + IntegrityProtectedInputStream integrityProtected = new IntegrityProtectedInputStream(decrypted, pkesk, options); + nestedInputStream = new OpenPgpMessageInputStream(integrityProtected, options, encryptedData, policy); + return true; + } catch (UnacceptableAlgorithmException e) { + throw e; + } catch (PGPException e) { + LOGGER.debug("Decryption of encrypted data packet using secret key failed.", e); + } + return false; + } + + private void throwIfUnacceptable(SymmetricKeyAlgorithm algorithm) + throws UnacceptableAlgorithmException { + if (!policy.getSymmetricKeyDecryptionAlgorithmPolicy().isAcceptable(algorithm)) { + throw new UnacceptableAlgorithmException("Symmetric-Key algorithm " + algorithm + " is not acceptable for message decryption."); + } + } + + private List> findPotentialDecryptionKeys(PGPPublicKeyEncryptedData pkesk) { + int algorithm = pkesk.getAlgorithm(); + List> decryptionKeyCandidates = new ArrayList<>(); + + for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) { + KeyRingInfo info = PGPainless.inspectKeyRing(secretKeys); + for (PGPPublicKey publicKey : info.getEncryptionSubkeys(EncryptionPurpose.ANY)) { + if (publicKey.getAlgorithm() == algorithm && info.isSecretKeyAvailable(publicKey.getKeyID())) { + PGPSecretKey candidate = secretKeys.getSecretKey(publicKey.getKeyID()); + decryptionKeyCandidates.add(new Tuple<>(secretKeys, candidate)); + } + } + } + return decryptionKeyCandidates; + } + + private PGPSecretKeyRing getDecryptionKey(long keyID) { + for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) { + PGPSecretKey decryptionKey = secretKeys.getSecretKey(keyID); + if (decryptionKey == null) { + continue; + } + + KeyRingInfo info = new KeyRingInfo(secretKeys, policy, new Date()); + List encryptionKeys = info.getEncryptionSubkeys(EncryptionPurpose.ANY); + for (PGPPublicKey key : encryptionKeys) { + if (key.getKeyID() == keyID) { + return secretKeys; + } + } + + LOGGER.debug("Subkey " + Long.toHexString(keyID) + " cannot be used for decryption."); + } + return null; + } + + @Override + public int read() throws IOException { + if (nestedInputStream == null) { + if (packetInputStream != null) { + syntaxVerifier.assertValid(); + } + return -1; + } + + int r; + try { + r = nestedInputStream.read(); + } catch (IOException e) { + r = -1; + } + boolean eos = r == -1; + if (!eos) { + byte b = (byte) r; + signatures.updateLiteral(b); + } else { + nestedInputStream.close(); + collectMetadata(); + nestedInputStream = null; + + if (packetInputStream != null) { + try { + consumePackets(); + } catch (PGPException e) { + throw new RuntimeException(e); + } + } + signatures.finish(metadata, policy); + } + return r; + } + + @Override + public int read(@Nonnull byte[] b, int off, int len) + throws IOException { + if (nestedInputStream == null) { + if (packetInputStream != null) { + syntaxVerifier.assertValid(); + } + return -1; + } + + int r = nestedInputStream.read(b, off, len); + if (r != -1) { + signatures.updateLiteral(b, off, r); + } else { + nestedInputStream.close(); + collectMetadata(); + nestedInputStream = null; + + if (packetInputStream != null) { + try { + consumePackets(); + } catch (PGPException e) { + throw new RuntimeException(e); + } + } + signatures.finish(metadata, policy); + } + return r; + } + + @Override + public void close() throws IOException { + super.close(); + if (closed) { + if (packetInputStream != null) { + syntaxVerifier.assertValid(); + } + return; + } + + if (nestedInputStream != null) { + nestedInputStream.close(); + collectMetadata(); + nestedInputStream = null; + } + + try { + consumePackets(); + } catch (PGPException e) { + throw new RuntimeException(e); + } + + if (packetInputStream != null) { + syntaxVerifier.next(InputSymbol.EndOfSequence); + syntaxVerifier.assertValid(); + packetInputStream.close(); + } + closed = true; + } + + private void collectMetadata() { + if (nestedInputStream instanceof OpenPgpMessageInputStream) { + OpenPgpMessageInputStream child = (OpenPgpMessageInputStream) nestedInputStream; + this.metadata.setChild((MessageMetadata.Nested) child.metadata); + } + } + + public MessageMetadata getMetadata() { + if (!closed) { + throw new IllegalStateException("Stream must be closed before access to metadata can be granted."); + } + + return new MessageMetadata((MessageMetadata.Message) metadata); + } + + private static class SortedESKs { + + private final List skesks = new ArrayList<>(); + private final List pkesks = new ArrayList<>(); + private final List anonPkesks = new ArrayList<>(); + + SortedESKs(PGPEncryptedDataList esks) { + for (PGPEncryptedData esk : esks) { + if (esk instanceof PGPPBEEncryptedData) { + skesks.add((PGPPBEEncryptedData) esk); + } + else if (esk instanceof PGPPublicKeyEncryptedData) { + PGPPublicKeyEncryptedData pkesk = (PGPPublicKeyEncryptedData) esk; + if (pkesk.getKeyID() != 0) { + pkesks.add(pkesk); + } else { + anonPkesks.add(pkesk); + } + } else { + throw new IllegalArgumentException("Unknown ESK class type."); + } + } + } + + public List all() { + List esks = new ArrayList<>(); + esks.addAll(skesks); + esks.addAll(pkesks); + esks.addAll(anonPkesks); + return esks; + } + } + + @Override + public OpenPgpMetadata getResult() { + MessageMetadata m = getMetadata(); + resultBuilder.setCompressionAlgorithm(m.getCompressionAlgorithm()); + resultBuilder.setModificationDate(m.getModificationDate()); + resultBuilder.setFileName(m.getFilename()); + resultBuilder.setFileEncoding(m.getFormat()); + resultBuilder.setSessionKey(m.getSessionKey()); + resultBuilder.setDecryptionKey(m.getDecryptionKey()); + + for (SignatureVerification accepted : m.getVerifiedDetachedSignatures()) { + resultBuilder.addVerifiedDetachedSignature(accepted); + } + for (SignatureVerification.Failure rejected : m.getRejectedDetachedSignatures()) { + resultBuilder.addInvalidDetachedSignature(rejected.getSignatureVerification(), rejected.getValidationException()); + } + + for (SignatureVerification accepted : m.getVerifiedInlineSignatures()) { + resultBuilder.addVerifiedInbandSignature(accepted); + } + for (SignatureVerification.Failure rejected : m.getRejectedInlineSignatures()) { + resultBuilder.addInvalidInbandSignature(rejected.getSignatureVerification(), rejected.getValidationException()); + } + + return resultBuilder.build(); + } + + // In 'OPS LIT("Foo") SIG', OPS is only updated with "Foo" + // In 'OPS[1] OPS LIT("Foo") SIG SIG', OPS[1] (nested) is updated with OPS LIT("Foo") SIG. + // Therefore, we need to handle the innermost signature layer differently when updating with Literal data. + // Furthermore, For 'OPS COMP(LIT("Foo")) SIG', the signature is updated with "Foo". CHAOS!!! + private static final class Signatures extends OutputStream { + final ConsumerOptions options; + final List detachedSignatures; + final List prependedSignatures; + final List onePassSignatures; + final Stack> opsUpdateStack; + List literalOPS = new ArrayList<>(); + final List correspondingSignatures; + final List prependedSignaturesWithMissingCert = new ArrayList<>(); + final List inbandSignaturesWithMissingCert = new ArrayList<>(); + final List detachedSignaturesWithMissingCert = new ArrayList<>(); + boolean isLiteral = true; + + private Signatures(ConsumerOptions options) { + this.options = options; + this.detachedSignatures = new ArrayList<>(); + this.prependedSignatures = new ArrayList<>(); + this.onePassSignatures = new ArrayList<>(); + this.opsUpdateStack = new Stack<>(); + this.correspondingSignatures = new ArrayList<>(); + } + + void addDetachedSignatures(Collection signatures) { + for (PGPSignature signature : signatures) { + addDetachedSignature(signature); + } + } + + void addDetachedSignature(PGPSignature signature) { + SignatureCheck check = initializeSignature(signature); + long keyId = SignatureUtils.determineIssuerKeyId(signature); + if (check != null) { + detachedSignatures.add(check); + } else { + LOGGER.debug("No suitable certificate for verification of signature by key " + KeyIdUtil.formatKeyId(keyId) + " found."); + this.detachedSignaturesWithMissingCert.add(new SignatureVerification.Failure( + new SignatureVerification(signature, null), + new SignatureValidationException("Missing verification key") + )); + } + } + + void addPrependedSignature(PGPSignature signature) { + SignatureCheck check = initializeSignature(signature); + long keyId = SignatureUtils.determineIssuerKeyId(signature); + if (check != null) { + this.prependedSignatures.add(check); + } else { + LOGGER.debug("No suitable certificate for verification of signature by key " + KeyIdUtil.formatKeyId(keyId) + " found."); + this.prependedSignaturesWithMissingCert.add(new SignatureVerification.Failure( + new SignatureVerification(signature, null), + new SignatureValidationException("Missing verification key") + )); + } + } + + SignatureCheck initializeSignature(PGPSignature signature) { + long keyId = SignatureUtils.determineIssuerKeyId(signature); + PGPPublicKeyRing certificate = findCertificate(keyId); + if (certificate == null) { + return null; + } + + SubkeyIdentifier verifierKey = new SubkeyIdentifier(certificate, keyId); + initialize(signature, certificate, keyId); + return new SignatureCheck(signature, certificate, verifierKey); + } + + void addOnePassSignature(PGPOnePassSignature signature) { + PGPPublicKeyRing certificate = findCertificate(signature.getKeyID()); + + if (certificate != null) { + OnePassSignatureCheck ops = new OnePassSignatureCheck(signature, certificate); + initialize(signature, certificate); + onePassSignatures.add(ops); + + literalOPS.add(ops); + } + if (signature.isContaining()) { + enterNesting(); + } + } + + void addCorrespondingOnePassSignature(PGPSignature signature, MessageMetadata.Layer layer, Policy policy) { + boolean found = false; + long keyId = SignatureUtils.determineIssuerKeyId(signature); + for (int i = onePassSignatures.size() - 1; i >= 0; i--) { + OnePassSignatureCheck onePassSignature = onePassSignatures.get(i); + if (onePassSignature.getOnePassSignature().getKeyID() != keyId) { + continue; + } + found = true; + + if (onePassSignature.getSignature() != null) { + continue; + } + + onePassSignature.setSignature(signature); + SignatureVerification verification = new SignatureVerification(signature, + new SubkeyIdentifier(onePassSignature.getVerificationKeys(), onePassSignature.getOnePassSignature().getKeyID())); + + try { + SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) + .verify(signature); + CertificateValidator.validateCertificateAndVerifyOnePassSignature(onePassSignature, policy); + LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); + layer.addVerifiedOnePassSignature(verification); + } catch (SignatureValidationException e) { + LOGGER.debug("Rejected signature by key " + verification.getSigningKey(), e); + layer.addRejectedOnePassSignature(new SignatureVerification.Failure(verification, e)); + } + break; + } + + if (!found) { + LOGGER.debug("No suitable certificate for verification of signature by key " + KeyIdUtil.formatKeyId(keyId) + " found."); + inbandSignaturesWithMissingCert.add(new SignatureVerification.Failure( + new SignatureVerification(signature, null), + new SignatureValidationException("Missing verification key."))); + } + } + + void enterNesting() { + opsUpdateStack.push(literalOPS); + literalOPS = new ArrayList<>(); + } + + void leaveNesting() { + if (opsUpdateStack.isEmpty()) { + return; + } + opsUpdateStack.pop(); + } + + private static void initialize(@Nonnull PGPSignature signature, @Nonnull PGPPublicKeyRing certificate, long keyId) { + PGPContentVerifierBuilderProvider verifierProvider = ImplementationFactory.getInstance() + .getPGPContentVerifierBuilderProvider(); + try { + signature.init(verifierProvider, certificate.getPublicKey(keyId)); + } catch (PGPException e) { + throw new RuntimeException(e); + } + } + + private static void initialize(@Nonnull PGPOnePassSignature ops, @Nonnull PGPPublicKeyRing certificate) { + PGPContentVerifierBuilderProvider verifierProvider = ImplementationFactory.getInstance() + .getPGPContentVerifierBuilderProvider(); + try { + ops.init(verifierProvider, certificate.getPublicKey(ops.getKeyID())); + } catch (PGPException e) { + throw new RuntimeException(e); + } + } + + private PGPPublicKeyRing findCertificate(long keyId) { + for (PGPPublicKeyRing cert : options.getCertificates()) { + PGPPublicKey verificationKey = cert.getPublicKey(keyId); + if (verificationKey != null) { + return cert; + } + } + + if (options.getMissingCertificateCallback() != null) { + return options.getMissingCertificateCallback().onMissingPublicKeyEncountered(keyId); + } + return null; // TODO: Missing cert for sig + } + + public void updateLiteral(byte b) { + for (OnePassSignatureCheck ops : literalOPS) { + ops.getOnePassSignature().update(b); + } + + for (SignatureCheck detached : detachedSignatures) { + detached.getSignature().update(b); + } + + for (SignatureCheck prepended : prependedSignatures) { + prepended.getSignature().update(b); + } + } + + public void updateLiteral(byte[] b, int off, int len) { + for (OnePassSignatureCheck ops : literalOPS) { + ops.getOnePassSignature().update(b, off, len); + } + + for (SignatureCheck detached : detachedSignatures) { + detached.getSignature().update(b, off, len); + } + + for (SignatureCheck prepended : prependedSignatures) { + prepended.getSignature().update(b, off, len); + } + } + + public void updatePacket(byte b) { + for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { + List nestedOPSs = opsUpdateStack.get(i); + for (OnePassSignatureCheck ops : nestedOPSs) { + ops.getOnePassSignature().update(b); + } + } + } + + public void updatePacket(byte[] buf, int off, int len) { + for (int i = opsUpdateStack.size() - 1; i >= 0; i--) { + List nestedOPSs = opsUpdateStack.get(i); + for (OnePassSignatureCheck ops : nestedOPSs) { + ops.getOnePassSignature().update(buf, off, len); + } + } + } + + public void finish(MessageMetadata.Layer layer, Policy policy) { + for (SignatureCheck detached : detachedSignatures) { + SignatureVerification verification = new SignatureVerification(detached.getSignature(), detached.getSigningKeyIdentifier()); + try { + SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) + .verify(detached.getSignature()); + CertificateValidator.validateCertificateAndVerifyInitializedSignature( + detached.getSignature(), KeyRingUtils.publicKeys(detached.getSigningKeyRing()), policy); + LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); + layer.addVerifiedDetachedSignature(verification); + } catch (SignatureValidationException e) { + LOGGER.debug("Rejected signature by key " + verification.getSigningKey(), e); + layer.addRejectedDetachedSignature(new SignatureVerification.Failure(verification, e)); + } + } + + for (SignatureCheck prepended : prependedSignatures) { + SignatureVerification verification = new SignatureVerification(prepended.getSignature(), prepended.getSigningKeyIdentifier()); + try { + SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) + .verify(prepended.getSignature()); + CertificateValidator.validateCertificateAndVerifyInitializedSignature( + prepended.getSignature(), KeyRingUtils.publicKeys(prepended.getSigningKeyRing()), policy); + LOGGER.debug("Acceptable signature by key " + verification.getSigningKey()); + layer.addVerifiedPrependedSignature(verification); + } catch (SignatureValidationException e) { + LOGGER.debug("Rejected signature by key " + verification.getSigningKey(), e); + layer.addRejectedPrependedSignature(new SignatureVerification.Failure(verification, e)); + } + } + + for (SignatureVerification.Failure rejected : inbandSignaturesWithMissingCert) { + layer.addRejectedOnePassSignature(rejected); + } + + for (SignatureVerification.Failure rejected : prependedSignaturesWithMissingCert) { + layer.addRejectedPrependedSignature(rejected); + } + + for (SignatureVerification.Failure rejected : detachedSignaturesWithMissingCert) { + layer.addRejectedDetachedSignature(rejected); + } + } + + @Override + public void write(int b) { + updatePacket((byte) b); + } + + @Override + public void write(@Nonnull byte[] b, int off, int len) { + updatePacket(b, off, len); + } + + public void nextPacket(OpenPgpPacket nextPacket) { + if (nextPacket == OpenPgpPacket.LIT) { + isLiteral = true; + if (literalOPS.isEmpty() && !opsUpdateStack.isEmpty()) { + literalOPS = opsUpdateStack.pop(); + } + } else { + isLiteral = false; + } + } + + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java deleted file mode 100644 index 275acc17..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/SignatureInputStream.java +++ /dev/null @@ -1,215 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import static org.pgpainless.signature.consumer.SignatureValidator.signatureWasCreatedInBounds; - -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import java.util.Map; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPObjectFactory; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPSignatureList; -import org.pgpainless.PGPainless; -import org.pgpainless.exception.SignatureValidationException; -import org.pgpainless.policy.Policy; -import org.pgpainless.signature.consumer.CertificateValidator; -import org.pgpainless.signature.consumer.DetachedSignatureCheck; -import org.pgpainless.signature.consumer.OnePassSignatureCheck; -import org.pgpainless.signature.SignatureUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public abstract class SignatureInputStream extends FilterInputStream { - - protected SignatureInputStream(InputStream inputStream) { - super(inputStream); - } - - public static class VerifySignatures extends SignatureInputStream { - - private static final Logger LOGGER = LoggerFactory.getLogger(VerifySignatures.class); - - private final PGPObjectFactory objectFactory; - private final List opSignatures; - private final Map opSignaturesWithMissingCert; - private final List detachedSignatures; - private final ConsumerOptions options; - private final OpenPgpMetadata.Builder resultBuilder; - - public VerifySignatures( - InputStream literalDataStream, - @Nullable PGPObjectFactory objectFactory, - List opSignatures, - Map onePassSignaturesWithMissingCert, - List detachedSignatures, - ConsumerOptions options, - OpenPgpMetadata.Builder resultBuilder) { - super(literalDataStream); - this.objectFactory = objectFactory; - this.opSignatures = opSignatures; - this.opSignaturesWithMissingCert = onePassSignaturesWithMissingCert; - this.detachedSignatures = detachedSignatures; - this.options = options; - this.resultBuilder = resultBuilder; - } - - @Override - public int read() throws IOException { - final int data = super.read(); - final boolean endOfStream = data == -1; - if (endOfStream) { - finalizeSignatures(); - } else { - byte b = (byte) data; - updateOnePassSignatures(b); - updateDetachedSignatures(b); - } - return data; - } - - @Override - public int read(@Nonnull byte[] b, int off, int len) throws IOException { - int read = super.read(b, off, len); - - final boolean endOfStream = read == -1; - if (endOfStream) { - finalizeSignatures(); - } else { - updateOnePassSignatures(b, off, read); - updateDetachedSignatures(b, off, read); - } - return read; - } - - private void finalizeSignatures() { - parseAndCombineSignatures(); - verifyOnePassSignatures(); - verifyDetachedSignatures(); - } - - public void parseAndCombineSignatures() { - if (objectFactory == null) { - return; - } - // Parse signatures from message - PGPSignatureList signatures; - try { - signatures = parseSignatures(objectFactory); - } catch (IOException e) { - return; - } - List signatureList = SignatureUtils.toList(signatures); - // Set signatures as comparison sigs in OPS checks - for (int i = 0; i < opSignatures.size(); i++) { - int reversedIndex = opSignatures.size() - i - 1; - opSignatures.get(i).setSignature(signatureList.get(reversedIndex)); - } - - for (PGPSignature signature : signatureList) { - if (opSignaturesWithMissingCert.containsKey(signature.getKeyID())) { - OnePassSignatureCheck check = opSignaturesWithMissingCert.remove(signature.getKeyID()); - check.setSignature(signature); - - resultBuilder.addInvalidInbandSignature(new SignatureVerification(signature, null), - new SignatureValidationException( - "Missing verification certificate " + Long.toHexString(signature.getKeyID()))); - } - } - } - - private PGPSignatureList parseSignatures(PGPObjectFactory objectFactory) throws IOException { - PGPSignatureList signatureList = null; - Object pgpObject = objectFactory.nextObject(); - while (pgpObject != null && signatureList == null) { - if (pgpObject instanceof PGPSignatureList) { - signatureList = (PGPSignatureList) pgpObject; - } else { - pgpObject = objectFactory.nextObject(); - } - } - - if (signatureList == null || signatureList.isEmpty()) { - throw new IOException("Verification failed - No Signatures found"); - } - - return signatureList; - } - - - private synchronized void verifyOnePassSignatures() { - Policy policy = PGPainless.getPolicy(); - for (OnePassSignatureCheck opSignature : opSignatures) { - if (opSignature.getSignature() == null) { - LOGGER.warn("Found OnePassSignature without respective signature packet -> skip"); - continue; - } - - try { - signatureWasCreatedInBounds(options.getVerifyNotBefore(), - options.getVerifyNotAfter()).verify(opSignature.getSignature()); - CertificateValidator.validateCertificateAndVerifyOnePassSignature(opSignature, policy); - resultBuilder.addVerifiedInbandSignature( - new SignatureVerification(opSignature.getSignature(), opSignature.getSigningKey())); - } catch (SignatureValidationException e) { - LOGGER.warn("One-pass-signature verification failed for signature made by key {}: {}", - opSignature.getSigningKey(), e.getMessage(), e); - resultBuilder.addInvalidInbandSignature( - new SignatureVerification(opSignature.getSignature(), opSignature.getSigningKey()), e); - } - } - } - - private void verifyDetachedSignatures() { - Policy policy = PGPainless.getPolicy(); - for (DetachedSignatureCheck s : detachedSignatures) { - try { - signatureWasCreatedInBounds(options.getVerifyNotBefore(), - options.getVerifyNotAfter()).verify(s.getSignature()); - CertificateValidator.validateCertificateAndVerifyInitializedSignature(s.getSignature(), - (PGPPublicKeyRing) s.getSigningKeyRing(), policy); - resultBuilder.addVerifiedDetachedSignature(new SignatureVerification(s.getSignature(), - s.getSigningKeyIdentifier())); - } catch (SignatureValidationException e) { - LOGGER.warn("One-pass-signature verification failed for signature made by key {}: {}", - s.getSigningKeyIdentifier(), e.getMessage(), e); - resultBuilder.addInvalidDetachedSignature(new SignatureVerification(s.getSignature(), - s.getSigningKeyIdentifier()), e); - } - } - } - - private void updateOnePassSignatures(byte data) { - for (OnePassSignatureCheck opSignature : opSignatures) { - opSignature.getOnePassSignature().update(data); - } - } - - private void updateOnePassSignatures(byte[] bytes, int offset, int length) { - for (OnePassSignatureCheck opSignature : opSignatures) { - opSignature.getOnePassSignature().update(bytes, offset, length); - } - } - - private void updateDetachedSignatures(byte b) { - for (DetachedSignatureCheck detachedSignature : detachedSignatures) { - detachedSignature.getSignature().update(b); - } - } - - private void updateDetachedSignatures(byte[] b, int off, int read) { - for (DetachedSignatureCheck detachedSignature : detachedSignatures) { - detachedSignature.getSignature().update(b, off, read); - } - } - - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java new file mode 100644 index 00000000..28b415e0 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/TeeBCPGInputStream.java @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.NoSuchElementException; + +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.MarkerPacket; +import org.bouncycastle.bcpg.Packet; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPOnePassSignature; +import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.algorithm.OpenPgpPacket; + +/** + * Since we need to update signatures with data from the underlying stream, this class is used to tee out the data. + * Unfortunately we cannot simply override {@link BCPGInputStream#read()} to tee the data out though, since + * {@link BCPGInputStream#readPacket()} inconsistently calls a mix of {@link BCPGInputStream#read()} and + * {@link InputStream#read()} of the underlying stream. This would cause the second length byte to get swallowed up. + * + * Therefore, this class delegates the teeing to an {@link DelayedTeeInputStream} which wraps the underlying + * stream. Since calling {@link BCPGInputStream#nextPacketTag()} reads up to and including the next packets tag, + * we need to delay teeing out that byte to signature verifiers. + * Hence, the reading methods of the {@link TeeBCPGInputStream} handle pushing this byte to the output stream using + * {@link DelayedTeeInputStream#squeeze()}. + */ +public class TeeBCPGInputStream { + + protected final DelayedTeeInputStream delayedTee; + // InputStream of OpenPGP packets of the current layer + protected final BCPGInputStream packetInputStream; + + public TeeBCPGInputStream(BCPGInputStream inputStream, OutputStream outputStream) { + this.delayedTee = new DelayedTeeInputStream(inputStream, outputStream); + this.packetInputStream = BCPGInputStream.wrap(delayedTee); + } + + public OpenPgpPacket nextPacketTag() throws IOException { + int tag = packetInputStream.nextPacketTag(); + if (tag == -1) { + return null; + } + + OpenPgpPacket packet; + try { + packet = OpenPgpPacket.requireFromTag(tag); + } catch (NoSuchElementException e) { + throw e; + } + return packet; + } + + public Packet readPacket() throws IOException { + return packetInputStream.readPacket(); + } + + public PGPCompressedData readCompressedData() throws IOException { + delayedTee.squeeze(); + PGPCompressedData compressedData = new PGPCompressedData(packetInputStream); + return compressedData; + } + + public PGPLiteralData readLiteralData() throws IOException { + delayedTee.squeeze(); + return new PGPLiteralData(packetInputStream); + } + + public PGPEncryptedDataList readEncryptedDataList() throws IOException { + delayedTee.squeeze(); + return new PGPEncryptedDataList(packetInputStream); + } + + public PGPOnePassSignature readOnePassSignature() throws PGPException, IOException { + PGPOnePassSignature onePassSignature = new PGPOnePassSignature(packetInputStream); + delayedTee.squeeze(); + return onePassSignature; + } + + public PGPSignature readSignature() throws PGPException, IOException { + PGPSignature signature = new PGPSignature(packetInputStream); + delayedTee.squeeze(); + return signature; + } + + public MarkerPacket readMarker() throws IOException { + MarkerPacket markerPacket = (MarkerPacket) readPacket(); + delayedTee.squeeze(); + return markerPacket; + } + + public void close() throws IOException { + this.packetInputStream.close(); + } + + public static class DelayedTeeInputStream extends InputStream { + + private int last = -1; + private final InputStream inputStream; + private final OutputStream outputStream; + + public DelayedTeeInputStream(InputStream inputStream, OutputStream outputStream) { + this.inputStream = inputStream; + this.outputStream = outputStream; + } + + @Override + public int read() throws IOException { + if (last != -1) { + outputStream.write(last); + } + try { + last = inputStream.read(); + return last; + } catch (IOException e) { + if ("crc check failed in armored message.".equals(e.getMessage())) { + throw e; + } + return -1; + } + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (last != -1) { + outputStream.write(last); + } + + int r = inputStream.read(b, off, len); + if (r > 0) { + outputStream.write(b, off, r - 1); + last = b[off + r - 1]; + } else { + last = -1; + } + return r; + } + + /** + * Squeeze the last byte out and update the output stream. + * + * @throws IOException in case of an IO error + */ + public void squeeze() throws IOException { + if (last != -1) { + outputStream.write(last); + } + last = -1; + } + + @Override + public void close() throws IOException { + inputStream.close(); + outputStream.close(); + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputSymbol.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputSymbol.java new file mode 100644 index 00000000..854c3305 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/InputSymbol.java @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check; + +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPOnePassSignatureList; +import org.bouncycastle.openpgp.PGPSignatureList; + +public enum InputSymbol { + /** + * A {@link PGPLiteralData} packet. + */ + LiteralData, + /** + * A {@link PGPSignatureList} object. + */ + Signature, + /** + * A {@link PGPOnePassSignatureList} object. + */ + OnePassSignature, + /** + * A {@link PGPCompressedData} packet. + * The contents of this packet MUST form a valid OpenPGP message, so a nested PDA is opened to verify + * its nested packet sequence. + */ + CompressedData, + /** + * A {@link PGPEncryptedDataList} object. + * This object combines multiple ESKs and the corresponding Symmetrically Encrypted + * (possibly Integrity Protected) Data packet. + */ + EncryptedData, + /** + * Marks the end of a (sub-) sequence. + * This input is given if the end of an OpenPGP message is reached. + * This might be the case for the end of the whole ciphertext, or the end of a packet with nested contents + * (e.g. the end of a Compressed Data packet). + */ + EndOfSequence +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java new file mode 100644 index 00000000..c6de8765 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/OpenPgpMessageSyntax.java @@ -0,0 +1,133 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check; + +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * This class describes the syntax for OpenPGP messages as specified by rfc4880. + * + * @see + * rfc4880 - §11.3. OpenPGP Messages + * @see + * Blog post about theoretic background and translation of grammar to PDA syntax + * @see + * Blog post about practically implementing the PDA for packet syntax validation + */ +public class OpenPgpMessageSyntax implements Syntax { + + @Override + public @Nonnull Transition transition(@Nonnull State from, @Nonnull InputSymbol input, @Nullable StackSymbol stackItem) + throws MalformedOpenPgpMessageException { + switch (from) { + case OpenPgpMessage: + return fromOpenPgpMessage(input, stackItem); + case LiteralMessage: + return fromLiteralMessage(input, stackItem); + case CompressedMessage: + return fromCompressedMessage(input, stackItem); + case EncryptedMessage: + return fromEncryptedMessage(input, stackItem); + case Valid: + return fromValid(input, stackItem); + } + + throw new MalformedOpenPgpMessageException(from, input, stackItem); + } + + Transition fromOpenPgpMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) + throws MalformedOpenPgpMessageException { + if (stackItem != StackSymbol.msg) { + throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem); + } + + switch (input) { + case LiteralData: + return new Transition(State.LiteralMessage); + + case Signature: + return new Transition(State.OpenPgpMessage, StackSymbol.msg); + + case OnePassSignature: + return new Transition(State.OpenPgpMessage, StackSymbol.ops, StackSymbol.msg); + + case CompressedData: + return new Transition(State.CompressedMessage); + + case EncryptedData: + return new Transition(State.EncryptedMessage); + + case EndOfSequence: + default: + throw new MalformedOpenPgpMessageException(State.OpenPgpMessage, input, stackItem); + } + } + + Transition fromLiteralMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) + throws MalformedOpenPgpMessageException { + switch (input) { + case Signature: + if (stackItem == StackSymbol.ops) { + return new Transition(State.LiteralMessage); + } + break; + + case EndOfSequence: + if (stackItem == StackSymbol.terminus) { + return new Transition(State.Valid); + } + break; + } + + throw new MalformedOpenPgpMessageException(State.LiteralMessage, input, stackItem); + } + + Transition fromCompressedMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) + throws MalformedOpenPgpMessageException { + switch (input) { + case Signature: + if (stackItem == StackSymbol.ops) { + return new Transition(State.CompressedMessage); + } + break; + + case EndOfSequence: + if (stackItem == StackSymbol.terminus) { + return new Transition(State.Valid); + } + break; + } + + throw new MalformedOpenPgpMessageException(State.CompressedMessage, input, stackItem); + } + + Transition fromEncryptedMessage(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) + throws MalformedOpenPgpMessageException { + switch (input) { + case Signature: + if (stackItem == StackSymbol.ops) { + return new Transition(State.EncryptedMessage); + } + break; + + case EndOfSequence: + if (stackItem == StackSymbol.terminus) { + return new Transition(State.Valid); + } + break; + } + + throw new MalformedOpenPgpMessageException(State.EncryptedMessage, input, stackItem); + } + + Transition fromValid(@Nonnull InputSymbol input, @Nullable StackSymbol stackItem) + throws MalformedOpenPgpMessageException { + // There is no applicable transition rule out of Valid + throw new MalformedOpenPgpMessageException(State.Valid, input, stackItem); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java new file mode 100644 index 00000000..07a5fdcb --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/PDA.java @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check; + +import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Stack; + +import static org.pgpainless.decryption_verification.syntax_check.StackSymbol.msg; +import static org.pgpainless.decryption_verification.syntax_check.StackSymbol.terminus; + +public class PDA { + + private static final Logger LOGGER = LoggerFactory.getLogger(PDA.class); + + // right now we implement what rfc4880 specifies. + // TODO: Consider implementing what we proposed here: + // https://mailarchive.ietf.org/arch/msg/openpgp/uepOF6XpSegMO4c59tt9e5H1i4g/ + private final Syntax syntax; + private final Stack stack = new Stack<>(); + private final List inputs = new ArrayList<>(); // Track inputs for debugging / error reporting + private State state; + + /** + * Default constructor which initializes the PDA to work with the {@link OpenPgpMessageSyntax}. + */ + public PDA() { + this(new OpenPgpMessageSyntax(), State.OpenPgpMessage, terminus, msg); + } + + public PDA(@Nonnull Syntax syntax, @Nonnull State initialState, @Nonnull StackSymbol... initialStack) { + this.syntax = syntax; + this.state = initialState; + for (StackSymbol symbol : initialStack) { + pushStack(symbol); + } + } + + public void next(InputSymbol input) throws MalformedOpenPgpMessageException { + StackSymbol stackSymbol = popStack(); + try { + Transition transition = syntax.transition(state, input, stackSymbol); + state = transition.getNewState(); + for (StackSymbol item : transition.getPushedItems()) { + pushStack(item); + } + inputs.add(input); + } catch (MalformedOpenPgpMessageException e) { + MalformedOpenPgpMessageException wrapped = new MalformedOpenPgpMessageException( + "Malformed message: After reading stream " + Arrays.toString(inputs.toArray()) + + ", token '" + input + "' is not allowed." + + "\nNo transition from state '" + state + "' with stack " + Arrays.toString(stack.toArray()) + + (stackSymbol != null ? "||'" + stackSymbol + "'." : "."), e); + LOGGER.debug("Invalid input '" + input + "'", wrapped); + throw wrapped; + } + } + + /** + * Return the current state of the PDA. + * + * @return state + */ + public State getState() { + return state; + } + + public StackSymbol peekStack() { + if (stack.isEmpty()) { + return null; + } + return stack.peek(); + } + + /** + * Return true, if the PDA is in a valid state (the OpenPGP message is valid). + * + * @return true if valid, false otherwise + */ + public boolean isValid() { + return getState() == State.Valid && stack.isEmpty(); + } + + public void assertValid() throws MalformedOpenPgpMessageException { + if (!isValid()) { + throw new MalformedOpenPgpMessageException("Pushdown Automaton is not in an acceptable state: " + toString()); + } + } + + /** + * Pop an item from the stack. + * + * @return stack item + */ + private StackSymbol popStack() { + if (stack.isEmpty()) { + return null; + } + return stack.pop(); + } + + /** + * Push an item onto the stack. + * + * @param item item + */ + private void pushStack(StackSymbol item) { + stack.push(item); + } + + @Override + public String toString() { + return "State: " + state + " Stack: " + stack; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackSymbol.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackSymbol.java new file mode 100644 index 00000000..120458e5 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/StackSymbol.java @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check; + +public enum StackSymbol { + /** + * OpenPGP Message. + */ + msg, + /** + * OnePassSignature (in case of BC this represents a OnePassSignatureList). + */ + ops, + /** + * Special symbol representing the end of the message. + */ + terminus +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/State.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/State.java new file mode 100644 index 00000000..9dee9af1 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/State.java @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check; + +/** + * Set of states of the automaton. + */ +public enum State { + OpenPgpMessage, + LiteralMessage, + CompressedMessage, + EncryptedMessage, + Valid +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java new file mode 100644 index 00000000..2f3d0a57 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Syntax.java @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check; + +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * This interface can be used to define a custom syntax for the {@link PDA}. + */ +public interface Syntax { + + /** + * Describe a transition rule from {@link State}
from
for {@link InputSymbol}
input
+ * with {@link StackSymbol}
stackItem
from the top of the {@link PDA PDAs} stack. + * The resulting {@link Transition} contains the new {@link State}, as well as a list of + * {@link StackSymbol StackSymbols} that get pushed onto the stack by the transition rule. + * If there is no applicable rule, a {@link MalformedOpenPgpMessageException} is thrown, since in this case + * the {@link InputSymbol} must be considered illegal. + * + * @param from current state of the PDA + * @param input input symbol + * @param stackItem item that got popped from the top of the stack + * @return applicable transition rule containing the new state and pushed stack symbols + * @throws MalformedOpenPgpMessageException if there is no applicable transition rule (the input symbol is illegal) + */ + @Nonnull Transition transition(@Nonnull State from, @Nonnull InputSymbol input, @Nullable StackSymbol stackItem) + throws MalformedOpenPgpMessageException; +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java new file mode 100644 index 00000000..ab0db5ef --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/Transition.java @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Result of applying a transition rule. + * Transition rules can be described by implementing the {@link Syntax} interface. + */ +public class Transition { + + private final List pushedItems = new ArrayList<>(); + private final State newState; + + public Transition(@Nonnull State newState, @Nonnull StackSymbol... pushedItems) { + this.newState = newState; + this.pushedItems.addAll(Arrays.asList(pushedItems)); + } + + /** + * Return the new {@link State} that is reached by applying the transition. + * + * @return new state + */ + @Nonnull + public State getNewState() { + return newState; + } + + /** + * Return a list of {@link StackSymbol StackSymbols} that are pushed onto the stack + * by applying the transition. + * The list contains items in the order in which they are pushed onto the stack. + * The list may be empty. + * + * @return list of items to be pushed onto the stack + */ + @Nonnull + public List getPushedItems() { + return new ArrayList<>(pushedItems); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/package-info.java new file mode 100644 index 00000000..4df6af5a --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/syntax_check/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Pushdown Automaton to verify validity of packet sequences according to the OpenPGP Message format. + */ +package org.pgpainless.decryption_verification.syntax_check; diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java deleted file mode 100644 index 6b6f86de..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/FinalIOException.java +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.exception; - -import java.io.IOException; - -/** - * Wrapper for {@link IOException} indicating that we need to throw this exception up. - */ -public class FinalIOException extends IOException { - - public FinalIOException(IOException e) { - super(e); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java new file mode 100644 index 00000000..f98a4048 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/MalformedOpenPgpMessageException.java @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.exception; + +import org.pgpainless.decryption_verification.syntax_check.InputSymbol; +import org.pgpainless.decryption_verification.syntax_check.StackSymbol; +import org.pgpainless.decryption_verification.syntax_check.State; + +/** + * Exception that gets thrown if the OpenPGP message is malformed. + * Malformed messages are messages which do not follow the grammar specified in the RFC. + * + * @see RFC4880 §11.3. OpenPGP Messages + */ +public class MalformedOpenPgpMessageException extends RuntimeException { + + public MalformedOpenPgpMessageException(String message) { + super(message); + } + + public MalformedOpenPgpMessageException(State state, InputSymbol input, StackSymbol stackItem) { + this("There is no legal transition from state '" + state + "' for input '" + input + "' when '" + stackItem + "' is on top of the stack."); + } + + public MalformedOpenPgpMessageException(String s, MalformedOpenPgpMessageException e) { + super(s, e); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/MissingLiteralDataException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/MissingLiteralDataException.java deleted file mode 100644 index e396b1df..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/exception/MissingLiteralDataException.java +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.exception; - -import org.bouncycastle.openpgp.PGPException; - -/** - * Exception that gets thrown if a {@link org.bouncycastle.bcpg.LiteralDataPacket} is expected, but not found. - */ -public class MissingLiteralDataException extends PGPException { - - public MissingLiteralDataException(String message) { - super(message); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java b/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java index 46aa2baf..6cf7102d 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/collection/PGPKeyRingCollection.java @@ -74,7 +74,7 @@ public class PGPKeyRingCollection { } public PGPKeyRingCollection(@Nonnull Collection collection, boolean isSilent) - throws IOException, PGPException { + throws PGPException { List secretKeyRings = new ArrayList<>(); List publicKeyRings = new ArrayList<>(); diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index b818290b..fa4168dd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -1100,29 +1100,33 @@ public class KeyRingInfo { List signingKeys = getSigningSubkeys(); for (PGPPublicKey pk : signingKeys) { - PGPSecretKey sk = getSecretKey(pk.getKeyID()); - if (sk == null) { - // Missing secret key - continue; - } - S2K s2K = sk.getS2K(); - // Unencrypted key - if (s2K == null) { - return true; - } - - // Secret key on smart-card - int s2kType = s2K.getType(); - if (s2kType >= 100 && s2kType <= 110) { - continue; - } - // protected secret key - return true; + return isSecretKeyAvailable(pk.getKeyID()); } // No usable secret key found return false; } + public boolean isSecretKeyAvailable(long keyId) { + PGPSecretKey sk = getSecretKey(keyId); + if (sk == null) { + // Missing secret key + return false; + } + S2K s2K = sk.getS2K(); + // Unencrypted key + if (s2K == null) { + return true; + } + + // Secret key on smart-card + int s2kType = s2K.getType(); + if (s2kType >= 100 && s2kType <= 110) { + return false; + } + // protected secret key + return true; + } + private KeyAccessor getKeyAccessor(@Nullable String userId, long keyID) { if (getPublicKey(keyID) == null) { throw new NoSuchElementException("No subkey with key id " + Long.toHexString(keyID) + " found on this key."); diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java index 20876c36..01c09903 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java @@ -327,7 +327,7 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { @Nonnull SecretKeyRingProtector primaryKeyProtector, @Nonnull KeyFlag keyFlag, KeyFlag... additionalKeyFlags) - throws PGPException, IOException, NoSuchAlgorithmException { + throws PGPException, IOException { KeyFlag[] flags = concat(keyFlag, additionalKeyFlags); PublicKeyAlgorithm subkeyAlgorithm = PublicKeyAlgorithm.requireFromId(subkey.getPublicKey().getAlgorithm()); SignatureSubpacketsUtil.assureKeyCanCarryFlags(subkeyAlgorithm); diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java index b5b8ea65..50cacf05 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/parsing/KeyRingReader.java @@ -41,7 +41,8 @@ public class KeyRingReader { * @return key ring * @throws IOException in case of an IO error */ - public PGPKeyRing keyRing(@Nonnull InputStream inputStream) throws IOException { + public PGPKeyRing keyRing(@Nonnull InputStream inputStream) + throws IOException { return readKeyRing(inputStream); } @@ -53,7 +54,8 @@ public class KeyRingReader { * @return key ring * @throws IOException in case of an IO error */ - public PGPKeyRing keyRing(@Nonnull byte[] bytes) throws IOException { + public PGPKeyRing keyRing(@Nonnull byte[] bytes) + throws IOException { return keyRing(new ByteArrayInputStream(bytes)); } @@ -65,19 +67,23 @@ public class KeyRingReader { * @return key ring * @throws IOException in case of an IO error */ - public PGPKeyRing keyRing(@Nonnull String asciiArmored) throws IOException { + public PGPKeyRing keyRing(@Nonnull String asciiArmored) + throws IOException { return keyRing(asciiArmored.getBytes(UTF8)); } - public PGPPublicKeyRing publicKeyRing(@Nonnull InputStream inputStream) throws IOException { + public PGPPublicKeyRing publicKeyRing(@Nonnull InputStream inputStream) + throws IOException { return readPublicKeyRing(inputStream); } - public PGPPublicKeyRing publicKeyRing(@Nonnull byte[] bytes) throws IOException { + public PGPPublicKeyRing publicKeyRing(@Nonnull byte[] bytes) + throws IOException { return publicKeyRing(new ByteArrayInputStream(bytes)); } - public PGPPublicKeyRing publicKeyRing(@Nonnull String asciiArmored) throws IOException { + public PGPPublicKeyRing publicKeyRing(@Nonnull String asciiArmored) + throws IOException { return publicKeyRing(asciiArmored.getBytes(UTF8)); } @@ -86,23 +92,28 @@ public class KeyRingReader { return readPublicKeyRingCollection(inputStream); } - public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull byte[] bytes) throws IOException, PGPException { + public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull byte[] bytes) + throws IOException, PGPException { return publicKeyRingCollection(new ByteArrayInputStream(bytes)); } - public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull String asciiArmored) throws IOException, PGPException { + public PGPPublicKeyRingCollection publicKeyRingCollection(@Nonnull String asciiArmored) + throws IOException, PGPException { return publicKeyRingCollection(asciiArmored.getBytes(UTF8)); } - public PGPSecretKeyRing secretKeyRing(@Nonnull InputStream inputStream) throws IOException { + public PGPSecretKeyRing secretKeyRing(@Nonnull InputStream inputStream) + throws IOException { return readSecretKeyRing(inputStream); } - public PGPSecretKeyRing secretKeyRing(@Nonnull byte[] bytes) throws IOException { + public PGPSecretKeyRing secretKeyRing(@Nonnull byte[] bytes) + throws IOException { return secretKeyRing(new ByteArrayInputStream(bytes)); } - public PGPSecretKeyRing secretKeyRing(@Nonnull String asciiArmored) throws IOException { + public PGPSecretKeyRing secretKeyRing(@Nonnull String asciiArmored) + throws IOException { return secretKeyRing(asciiArmored.getBytes(UTF8)); } @@ -111,11 +122,13 @@ public class KeyRingReader { return readSecretKeyRingCollection(inputStream); } - public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull byte[] bytes) throws IOException, PGPException { + public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull byte[] bytes) + throws IOException, PGPException { return secretKeyRingCollection(new ByteArrayInputStream(bytes)); } - public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull String asciiArmored) throws IOException, PGPException { + public PGPSecretKeyRingCollection secretKeyRingCollection(@Nonnull String asciiArmored) + throws IOException, PGPException { return secretKeyRingCollection(asciiArmored.getBytes(UTF8)); } @@ -124,11 +137,13 @@ public class KeyRingReader { return readKeyRingCollection(inputStream, isSilent); } - public PGPKeyRingCollection keyRingCollection(@Nonnull byte[] bytes, boolean isSilent) throws IOException, PGPException { + public PGPKeyRingCollection keyRingCollection(@Nonnull byte[] bytes, boolean isSilent) + throws IOException, PGPException { return keyRingCollection(new ByteArrayInputStream(bytes), isSilent); } - public PGPKeyRingCollection keyRingCollection(@Nonnull String asciiArmored, boolean isSilent) throws IOException, PGPException { + public PGPKeyRingCollection keyRingCollection(@Nonnull String asciiArmored, boolean isSilent) + throws IOException, PGPException { return keyRingCollection(asciiArmored.getBytes(UTF8), isSilent); } @@ -142,7 +157,8 @@ public class KeyRingReader { * @return key ring * @throws IOException in case of an IO error */ - public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream) throws IOException { + public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream) + throws IOException { return readKeyRing(inputStream, MAX_ITERATIONS); } @@ -157,7 +173,8 @@ public class KeyRingReader { * @return key ring * @throws IOException in case of an IO error */ - public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream, int maxIterations) throws IOException { + public static PGPKeyRing readKeyRing(@Nonnull InputStream inputStream, int maxIterations) + throws IOException { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( ArmorUtils.getDecoderStream(inputStream)); int i = 0; @@ -181,7 +198,8 @@ public class KeyRingReader { throw new IOException("Loop exceeded max iteration count."); } - public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream) throws IOException { + public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream) + throws IOException { return readPublicKeyRing(inputStream, MAX_ITERATIONS); } @@ -196,7 +214,8 @@ public class KeyRingReader { * * @throws IOException in case of an IO error or exceeding of max iterations */ - public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream, int maxIterations) throws IOException { + public static PGPPublicKeyRing readPublicKeyRing(@Nonnull InputStream inputStream, int maxIterations) + throws IOException { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( ArmorUtils.getDecoderStream(inputStream)); int i = 0; @@ -218,7 +237,7 @@ public class KeyRingReader { } public static PGPPublicKeyRingCollection readPublicKeyRingCollection(@Nonnull InputStream inputStream) - throws IOException, PGPException { + throws IOException { return readPublicKeyRingCollection(inputStream, MAX_ITERATIONS); } @@ -232,10 +251,9 @@ public class KeyRingReader { * @return public key ring collection * * @throws IOException in case of an IO error or exceeding of max iterations - * @throws PGPException in case of a broken key */ public static PGPPublicKeyRingCollection readPublicKeyRingCollection(@Nonnull InputStream inputStream, int maxIterations) - throws IOException, PGPException { + throws IOException { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( ArmorUtils.getDecoderStream(inputStream)); @@ -265,7 +283,8 @@ public class KeyRingReader { throw new IOException("Loop exceeded max iteration count."); } - public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream) throws IOException { + public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream) + throws IOException { return readSecretKeyRing(inputStream, MAX_ITERATIONS); } @@ -280,7 +299,8 @@ public class KeyRingReader { * * @throws IOException in case of an IO error or exceeding of max iterations */ - public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream, int maxIterations) throws IOException { + public static PGPSecretKeyRing readSecretKeyRing(@Nonnull InputStream inputStream, int maxIterations) + throws IOException { InputStream decoderStream = ArmorUtils.getDecoderStream(inputStream); PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(decoderStream); int i = 0; @@ -303,7 +323,7 @@ public class KeyRingReader { } public static PGPSecretKeyRingCollection readSecretKeyRingCollection(@Nonnull InputStream inputStream) - throws IOException, PGPException { + throws IOException { return readSecretKeyRingCollection(inputStream, MAX_ITERATIONS); } @@ -317,11 +337,10 @@ public class KeyRingReader { * @return secret key ring collection * * @throws IOException in case of an IO error or exceeding of max iterations - * @throws PGPException in case of a broken secret key */ public static PGPSecretKeyRingCollection readSecretKeyRingCollection(@Nonnull InputStream inputStream, int maxIterations) - throws IOException, PGPException { + throws IOException { PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory( ArmorUtils.getDecoderStream(inputStream)); diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java index e6ab4f48..7ebac8fd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyIdUtil.java @@ -30,4 +30,8 @@ public final class KeyIdUtil { return new BigInteger(longKeyId, 16).longValue(); } + + public static String formatKeyId(long keyId) { + return Long.toHexString(keyId).toUpperCase(); + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java index 0bcdf8d1..2ce058fd 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java @@ -144,6 +144,17 @@ public final class KeyRingUtils { return secretKey; } + @Nonnull + public static PGPPublicKeyRing publicKeys(@Nonnull PGPKeyRing keys) { + if (keys instanceof PGPPublicKeyRing) { + return (PGPPublicKeyRing) keys; + } else if (keys instanceof PGPSecretKeyRing) { + return publicKeyRingFrom((PGPSecretKeyRing) keys); + } else { + throw new IllegalArgumentException("Unknown keys class: " + keys.getClass().getName()); + } + } + /** * Extract a {@link PGPPublicKeyRing} containing all public keys from the provided {@link PGPSecretKeyRing}. * @@ -167,12 +178,9 @@ public final class KeyRingUtils { * * @param secretKeyRings secret key ring collection * @return public key ring collection - * @throws PGPException TODO: remove - * @throws IOException TODO: remove */ @Nonnull - public static PGPPublicKeyRingCollection publicKeyRingCollectionFrom(@Nonnull PGPSecretKeyRingCollection secretKeyRings) - throws PGPException, IOException { + public static PGPPublicKeyRingCollection publicKeyRingCollectionFrom(@Nonnull PGPSecretKeyRingCollection secretKeyRings) { List certificates = new ArrayList<>(); for (PGPSecretKeyRing secretKey : secretKeyRings) { certificates.add(PGPainless.extractCertificate(secretKey)); @@ -200,13 +208,9 @@ public final class KeyRingUtils { * * @param rings array of public key rings * @return key ring collection - * - * @throws IOException in case of an io error - * @throws PGPException in case of a broken key */ @Nonnull - public static PGPPublicKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPPublicKeyRing... rings) - throws IOException, PGPException { + public static PGPPublicKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPPublicKeyRing... rings) { return new PGPPublicKeyRingCollection(Arrays.asList(rings)); } @@ -215,13 +219,9 @@ public final class KeyRingUtils { * * @param rings array of secret key rings * @return secret key ring collection - * - * @throws IOException in case of an io error - * @throws PGPException in case of a broken key */ @Nonnull - public static PGPSecretKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPSecretKeyRing... rings) - throws IOException, PGPException { + public static PGPSecretKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPSecretKeyRing... rings) { return new PGPSecretKeyRingCollection(Arrays.asList(rings)); } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java index d3c7fe68..69940691 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java @@ -237,7 +237,7 @@ public class PublicKeyParameterValidationUtil { * Validate ElGamal public key parameters. * * Original implementation by the openpgpjs authors: - * https://github.com/openpgpjs/openpgpjs/blob/main/src/crypto/public_key/elgamal.js#L76-L143 + * Streams.pipeAll(decryptionStream, out)); } } diff --git a/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java b/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java index c3af0bd6..f9bd7a61 100644 --- a/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java +++ b/pgpainless-core/src/test/java/org/bouncycastle/AsciiArmorCRCTests.java @@ -489,7 +489,7 @@ public class AsciiArmorCRCTests { Passphrase passphrase = Passphrase.fromPassword("flowcrypt compatibility tests"); @Test - public void testInvalidArmorCRCThrowsOnClose() throws PGPException, IOException { + public void testInvalidArmorCRCThrowsOnClose() throws IOException { String message = "-----BEGIN PGP MESSAGE-----\n" + "Version: FlowCrypt 5.0.4 Gmail Encryption flowcrypt.com\n" + "Comment: Seamlessly send, receive and search encrypted email\n" + @@ -542,14 +542,16 @@ public class AsciiArmorCRCTests { "-----END PGP MESSAGE-----\n"; PGPSecretKeyRing key = PGPainless.readKeyRing().secretKeyRing(ASCII_KEY); - DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() - .onInputStream(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))) - .withOptions(new ConsumerOptions().addDecryptionKey( - key, SecretKeyRingProtector.unlockAnyKeyWith(passphrase) - )); + assertThrows(IOException.class, () -> { + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8))) + .withOptions(new ConsumerOptions().addDecryptionKey( + key, SecretKeyRingProtector.unlockAnyKeyWith(passphrase) + )); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, outputStream); - assertThrows(IOException.class, decryptionStream::close); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, outputStream); + decryptionStream.close(); + }); } } diff --git a/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java new file mode 100644 index 00000000..6a43772d --- /dev/null +++ b/pgpainless-core/src/test/java/org/bouncycastle/CachingBcPublicKeyDataDecryptorFactoryTest.java @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.bouncycastle; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +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(); + } +} diff --git a/pgpainless-core/src/test/java/org/bouncycastle/PGPPublicKeyRingTest.java b/pgpainless-core/src/test/java/org/bouncycastle/PGPPublicKeyRingTest.java index 411e81e5..ebb344b0 100644 --- a/pgpainless-core/src/test/java/org/bouncycastle/PGPPublicKeyRingTest.java +++ b/pgpainless-core/src/test/java/org/bouncycastle/PGPPublicKeyRingTest.java @@ -28,7 +28,7 @@ public class PGPPublicKeyRingTest { * Learning test to see if BC also makes userids available on subkeys. * It does not. * - * see also https://security.stackexchange.com/questions/92635/is-it-possible-to-assign-different-uids-to-subkeys-for-the-purpose-of-having-mul + * @see \n" + + "\n" + + "lFgEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTQAAPwJN+Xmr0jjN7RA9jgqXnxC/rcWHmdp/j9NdEd7K2Wbxw/rtCBIYXJk\n" + + "eSBIYXJkd2FyZSA8aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9\n" + + "qfdqTxYhBAH9q2zgSlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJ\n" + + "CAsCmQEAAPk2AP922T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1M\n" + + "l9qqx6QGcaNKe8deMe3EhTant6mS9tqMHp2/3gmcXQRjW9KIEgorBgEEAZdVAQUB\n" + + "AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgHAAD/fw9hnzeH\n" + + "VtBaHi6efXvnc4rdVj8zWk0LKo1clFd3bTAN+oh1BBgWCgAdBQJjW9KIAp4BApsM\n" + + "BRYCAwEABAsJCAcFFQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdi\n" + + "Ags0yZrQPkMs6eL+83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUT\n" + + "vP0BnFgEY1vSiBYJKwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D\n" + + "6nfwJtbDT0YAAQCgnCsN9iX7s2TQd8NPggWs4QdhaFpb6olt3SlAvUy/wRBDiNUE\n" + + "GBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KI\n" + + "AAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5f\n" + + "AQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6\n" + + "AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwG\n" + + "jF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" + + "=+vXp\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + private static final long primaryKeyId = KeyIdUtil.fromLongKeyId("C312C97DA9F76A4F"); + private static final long encryptionKeyId = KeyIdUtil.fromLongKeyId("6924D066714CE8C6"); + private static final long signatureKeyId = KeyIdUtil.fromLongKeyId("94022FA56DC05B49"); + private static final byte[] cardSerial = new byte[] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}; + + public static final String ALL_KEYS_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lEwEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTT+AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + + "aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9qfdqTxYhBAH9q2zg\n" + + "SlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAPk2AP92\n" + + "2T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1Ml9qqx6QGcaNKe8de\n" + + "Me3EhTant6mS9tqMHp2/3gmcUQRjW9KIEgorBgEEAZdVAQUBAQdAVXBLNvNmFh9K\n" + + "X6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/gBlAEdOVQIQAAECAwQFBgcICQoL\n" + + "DA0OD4h1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcFFQoJCAsACgkQwxLJ\n" + + "fan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+83te770A/jG0DeJy\n" + + "+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnEwEY1vSiBYJKwYBBAHaRw8BAQdA\n" + + "vSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0b+AGUAR05VAhAAAQIDBAUG\n" + + "BwgJCgsMDQ4PiNUEGBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18g\n" + + "BBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9f\n" + + "MM0yUxa76cmSWe5fAQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAK\n" + + "CRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEA\n" + + "hE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" + + "=rYoa\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String ALL_KEYS_REMOVED = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lDsEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTT+AGUAR05VAbQgSGFyZHkgSGFyZHdhcmUgPGhhcmR5QGhhcmQud2FyZT6I\n" + + "jwQTFgoAQQUCY1vSiAkQwxLJfan3ak8WIQQB/ats4EpQeHn+ShjDEsl9qfdqTwKe\n" + + "AQKbAQUWAgMBAAQLCQgHBRUKCQgLApkBAAD5NgD/dtk+U0O4bpBZacV904TIYniZ\n" + + "xAhmORKreVNP7xGNV3YA/3hNTJfaqsekBnGjSnvHXjHtxIU2p7epkvbajB6dv94J\n" + + "nEAEY1vSiBIKKwYBBAGXVQEFAQEHQFVwSzbzZhYfSl+oi5nTSTNvGXPTxp8xKAA/\n" + + "fk+KdJQ8AwEIB/4AZQBHTlUBiHUEGBYKAB0FAmNb0ogCngECmwwFFgIDAQAECwkI\n" + + "BwUVCgkICwAKCRDDEsl9qfdqT8nJAP0YGPS+O1hkB/kWLR4Qp2ICCzTJmtA+Qyzp\n" + + "4v7ze17vvQD+MbQN4nL7zx859ZOP6aLE73w9k+dDQzJtYL/VBRO8/QGcOwRjW9KI\n" + + "FgkrBgEEAdpHDwEBB0C9JhMPrS3y/HXR1IQEAJSgh9UKl44HfQPqd/Am1sNPRv4A\n" + + "ZQBHTlUBiNUEGBYKAH0FAmNb0ogCngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkW\n" + + "CgAGBQJjW9KIAAoJEJQCL6VtwFtJDmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0y\n" + + "Uxa76cmSWe5fAQD2oLSEW1GOgIs64+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDD\n" + + "Esl9qfdqTwR6AP9Xftw8xZ7/MWhYImk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r\n" + + "0oTcJn+KVCwGjF6AYiLOzO/R1x5bSlYD3FeJ3Qo=\n" + + "=GEN/\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String PRIMARY_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lEwEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTT+AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PtCBIYXJkeSBIYXJkd2FyZSA8\n" + + "aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9qfdqTxYhBAH9q2zg\n" + + "SlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJCAsCmQEAAPk2AP92\n" + + "2T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1Ml9qqx6QGcaNKe8de\n" + + "Me3EhTant6mS9tqMHp2/3gmcXQRjW9KIEgorBgEEAZdVAQUBAQdAVXBLNvNmFh9K\n" + + "X6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgHAAD/fw9hnzeHVtBaHi6efXvnc4rd\n" + + "Vj8zWk0LKo1clFd3bTAN+oh1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcF\n" + + "FQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+\n" + + "83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnFgEY1vSiBYJ\n" + + "KwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0YAAQCg\n" + + "nCsN9iX7s2TQd8NPggWs4QdhaFpb6olt3SlAvUy/wRBDiNUEGBYKAH0FAmNb0ogC\n" + + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJ\n" + + "DmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5fAQD2oLSEW1GOgIs6\n" + + "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + + "SlYD3FeJ3Qo=\n" + + "=zQLi\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String ENCRYPTION_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lFgEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTQAAPwJN+Xmr0jjN7RA9jgqXnxC/rcWHmdp/j9NdEd7K2Wbxw/rtCBIYXJk\n" + + "eSBIYXJkd2FyZSA8aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9\n" + + "qfdqTxYhBAH9q2zgSlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJ\n" + + "CAsCmQEAAPk2AP922T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1M\n" + + "l9qqx6QGcaNKe8deMe3EhTant6mS9tqMHp2/3gmcUQRjW9KIEgorBgEEAZdVAQUB\n" + + "AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgH/gBlAEdOVQIQ\n" + + "AAECAwQFBgcICQoLDA0OD4h1BBgWCgAdBQJjW9KIAp4BApsMBRYCAwEABAsJCAcF\n" + + "FQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdiAgs0yZrQPkMs6eL+\n" + + "83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUTvP0BnFgEY1vSiBYJ\n" + + "KwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D6nfwJtbDT0YAAQCg\n" + + "nCsN9iX7s2TQd8NPggWs4QdhaFpb6olt3SlAvUy/wRBDiNUEGBYKAH0FAmNb0ogC\n" + + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJ\n" + + "DmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5fAQD2oLSEW1GOgIs6\n" + + "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + + "SlYD3FeJ3Qo=\n" + + "=7OZu\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String SIGNATURE_KEY_ON_CARD = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: 01FD AB6C E04A 5078 79FE 4A18 C312 C97D A9F7 6A4F\n" + + "Comment: Hardy Hardware \n" + + "\n" + + "lFgEY1vSiBYJKwYBBAHaRw8BAQdAQ58lZn/HOtg+1b1KS18odyQ6M4LaDdbJAyRf\n" + + "eBwCeTQAAPwJN+Xmr0jjN7RA9jgqXnxC/rcWHmdp/j9NdEd7K2Wbxw/rtCBIYXJk\n" + + "eSBIYXJkd2FyZSA8aGFyZHlAaGFyZC53YXJlPoiPBBMWCgBBBQJjW9KICRDDEsl9\n" + + "qfdqTxYhBAH9q2zgSlB4ef5KGMMSyX2p92pPAp4BApsBBRYCAwEABAsJCAcFFQoJ\n" + + "CAsCmQEAAPk2AP922T5TQ7hukFlpxX3ThMhieJnECGY5Eqt5U0/vEY1XdgD/eE1M\n" + + "l9qqx6QGcaNKe8deMe3EhTant6mS9tqMHp2/3gmcXQRjW9KIEgorBgEEAZdVAQUB\n" + + "AQdAVXBLNvNmFh9KX6iLmdNJM28Zc9PGnzEoAD9+T4p0lDwDAQgHAAD/fw9hnzeH\n" + + "VtBaHi6efXvnc4rdVj8zWk0LKo1clFd3bTAN+oh1BBgWCgAdBQJjW9KIAp4BApsM\n" + + "BRYCAwEABAsJCAcFFQoJCAsACgkQwxLJfan3ak/JyQD9GBj0vjtYZAf5Fi0eEKdi\n" + + "Ags0yZrQPkMs6eL+83te770A/jG0DeJy+88fOfWTj+mixO98PZPnQ0MybWC/1QUT\n" + + "vP0BnEwEY1vSiBYJKwYBBAHaRw8BAQdAvSYTD60t8vx10dSEBACUoIfVCpeOB30D\n" + + "6nfwJtbDT0b+AGUAR05VAhAAAQIDBAUGBwgJCgsMDQ4PiNUEGBYKAH0FAmNb0ogC\n" + + "ngECmwIFFgIDAQAECwkIBwUVCgkIC18gBBkWCgAGBQJjW9KIAAoJEJQCL6VtwFtJ\n" + + "DmMBAKqsGfRFQxJXyPgugWBgEaO5lt9fMM0yUxa76cmSWe5fAQD2oLSEW1GOgIs6\n" + + "4+Z3gvtXopmeupT09HhI7ger98zDAwAKCRDDEsl9qfdqTwR6AP9Xftw8xZ7/MWhY\n" + + "Imk/xheqPy07K4qo3T1pGKUvUqjWQQEAhE3r0oTcJn+KVCwGjF6AYiLOzO/R1x5b\n" + + "SlYD3FeJ3Qo=\n" + + "=GpEw\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + @Test + public void testMoveAllKeysToCard() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ALL_KEYS_ON_CARD); + + PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.any(), cardSerial); + + for (PGPSecretKey key : onCard) { + assertEquals(SecretKeyPacket.USAGE_SHA1, key.getS2KUsage()); + S2K s2K = key.getS2K(); + assertEquals(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD, s2K.getProtectionMode()); + } + + assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + } + + @Test + public void testMovePrimaryKeyToCard() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(PRIMARY_KEY_ON_CARD); + + PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(primaryKeyId), cardSerial); + + assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + } + + @Test + public void testMoveEncryptionKeyToCard() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ENCRYPTION_KEY_ON_CARD); + + PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId), cardSerial); + + assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + } + + @Test + public void testMoveSigningKeyToCard() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(SIGNATURE_KEY_ON_CARD); + + PGPSecretKeyRing onCard = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(signatureKeyId), cardSerial); + + assertArrayEquals(expected.getEncoded(), onCard.getEncoded()); + } + + @Test + public void testRemoveAllKeys() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + PGPSecretKeyRing expected = PGPainless.readKeyRing().secretKeyRing(ALL_KEYS_REMOVED); + + PGPSecretKeyRing removedSecretKeys = GnuPGDummyKeyUtil.modify(secretKeys) + .removePrivateKeys(GnuPGDummyKeyUtil.KeyFilter.any()); + + for (PGPSecretKey key : removedSecretKeys) { + assertEquals(key.getS2KUsage(), SecretKeyPacket.USAGE_SHA1); + S2K s2k = key.getS2K(); + assertEquals(GnuPGDummyExtension.NO_PRIVATE_KEY.getId(), s2k.getProtectionMode()); + } + + assertArrayEquals(expected.getEncoded(), removedSecretKeys.getEncoded()); + } + + @Test + public void testGetSingleIdOfHardwareBackedKey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + assertTrue(GnuPGDummyKeyUtil.getIdsOfKeysWithGnuPGS2KDivertedToCard(secretKeys).isEmpty()); + + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.only(encryptionKeyId)); + + Set hardwareBackedKeys = GnuPGDummyKeyUtil + .getIdsOfKeysWithGnuPGS2KDivertedToCard(withHardwareBackedEncryptionKey); + assertEquals(Collections.singleton(new SubkeyIdentifier(secretKeys, encryptionKeyId)), hardwareBackedKeys); + } + + + @Test + public void testGetIdsOfFullyHardwareBackedKey() throws IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(FULL_KEY); + assertTrue(GnuPGDummyKeyUtil.getIdsOfKeysWithGnuPGS2KDivertedToCard(secretKeys).isEmpty()); + + PGPSecretKeyRing withHardwareBackedEncryptionKey = GnuPGDummyKeyUtil.modify(secretKeys) + .divertPrivateKeysToCard(GnuPGDummyKeyUtil.KeyFilter.any()); + Set expected = new HashSet<>(); + for (PGPSecretKey key : secretKeys) { + expected.add(new SubkeyIdentifier(secretKeys, key.getKeyID())); + } + + Set hardwareBackedKeys = GnuPGDummyKeyUtil + .getIdsOfKeysWithGnuPGS2KDivertedToCard(withHardwareBackedEncryptionKey); + + assertEquals(expected, hardwareBackedKeys); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/algorithm/OpenPgpPacketTest.java b/pgpainless-core/src/test/java/org/pgpainless/algorithm/OpenPgpPacketTest.java new file mode 100644 index 00000000..88146605 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/algorithm/OpenPgpPacketTest.java @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm; + +import org.bouncycastle.bcpg.PacketTags; +import org.junit.jupiter.api.Test; + +import java.util.NoSuchElementException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class OpenPgpPacketTest { + + @Test + public void testFromInvalidTag() { + int tag = PacketTags.RESERVED; + assertNull(OpenPgpPacket.fromTag(tag)); + assertThrows(NoSuchElementException.class, + () -> OpenPgpPacket.requireFromTag(tag)); + } + + @Test + public void testFromExistingTags() { + for (OpenPgpPacket p : OpenPgpPacket.values()) { + assertEquals(p, OpenPgpPacket.fromTag(p.getTag())); + assertEquals(p, OpenPgpPacket.requireFromTag(p.getTag())); + } + } + + @Test + public void testPKESKTagMatches() { + assertEquals(PacketTags.PUBLIC_KEY_ENC_SESSION, OpenPgpPacket.PKESK.getTag()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java new file mode 100644 index 00000000..73c3bf56 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactoryTest.java @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.util.Passphrase; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CustomPublicKeyDataDecryptorFactoryTest { + + @Test + public void testDecryptionWithEmulatedHardwareDecryptionCallback() + throws PGPException, IOException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKey = PGPainless.generateKeyRing().modernKeyRing("Alice"); + PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKey); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + PGPPublicKey encryptionKey = info.getEncryptionSubkeys(EncryptionPurpose.ANY).get(0); + + // Encrypt a test message + String plaintext = "Hello, World!\n"; + ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertextOut) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .addRecipient(cert))); + encryptionStream.write(plaintext.getBytes(StandardCharsets.UTF_8)); + encryptionStream.close(); + + HardwareSecurity.DecryptionCallback hardwareDecryptionCallback = new HardwareSecurity.DecryptionCallback() { + @Override + public byte[] decryptSessionKey(long keyId, int keyAlgorithm, byte[] sessionKeyData) + throws HardwareSecurity.HardwareSecurityException { + // Emulate hardware decryption. + try { + PGPSecretKey decryptionKey = secretKey.getSecretKey(encryptionKey.getKeyID()); + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(decryptionKey, Passphrase.emptyPassphrase()); + PublicKeyDataDecryptorFactory internal = new BcPublicKeyDataDecryptorFactory(privateKey); + return internal.recoverSessionData(keyAlgorithm, new byte[][] {sessionKeyData}); + } catch (PGPException e) { + throw new HardwareSecurity.HardwareSecurityException(); + } + } + }; + + // Decrypt + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(ciphertextOut.toByteArray())) + .withOptions(ConsumerOptions.get() + .addCustomDecryptorFactory( + new HardwareSecurity.HardwareDataDecryptorFactory( + new SubkeyIdentifier(cert, encryptionKey.getKeyID()), + hardwareDecryptionCallback))); + + ByteArrayOutputStream decryptedOut = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, decryptedOut); + decryptionStream.close(); + + assertEquals(plaintext, decryptedOut.toString()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java new file mode 100644 index 00000000..d87fc6bf --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MessageMetadataTest.java @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.junit.JUtils; +import org.junit.jupiter.api.Test; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.StreamEncoding; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.util.DateUtil; + +import java.util.Date; +import java.util.Iterator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MessageMetadataTest { + + @Test + public void processTestMessage_COMP_ENC_ENC_LIT() { + // Note: COMP of ENC does not make sense, since ENC is indistinguishable from randomness + // and randomness cannot be encrypted. + // For the sake of testing though, this is okay. + MessageMetadata.Message message = new MessageMetadata.Message(); + + MessageMetadata.CompressedData compressedData = new MessageMetadata.CompressedData(CompressionAlgorithm.ZIP, message.depth + 1); + MessageMetadata.EncryptedData encryptedData = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_128, compressedData.depth + 1); + MessageMetadata.EncryptedData encryptedData1 = new MessageMetadata.EncryptedData(SymmetricKeyAlgorithm.AES_256, encryptedData.depth + 1); + MessageMetadata.LiteralData literalData = new MessageMetadata.LiteralData(); + + message.setChild(compressedData); + compressedData.setChild(encryptedData); + encryptedData.setChild(encryptedData1); + encryptedData1.setChild(literalData); + + MessageMetadata metadata = new MessageMetadata(message); + + // Check encryption algs + assertEquals(SymmetricKeyAlgorithm.AES_128, metadata.getEncryptionAlgorithm(), "getEncryptionAlgorithm() returns alg of outermost EncryptedData"); + Iterator encryptionAlgs = metadata.getEncryptionAlgorithms(); + assertTrue(encryptionAlgs.hasNext(), "There is at least one EncryptedData child"); + assertTrue(encryptionAlgs.hasNext(), "The child is still there"); + assertEquals(SymmetricKeyAlgorithm.AES_128, encryptionAlgs.next(), "The first algo is AES128"); + assertTrue(encryptionAlgs.hasNext(), "There is another EncryptedData"); + assertTrue(encryptionAlgs.hasNext(), "There is *still* another EncryptedData"); + assertEquals(SymmetricKeyAlgorithm.AES_256, encryptionAlgs.next(), "The second algo is AES256"); + assertFalse(encryptionAlgs.hasNext(), "There is no more EncryptedData"); + assertFalse(encryptionAlgs.hasNext(), "There *still* is no more EncryptedData"); + + assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm(), "getCompressionAlgorithm() returns alg of outermost CompressedData"); + Iterator compAlgs = metadata.getCompressionAlgorithms(); + assertTrue(compAlgs.hasNext()); + assertTrue(compAlgs.hasNext()); + assertEquals(CompressionAlgorithm.ZIP, compAlgs.next()); + assertFalse(compAlgs.hasNext()); + assertFalse(compAlgs.hasNext()); + + assertEquals("", metadata.getFilename()); + JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate()); + assertEquals(StreamEncoding.BINARY, metadata.getFormat()); + } + + @Test + public void testProcessLiteralDataMessage() { + MessageMetadata.LiteralData literalData = new MessageMetadata.LiteralData( + "collateral_murder.zip", + DateUtil.parseUTCDate("2010-04-05 10:12:03 UTC"), + StreamEncoding.BINARY); + MessageMetadata.Message message = new MessageMetadata.Message(); + message.setChild(literalData); + + MessageMetadata metadata = new MessageMetadata(message); + assertNull(metadata.getCompressionAlgorithm()); + assertNull(metadata.getEncryptionAlgorithm()); + assertEquals("collateral_murder.zip", metadata.getFilename()); + assertEquals(DateUtil.parseUTCDate("2010-04-05 10:12:03 UTC"), metadata.getModificationDate()); + assertEquals(StreamEncoding.BINARY, metadata.getFormat()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java index 2ca265ea..9ecaa38a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/ModificationDetectionTests.java @@ -238,8 +238,10 @@ public class ModificationDetectionTests { ); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - assertThrows(ModificationDetectionException.class, decryptionStream::close); + assertThrows(ModificationDetectionException.class, () -> { + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + }); } @TestTemplate @@ -269,8 +271,10 @@ public class ModificationDetectionTests { ); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - assertThrows(ModificationDetectionException.class, decryptionStream::close); + assertThrows(ModificationDetectionException.class, () -> { + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + }); } @TestTemplate @@ -313,8 +317,10 @@ public class ModificationDetectionTests { ); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - assertThrows(ModificationDetectionException.class, decryptionStream::close); + assertThrows(ModificationDetectionException.class, () -> { + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + }); } @TestTemplate @@ -344,8 +350,10 @@ public class ModificationDetectionTests { ); ByteArrayOutputStream out = new ByteArrayOutputStream(); - Streams.pipeAll(decryptionStream, out); - assertThrows(ModificationDetectionException.class, decryptionStream::close); + assertThrows(ModificationDetectionException.class, () -> { + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + }); } @TestTemplate @@ -530,7 +538,7 @@ public class ModificationDetectionTests { ); } - private PGPSecretKeyRingCollection getDecryptionKey() throws IOException, PGPException { + private PGPSecretKeyRingCollection getDecryptionKey() throws IOException { PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(keyAscii); return new PGPSecretKeyRingCollection(Collections.singletonList(secretKeys)); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java new file mode 100644 index 00000000..d2c62fb2 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpMessageInputStreamTest.java @@ -0,0 +1,687 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Date; +import java.util.Iterator; +import java.util.stream.Stream; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.util.io.Streams; +import org.junit.JUtils; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.StreamEncoding; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionResult; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.exception.MalformedOpenPgpMessageException; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.ArmoredInputStreamFactory; +import org.pgpainless.util.Passphrase; +import org.pgpainless.util.Tuple; + +public class OpenPgpMessageInputStreamTest { + + public static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "Comment: DA05 848F 37D4 68E6 F982 C889 7A70 1FC6 904D 3F4C\n" + + "Comment: Alice \n" + + "\n" + + "lFgEYxzSCBYJKwYBBAHaRw8BAQdAeJU8m4GOJb1eQgv/ryilFHRfNLTYFMNqL6zj\n" + + "r0vF7dsAAP42rAtngpJ6dZxoZlJX0Je65zk1VMPeTrXaWfPS2HSKBRGptBxBbGlj\n" + + "ZSA8YWxpY2VAcGdwYWlubGVzcy5vcmc+iI8EExYKAEEFAmMc0ggJEHpwH8aQTT9M\n" + + "FiEE2gWEjzfUaOb5gsiJenAfxpBNP0wCngECmwEFFgIDAQAECwkIBwUVCgkICwKZ\n" + + "AQAApZEBALUXHtvswPZG28YO+16Men6/fpk+scvqpNMnD4ty3IkAAPwK6TuXjNnZ\n" + + "0XuWdnilvLMV23Ai1d5g6em+lwLK5M2SApxdBGMc0ggSCisGAQQBl1UBBQEBB0D8\n" + + "mNUVX8y2MXFaSeFYqOTPFnGT7dgNVdn6yc0UtkkHOgMBCAcAAP9y9OtP4SX9voPb\n" + + "ID2u9PkJKgo4hTB8NK5LouGppdRtEBGriHUEGBYKAB0FAmMc0ggCngECmwwFFgID\n" + + "AQAECwkIBwUVCgkICwAKCRB6cB/GkE0/TAywAQDpZRJS/joFH4+xcwheqWfI7ay/\n" + + "WfojUoGQMYGnUjsgYwEAkceRUsgkqI0SVgYvuglfaQpZ9k2ns1mZGVLkXvu/yQyc\n" + + "WARjHNIIFgkrBgEEAdpHDwEBB0BGN9BybSOrj8B6gim1SjbB/IiqAshlzMDunVkQ\n" + + "X23npQABAJqvjOOY7qhBuTusC5/Q5+25iLrhMn4TI+LXlJHMVNOaE0OI1QQYFgoA\n" + + "fQUCYxzSCAKeAQKbAgUWAgMBAAQLCQgHBRUKCQgLXyAEGRYKAAYFAmMc0ggACgkQ\n" + + "KALh4BJQXl6yTQD/dh0N5228Uwtu7XHy6dmpMRX62cac5tXQ9WaDzpy8STgBAMdn\n" + + "Mq948UOYEhdk/ZY2/hwux/4t+FHvqrXW8ziBe4cLAAoJEHpwH8aQTT9M1hQA/3Ms\n" + + "P3kzoed3VsWu1ZMr7dKEngbc6SoJ2XPayzN0QYJaAQCIY5NcT9mZF97HWV3Vgeum\n" + + "00sWMHXfkW3+nl5OpUZaDA==\n" + + "=THgv\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + public static final String PLAINTEXT = "Hello, World!\n"; + public static final String PASSPHRASE = "sw0rdf1sh"; + + public static final String LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "yxRiAAAAAABIZWxsbywgV29ybGQhCg==\n" + + "=WGju\n" + + "-----END PGP MESSAGE-----"; + + public static final String LIT_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "yxRiAAAAAABIZWxsbywgV29ybGQhCssUYgAAAAAASGVsbG8sIFdvcmxkIQo=\n" + + "=A91Q\n" + + "-----END PGP MESSAGE-----"; + + public static final String COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owE7LZLEAAIeqTk5+ToK4flFOSmKXAA=\n" + + "=ZYDg\n" + + "-----END PGP MESSAGE-----"; + + public static final String COMP = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owEDAA==\n" + + "=MDzg\n" + + "-----END PGP MESSAGE-----"; + + public static final String COMP_COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "owEBRwC4/6MDQlpoOTFBWSZTWVuW2KAAAAr3hGAQBABgBABAAIAWBJAAAAggADFM\n" + + "ABNBqBo00N6puqWR+TqInoXQ58XckU4UJBbltigA\n" + + "=K9Zl\n" + + "-----END PGP MESSAGE-----"; + + public static final String SIG_COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: BCPG v1.71\n" + + "\n" + + "iHUEABYKACcFAmMc1i0JECgC4eASUF5eFiEEjN3RiJxCf/TyYOQjKALh4BJQXl4A\n" + + "AHkrAP98uPpqrgIix7epgL7MM1cjXXGSxqbDfXHwgptk1YGQlgD/fw89VGcXwFaI\n" + + "2k7kpXQYy/1BqnovM/jZ3X3mXhhTaAOjATstksQAAh6pOTn5Ogrh+UU5KYpcAA==\n" + + "=WKPn\n" + + "-----END PGP MESSAGE-----"; + + public static final String SENC_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "jA0ECQMCuZ0qHNXWnGhg0j8Bdm1cxV65sYb7jDgb4rRMtdNpQ1dC4UpSYuk9YWS2\n" + + "DpNEijbX8b/P1UOK2kJczNDADMRegZuLEI+dNsBnJjk=\n" + + "=i4Y0\n" + + "-----END PGP MESSAGE-----"; + + public static final String PENC_COMP_LIT = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "hF4Dyqa/GWUy6WsSAQdAuGt49sQwdAHH3jPx11V3wSh7Amur3TbnONiQYJmMo3Qw\n" + + "87yBnZCsaB7evxLBgi6PpF3tiytHM60xlrPeKKPpJhu60vNafRM2OOwqk7AdcZw4\n" + + "0kYBEhiioO2btSuafNrQEjYzAgC7K6l7aPCcQObNp4ofryXu1P5vN+vUZp357hyS\n" + + "6zZqP+0wJQ9yJZMvFTtFeSaSi0oMP2sb\n" + + "=LvRL\n" + + "-----END PGP MESSAGE-----"; + + public static final String OPS_LIT_SIG = "" + + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "kA0DAAoWKALh4BJQXl4ByxRiAAAAAABIZWxsbywgV29ybGQhCoh1BAAWCgAnBQJj\n" + + "I3fSCRAoAuHgElBeXhYhBIzd0YicQn/08mDkIygC4eASUF5eAADLOgEA766VyMMv\n" + + "sxfQwQHly3T6ySHSNhYEpoyvdxVqhjBBR+EA/3i6C8lKFPPTh/PvTGbVFOl+eUSV\n" + + "I0w3c+BRY/pO0m4H\n" + + "=tkTV\n" + + "-----END PGP MESSAGE-----"; + + public static void main(String[] args) throws Exception { + // genLIT(); + // genLIT_LIT(); + // genCOMP_LIT(); + // genCOMP(); + // genCOMP_COMP_LIT(); + // genKey(); + // genSIG_LIT(); + // genSENC_LIT(); + genPENC_COMP_LIT(); + // genOPS_LIT_SIG(); + } + + public static void genLIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + armorOut.close(); + } + + public static void genLIT_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + + litOut = litGen.open(armorOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + + armorOut.close(); + } + + public static void genCOMP_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut = compGen.open(armorOut); + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(compOut, PGPLiteralDataGenerator.BINARY, "", PGPLiteralData.NOW, new byte[1 << 9]); + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + compOut.close(); + armorOut.close(); + } + + public static void genCOMP() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + PGPCompressedDataGenerator compGen = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut = compGen.open(armorOut); + compOut.close(); + armorOut.close(); + } + + public static void genCOMP_COMP_LIT() throws IOException { + ArmoredOutputStream armorOut = new ArmoredOutputStream(System.out); + + PGPCompressedDataGenerator compGen1 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + OutputStream compOut1 = compGen1.open(armorOut); + + PGPCompressedDataGenerator compGen2 = new PGPCompressedDataGenerator(CompressionAlgorithmTags.BZIP2); + OutputStream compOut2 = compGen2.open(compOut1); + + PGPLiteralDataGenerator litGen = new PGPLiteralDataGenerator(); + OutputStream litOut = litGen.open(compOut2, PGPLiteralDataGenerator.BINARY, "", PGPLiteralDataGenerator.NOW, new byte[1 << 9]); + + litOut.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + litOut.close(); + compOut2.close(); + compOut1.close(); + armorOut.close(); + } + + public static void genKey() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPainless.asciiArmor( + PGPainless.generateKeyRing().modernKeyRing("Alice "), + System.out); + } + + public static void genSIG_COMP_LIT() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + ByteArrayOutputStream msgOut = new ByteArrayOutputStream(); + EncryptionStream signer = PGPainless.encryptAndOrSign() + .onOutputStream(msgOut) + .withOptions( + ProducerOptions.sign( + SigningOptions.get() + .addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys) + ).setAsciiArmor(false) + ); + + Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), signer); + signer.close(); + EncryptionResult result = signer.getResult(); + PGPSignature detachedSignature = result.getDetachedSignatures().get(result.getDetachedSignatures().keySet().iterator().next()).iterator().next(); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArmoredOutputStream armorOut = new ArmoredOutputStream(out); + armorOut.flush(); + detachedSignature.encode(armorOut); + armorOut.write(msgOut.toByteArray()); + armorOut.close(); + + String armored = out.toString(); + // CHECKSTYLE:OFF + System.out.println(armored + .replace("-----BEGIN PGP SIGNATURE-----\n", "-----BEGIN PGP MESSAGE-----\n") + .replace("-----END PGP SIGNATURE-----", "-----END PGP MESSAGE-----")); + // CHECKSTYLE:ON + } + + public static void genSENC_LIT() throws PGPException, IOException { + EncryptionStream enc = PGPainless.encryptAndOrSign() + .onOutputStream(System.out) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .addPassphrase(Passphrase.fromPassword(PASSPHRASE))) + .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); + enc.write(PLAINTEXT.getBytes(StandardCharsets.UTF_8)); + enc.close(); + } + + public static void genPENC_COMP_LIT() throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + PGPPublicKeyRing cert = PGPainless.extractCertificate(secretKeys); + EncryptionStream enc = PGPainless.encryptAndOrSign() + .onOutputStream(System.out) + .withOptions(ProducerOptions.encrypt(EncryptionOptions.get() + .addRecipient(cert)) + .overrideCompressionAlgorithm(CompressionAlgorithm.ZLIB)); + + Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); + enc.close(); + } + + public static void genOPS_LIT_SIG() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + + EncryptionStream enc = PGPainless.encryptAndOrSign() + .onOutputStream(System.out) + .withOptions(ProducerOptions.sign(SigningOptions.get() + .addSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys)) + .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)); + Streams.pipeAll(new ByteArrayInputStream(PLAINTEXT.getBytes(StandardCharsets.UTF_8)), enc); + enc.close(); + } + + interface Processor { + Tuple process(String armoredMessage, ConsumerOptions options) throws PGPException, IOException; + } + + private static Stream provideMessageProcessors() { + return Stream.of( + Arguments.of(Named.of("read(buf,off,len)", (Processor) OpenPgpMessageInputStreamTest::processReadBuffered)), + Arguments.of(Named.of("read()", (Processor) OpenPgpMessageInputStreamTest::processReadSequential)) + ); + } + + @ParameterizedTest(name = "Process LIT using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessLIT(Processor processor) throws IOException, PGPException { + Tuple result = processor.process(LIT, ConsumerOptions.get()); + String plain = result.getA(); + assertEquals(PLAINTEXT, plain); + + MessageMetadata metadata = result.getB(); + assertNull(metadata.getCompressionAlgorithm()); + assertNull(metadata.getEncryptionAlgorithm()); + assertEquals("", metadata.getFilename()); + JUtils.assertDateEquals(new Date(0L), metadata.getModificationDate()); + assertEquals(StreamEncoding.BINARY, metadata.getFormat()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); + } + + @ParameterizedTest(name = "Process LIT LIT using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessLIT_LIT_fails(Processor processor) { + assertThrows(MalformedOpenPgpMessageException.class, + () -> processor.process(LIT_LIT, ConsumerOptions.get())); + } + + @ParameterizedTest(name = "Process COMP(LIT) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessCOMP_LIT(Processor processor) + throws PGPException, IOException { + Tuple result = processor.process(COMP_LIT, ConsumerOptions.get()); + String plain = result.getA(); + assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); + } + + @ParameterizedTest(name = "Process COMP using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessCOMP_fails(Processor processor) { + assertThrows(MalformedOpenPgpMessageException.class, + () -> processor.process(COMP, ConsumerOptions.get())); + } + + @ParameterizedTest(name = "Process COMP(COMP(LIT)) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessCOMP_COMP_LIT(Processor processor) + throws PGPException, IOException { + Tuple result = processor.process(COMP_COMP_LIT, ConsumerOptions.get()); + String plain = result.getA(); + assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); + Iterator compressionAlgorithms = metadata.getCompressionAlgorithms(); + assertEquals(CompressionAlgorithm.ZIP, compressionAlgorithms.next()); + assertEquals(CompressionAlgorithm.BZIP2, compressionAlgorithms.next()); + assertFalse(compressionAlgorithms.hasNext()); + assertNull(metadata.getEncryptionAlgorithm()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); + } + + @ParameterizedTest(name = "Process SIG COMP(LIT) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessSIG_COMP_LIT(Processor processor) + throws PGPException, IOException { + PGPPublicKeyRing cert = PGPainless.extractCertificate( + PGPainless.readKeyRing().secretKeyRing(KEY)); + + Tuple result = processor.process(SIG_COMP_LIT, ConsumerOptions.get() + .addVerificationCert(cert)); + String plain = result.getA(); + assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertEquals(CompressionAlgorithm.ZIP, metadata.getCompressionAlgorithm()); + assertNull(metadata.getEncryptionAlgorithm()); + assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); + } + + @ParameterizedTest(name = "Process SENC(LIT) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessSENC_LIT(Processor processor) + throws PGPException, IOException { + Tuple result = processor.process(SENC_LIT, ConsumerOptions.get() + .addDecryptionPassphrase(Passphrase.fromPassword(PASSPHRASE))); + String plain = result.getA(); + assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertNull(metadata.getCompressionAlgorithm()); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); + } + + @ParameterizedTest(name = "Process PENC(COMP(LIT)) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessPENC_COMP_LIT(Processor processor) + throws IOException, PGPException { + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(KEY); + Tuple result = processor.process(PENC_COMP_LIT, ConsumerOptions.get() + .addDecryptionKey(secretKeys)); + String plain = result.getA(); + assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertEquals(CompressionAlgorithm.ZLIB, metadata.getCompressionAlgorithm()); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertTrue(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); + } + + @ParameterizedTest(name = "Process OPS LIT SIG using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessOPS_LIT_SIG(Processor processor) + throws IOException, PGPException { + PGPPublicKeyRing cert = PGPainless.extractCertificate(PGPainless.readKeyRing().secretKeyRing(KEY)); + Tuple result = processor.process(OPS_LIT_SIG, ConsumerOptions.get() + .addVerificationCert(cert)); + String plain = result.getA(); + assertEquals(PLAINTEXT, plain); + MessageMetadata metadata = result.getB(); + assertNull(metadata.getEncryptionAlgorithm()); + assertNull(metadata.getCompressionAlgorithm()); + assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); + } + + String BOB_KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: Bob's OpenPGP Transferable Secret Key\n" + + "\n" + + "lQVYBF2lnPIBDAC5cL9PQoQLTMuhjbYvb4Ncuuo0bfmgPRFywX53jPhoFf4Zg6mv\n" + + "/seOXpgecTdOcVttfzC8ycIKrt3aQTiwOG/ctaR4Bk/t6ayNFfdUNxHWk4WCKzdz\n" + + "/56fW2O0F23qIRd8UUJp5IIlN4RDdRCtdhVQIAuzvp2oVy/LaS2kxQoKvph/5pQ/\n" + + "5whqsyroEWDJoSV0yOb25B/iwk/pLUFoyhDG9bj0kIzDxrEqW+7Ba8nocQlecMF3\n" + + "X5KMN5kp2zraLv9dlBBpWW43XktjcCZgMy20SouraVma8Je/ECwUWYUiAZxLIlMv\n" + + "9CurEOtxUw6N3RdOtLmYZS9uEnn5y1UkF88o8Nku890uk6BrewFzJyLAx5wRZ4F0\n" + + "qV/yq36UWQ0JB/AUGhHVPdFf6pl6eaxBwT5GXvbBUibtf8YI2og5RsgTWtXfU7eb\n" + + "SGXrl5ZMpbA6mbfhd0R8aPxWfmDWiIOhBufhMCvUHh1sApMKVZnvIff9/0Dca3wb\n" + + "vLIwa3T4CyshfT0AEQEAAQAL/RZqbJW2IqQDCnJi4Ozm++gPqBPiX1RhTWSjwxfM\n" + + "cJKUZfzLj414rMKm6Jh1cwwGY9jekROhB9WmwaaKT8HtcIgrZNAlYzANGRCM4TLK\n" + + "3VskxfSwKKna8l+s+mZglqbAjUg3wmFuf9Tj2xcUZYmyRm1DEmcN2ZzpvRtHgX7z\n" + + "Wn1mAKUlSDJZSQks0zjuMNbupcpyJokdlkUg2+wBznBOTKzgMxVNC9b2g5/tMPUs\n" + + "hGGWmF1UH+7AHMTaS6dlmr2ZBIyogdnfUqdNg5sZwsxSNrbglKP4sqe7X61uEAIQ\n" + + "bD7rT3LonLbhkrj3I8wilUD8usIwt5IecoHhd9HziqZjRCc1BUBkboUEoyedbDV4\n" + + "i4qfsFZ6CEWoLuD5pW7dEp0M+WeuHXO164Rc+LnH6i1VQrpb1Okl4qO6ejIpIjBI\n" + + "1t3GshtUu/mwGBBxs60KBX5g77mFQ9lLCRj8lSYqOsHRKBhUp4qM869VA+fD0BRP\n" + + "fqPT0I9IH4Oa/A3jYJcg622GwQYA1LhnP208Waf6PkQSJ6kyr8ymY1yVh9VBE/g6\n" + + "fRDYA+pkqKnw9wfH2Qho3ysAA+OmVOX8Hldg+Pc0Zs0e5pCavb0En8iFLvTA0Q2E\n" + + "LR5rLue9uD7aFuKFU/VdcddY9Ww/vo4k5p/tVGp7F8RYCFn9rSjIWbfvvZi1q5Tx\n" + + "+akoZbga+4qQ4WYzB/obdX6SCmi6BndcQ1QdjCCQU6gpYx0MddVERbIp9+2SXDyL\n" + + "hpxjSyz+RGsZi/9UAshT4txP4+MZBgDfK3ZqtW+h2/eMRxkANqOJpxSjMyLO/FXN\n" + + "WxzTDYeWtHNYiAlOwlQZEPOydZFty9IVzzNFQCIUCGjQ/nNyhw7adSgUk3+BXEx/\n" + + "MyJPYY0BYuhLxLYcrfQ9nrhaVKxRJj25SVHj2ASsiwGJRZW4CC3uw40OYxfKEvNC\n" + + "mer/VxM3kg8qqGf9KUzJ1dVdAvjyx2Hz6jY2qWCyRQ6IMjWHyd43C4r3jxooYKUC\n" + + "YnstRQyb/gCSKahveSEjo07CiXMr88UGALwzEr3npFAsPW3osGaFLj49y1oRe11E\n" + + "he9gCHFm+fuzbXrWmdPjYU5/ZdqdojzDqfu4ThfnipknpVUM1o6MQqkjM896FHm8\n" + + "zbKVFSMhEP6DPHSCexMFrrSgN03PdwHTO6iBaIBBFqmGY01tmJ03SxvSpiBPON9P\n" + + "NVvy/6UZFedTq8A07OUAxO62YUSNtT5pmK2vzs3SAZJmbFbMh+NN204TRI72GlqT\n" + + "t5hcfkuv8hrmwPS/ZR6q312mKQ6w/1pqO9qitCFCb2IgQmFiYmFnZSA8Ym9iQG9w\n" + + "ZW5wZ3AuZXhhbXBsZT6JAc4EEwEKADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgEC\n" + + "F4AWIQTRpm4aI7GCyZgPeIz7/MgqAV5zMAUCXaWe+gAKCRD7/MgqAV5zMG9sC/9U\n" + + "2T3RrqEbw533FPNfEflhEVRIZ8gDXKM8hU6cqqEzCmzZT6xYTe6sv4y+PJBGXJFX\n" + + "yhj0g6FDkSyboM5litOcTupURObVqMgA/Y4UKERznm4fzzH9qek85c4ljtLyNufe\n" + + "doL2pp3vkGtn7eD0QFRaLLmnxPKQ/TlZKdLE1G3u8Uot8QHicaR6GnAdc5UXQJE3\n" + + "BiV7jZuDyWmZ1cUNwJkKL6oRtp+ZNDOQCrLNLecKHcgCqrpjSQG5oouba1I1Q6Vl\n" + + "sP44dhA1nkmLHtxlTOzpeHj4jnk1FaXmyasurrrI5CgU/L2Oi39DGKTH/A/cywDN\n" + + "4ZplIQ9zR8enkbXquUZvFDe+Xz+6xRXtb5MwQyWODB3nHw85HocLwRoIN9WdQEI+\n" + + "L8a/56AuOwhs8llkSuiITjR7r9SgKJC2WlAHl7E8lhJ3VDW3ELC56KH308d6mwOG\n" + + "ZRAqIAKzM1T5FGjMBhq7ZV0eqdEntBh3EcOIfj2M8rg1MzJv+0mHZOIjByawikad\n" + + "BVgEXaWc8gEMANYwv1xsYyunXYK0X1vY/rP1NNPvhLyLIE7NpK90YNBj+xS1ldGD\n" + + "bUdZqZeef2xJe8gMQg05DoD1DF3GipZ0Ies65beh+d5hegb7N4pzh0LzrBrVNHar\n" + + "29b5ExdI7i4iYD5TO6Vr/qTUOiAN/byqELEzAb+L+b2DVz/RoCm4PIp1DU9ewcc2\n" + + "WB38Ofqut3nLYA5tqJ9XvAiEQme+qAVcM3ZFcaMt4I4dXhDZZNg+D9LiTWcxdUPB\n" + + "leu8iwDRjAgyAhPzpFp+nWoqWA81uIiULWD1Fj+IVoY3ZvgivoYOiEFBJ9lbb4te\n" + + "g9m5UT/AaVDTWuHzbspVlbiVe+qyB77C2daWzNyx6UYBPLOo4r0t0c91kbNE5lgj\n" + + "Z7xz6los0N1U8vq91EFSeQJoSQ62XWavYmlCLmdNT6BNfgh4icLsT7Vr1QMX9jzn\n" + + "JtTPxdXytSdHvpSpULsqJ016l0dtmONcK3z9mj5N5z0k1tg1AH970TGYOe2aUcSx\n" + + "IRDMXDOPyzEfjwARAQABAAv9F2CwsjS+Sjh1M1vegJbZjei4gF1HHpEM0K0PSXsp\n" + + "SfVvpR4AoSJ4He6CXSMWg0ot8XKtDuZoV9jnJaES5UL9pMAD7JwIOqZm/DYVJM5h\n" + + "OASCh1c356/wSbFbzRHPtUdZO9Q30WFNJM5pHbCJPjtNoRmRGkf71RxtvHBzy7np\n" + + "Ga+W6U/NVKHw0i0CYwMI0YlKDakYW3Pm+QL+gHZFvngGweTod0f9l2VLLAmeQR/c\n" + + "+EZs7lNumhuZ8mXcwhUc9JQIhOkpO+wreDysEFkAcsKbkQP3UDUsA1gFx9pbMzT0\n" + + "tr1oZq2a4QBtxShHzP/ph7KLpN+6qtjks3xB/yjTgaGmtrwM8tSe0wD1RwXS+/1o\n" + + "BHpXTnQ7TfeOGUAu4KCoOQLv6ELpKWbRBLWuiPwMdbGpvVFALO8+kvKAg9/r+/ny\n" + + "zM2GQHY+J3Jh5JxPiJnHfXNZjIKLbFbIPdSKNyJBuazXW8xIa//mEHMI5OcvsZBK\n" + + "clAIp7LXzjEjKXIwHwDcTn9pBgDpdOKTHOtJ3JUKx0rWVsDH6wq6iKV/FTVSY5jl\n" + + "zN+puOEsskF1Lfxn9JsJihAVO3yNsp6RvkKtyNlFazaCVKtDAmkjoh60XNxcNRqr\n" + + "gCnwdpbgdHP6v/hvZY54ZaJjz6L2e8unNEkYLxDt8cmAyGPgH2XgL7giHIp9jrsQ\n" + + "aS381gnYwNX6wE1aEikgtY91nqJjwPlibF9avSyYQoMtEqM/1UjTjB2KdD/MitK5\n" + + "fP0VpvuXpNYZedmyq4UOMwdkiNMGAOrfmOeT0olgLrTMT5H97Cn3Yxbk13uXHNu/\n" + + "ZUZZNe8s+QtuLfUlKAJtLEUutN33TlWQY522FV0m17S+b80xJib3yZVJteVurrh5\n" + + "HSWHAM+zghQAvCesg5CLXa2dNMkTCmZKgCBvfDLZuZbjFwnwCI6u/NhOY9egKuUf\n" + + "SA/je/RXaT8m5VxLYMxwqQXKApzD87fv0tLPlVIEvjEsaf992tFEFSNPcG1l/jpd\n" + + "5AVXw6kKuf85UkJtYR1x2MkQDrqY1QX/XMw00kt8y9kMZUre19aCArcmor+hDhRJ\n" + + "E3Gt4QJrD9z/bICESw4b4z2DbgD/Xz9IXsA/r9cKiM1h5QMtXvuhyfVeM01enhxM\n" + + "GbOH3gjqqGNKysx0UODGEwr6AV9hAd8RWXMchJLaExK9J5SRawSg671ObAU24SdY\n" + + "vMQ9Z4kAQ2+1ReUZzf3ogSMRZtMT+d18gT6L90/y+APZIaoArLPhebIAGq39HLmJ\n" + + "26x3z0WAgrpA1kNsjXEXkoiZGPLKIGoe3hqJAbYEGAEKACAWIQTRpm4aI7GCyZgP\n" + + "eIz7/MgqAV5zMAUCXaWc8gIbDAAKCRD7/MgqAV5zMOn/C/9ugt+HZIwX308zI+QX\n" + + "c5vDLReuzmJ3ieE0DMO/uNSC+K1XEioSIZP91HeZJ2kbT9nn9fuReuoff0T0Dief\n" + + "rbwcIQQHFFkrqSp1K3VWmUGp2JrUsXFVdjy/fkBIjTd7c5boWljv/6wAsSfiv2V0\n" + + "JSM8EFU6TYXxswGjFVfc6X97tJNeIrXL+mpSmPPqy2bztcCCHkWS5lNLWQw+R7Vg\n" + + "71Fe6yBSNVrqC2/imYG2J9zlowjx1XU63Wdgqp2Wxt0l8OmsB/W80S1fRF5G4SDH\n" + + "s9HXglXXqPsBRZJYfP+VStm9L5P/sKjCcX6WtZR7yS6G8zj/X767MLK/djANvpPd\n" + + "NVniEke6hM3CNBXYPAMhQBMWhCulcoz+0lxi8L34rMN+Dsbma96psdUrn7uLaB91\n" + + "6we0CTfF8qqm7BsVAgalon/UUiuMY80U3ueoj3okiSTiHIjD/YtpXSPioC8nMng7\n" + + "xqAY9Bwizt4FWgXuLm1a4+So4V9j1TRCXd12Uc2l2RNmgDE=\n" + + "=miES\n" + + "-----END PGP PRIVATE KEY BLOCK-----"; + + @ParameterizedTest(name = "Process PENC(OPS OPS LIT SIG SIG) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessPENC_OPS_OPS_LIT_SIG_SIG(Processor processor) throws IOException, PGPException { + String MSG = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "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\n" + + "=mdYs\n" + + "-----END PGP MESSAGE-----\n"; + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(BOB_KEY); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + Tuple result = processor.process(MSG, ConsumerOptions.get() + .addVerificationCert(certificate) + .addDecryptionKey(secretKeys)); + String plain = result.getA(); + assertEquals("encrypt ∘ sign ∘ sign", plain); + MessageMetadata metadata = result.getB(); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertNull(metadata.getCompressionAlgorithm()); + assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); + } + + @ParameterizedTest(name = "Process PENC(OPS OPS OPS LIT SIG SIG SIG) using {0}") + @MethodSource("provideMessageProcessors") + public void testProcessOPS_OPS_OPS_LIT_SIG_SIG_SIG(Processor processor) throws IOException, PGPException { + String MSG = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "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\n" + + "=JP9T\n" + + "-----END PGP MESSAGE-----"; + PGPSecretKeyRing secretKeys = PGPainless.readKeyRing().secretKeyRing(BOB_KEY); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + Tuple result = processor.process(MSG, ConsumerOptions.get() + .addVerificationCert(certificate) + .addDecryptionKey(secretKeys)); + String plain = result.getA(); + assertEquals("encrypt ∘ sign ∘ sign ∘ sign", plain); + MessageMetadata metadata = result.getB(); + assertEquals(SymmetricKeyAlgorithm.AES_256, metadata.getEncryptionAlgorithm()); + assertNull(metadata.getCompressionAlgorithm()); + assertFalse(metadata.getVerifiedInlineSignatures().isEmpty()); + assertTrue(metadata.getRejectedInlineSignatures().isEmpty()); + } + + private static Tuple processReadBuffered(String armoredMessage, ConsumerOptions options) + throws PGPException, IOException { + OpenPgpMessageInputStream in = get(armoredMessage, options); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(in, out); + in.close(); + MessageMetadata metadata = in.getMetadata(); + return new Tuple<>(out.toString(), metadata); + } + + private static Tuple processReadSequential(String armoredMessage, ConsumerOptions options) + throws PGPException, IOException { + OpenPgpMessageInputStream in = get(armoredMessage, options); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + int r; + while ((r = in.read()) != -1) { + out.write(r); + } + + in.close(); + MessageMetadata metadata = in.getMetadata(); + return new Tuple<>(out.toString(), metadata); + } + + private static OpenPgpMessageInputStream get(String armored, ConsumerOptions options) + throws IOException, PGPException { + ByteArrayInputStream bytesIn = new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(bytesIn); + OpenPgpMessageInputStream pgpIn = OpenPgpMessageInputStream.create(armorIn, options); + return pgpIn; + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java index 851a1bef..ba80c69d 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/PreventDecryptionUsingNonEncryptionKeyTest.java @@ -185,7 +185,7 @@ public class PreventDecryptionUsingNonEncryptionKeyTest { decryptionStream.close(); OpenPgpMetadata metadata = decryptionStream.getResult(); - assertEquals(metadata.getDecryptionKey(), new SubkeyIdentifier(secretKeys, secretKeys.getPublicKey().getKeyID())); + assertEquals(new SubkeyIdentifier(secretKeys, secretKeys.getPublicKey().getKeyID()), metadata.getDecryptionKey()); } @Test diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RecursionDepthTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RecursionDepthTest.java index b253cf7f..52c5a906 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RecursionDepthTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/RecursionDepthTest.java @@ -11,12 +11,12 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; -import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.util.io.Streams; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; import org.pgpainless.PGPainless; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.util.TestAllImplementations; public class RecursionDepthTest { @@ -143,7 +143,7 @@ public class RecursionDepthTest { "-----END PGP ARMORED FILE-----\n"; - assertThrows(PGPException.class, () -> { + assertThrows(MalformedOpenPgpMessageException.class, () -> { DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() .onInputStream(new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8))) .withOptions(new ConsumerOptions().addDecryptionKey(secretKey)); diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java new file mode 100644 index 00000000..640025b1 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/TryDecryptWithUnavailableGnuDummyKeyTest.java @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.gnupg.GnuPGDummyKeyUtil; +import org.pgpainless.exception.MissingDecryptionMethodException; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class TryDecryptWithUnavailableGnuDummyKeyTest { + + @Test + public void testAttemptToDecryptWithRemovedPrivateKeysThrows() + throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Hardy Hardware "); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKeys); + + ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertextOut) + .withOptions( + ProducerOptions.encrypt(EncryptionOptions.get().addRecipient(certificate))); + ByteArrayInputStream plaintextIn = new ByteArrayInputStream("Hello, World!\n".getBytes()); + Streams.pipeAll(plaintextIn, encryptionStream); + encryptionStream.close(); + + PGPSecretKeyRing removedKeys = GnuPGDummyKeyUtil.modify(secretKeys) + .removePrivateKeys(GnuPGDummyKeyUtil.KeyFilter.any()); + + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertextOut.toByteArray()); + assertThrows(MissingDecryptionMethodException.class, () -> PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get().addDecryptionKey(removedKeys))); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java new file mode 100644 index 00000000..68916348 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/UnsupportedPacketVersionsTest.java @@ -0,0 +1,410 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.Charset; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; + +public class UnsupportedPacketVersionsTest { + + private static final String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: Bob's OpenPGP Transferable Secret Key\n" + + "\n" + + "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\n" + + "=miES\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + private static final String PKESK3_PKESK23_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/YP+fDWtifT7KSk+tWrgbyvsYCt5Wh0IPESTZuiptwvto\n" + + "CGbOfwuPbqqzzlFqSvX3UiJwhxjSSB3a1EBIOsbhc4grip/wm+fB50S/nTJxkJ14\n" + + "qid40D7HOcIvuz1iQr1QoMNB0oT3nCwMec8mPUX2yOzx1eqr62SZUTCr6FdAmdYI\n" + + "1u4EAeEFhRO0rcPRrpMZqwkXtUfx+pu7OzBS0qmOlfkQ50kbETDXBik4iXi30AGl\n" + + "Ifo792oRo6DFK7ENquTNRqFPfezjrGZfkJrPWulWh28GogWTpOBwfXG8X262QoIp\n" + + "VwZygi7wfj1jh2sXPvWgHjsjjTt7HPAiLI1f6IUl8WCQfPuQkFwCwPv63/rve59v\n" + + "sBaeCEykAxdzMbP1oYSBBtONSAPYW9fsUsJSpuuLvxH252+luk09uQXWd6z4aCDm\n" + + "EXiolhbkzL3mXCpVP6nMjRkm2ERE1yAWgXGT9JON0gcCb3eVqw6wzOYu+Vwq70ND\n" + + "vKYlTMY+9RUx7wLn51UgwUoXQUFBQUFBQUEJYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYdJMAQZo\n" + + "tUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5QDO4\n" + + "wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=VS1M\n" + + "-----END PGP MESSAGE-----"; + + private static final String PKESK23_PKESK3_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wUoXQUFBQUFBQUEJYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYcHAzAN8L6pN+Tw3sgEL/2D/nw1r\n" + + "Yn0+ykpPrVq4G8r7GAreVodCDxEk2boqbcL7aAhmzn8Lj26qs85Rakr191IicIcY\n" + + "0kgd2tRASDrG4XOIK4qf8JvnwedEv50ycZCdeKoneNA+xznCL7s9YkK9UKDDQdKE\n" + + "95wsDHnPJj1F9sjs8dXqq+tkmVEwq+hXQJnWCNbuBAHhBYUTtK3D0a6TGasJF7VH\n" + + "8fqbuzswUtKpjpX5EOdJGxEw1wYpOIl4t9ABpSH6O/dqEaOgxSuxDarkzUahT33s\n" + + "46xmX5Caz1rpVodvBqIFk6TgcH1xvF9utkKCKVcGcoIu8H49Y4drFz71oB47I407\n" + + "exzwIiyNX+iFJfFgkHz7kJBcAsD7+t/673ufb7AWnghMpAMXczGz9aGEgQbTjUgD\n" + + "2FvX7FLCUqbri78R9udvpbpNPbkF1nes+Ggg5hF4qJYW5My95lwqVT+pzI0ZJthE\n" + + "RNcgFoFxk/STjdIHAm93lasOsMzmLvlcKu9DQ7ymJUzGPvUVMe8C5+dVINJMAQZo\n" + + "tUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5QDO4\n" + + "wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=EhNy\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SKESK23_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/YP+fDWtifT7KSk+tWrgbyvsYCt5Wh0IPESTZuiptwvto\n" + + "CGbOfwuPbqqzzlFqSvX3UiJwhxjSSB3a1EBIOsbhc4grip/wm+fB50S/nTJxkJ14\n" + + "qid40D7HOcIvuz1iQr1QoMNB0oT3nCwMec8mPUX2yOzx1eqr62SZUTCr6FdAmdYI\n" + + "1u4EAeEFhRO0rcPRrpMZqwkXtUfx+pu7OzBS0qmOlfkQ50kbETDXBik4iXi30AGl\n" + + "Ifo792oRo6DFK7ENquTNRqFPfezjrGZfkJrPWulWh28GogWTpOBwfXG8X262QoIp\n" + + "VwZygi7wfj1jh2sXPvWgHjsjjTt7HPAiLI1f6IUl8WCQfPuQkFwCwPv63/rve59v\n" + + "sBaeCEykAxdzMbP1oYSBBtONSAPYW9fsUsJSpuuLvxH252+luk09uQXWd6z4aCDm\n" + + "EXiolhbkzL3mXCpVP6nMjRkm2ERE1yAWgXGT9JON0gcCb3eVqw6wzOYu+Vwq70ND\n" + + "vKYlTMY+9RUx7wLn51Ugw00XCQMINQp7MFzAc6T/YWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYdJM\n" + + "AQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5\n" + + "QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=pvWj\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String SKESK23_PKESK3_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "w00XCQMINQp7MFzAc6T/YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYcHAzAN8L6pN+Tw3sgEL/2D/\n" + + "nw1rYn0+ykpPrVq4G8r7GAreVodCDxEk2boqbcL7aAhmzn8Lj26qs85Rakr191Ii\n" + + "cIcY0kgd2tRASDrG4XOIK4qf8JvnwedEv50ycZCdeKoneNA+xznCL7s9YkK9UKDD\n" + + "QdKE95wsDHnPJj1F9sjs8dXqq+tkmVEwq+hXQJnWCNbuBAHhBYUTtK3D0a6TGasJ\n" + + "F7VH8fqbuzswUtKpjpX5EOdJGxEw1wYpOIl4t9ABpSH6O/dqEaOgxSuxDarkzUah\n" + + "T33s46xmX5Caz1rpVodvBqIFk6TgcH1xvF9utkKCKVcGcoIu8H49Y4drFz71oB47\n" + + "I407exzwIiyNX+iFJfFgkHz7kJBcAsD7+t/673ufb7AWnghMpAMXczGz9aGEgQbT\n" + + "jUgD2FvX7FLCUqbri78R9udvpbpNPbkF1nes+Ggg5hF4qJYW5My95lwqVT+pzI0Z\n" + + "JthERNcgFoFxk/STjdIHAm93lasOsMzmLvlcKu9DQ7ymJUzGPvUVMe8C5+dVINJM\n" + + "AQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNVCHg5\n" + + "QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=STOd\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SKESK4wS2K23_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/YP+fDWtifT7KSk+tWrgbyvsYCt5Wh0IPESTZuiptwvto\n" + + "CGbOfwuPbqqzzlFqSvX3UiJwhxjSSB3a1EBIOsbhc4grip/wm+fB50S/nTJxkJ14\n" + + "qid40D7HOcIvuz1iQr1QoMNB0oT3nCwMec8mPUX2yOzx1eqr62SZUTCr6FdAmdYI\n" + + "1u4EAeEFhRO0rcPRrpMZqwkXtUfx+pu7OzBS0qmOlfkQ50kbETDXBik4iXi30AGl\n" + + "Ifo792oRo6DFK7ENquTNRqFPfezjrGZfkJrPWulWh28GogWTpOBwfXG8X262QoIp\n" + + "VwZygi7wfj1jh2sXPvWgHjsjjTt7HPAiLI1f6IUl8WCQfPuQkFwCwPv63/rve59v\n" + + "sBaeCEykAxdzMbP1oYSBBtONSAPYW9fsUsJSpuuLvxH252+luk09uQXWd6z4aCDm\n" + + "EXiolhbkzL3mXCpVP6nMjRkm2ERE1yAWgXGT9JON0gcCb3eVqw6wzOYu+Vwq70ND\n" + + "vKYlTMY+9RUx7wLn51Ugw1AECRcIYWFhYWFhYWFBQUFBYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YdJMAQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNV\n" + + "CHg5QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=/uxY\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String SKESK4wS2K23_PKESK3_SEIP = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "w1AECRcIYWFhYWFhYWFBQUFBYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\n" + + "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYcHAzAN8L6pN+Tw3sgEL\n" + + "/2D/nw1rYn0+ykpPrVq4G8r7GAreVodCDxEk2boqbcL7aAhmzn8Lj26qs85Rakr1\n" + + "91IicIcY0kgd2tRASDrG4XOIK4qf8JvnwedEv50ycZCdeKoneNA+xznCL7s9YkK9\n" + + "UKDDQdKE95wsDHnPJj1F9sjs8dXqq+tkmVEwq+hXQJnWCNbuBAHhBYUTtK3D0a6T\n" + + "GasJF7VH8fqbuzswUtKpjpX5EOdJGxEw1wYpOIl4t9ABpSH6O/dqEaOgxSuxDark\n" + + "zUahT33s46xmX5Caz1rpVodvBqIFk6TgcH1xvF9utkKCKVcGcoIu8H49Y4drFz71\n" + + "oB47I407exzwIiyNX+iFJfFgkHz7kJBcAsD7+t/673ufb7AWnghMpAMXczGz9aGE\n" + + "gQbTjUgD2FvX7FLCUqbri78R9udvpbpNPbkF1nes+Ggg5hF4qJYW5My95lwqVT+p\n" + + "zI0ZJthERNcgFoFxk/STjdIHAm93lasOsMzmLvlcKu9DQ7ymJUzGPvUVMe8C5+dV\n" + + "INJMAQZotUFmYcTQrd5IFriHbF8h/Ov/xKlkW1QOPrZ+ziMQRbwyY4pVwNbZjCNV\n" + + "CHg5QDO4wjF686DfZt83NvVYbJ7QNuENoI4YcFj8nw==\n" + + "=cIwV\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SEIP_OPS3_LIT_SIG4 = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv+IhkCMhbdRcMnIPZNPGU6OK1Jk5xuRdIEIBsvv7b8jmAr\n" + + "9IwjfnV/RDMtH+xR/T9K7qJGGFYnhLY5w0CmYHQcDKpcBqk0Dw6l/eKCNhgRXKAk\n" + + "gfaKL1Utt1Pw0nz0mOwHyPEN/pGc0xlVhsjVkRvIOsKcpfuc1EpSZMFgDcBQDhe/\n" + + "jAsR/MvRugkW8xLpyQyfeGLJUOEYVrkpam3rLKB1KywAgBmpr9WDwfYITW/VE9k5\n" + + "cKIOPMDJFU+u9lzBx6nSS4JRBuCO2mhR4gjcRaGPWiiz+0qZfS+AXYV/MAU5OwK/\n" + + "6nhX97zwS4r1Avztjh4taBhLVsY4pw6PuLtACJNwPrev63Yc+a4hJJ4pm1AnHV58\n" + + "Y1pZKQL8vA61+/tbhFQ18vbJ8E1NOka/euFLQu9Mg58jhpcscqMouyr3JFMwgH2Y\n" + + "eFuRJncJAKotXxlfnF37qz5LG3bamACXWZSObjp9d4quIAoCDUteZlDWQ1xq5R4y\n" + + "QYXtk9ZuHsHsmY9A0CiI0sGYAUBtLubJ5qhLLr/GqKAmy8jTSA3MjgtrB55NWj9J\n" + + "bjgFNsd1BNGklgnwhtmApHJWY8skAAQkJj0rXj/aOMc734ypiEWDiU1quRbEeRLR\n" + + "kDvBNUXx2j2rVF+MmQS/sm5Yk/op+4lH/Wounsci3qWH76GaNZoIlvNE3mdFoVTe\n" + + "cRh4W2Em8uAUH4bKwazltRJUhZmXvuGfUnQCmolJTpyPl4DaQQgzdBXLTRcPxwdU\n" + + "30e7HnxZWESKx1LnGxp3Oan1k1lXyHwvnEk26EXhve+dhsQ6YsKgvtSLNFqGsfKe\n" + + "MVOq37cpOGFQsYStWHZd0tcHjIjWmAeZ8kH+ZzR9tgYKxxjimsxafLS/lo415SkC\n" + + "LnOCz6hywI7CufSUcXUlHGJuobZ5HDJcygsQhQmNVLDmKh4xUJDZrORS0ciMy2kc\n" + + "XAnxCDYbltVQktc/F/Gl1lTx0UNmV5d9G0utVmxxGbXna3BLkB+6qMuux/ngC+cI\n" + + "+0GjeuXDVzUqhKDDC3Sq4T2nwix4CjKvawnHC+vpHzRmdZSkXz3nrSdhJS5JnOap\n" + + "I4CdyYkP3jQs5P03dkv5RZAoqNPkeftwadu0cjZj8HEC8bVSd7YCS/0Gbgvp/eK3\n" + + "aOXZAWhELyQON4bbXiWzMTcO2AA5soBmP2QnBNdq7NxbEkBec8aAXZiyXn17JYit\n" + + "HRtJ7beptXjN0y2bEhOvsBHbDpjGO22fZfQ0aywP2k4/XanDf4WJolEFgLj2Qp8F\n" + + "pw1Olc2UhApxhAqjkme09hggli3wUhthQrpBlmntfbvBcmmO+p/jV8Zn\n" + + "=Roko\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SEIP_OPS23_LIT_SIG23 = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "wcDMA3wvqk35PDeyAQv/byYGOehnfgZu/HJSSDEQhYE27lh5j+oQPktYBxsTI3Ii\n" + + "AJ0pyJggtUM3c/e6z4HioAs4oQiaH1eoGBIl7noROhHJgT6E/oKdHJHmQndo0gT7\n" + + "xzSHKrEZYEqT45LD6jT5teOiwGX/B7as6jaQOT+Nh+M0ZqpPrBdWDeUoY/I/lx9j\n" + + "IcXQUhKuqJwZ16xsnv0JJ80rdp5qv0g2NHT1hK1JEONyT2fefov3EXaSpQZHRBEi\n" + + "XfwToHcJrgemFoGwZBQhXsPWBgKH92aW6r7ZJZMQ4BE2SwqEw+cbaaqwfFRJ9puj\n" + + "ZUBi2JGwnYImZyD7jYverkjH05vI7d5qQDhCT6GPD8Q0WKfY225LFTj/zzbC23lp\n" + + "VLlbT2Ap8ZFOESpM+crOUaOguBhnTOF05s1eXhQYxIKzJTW8UVrzbwI8ut3BnEDj\n" + + "0aDqUR+QDYgYmz8hnEjanHk2McvfaNdV6uOPZNph2RuPzhZeoNcz1PLy+XPFJsQT\n" + + "EdqURJ2D35qmrBvq6klN0sGYAXEtW4hxMaKhH7/SIk2m3kUAChjtzr4XRcE2i8h3\n" + + "9uD6CRWxokArfRVAp5RpXt3ywoYrl1Mp5prVwWcrTzYVPZwwe/bYAFTIfavi0Ezb\n" + + "A/ah2e1EYCTWxLY0Klzil2Xw9/Dc/JPTRqzWJxIn0AU4DVfYNwlH3QimDUDKOKbu\n" + + "bw4bLEEBKRr5BcNMA2rJOw/n3AmKxQcdSFJh3ZNtDPWzRwflIzE2qB686hpeOs34\n" + + "T2iJcfr9W9rKcYI7+WYcZA3fWokaWUfXdWrPMXBVJuPdGezGLSfe/OM4kw/8s2vR\n" + + "Bk88WZ1ZiFXE2CRaHP80fHFpxAZioWTuC5UGhF7NgZ7Q1E85GaGVe1fQeqmeX3mo\n" + + "gwAWwq9WFhPQQPwdrDz+1h/pzD0RVW7D+zfWdF//vesc4z1Bpi5prbMdpVdmyvO9\n" + + "8Rcc42GtmhtYSB9SPjzPuN8PrWvD3AKgw5vQro6oiNh0TGmj5Se4lXCfRfCl4tak\n" + + "cmvdi+1wAvn5OFdxYKKHNvjavwj2SY70nx0ACasBpbMEwoQ0StZmOxCaofkgvEwN\n" + + "t8jMq/MVrIfunhMjx0/GDpBZBb8kze8zrvWxlbTnoIfh2yVEqmTZWue9HX5Mnh8P\n" + + "wexxNrPaafTjA3nUgXXzlItJ/Wa43pYw2sgcBPlF/jRKSFiD+pDLysqwpH0ANsJ3\n" + + "G7t8Qavq1DlrHFgV5jhfR1tjA5ohjxx7yzceQBvZUFxKM1WEWR+9dRb3bpfZJr0g\n" + + "qgO36bpCeCEej+ubXpXXTN28LQLXjQHlE2o1NGLoGl+G72tXOTx30kPS\n" + + "=wUZn\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SEIP_OPS23_OPS3_LIT_SIG4_SIG23 = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "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\n" + + "=qFHk\n" + + "-----END PGP MESSAGE-----\n"; + + private static final String PKESK3_SEIP_OPS3_OPS23_LIT_SIG23_SIG4 = "-----BEGIN PGP MESSAGE-----\n" + + "\n" + + "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\n" + + "=jGpv\n" + + "-----END PGP MESSAGE-----\n"; + + private final PGPSecretKeyRing key; + private final PGPPublicKeyRing cert; + + public UnsupportedPacketVersionsTest() throws IOException { + key = PGPainless.readKeyRing().secretKeyRing(KEY); + cert = PGPainless.extractCertificate(key); + } + + @Test + public void pkesk3_pkesk23_seip() throws PGPException, IOException { + decryptAndCompare(PKESK3_PKESK23_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void pkesk23_pkesk_seip() throws PGPException, IOException { + decryptAndCompare(PKESK23_PKESK3_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void pkesk3_skesk23_seip() throws PGPException, IOException { + decryptAndCompare(PKESK3_SKESK23_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void skesk23_pkesk3_seip() throws PGPException, IOException { + decryptAndCompare(SKESK23_PKESK3_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + @Disabled("Enable once https://github.com/bcgit/bc-java/pull/1268 is available") + public void pkesk3_skesk4Ws2k23_seip() throws PGPException, IOException { + decryptAndCompare(PKESK3_SKESK4wS2K23_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + @Disabled("Enable once https://github.com/bcgit/bc-java/pull/1268 is available") + public void skesk4Ws2k23_pkesk3_seip() throws PGPException, IOException { + decryptAndCompare(SKESK4wS2K23_PKESK3_SEIP, "Encrypted using SEIP + MDC."); + } + + @Test + public void pkesk3_seip_ops3_lit_sig4() throws PGPException, IOException { + decryptAndCompare(PKESK3_SEIP_OPS3_LIT_SIG4, "Encrypted, signed message."); + } + + @Test + public void pkesk3_seip_ops23_lit_sig23() throws PGPException, IOException { + decryptAndCompare(PKESK3_SEIP_OPS23_LIT_SIG23, "Encrypted, signed message."); + } + + @Test + public void pkesk3_seip_ops23_ops3_lit_sig4_sig23() throws PGPException, IOException { + decryptAndCompare(PKESK3_SEIP_OPS23_OPS3_LIT_SIG4_SIG23, "Encrypted, signed message."); + } + + @Test + public void pkesk3_seip_ops3_ops23_lit_sig23_sig4() throws PGPException, IOException { + decryptAndCompare(PKESK3_SEIP_OPS3_OPS23_LIT_SIG23_SIG4, "Encrypted, signed message."); + } + + public void decryptAndCompare(String msg, String plain) throws IOException, PGPException { + // noinspection CharsetObjectCanBeUsed + ByteArrayInputStream inputStream = new ByteArrayInputStream(msg.getBytes(Charset.forName("UTF8"))); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(inputStream) + .withOptions(ConsumerOptions.get() + .addDecryptionKey(key) + .addVerificationCert(cert)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Streams.pipeAll(decryptionStream, out); + decryptionStream.close(); + + assertEquals(plain, out.toString()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java new file mode 100644 index 00000000..d0486a3e --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/syntax_check/PDATest.java @@ -0,0 +1,197 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification.syntax_check; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.pgpainless.exception.MalformedOpenPgpMessageException; + +public class PDATest { + + + /** + * MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleLiteralMessageIsValid() throws MalformedOpenPgpMessageException { + PDA check = new PDA(); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.EndOfSequence); + + assertTrue(check.isValid()); + } + + /** + * OPS MSG SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimpleOpsSignedMesssageIsValid() throws MalformedOpenPgpMessageException { + PDA check = new PDA(); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); + + assertTrue(check.isValid()); + } + + + /** + * SIG MSG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testSimplePrependSignedMessageIsValid() throws MalformedOpenPgpMessageException { + PDA check = new PDA(); + check.next(InputSymbol.Signature); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.EndOfSequence); + + assertTrue(check.isValid()); + } + + + /** + * OPS COMP(MSG) SIG is valid. + * + * @throws MalformedOpenPgpMessageException fail + */ + @Test + public void testOPSSignedCompressedMessageIsValid() throws MalformedOpenPgpMessageException { + PDA check = new PDA(); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.CompressedData); + // Here would be a nested PDA for the LiteralData packet + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); + + assertTrue(check.isValid()); + } + + @Test + public void testOPSSignedEncryptedMessageIsValid() { + PDA check = new PDA(); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.EncryptedData); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); + assertTrue(check.isValid()); + } + + @Test + public void anyInputAfterEOSIsNotValid() { + PDA check = new PDA(); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.EndOfSequence); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputSymbol.Signature)); + } + + @Test + public void testEncryptedMessageWithAppendedStandaloneSigIsNotValid() { + PDA check = new PDA(); + check.next(InputSymbol.EncryptedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputSymbol.Signature)); + } + + @Test + public void testOPSSignedEncryptedMessageWithMissingSigIsNotValid() { + PDA check = new PDA(); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.EncryptedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputSymbol.EndOfSequence)); + } + + @Test + public void testTwoLiteralDataIsNotValid() { + PDA check = new PDA(); + check.next(InputSymbol.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputSymbol.LiteralData)); + } + + @Test + public void testTrailingSigIsNotValid() { + PDA check = new PDA(); + check.next(InputSymbol.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputSymbol.Signature)); + } + + @Test + public void testOPSAloneIsNotValid() { + PDA check = new PDA(); + check.next(InputSymbol.OnePassSignature); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputSymbol.EndOfSequence)); + } + + @Test + public void testOPSLitWithMissingSigIsNotValid() { + PDA check = new PDA(); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.LiteralData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputSymbol.EndOfSequence)); + } + + @Test + public void testCompressedMessageWithStandalongAppendedSigIsNotValid() { + PDA check = new PDA(); + check.next(InputSymbol.CompressedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputSymbol.Signature)); + } + + @Test + public void testOPSCompressedDataWithMissingSigIsNotValid() { + PDA check = new PDA(); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.CompressedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputSymbol.EndOfSequence)); + } + + @Test + public void testCompressedMessageFollowedByTrailingLiteralDataIsNotValid() { + PDA check = new PDA(); + check.next(InputSymbol.CompressedData); + assertThrows(MalformedOpenPgpMessageException.class, + () -> check.next(InputSymbol.LiteralData)); + } + + @Test + public void testOPSWithPrependedSigIsValid() { + PDA check = new PDA(); + check.next(InputSymbol.Signature); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); + + assertTrue(check.isValid()); + } + + @Test + public void testPrependedSigInsideOPSSignedMessageIsValid() { + PDA check = new PDA(); + check.next(InputSymbol.OnePassSignature); + check.next(InputSymbol.Signature); + check.next(InputSymbol.LiteralData); + check.next(InputSymbol.Signature); + check.next(InputSymbol.EndOfSequence); + + assertTrue(check.isValid()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java index 3e7ccb4d..3436ba69 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java @@ -9,7 +9,6 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; @@ -165,7 +164,7 @@ public class EncryptionOptionsTest { } @Test - public void testAddRecipients_PGPPublicKeyRingCollection() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void testAddRecipients_PGPPublicKeyRingCollection() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPPublicKeyRing secondKeyRing = KeyRingUtils.publicKeyRingFrom( PGPainless.generateKeyRing().modernKeyRing("other@pgpainless.org")); diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java b/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java index b02b07c5..7500bab4 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/WeirdKeys.java @@ -13,7 +13,7 @@ import org.pgpainless.PGPainless; * This class contains a set of slightly out of spec or weird keys. * Those are used to test whether implementations behave correctly when dealing with such keys. * - * Original source: https://gitlab.com/sequoia-pgp/weird-keys + * @see Original Source */ public class WeirdKeys { diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java index f837be1b..eb0069f5 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/certification/CertifyCertificateTest.java @@ -140,7 +140,7 @@ public class CertifyCertificateTest { } @Test - public void testScopedDelegation() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + public void testScopedDelegation() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { PGPSecretKeyRing aliceKey = PGPainless.generateKeyRing() .modernKeyRing("Alice "); PGPSecretKeyRing caKey = PGPainless.generateKeyRing() diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java index 2e2ffc8d..e68d8e9b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithAdditionalUserIdTest.java @@ -7,7 +7,6 @@ package org.pgpainless.key.generation; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; import java.util.Date; @@ -26,14 +25,15 @@ import org.pgpainless.key.generation.type.rsa.RsaLength; import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.key.util.UserId; import org.pgpainless.timeframe.TestTimeFrameProvider; +import org.pgpainless.util.DateUtil; import org.pgpainless.util.TestAllImplementations; public class GenerateKeyWithAdditionalUserIdTest { @TestTemplate @ExtendWith(TestAllImplementations.class) - public void test() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException { - Date now = new Date(); + public void test() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { + Date now = DateUtil.now(); Date expiration = TestTimeFrameProvider.defaultExpirationForCreationDate(now); PGPSecretKeyRing secretKeys = PGPainless.buildKeyRing() .setPrimaryKey(KeySpec.getBuilder( diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateWithEmptyPassphraseTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateWithEmptyPassphraseTest.java index dbb3f49b..c1375883 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateWithEmptyPassphraseTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateWithEmptyPassphraseTest.java @@ -20,7 +20,7 @@ import org.pgpainless.util.TestAllImplementations; import org.pgpainless.util.Passphrase; /** - * Reproduce behavior of https://github.com/pgpainless/pgpainless/issues/16 + * Reproduce behavior of map = new MultiMap<>(); diff --git a/pgpainless-core/src/test/resources/logback-test.xml b/pgpainless-core/src/test/resources/logback-test.xml index 7e4c3194..bb16e293 100644 --- a/pgpainless-core/src/test/resources/logback-test.xml +++ b/pgpainless-core/src/test/resources/logback-test.xml @@ -19,7 +19,7 @@ SPDX-License-Identifier: Apache-2.0 - + diff --git a/pgpainless-sop/README.md b/pgpainless-sop/README.md index ebb1736c..ffd6c84b 100644 --- a/pgpainless-sop/README.md +++ b/pgpainless-sop/README.md @@ -23,7 +23,7 @@ To start using pgpainless-sop in your code, include the following lines in your ... dependencies { ... - implementation "org.pgpainless:pgpainless-sop:1.3.8" + implementation "org.pgpainless:pgpainless-sop:1.4.0-rc1" ... } @@ -34,7 +34,7 @@ dependencies { org.pgpainless pgpainless-sop - 1.3.8 + 1.4.0-rc1 ... diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java index ac53d415..f0b21a6d 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DearmorImpl.java @@ -18,7 +18,7 @@ import sop.operation.Dearmor; public class DearmorImpl implements Dearmor { @Override - public Ready data(InputStream data) throws IOException { + public Ready data(InputStream data) { InputStream decoder; try { decoder = PGPUtil.getDecoderStream(data); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index 4957f748..68be2793 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -23,6 +23,7 @@ import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.decryption_verification.SignatureVerification; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import org.pgpainless.exception.MissingDecryptionMethodException; import org.pgpainless.exception.WrongPassphraseException; import org.pgpainless.util.Passphrase; @@ -135,7 +136,7 @@ public class DecryptImpl implements Decrypt { throw new SOPGPException.CannotDecrypt(); } catch (WrongPassphraseException e) { throw new SOPGPException.KeyIsProtected(); - } catch (PGPException | IOException e) { + } catch (MalformedOpenPgpMessageException | PGPException | IOException e) { throw new SOPGPException.BadData(e); } finally { // Forget passphrases after decryption diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java index e6e2768e..52ba616f 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DetachedVerifyImpl.java @@ -18,6 +18,7 @@ import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.decryption_verification.SignatureVerification; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import sop.Verification; import sop.exception.SOPGPException; import sop.operation.DetachedVerify; @@ -85,7 +86,7 @@ public class DetachedVerifyImpl implements DetachedVerify { } return verificationList; - } catch (PGPException e) { + } catch (MalformedOpenPgpMessageException | PGPException e) { throw new SOPGPException.BadData(e); } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java index 3bdc8fc3..99ad19e0 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineSignImpl.java @@ -76,7 +76,7 @@ public class InlineSignImpl implements InlineSign { } @Override - public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, IOException, SOPGPException.ExpectedText { + public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, SOPGPException.ExpectedText { for (PGPSecretKeyRing key : signingKeys) { try { if (mode == InlineSignAs.CleartextSigned) { diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java index 1e8c4fee..66f5d1aa 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/InlineVerifyImpl.java @@ -19,6 +19,7 @@ import org.pgpainless.decryption_verification.ConsumerOptions; import org.pgpainless.decryption_verification.DecryptionStream; import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.decryption_verification.SignatureVerification; +import org.pgpainless.exception.MalformedOpenPgpMessageException; import sop.ReadyWithResult; import sop.Verification; import sop.exception.SOPGPException; @@ -53,7 +54,7 @@ public class InlineVerifyImpl implements InlineVerify { } @Override - public ReadyWithResult> data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { + public ReadyWithResult> data(InputStream data) throws SOPGPException.NoSignature, SOPGPException.BadData { return new ReadyWithResult>() { @Override public List writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { @@ -84,7 +85,7 @@ public class InlineVerifyImpl implements InlineVerify { } return verificationList; - } catch (PGPException e) { + } catch (MalformedOpenPgpMessageException | PGPException e) { throw new SOPGPException.BadData(e); } } diff --git a/version.gradle b/version.gradle index 680b9d45..a756a407 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '1.3.9' - isSnapshot = true + shortVersion = '1.4.0-rc1' + isSnapshot = false pgpainlessMinAndroidSdk = 10 javaSourceCompatibility = 1.8 bouncyCastleVersion = '1.72'