From 702db4d75c5f4ce99c517eee5f82a6b1954dc74e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 8 May 2025 14:38:48 +0200 Subject: [PATCH] Port OpenPGPInputStream to Kotlin as OpenPGPAnimalSnifferInputStream --- .../OpenPgpInputStream.java | 340 ------------------ .../decryption_verification/package-info.java | 8 - .../OpenPGPAnimalSnifferInputStream.kt | 321 +++++++++++++++++ .../OpenPgpMessageInputStream.kt | 2 +- .../kotlin/org/pgpainless/util/ArmorUtils.kt | 4 +- ... OpenPGPAnimalSnifferInputStreamTest.java} | 22 +- .../kotlin/org/pgpainless/sop/ArmorImpl.kt | 4 +- .../org/pgpainless/sop/InlineDetachImpl.kt | 6 +- 8 files changed, 340 insertions(+), 367 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPGPAnimalSnifferInputStream.kt rename pgpainless-core/src/test/java/org/pgpainless/decryption_verification/{OpenPgpInputStreamTest.java => OpenPGPAnimalSnifferInputStreamTest.java} (97%) 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 deleted file mode 100644 index 491f5d43..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java +++ /dev/null @@ -1,340 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import static org.bouncycastle.bcpg.PacketTags.AEAD_ENC_DATA; -import static org.bouncycastle.bcpg.PacketTags.COMPRESSED_DATA; -import static org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_1; -import static org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_2; -import static org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_3; -import static org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_4; -import static org.bouncycastle.bcpg.PacketTags.LITERAL_DATA; -import static org.bouncycastle.bcpg.PacketTags.MARKER; -import static org.bouncycastle.bcpg.PacketTags.MOD_DETECTION_CODE; -import static org.bouncycastle.bcpg.PacketTags.ONE_PASS_SIGNATURE; -import static org.bouncycastle.bcpg.PacketTags.PADDING; -import static org.bouncycastle.bcpg.PacketTags.PUBLIC_KEY; -import static org.bouncycastle.bcpg.PacketTags.PUBLIC_KEY_ENC_SESSION; -import static org.bouncycastle.bcpg.PacketTags.PUBLIC_SUBKEY; -import static org.bouncycastle.bcpg.PacketTags.RESERVED; -import static org.bouncycastle.bcpg.PacketTags.SECRET_KEY; -import static org.bouncycastle.bcpg.PacketTags.SECRET_SUBKEY; -import static org.bouncycastle.bcpg.PacketTags.SIGNATURE; -import static org.bouncycastle.bcpg.PacketTags.SYMMETRIC_KEY_ENC; -import static org.bouncycastle.bcpg.PacketTags.SYMMETRIC_KEY_ENC_SESSION; -import static org.bouncycastle.bcpg.PacketTags.SYM_ENC_INTEGRITY_PRO; -import static org.bouncycastle.bcpg.PacketTags.TRUST; -import static org.bouncycastle.bcpg.PacketTags.USER_ATTRIBUTE; -import static org.bouncycastle.bcpg.PacketTags.USER_ID; - -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; - -import org.bouncycastle.bcpg.AEADEncDataPacket; -import org.bouncycastle.bcpg.BCPGInputStream; -import org.bouncycastle.bcpg.CompressedDataPacket; -import org.bouncycastle.bcpg.LiteralDataPacket; -import org.bouncycastle.bcpg.MarkerPacket; -import org.bouncycastle.bcpg.OnePassSignaturePacket; -import org.bouncycastle.bcpg.Packet; -import org.bouncycastle.bcpg.PacketFormat; -import org.bouncycastle.bcpg.PublicKeyEncSessionPacket; -import org.bouncycastle.bcpg.PublicKeyPacket; -import org.bouncycastle.bcpg.SecretKeyPacket; -import org.bouncycastle.bcpg.SignaturePacket; -import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket; -import org.bouncycastle.bcpg.SymmetricKeyEncSessionPacket; -import org.bouncycastle.bcpg.UnsupportedPacketVersionException; -import org.bouncycastle.openpgp.PGPCompressedData; -import org.bouncycastle.openpgp.PGPEncryptedData; -import org.bouncycastle.openpgp.PGPLiteralData; -import org.bouncycastle.openpgp.PGPOnePassSignature; -import org.bouncycastle.util.Arrays; -import org.pgpainless.algorithm.AEADAlgorithm; -import org.pgpainless.algorithm.CompressionAlgorithm; -import org.pgpainless.algorithm.HashAlgorithm; -import org.pgpainless.algorithm.PublicKeyAlgorithm; -import org.pgpainless.algorithm.SignatureType; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; - -/** - * InputStream used to determine the nature of potential OpenPGP data. - */ -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 - public static final int MAX_BUFFER_SIZE = 8192 * 2; - - private final byte[] buffer; - private final int bufferLen; - - private boolean containsArmorHeader; - private boolean containsOpenPgpPackets; - private boolean isLikelyOpenPgpMessage; - - public OpenPgpInputStream(InputStream in, boolean check) throws IOException { - super(in, MAX_BUFFER_SIZE); - - mark(MAX_BUFFER_SIZE); - buffer = new byte[MAX_BUFFER_SIZE]; - bufferLen = read(buffer); - reset(); - - if (check) { - inspectBuffer(); - } - } - - public OpenPgpInputStream(InputStream in) throws IOException { - this(in, true); - } - - private void inspectBuffer() throws IOException { - if (checkForAsciiArmor()) { - return; - } - - checkForBinaryOpenPgp(); - } - - private boolean checkForAsciiArmor() { - if (startsWithIgnoringWhitespace(buffer, ARMOR_HEADER, bufferLen)) { - containsArmorHeader = true; - return true; - } - return false; - } - - /** - * This method is still brittle. - * Basically we try to parse OpenPGP packets from the buffer. - * If we run into exceptions, then we know that the data is non-OpenPGP'ish. - *

- * This breaks down though if we read plausible garbage where the data accidentally makes sense, - * or valid, yet incomplete packets (remember, we are still only working on a portion of the data). - */ - private void checkForBinaryOpenPgp() throws IOException { - if (bufferLen == -1) { - // Empty data - return; - } - - ByteArrayInputStream bufferIn = new ByteArrayInputStream(buffer, 0, bufferLen); - BCPGInputStream pIn = new BCPGInputStream(bufferIn); - try { - nonExhaustiveParseAndCheckPlausibility(pIn); - } catch (IOException | UnsupportedPacketVersionException | NegativeArraySizeException e) { - return; - } - } - - private void nonExhaustiveParseAndCheckPlausibility(BCPGInputStream packetIn) - throws IOException { - Packet packet = packetIn.readPacket(); - switch (packet.getPacketTag()) { - case PUBLIC_KEY_ENC_SESSION: - PublicKeyEncSessionPacket pkesk = (PublicKeyEncSessionPacket) packet; - if (PublicKeyAlgorithm.fromId(pkesk.getAlgorithm()) == null) { - return; - } - break; - - case SIGNATURE: - SignaturePacket sig = (SignaturePacket) packet; - if (SignatureType.fromCode(sig.getSignatureType()) == null) { - return; - } - if (PublicKeyAlgorithm.fromId(sig.getKeyAlgorithm()) == null) { - return; - } - if (HashAlgorithm.fromId(sig.getHashAlgorithm()) == null) { - return; - } - break; - - case ONE_PASS_SIGNATURE: - OnePassSignaturePacket ops = (OnePassSignaturePacket) packet; - if (SignatureType.fromCode(ops.getSignatureType()) == null) { - return; - } - if (PublicKeyAlgorithm.fromId(ops.getKeyAlgorithm()) == null) { - return; - } - if (HashAlgorithm.fromId(ops.getHashAlgorithm()) == null) { - return; - } - break; - - case SYMMETRIC_KEY_ENC_SESSION: - SymmetricKeyEncSessionPacket skesk = (SymmetricKeyEncSessionPacket) packet; - if (SymmetricKeyAlgorithm.fromId(skesk.getEncAlgorithm()) == null) { - return; - } - break; - - case SECRET_KEY: - SecretKeyPacket secKey = (SecretKeyPacket) packet; - PublicKeyPacket sPubKey = secKey.getPublicKeyPacket(); - if (PublicKeyAlgorithm.fromId(sPubKey.getAlgorithm()) == null) { - return; - } - if (sPubKey.getVersion() < 3 && sPubKey.getVersion() > 6) { - return; - } - break; - - case PUBLIC_KEY: - PublicKeyPacket pubKey = (PublicKeyPacket) packet; - if (PublicKeyAlgorithm.fromId(pubKey.getAlgorithm()) == null) { - return; - } - if (pubKey.getVersion() < 3 && pubKey.getVersion() > 6) { - return; - } - break; - - case COMPRESSED_DATA: - CompressedDataPacket comp = (CompressedDataPacket) packet; - if (CompressionAlgorithm.fromId(comp.getAlgorithm()) == null) { - return; - } - break; - - case SYMMETRIC_KEY_ENC: - // Not much we can check here - break; - - case MARKER: - MarkerPacket m = (MarkerPacket) packet; - if (!Arrays.areEqual( - m.getEncoded(PacketFormat.CURRENT), - new byte[] {(byte) 0xca, 0x03, 0x50, 0x47, 0x50})) { - return; - } - break; - - case LITERAL_DATA: - LiteralDataPacket lit = (LiteralDataPacket) packet; - if (lit.getFormat() != 'b' && - lit.getFormat() != 'u' && - lit.getFormat() != 't' && - lit.getFormat() != 'l' && - lit.getFormat() != '1' && - lit.getFormat() != 'm') { - return; - } - break; - - case SYM_ENC_INTEGRITY_PRO: - SymmetricEncIntegrityPacket seipd = (SymmetricEncIntegrityPacket) packet; - if (seipd.getVersion() == SymmetricEncIntegrityPacket.VERSION_1) { - break; // not much to check here - } - if (seipd.getVersion() != SymmetricEncIntegrityPacket.VERSION_2) { - if (SymmetricKeyAlgorithm.fromId(seipd.getCipherAlgorithm()) == null) { - return; - } - if (AEADAlgorithm.fromId(seipd.getAeadAlgorithm()) == null) { - return; - } - } - break; - - case AEAD_ENC_DATA: - AEADEncDataPacket oed = (AEADEncDataPacket) packet; - if (SymmetricKeyAlgorithm.fromId(oed.getAlgorithm()) == null) { - return; - } - break; - - case RESERVED: // this Packet Type ID MUST NOT be used - case PUBLIC_SUBKEY: // Never found at the start of a stream - case SECRET_SUBKEY: // Never found at the start of a stream - case TRUST: // Never found at the start of a stream - case MOD_DETECTION_CODE: // At the end of SED data - Never found at the start of a stream - case USER_ID: // Never found at the start of a stream - case USER_ATTRIBUTE: // Never found at the start of a stream - case PADDING: // At the end of messages (optionally padded message) or certificates - case EXPERIMENTAL_1: // experimental - case EXPERIMENTAL_2: // experimental - case EXPERIMENTAL_3: // experimental - case EXPERIMENTAL_4: // experimental - containsOpenPgpPackets = true; - isLikelyOpenPgpMessage = false; - return; - default: - return; - } - - containsOpenPgpPackets = true; - if (packet.getPacketTag() != SYMMETRIC_KEY_ENC) { - isLikelyOpenPgpMessage = true; - } - } - - private boolean startsWithIgnoringWhitespace(byte[] bytes, byte[] subsequence, int bufferLen) { - if (bufferLen == -1) { - return false; - } - - for (int i = 0; i < bufferLen; i++) { - // Working on bytes is not trivial with unicode data, but its good enough here - if (Character.isWhitespace(bytes[i])) { - continue; - } - - if ((i + subsequence.length) > bytes.length) { - return false; - } - - for (int j = 0; j < subsequence.length; j++) { - if (bytes[i + j] != subsequence[j]) { - return false; - } - } - return true; - } - return false; - } - - public boolean isAsciiArmored() { - return containsArmorHeader; - } - - /** - * Return true, if the data is possibly binary OpenPGP. - * The criterion for this are less strict than for {@link #isLikelyOpenPgpMessage()}, - * as it also accepts other OpenPGP packets at the beginning of the data stream. - *

- * Use with caution. - * - * @return true if data appears to be binary OpenPGP data - */ - public boolean isBinaryOpenPgp() { - return containsOpenPgpPackets; - } - - /** - * Returns true, if the underlying data is very likely (more than 99,9%) an OpenPGP message. - * OpenPGP Message means here that it starts with either an {@link PGPEncryptedData}, - * {@link PGPCompressedData}, {@link PGPOnePassSignature} or {@link PGPLiteralData} packet. - * The plausibility of these data packets is checked as far as possible. - * - * @return true if likely OpenPGP message - */ - public boolean isLikelyOpenPgpMessage() { - return isLikelyOpenPgpMessage; - } - - public boolean isNonOpenPgp() { - return !isAsciiArmored() && !isBinaryOpenPgp(); - } -} diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/package-info.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/package-info.java deleted file mode 100644 index 07e7cd3d..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2018 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Classes used to decryption and verification of OpenPGP encrypted / signed data. - */ -package org.pgpainless.decryption_verification; diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPGPAnimalSnifferInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPGPAnimalSnifferInputStream.kt new file mode 100644 index 00000000..8eb7f44c --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPGPAnimalSnifferInputStream.kt @@ -0,0 +1,321 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import java.io.BufferedInputStream +import java.io.ByteArrayInputStream +import java.io.InputStream +import org.bouncycastle.bcpg.AEADEncDataPacket +import org.bouncycastle.bcpg.BCPGInputStream +import org.bouncycastle.bcpg.CompressedDataPacket +import org.bouncycastle.bcpg.LiteralDataPacket +import org.bouncycastle.bcpg.MarkerPacket +import org.bouncycastle.bcpg.OnePassSignaturePacket +import org.bouncycastle.bcpg.PacketFormat +import org.bouncycastle.bcpg.PacketTags.AEAD_ENC_DATA +import org.bouncycastle.bcpg.PacketTags.COMPRESSED_DATA +import org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_1 +import org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_2 +import org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_3 +import org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_4 +import org.bouncycastle.bcpg.PacketTags.LITERAL_DATA +import org.bouncycastle.bcpg.PacketTags.MARKER +import org.bouncycastle.bcpg.PacketTags.MOD_DETECTION_CODE +import org.bouncycastle.bcpg.PacketTags.ONE_PASS_SIGNATURE +import org.bouncycastle.bcpg.PacketTags.PADDING +import org.bouncycastle.bcpg.PacketTags.PUBLIC_KEY +import org.bouncycastle.bcpg.PacketTags.PUBLIC_KEY_ENC_SESSION +import org.bouncycastle.bcpg.PacketTags.PUBLIC_SUBKEY +import org.bouncycastle.bcpg.PacketTags.RESERVED +import org.bouncycastle.bcpg.PacketTags.SECRET_KEY +import org.bouncycastle.bcpg.PacketTags.SECRET_SUBKEY +import org.bouncycastle.bcpg.PacketTags.SIGNATURE +import org.bouncycastle.bcpg.PacketTags.SYMMETRIC_KEY_ENC +import org.bouncycastle.bcpg.PacketTags.SYMMETRIC_KEY_ENC_SESSION +import org.bouncycastle.bcpg.PacketTags.SYM_ENC_INTEGRITY_PRO +import org.bouncycastle.bcpg.PacketTags.TRUST +import org.bouncycastle.bcpg.PacketTags.USER_ATTRIBUTE +import org.bouncycastle.bcpg.PacketTags.USER_ID +import org.bouncycastle.bcpg.PublicKeyEncSessionPacket +import org.bouncycastle.bcpg.PublicKeyPacket +import org.bouncycastle.bcpg.SecretKeyPacket +import org.bouncycastle.bcpg.SignaturePacket +import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket +import org.bouncycastle.bcpg.SymmetricKeyEncSessionPacket +import org.bouncycastle.util.Arrays +import org.pgpainless.algorithm.AEADAlgorithm +import org.pgpainless.algorithm.CompressionAlgorithm +import org.pgpainless.algorithm.HashAlgorithm +import org.pgpainless.algorithm.PublicKeyAlgorithm +import org.pgpainless.algorithm.SignatureType +import org.pgpainless.algorithm.SymmetricKeyAlgorithm + +/** + * InputStream used to determine the nature of potential OpenPGP data. + * + * @param input underlying input stream + * @param check whether to perform the costly checking inside the constructor + */ +class OpenPGPAnimalSnifferInputStream(input: InputStream, check: Boolean) : + BufferedInputStream(input) { + + private val buffer: ByteArray + private val bufferLen: Int + + private var containsArmorHeader: Boolean = false + private var containsOpenPgpPackets: Boolean = false + private var resemblesMessage: Boolean = false + + init { + mark(MAX_BUFFER_SIZE) + buffer = ByteArray(MAX_BUFFER_SIZE) + bufferLen = read(buffer) + reset() + + if (check) { + inspectBuffer() + } + } + + constructor(input: InputStream) : this(input, true) + + /** Return true, if the underlying data is ASCII armored. */ + val isAsciiArmored: Boolean + get() = containsArmorHeader + + /** + * Return true, if the data is possibly binary OpenPGP. The criterion for this are less strict + * than for [resemblesMessage], as it also accepts other OpenPGP packets at the beginning of the + * data stream. + * + *

+ * Use with caution. + * + * @return true if data appears to be binary OpenPGP data + */ + val isBinaryOpenPgp: Boolean + get() = containsOpenPgpPackets + + /** + * Returns true, if the underlying data is very likely (more than 99,9%) an OpenPGP message. + * OpenPGP Message means here that it starts with either a [PGPEncryptedData], + * [PGPCompressedData], [PGPOnePassSignature] or [PGPLiteralData] packet. The plausibility of + * these data packets is checked as far as possible. + * + * @return true if likely OpenPGP message + */ + val isLikelyOpenPgpMessage: Boolean + get() = resemblesMessage + + /** Return true, if the underlying data is non-OpenPGP data. */ + val isNonOpenPgp: Boolean + get() = !isAsciiArmored && !isBinaryOpenPgp + + /** Costly perform a plausibility check of the first encountered OpenPGP packet. */ + fun inspectBuffer() { + if (checkForAsciiArmor()) { + return + } + + checkForBinaryOpenPgp() + } + + private fun checkForAsciiArmor(): Boolean { + if (startsWithIgnoringWhitespace(buffer, ARMOR_HEADER, bufferLen)) { + containsArmorHeader = true + return true + } + return false + } + + /** + * This method is still brittle. Basically we try to parse OpenPGP packets from the buffer. If + * we run into exceptions, then we know that the data is non-OpenPGP'ish. + * + *

+ * This breaks down though if we read plausible garbage where the data accidentally makes sense, + * or valid, yet incomplete packets (remember, we are still only working on a portion of the + * data). + */ + private fun checkForBinaryOpenPgp() { + if (bufferLen == -1) { + // empty data + return + } + + val bufferIn = ByteArrayInputStream(buffer, 0, bufferLen) + val pIn = BCPGInputStream(bufferIn) + try { + nonExhaustiveParseAndCheckPlausibility(pIn) + } catch (e: Exception) { + return + } + } + + private fun nonExhaustiveParseAndCheckPlausibility(packetIn: BCPGInputStream) { + val packet = packetIn.readPacket() + when (packet.packetTag) { + PUBLIC_KEY_ENC_SESSION -> { + packet as PublicKeyEncSessionPacket + if (PublicKeyAlgorithm.fromId(packet.algorithm) == null) { + return + } + } + SIGNATURE -> { + packet as SignaturePacket + if (SignatureType.fromCode(packet.signatureType) == null) { + return + } + if (PublicKeyAlgorithm.fromId(packet.keyAlgorithm) == null) { + return + } + if (HashAlgorithm.fromId(packet.hashAlgorithm) == null) { + return + } + } + ONE_PASS_SIGNATURE -> { + packet as OnePassSignaturePacket + if (SignatureType.fromCode(packet.signatureType) == null) { + return + } + if (PublicKeyAlgorithm.fromId(packet.keyAlgorithm) == null) { + return + } + if (HashAlgorithm.fromId(packet.hashAlgorithm) == null) { + return + } + } + SYMMETRIC_KEY_ENC_SESSION -> { + packet as SymmetricKeyEncSessionPacket + if (SymmetricKeyAlgorithm.fromId(packet.encAlgorithm) == null) { + return + } + } + SECRET_KEY -> { + packet as SecretKeyPacket + val publicKey = packet.publicKeyPacket + if (PublicKeyAlgorithm.fromId(publicKey.algorithm) == null) { + return + } + if (publicKey.version !in 3..6) { + return + } + } + PUBLIC_KEY -> { + packet as PublicKeyPacket + if (PublicKeyAlgorithm.fromId(packet.algorithm) == null) { + return + } + if (packet.version !in 3..6) { + return + } + } + COMPRESSED_DATA -> { + packet as CompressedDataPacket + if (CompressionAlgorithm.fromId(packet.algorithm) == null) { + return + } + } + SYMMETRIC_KEY_ENC -> { + // Not much we can check here + } + MARKER -> { + packet as MarkerPacket + if (!Arrays.areEqual( + packet.getEncoded(PacketFormat.CURRENT), + byteArrayOf(0xca.toByte(), 0x03, 0x50, 0x47, 0x50), + )) { + return + } + } + LITERAL_DATA -> { + packet as LiteralDataPacket + if (packet.format.toChar() !in charArrayOf('b', 'u', 't', 'l', '1', 'm')) { + return + } + } + SYM_ENC_INTEGRITY_PRO -> { + packet as SymmetricEncIntegrityPacket + if (packet.version !in + intArrayOf( + SymmetricEncIntegrityPacket.VERSION_1, + SymmetricEncIntegrityPacket.VERSION_2)) { + return + } + + if (packet.version == SymmetricEncIntegrityPacket.VERSION_2) { + if (SymmetricKeyAlgorithm.fromId(packet.cipherAlgorithm) == null) { + return + } + if (AEADAlgorithm.fromId(packet.aeadAlgorithm) == null) { + return + } + } + } + AEAD_ENC_DATA -> { + packet as AEADEncDataPacket + if (SymmetricKeyAlgorithm.fromId(packet.algorithm.toInt()) == null) { + return + } + } + RESERVED, // this Packet Type ID MUST NOT be used + PUBLIC_SUBKEY, // Never found at the start of a stream + SECRET_SUBKEY, // Never found at the start of a stream + TRUST, // Never found at the start of a stream + MOD_DETECTION_CODE, // At the end of SED data - Never found at the start of a stream + USER_ID, // Never found at the start of a stream + USER_ATTRIBUTE, // Never found at the start of a stream + PADDING, // At the end of messages (optionally padded message) or certificates + EXPERIMENTAL_1, // experimental + EXPERIMENTAL_2, // experimental + EXPERIMENTAL_3, // experimental + EXPERIMENTAL_4 -> { // experimental + containsOpenPgpPackets = true + resemblesMessage = false + return + } + else -> return + } + + containsOpenPgpPackets = true + if (packet.packetTag != SYMMETRIC_KEY_ENC) { + resemblesMessage = true + } + } + + private fun startsWithIgnoringWhitespace( + bytes: ByteArray, + subSequence: CharSequence, + bufferLen: Int + ): Boolean { + if (bufferLen == -1) { + return false + } + + for (i in 0 until bufferLen) { + // Working on bytes is not trivial with unicode data, but its good enough here + if (Character.isWhitespace(bytes[i].toInt())) { + continue + } + + if ((i + subSequence.length) > bytes.size) { + return false + } + + for (j in subSequence.indices) { + if (bytes[i + j].toInt().toChar() != subSequence[j]) { + return false + } + } + return true + } + return false + } + + companion object { + const val ARMOR_HEADER = "-----BEGIN PGP " + const val MAX_BUFFER_SIZE = 8192 * 2 + } +} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index a7f1cb22..15695fd6 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -1114,7 +1114,7 @@ class OpenPgpMessageInputStream( metadata: Layer, api: PGPainless ): OpenPgpMessageInputStream { - val openPgpIn = OpenPgpInputStream(inputStream) + val openPgpIn = OpenPGPAnimalSnifferInputStream(inputStream) openPgpIn.reset() if (openPgpIn.isNonOpenPgp || options.isForceNonOpenPgpData()) { diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt index ede45ce6..18b64453 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/util/ArmorUtils.kt @@ -21,7 +21,7 @@ import org.bouncycastle.openpgp.PGPSignature import org.bouncycastle.openpgp.PGPUtil import org.bouncycastle.util.io.Streams import org.pgpainless.algorithm.HashAlgorithm -import org.pgpainless.decryption_verification.OpenPgpInputStream +import org.pgpainless.decryption_verification.OpenPGPAnimalSnifferInputStream import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.util.KeyRingUtils @@ -422,7 +422,7 @@ class ArmorUtils { @JvmStatic @Throws(IOException::class) fun getDecoderStream(inputStream: InputStream): InputStream = - OpenPgpInputStream(inputStream).let { + OpenPGPAnimalSnifferInputStream(inputStream).let { if (it.isAsciiArmored) { PGPUtil.getDecoderStream(ArmoredInputStreamFactory.get(it)) } else { diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPGPAnimalSnifferInputStreamTest.java similarity index 97% rename from pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java rename to pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPGPAnimalSnifferInputStreamTest.java index 8534cb60..2d00c8ae 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPgpInputStreamTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/OpenPGPAnimalSnifferInputStreamTest.java @@ -30,7 +30,7 @@ import org.pgpainless.encryption_signing.ProducerOptions; import org.pgpainless.encryption_signing.SigningOptions; import org.pgpainless.key.protection.SecretKeyRingProtector; -public class OpenPgpInputStreamTest { +public class OpenPGPAnimalSnifferInputStreamTest { private static final Random RANDOM = new Random(); @@ -40,7 +40,7 @@ public class OpenPgpInputStreamTest { RANDOM.nextBytes(randomBytes); ByteArrayInputStream randomIn = new ByteArrayInputStream(randomBytes); - OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(randomIn); + OpenPGPAnimalSnifferInputStream openPgpInputStream = new OpenPGPAnimalSnifferInputStream(randomIn); assertFalse(openPgpInputStream.isAsciiArmored()); assertFalse(openPgpInputStream.isLikelyOpenPgpMessage(), Hex.toHexString(randomBytes, 0, 150)); @@ -56,7 +56,7 @@ public class OpenPgpInputStreamTest { public void largeCompressedDataIsBinaryOpenPgp() throws IOException { // Since we are compressing RANDOM data, the output will likely be roughly the same size // So we very likely will end up with data larger than the MAX_BUFFER_SIZE - byte[] randomBytes = new byte[OpenPgpInputStream.MAX_BUFFER_SIZE * 10]; + byte[] randomBytes = new byte[OpenPGPAnimalSnifferInputStream.MAX_BUFFER_SIZE * 10]; RANDOM.nextBytes(randomBytes); ByteArrayOutputStream compressedDataPacket = new ByteArrayOutputStream(); @@ -65,7 +65,7 @@ public class OpenPgpInputStreamTest { compressor.write(randomBytes); compressor.close(); - OpenPgpInputStream inputStream = new OpenPgpInputStream(new ByteArrayInputStream(compressedDataPacket.toByteArray())); + OpenPGPAnimalSnifferInputStream inputStream = new OpenPGPAnimalSnifferInputStream(new ByteArrayInputStream(compressedDataPacket.toByteArray())); assertFalse(inputStream.isAsciiArmored()); assertFalse(inputStream.isNonOpenPgp()); assertTrue(inputStream.isBinaryOpenPgp()); @@ -90,7 +90,7 @@ public class OpenPgpInputStreamTest { "-----END PGP MESSAGE-----"; ByteArrayInputStream asciiIn = new ByteArrayInputStream(asciiArmoredMessage.getBytes(StandardCharsets.UTF_8)); - OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(asciiIn); + OpenPGPAnimalSnifferInputStream openPgpInputStream = new OpenPGPAnimalSnifferInputStream(asciiIn); assertTrue(openPgpInputStream.isAsciiArmored()); assertFalse(openPgpInputStream.isNonOpenPgp()); @@ -663,9 +663,9 @@ public class OpenPgpInputStreamTest { @Test public void longAsciiArmoredMessageIsAsciiArmored() throws IOException { byte[] asciiArmoredBytes = longAsciiArmoredMessage.getBytes(StandardCharsets.UTF_8); - assertTrue(asciiArmoredBytes.length > OpenPgpInputStream.MAX_BUFFER_SIZE); + assertTrue(asciiArmoredBytes.length > OpenPGPAnimalSnifferInputStream.MAX_BUFFER_SIZE); ByteArrayInputStream asciiIn = new ByteArrayInputStream(asciiArmoredBytes); - OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(asciiIn); + OpenPGPAnimalSnifferInputStream openPgpInputStream = new OpenPGPAnimalSnifferInputStream(asciiIn); assertTrue(openPgpInputStream.isAsciiArmored()); assertFalse(openPgpInputStream.isNonOpenPgp()); @@ -694,7 +694,7 @@ public class OpenPgpInputStreamTest { byte[] binaryBytes = binaryOut.toByteArray(); ByteArrayInputStream binaryIn = new ByteArrayInputStream(binaryBytes); - OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(binaryIn); + OpenPGPAnimalSnifferInputStream openPgpInputStream = new OpenPGPAnimalSnifferInputStream(binaryIn); assertTrue(openPgpInputStream.isBinaryOpenPgp()); assertFalse(openPgpInputStream.isAsciiArmored()); @@ -714,7 +714,7 @@ public class OpenPgpInputStreamTest { byte[] binaryBytes = binaryOut.toByteArray(); ByteArrayInputStream binaryIn = new ByteArrayInputStream(binaryBytes); - OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(binaryIn); + OpenPGPAnimalSnifferInputStream openPgpInputStream = new OpenPGPAnimalSnifferInputStream(binaryIn); assertTrue(openPgpInputStream.isBinaryOpenPgp()); assertFalse(openPgpInputStream.isAsciiArmored()); @@ -728,7 +728,7 @@ public class OpenPgpInputStreamTest { @Test public void emptyStreamTest() throws IOException { ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]); - OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(in); + OpenPGPAnimalSnifferInputStream openPgpInputStream = new OpenPGPAnimalSnifferInputStream(in); assertFalse(openPgpInputStream.isBinaryOpenPgp()); assertFalse(openPgpInputStream.isAsciiArmored()); @@ -755,7 +755,7 @@ public class OpenPgpInputStreamTest { byte[] binary = signedOut.toByteArray(); - OpenPgpInputStream openPgpIn = new OpenPgpInputStream(new ByteArrayInputStream(binary)); + OpenPGPAnimalSnifferInputStream openPgpIn = new OpenPGPAnimalSnifferInputStream(new ByteArrayInputStream(binary)); assertFalse(openPgpIn.isAsciiArmored()); assertTrue(openPgpIn.isLikelyOpenPgpMessage()); } diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt index daffcd30..be2f272f 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/ArmorImpl.kt @@ -10,7 +10,7 @@ import java.io.OutputStream import kotlin.jvm.Throws import org.bouncycastle.util.io.Streams import org.pgpainless.PGPainless -import org.pgpainless.decryption_verification.OpenPgpInputStream +import org.pgpainless.decryption_verification.OpenPGPAnimalSnifferInputStream import org.pgpainless.util.ArmoredOutputStreamFactory import sop.Ready import sop.exception.SOPGPException @@ -27,7 +27,7 @@ class ArmorImpl(private val api: PGPainless) : Armor { val bufferedOutputStream = BufferedOutputStream(outputStream) // Determine the nature of the given data - val openPgpIn = OpenPgpInputStream(data) + val openPgpIn = OpenPGPAnimalSnifferInputStream(data) openPgpIn.reset() if (openPgpIn.isAsciiArmored) { diff --git a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt index 6a751550..6c571163 100644 --- a/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt +++ b/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/InlineDetachImpl.kt @@ -15,7 +15,7 @@ import org.bouncycastle.openpgp.PGPOnePassSignatureList import org.bouncycastle.openpgp.PGPSignatureList import org.bouncycastle.util.io.Streams import org.pgpainless.PGPainless -import org.pgpainless.decryption_verification.OpenPgpInputStream +import org.pgpainless.decryption_verification.OpenPGPAnimalSnifferInputStream import org.pgpainless.decryption_verification.cleartext_signatures.ClearsignedMessageUtil import org.pgpainless.exception.WrongConsumingMethodException import org.pgpainless.util.ArmoredOutputStreamFactory @@ -35,7 +35,7 @@ class InlineDetachImpl(private val api: PGPainless) : InlineDetach { private val sigOut = ByteArrayOutputStream() override fun writeTo(outputStream: OutputStream): Signatures { - var pgpIn = OpenPgpInputStream(messageInputStream) + var pgpIn = OpenPGPAnimalSnifferInputStream(messageInputStream) if (pgpIn.isNonOpenPgp) { throw SOPGPException.BadData("Data appears to be non-OpenPGP.") } @@ -61,7 +61,7 @@ class InlineDetachImpl(private val api: PGPainless) : InlineDetach { } // else just dearmor - pgpIn = OpenPgpInputStream(armorIn) + pgpIn = OpenPGPAnimalSnifferInputStream(armorIn) } // If data was not using cleartext signature framework