• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pgpainless / pgpainless / #1077

21 Jan 2026 04:53PM UTC coverage: 85.293% (-0.01%) from 85.306%
#1077

push

github

vanitasvitae
OpenPgpV5FingerprintTest: Make use of assertInstanceOf()

6774 of 7942 relevant lines covered (85.29%)

0.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.22
/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPGPAnimalSnifferInputStream.kt
1
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
2
//
3
// SPDX-License-Identifier: Apache-2.0
4

5
package org.pgpainless.decryption_verification
6

7
import java.io.BufferedInputStream
8
import java.io.ByteArrayInputStream
9
import java.io.InputStream
10
import org.bouncycastle.bcpg.AEADEncDataPacket
11
import org.bouncycastle.bcpg.BCPGInputStream
12
import org.bouncycastle.bcpg.CompressedDataPacket
13
import org.bouncycastle.bcpg.LiteralDataPacket
14
import org.bouncycastle.bcpg.MarkerPacket
15
import org.bouncycastle.bcpg.OnePassSignaturePacket
16
import org.bouncycastle.bcpg.PacketFormat
17
import org.bouncycastle.bcpg.PacketTags.AEAD_ENC_DATA
18
import org.bouncycastle.bcpg.PacketTags.COMPRESSED_DATA
19
import org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_1
20
import org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_2
21
import org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_3
22
import org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_4
23
import org.bouncycastle.bcpg.PacketTags.LITERAL_DATA
24
import org.bouncycastle.bcpg.PacketTags.MARKER
25
import org.bouncycastle.bcpg.PacketTags.MOD_DETECTION_CODE
26
import org.bouncycastle.bcpg.PacketTags.ONE_PASS_SIGNATURE
27
import org.bouncycastle.bcpg.PacketTags.PADDING
28
import org.bouncycastle.bcpg.PacketTags.PUBLIC_KEY
29
import org.bouncycastle.bcpg.PacketTags.PUBLIC_KEY_ENC_SESSION
30
import org.bouncycastle.bcpg.PacketTags.PUBLIC_SUBKEY
31
import org.bouncycastle.bcpg.PacketTags.RESERVED
32
import org.bouncycastle.bcpg.PacketTags.SECRET_KEY
33
import org.bouncycastle.bcpg.PacketTags.SECRET_SUBKEY
34
import org.bouncycastle.bcpg.PacketTags.SIGNATURE
35
import org.bouncycastle.bcpg.PacketTags.SYMMETRIC_KEY_ENC
36
import org.bouncycastle.bcpg.PacketTags.SYMMETRIC_KEY_ENC_SESSION
37
import org.bouncycastle.bcpg.PacketTags.SYM_ENC_INTEGRITY_PRO
38
import org.bouncycastle.bcpg.PacketTags.TRUST
39
import org.bouncycastle.bcpg.PacketTags.USER_ATTRIBUTE
40
import org.bouncycastle.bcpg.PacketTags.USER_ID
41
import org.bouncycastle.bcpg.PublicKeyEncSessionPacket
42
import org.bouncycastle.bcpg.PublicKeyPacket
43
import org.bouncycastle.bcpg.SecretKeyPacket
44
import org.bouncycastle.bcpg.SignaturePacket
45
import org.bouncycastle.bcpg.SymmetricEncIntegrityPacket
46
import org.bouncycastle.bcpg.SymmetricKeyEncSessionPacket
47
import org.bouncycastle.util.Arrays
48
import org.pgpainless.algorithm.AEADAlgorithm
49
import org.pgpainless.algorithm.CompressionAlgorithm
50
import org.pgpainless.algorithm.HashAlgorithm
51
import org.pgpainless.algorithm.PublicKeyAlgorithm
52
import org.pgpainless.algorithm.SignatureType
53
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
54

55
/**
56
 * InputStream used to determine the nature of potential OpenPGP data.
57
 *
58
 * @param input underlying input stream
59
 * @param check whether to perform the costly checking inside the constructor
60
 */
61
class OpenPGPAnimalSnifferInputStream(input: InputStream, check: Boolean) :
1✔
62
    BufferedInputStream(input) {
1✔
63

64
    private val buffer: ByteArray
65
    private val bufferLen: Int
66

67
    private var containsArmorHeader: Boolean = false
68
    private var containsOpenPgpPackets: Boolean = false
69
    private var resemblesMessage: Boolean = false
70

71
    init {
1✔
72
        mark(MAX_BUFFER_SIZE)
1✔
73
        buffer = ByteArray(MAX_BUFFER_SIZE)
1✔
74
        bufferLen = read(buffer)
1✔
75
        reset()
1✔
76

77
        if (check) {
1✔
78
            inspectBuffer()
1✔
79
        }
80
    }
1✔
81

82
    constructor(input: InputStream) : this(input, true)
1✔
83

84
    /** Return true, if the underlying data is ASCII armored. */
85
    val isAsciiArmored: Boolean
86
        get() = containsArmorHeader
1✔
87

88
    /**
89
     * Return true, if the data is possibly binary OpenPGP. The criterion for this are less strict
90
     * than for [resemblesMessage], as it also accepts other OpenPGP packets at the beginning of the
91
     * data stream.
92
     *
93
     * <p>
94
     * Use with caution.
95
     *
96
     * @return true if data appears to be binary OpenPGP data
97
     */
98
    val isBinaryOpenPgp: Boolean
99
        get() = containsOpenPgpPackets
1✔
100

101
    /**
102
     * Returns true, if the underlying data is very likely (more than 99,9%) an OpenPGP message.
103
     * OpenPGP Message means here that it starts with either a [PGPEncryptedData],
104
     * [PGPCompressedData], [PGPOnePassSignature] or [PGPLiteralData] packet. The plausibility of
105
     * these data packets is checked as far as possible.
106
     *
107
     * @return true if likely OpenPGP message
108
     */
109
    val isLikelyOpenPgpMessage: Boolean
110
        get() = resemblesMessage
1✔
111

112
    /** Return true, if the underlying data is non-OpenPGP data. */
113
    val isNonOpenPgp: Boolean
114
        get() = !isAsciiArmored && !isBinaryOpenPgp
1✔
115

116
    /** Costly perform a plausibility check of the first encountered OpenPGP packet. */
117
    fun inspectBuffer() {
118
        if (checkForAsciiArmor()) {
1✔
119
            return
1✔
120
        }
121

122
        checkForBinaryOpenPgp()
1✔
123
    }
1✔
124

125
    private fun checkForAsciiArmor(): Boolean {
126
        if (startsWithIgnoringWhitespace(buffer, ARMOR_HEADER, bufferLen)) {
1✔
127
            containsArmorHeader = true
1✔
128
            return true
1✔
129
        }
130
        return false
1✔
131
    }
132

133
    /**
134
     * This method is still brittle. Basically we try to parse OpenPGP packets from the buffer. If
135
     * we run into exceptions, then we know that the data is non-OpenPGP'ish.
136
     *
137
     * <p>
138
     * This breaks down though if we read plausible garbage where the data accidentally makes sense,
139
     * or valid, yet incomplete packets (remember, we are still only working on a portion of the
140
     * data).
141
     */
142
    private fun checkForBinaryOpenPgp() {
143
        if (bufferLen == -1) {
1✔
144
            // empty data
145
            return
1✔
146
        }
147

148
        val bufferIn = ByteArrayInputStream(buffer, 0, bufferLen)
1✔
149
        val pIn = BCPGInputStream(bufferIn)
1✔
150
        try {
1✔
151
            nonExhaustiveParseAndCheckPlausibility(pIn)
1✔
152
        } catch (e: Exception) {
1✔
153
            return
1✔
154
        }
155
    }
1✔
156

157
    private fun nonExhaustiveParseAndCheckPlausibility(packetIn: BCPGInputStream) {
158
        val packet = packetIn.readPacket()
1✔
159
        when (packet.packetTag) {
1✔
160
            PUBLIC_KEY_ENC_SESSION -> {
161
                packet as PublicKeyEncSessionPacket
1✔
162
                if (PublicKeyAlgorithm.fromId(packet.algorithm) == null) {
1✔
163
                    return
×
164
                }
165
            }
166
            SIGNATURE -> {
167
                packet as SignaturePacket
1✔
168
                if (SignatureType.fromCode(packet.signatureType) == null) {
1✔
169
                    return
×
170
                }
171
                if (PublicKeyAlgorithm.fromId(packet.keyAlgorithm) == null) {
1✔
172
                    return
×
173
                }
174
                if (HashAlgorithm.fromId(packet.hashAlgorithm) == null) {
1✔
175
                    return
×
176
                }
177
            }
178
            ONE_PASS_SIGNATURE -> {
179
                packet as OnePassSignaturePacket
1✔
180
                if (SignatureType.fromCode(packet.signatureType) == null) {
1✔
181
                    return
×
182
                }
183
                if (PublicKeyAlgorithm.fromId(packet.keyAlgorithm) == null) {
1✔
184
                    return
×
185
                }
186
                if (HashAlgorithm.fromId(packet.hashAlgorithm) == null) {
1✔
187
                    return
×
188
                }
189
            }
190
            SYMMETRIC_KEY_ENC_SESSION -> {
191
                packet as SymmetricKeyEncSessionPacket
1✔
192
                if (SymmetricKeyAlgorithm.fromId(packet.encAlgorithm) == null) {
1✔
193
                    return
×
194
                }
195
            }
196
            SECRET_KEY -> {
197
                packet as SecretKeyPacket
1✔
198
                val publicKey = packet.publicKeyPacket
1✔
199
                if (PublicKeyAlgorithm.fromId(publicKey.algorithm) == null) {
1✔
200
                    return
×
201
                }
202
                if (publicKey.version !in 3..6) {
1✔
203
                    return
×
204
                }
205
            }
206
            PUBLIC_KEY -> {
207
                packet as PublicKeyPacket
1✔
208
                if (PublicKeyAlgorithm.fromId(packet.algorithm) == null) {
1✔
209
                    return
×
210
                }
211
                if (packet.version !in 3..6) {
1✔
212
                    return
×
213
                }
214
            }
215
            COMPRESSED_DATA -> {
216
                packet as CompressedDataPacket
1✔
217
                if (CompressionAlgorithm.fromId(packet.algorithm) == null) {
1✔
218
                    return
×
219
                }
220
            }
221
            SYMMETRIC_KEY_ENC -> {
222
                // Not much we can check here
223
            }
224
            MARKER -> {
225
                packet as MarkerPacket
×
226
                if (!Arrays.areEqual(
×
227
                    packet.getEncoded(PacketFormat.CURRENT),
×
228
                    byteArrayOf(0xca.toByte(), 0x03, 0x50, 0x47, 0x50),
×
229
                )) {
230
                    return
×
231
                }
232
            }
233
            LITERAL_DATA -> {
234
                packet as LiteralDataPacket
1✔
235
                if (packet.format.toChar() !in charArrayOf('b', 'u', 't', 'l', '1', 'm')) {
1✔
236
                    return
×
237
                }
238
            }
239
            SYM_ENC_INTEGRITY_PRO -> {
240
                packet as SymmetricEncIntegrityPacket
×
241
                if (packet.version !in
×
242
                    intArrayOf(
243
                        SymmetricEncIntegrityPacket.VERSION_1,
×
244
                        SymmetricEncIntegrityPacket.VERSION_2)) {
×
245
                    return
×
246
                }
247

248
                if (packet.version == SymmetricEncIntegrityPacket.VERSION_2) {
×
249
                    if (SymmetricKeyAlgorithm.fromId(packet.cipherAlgorithm) == null) {
×
250
                        return
×
251
                    }
252
                    if (AEADAlgorithm.fromId(packet.aeadAlgorithm) == null) {
×
253
                        return
×
254
                    }
255
                }
256
            }
257
            AEAD_ENC_DATA -> {
258
                packet as AEADEncDataPacket
×
259
                if (SymmetricKeyAlgorithm.fromId(packet.algorithm.toInt()) == null) {
×
260
                    return
×
261
                }
262
            }
263
            RESERVED, // this Packet Type ID MUST NOT be used
264
            PUBLIC_SUBKEY, // Never found at the start of a stream
265
            SECRET_SUBKEY, // Never found at the start of a stream
266
            TRUST, // Never found at the start of a stream
267
            MOD_DETECTION_CODE, // At the end of SED data - Never found at the start of a stream
268
            USER_ID, // Never found at the start of a stream
269
            USER_ATTRIBUTE, // Never found at the start of a stream
270
            PADDING, // At the end of messages (optionally padded message) or certificates
271
            EXPERIMENTAL_1, // experimental
272
            EXPERIMENTAL_2, // experimental
273
            EXPERIMENTAL_3, // experimental
274
            EXPERIMENTAL_4 -> { // experimental
275
                containsOpenPgpPackets = true
×
276
                resemblesMessage = false
×
277
                return
×
278
            }
279
            else -> return
×
280
        }
281

282
        containsOpenPgpPackets = true
1✔
283
        if (packet.packetTag != SYMMETRIC_KEY_ENC) {
1✔
284
            resemblesMessage = true
1✔
285
        }
286
    }
1✔
287

288
    private fun startsWithIgnoringWhitespace(
289
        bytes: ByteArray,
290
        subSequence: CharSequence,
291
        bufferLen: Int
292
    ): Boolean {
293
        if (bufferLen == -1) {
1✔
294
            return false
1✔
295
        }
296

297
        for (i in 0 until bufferLen) {
1✔
298
            // Working on bytes is not trivial with unicode data, but its good enough here
299
            if (Character.isWhitespace(bytes[i].toInt())) {
1✔
300
                continue
×
301
            }
302

303
            if ((i + subSequence.length) > bytes.size) {
1✔
304
                return false
×
305
            }
306

307
            for (j in subSequence.indices) {
1✔
308
                if (bytes[i + j].toInt().toChar() != subSequence[j]) {
1✔
309
                    return false
1✔
310
                }
311
            }
312
            return true
1✔
313
        }
314
        return false
×
315
    }
316

317
    companion object {
318
        const val ARMOR_HEADER = "-----BEGIN PGP "
319
        const val MAX_BUFFER_SIZE = 8192 * 2
320
    }
321
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc