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

pgpainless / pgpainless / #1055

14 Apr 2025 02:05PM CUT coverage: 86.84% (-0.01%) from 86.854%
#1055

push

github

vanitasvitae
PGPainless 1.7.7-SNAPSHOT

6566 of 7561 relevant lines covered (86.84%)

0.87 hits per line

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

70.95
/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/OpenPgpInputStream.java
1
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
2
//
3
// SPDX-License-Identifier: Apache-2.0
4

5
package org.pgpainless.decryption_verification;
6

7
import static org.bouncycastle.bcpg.PacketTags.COMPRESSED_DATA;
8
import static org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_1;
9
import static org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_2;
10
import static org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_3;
11
import static org.bouncycastle.bcpg.PacketTags.EXPERIMENTAL_4;
12
import static org.bouncycastle.bcpg.PacketTags.LITERAL_DATA;
13
import static org.bouncycastle.bcpg.PacketTags.MARKER;
14
import static org.bouncycastle.bcpg.PacketTags.MOD_DETECTION_CODE;
15
import static org.bouncycastle.bcpg.PacketTags.ONE_PASS_SIGNATURE;
16
import static org.bouncycastle.bcpg.PacketTags.PUBLIC_KEY;
17
import static org.bouncycastle.bcpg.PacketTags.PUBLIC_KEY_ENC_SESSION;
18
import static org.bouncycastle.bcpg.PacketTags.PUBLIC_SUBKEY;
19
import static org.bouncycastle.bcpg.PacketTags.RESERVED;
20
import static org.bouncycastle.bcpg.PacketTags.SECRET_KEY;
21
import static org.bouncycastle.bcpg.PacketTags.SECRET_SUBKEY;
22
import static org.bouncycastle.bcpg.PacketTags.SIGNATURE;
23
import static org.bouncycastle.bcpg.PacketTags.SYMMETRIC_KEY_ENC;
24
import static org.bouncycastle.bcpg.PacketTags.SYMMETRIC_KEY_ENC_SESSION;
25
import static org.bouncycastle.bcpg.PacketTags.SYM_ENC_INTEGRITY_PRO;
26
import static org.bouncycastle.bcpg.PacketTags.TRUST;
27
import static org.bouncycastle.bcpg.PacketTags.USER_ATTRIBUTE;
28
import static org.bouncycastle.bcpg.PacketTags.USER_ID;
29

30
import java.io.BufferedInputStream;
31
import java.io.ByteArrayInputStream;
32
import java.io.IOException;
33
import java.io.InputStream;
34
import java.nio.charset.Charset;
35
import java.util.NoSuchElementException;
36

37
import org.bouncycastle.bcpg.BCPGInputStream;
38
import org.bouncycastle.openpgp.PGPCompressedData;
39
import org.bouncycastle.openpgp.PGPEncryptedData;
40
import org.bouncycastle.openpgp.PGPLiteralData;
41
import org.bouncycastle.openpgp.PGPOnePassSignature;
42
import org.pgpainless.algorithm.CompressionAlgorithm;
43
import org.pgpainless.algorithm.HashAlgorithm;
44
import org.pgpainless.algorithm.PublicKeyAlgorithm;
45
import org.pgpainless.algorithm.SignatureType;
46
import org.pgpainless.algorithm.StreamEncoding;
47
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
48

49
/**
50
 * InputStream used to determine the nature of potential OpenPGP data.
51
 */
52
public class OpenPgpInputStream extends BufferedInputStream {
53

54
    @SuppressWarnings("CharsetObjectCanBeUsed")
55
    private static final byte[] ARMOR_HEADER = "-----BEGIN PGP ".getBytes(Charset.forName("UTF8"));
1✔
56

57
    // Buffer beginning bytes of the data
58
    public static final int MAX_BUFFER_SIZE = 8192 * 2;
59

60
    private final byte[] buffer;
61
    private final int bufferLen;
62

63
    private boolean containsArmorHeader;
64
    private boolean containsOpenPgpPackets;
65
    private boolean isLikelyOpenPgpMessage;
66

67
    public OpenPgpInputStream(InputStream in, boolean check) throws IOException {
68
        super(in, MAX_BUFFER_SIZE);
1✔
69

70
        mark(MAX_BUFFER_SIZE);
1✔
71
        buffer = new byte[MAX_BUFFER_SIZE];
1✔
72
        bufferLen = read(buffer);
1✔
73
        reset();
1✔
74

75
        if (check) {
1✔
76
            inspectBuffer();
1✔
77
        }
78
    }
1✔
79

80
    public OpenPgpInputStream(InputStream in) throws IOException {
81
        this(in, true);
1✔
82
    }
1✔
83

84
    private void inspectBuffer() throws IOException {
85
        if (checkForAsciiArmor()) {
1✔
86
            return;
1✔
87
        }
88

89
        checkForBinaryOpenPgp();
1✔
90
    }
1✔
91

92
    private boolean checkForAsciiArmor() {
93
        if (startsWithIgnoringWhitespace(buffer, ARMOR_HEADER, bufferLen)) {
1✔
94
            containsArmorHeader = true;
1✔
95
            return true;
1✔
96
        }
97
        return false;
1✔
98
    }
99

100
    /**
101
     * This method is still brittle.
102
     * Basically we try to parse OpenPGP packets from the buffer.
103
     * If we run into exceptions, then we know that the data is non-OpenPGP'ish.
104
     *
105
     * This breaks down though if we read plausible garbage where the data accidentally makes sense,
106
     * or valid, yet incomplete packets (remember, we are still only working on a portion of the data).
107
     */
108
    private void checkForBinaryOpenPgp() throws IOException {
109
        if (bufferLen == -1) {
1✔
110
            // Empty data
111
            return;
1✔
112
        }
113

114
        ByteArrayInputStream bufferIn = new ByteArrayInputStream(buffer, 0, bufferLen);
1✔
115
        nonExhaustiveParseAndCheckPlausibility(bufferIn);
1✔
116
    }
1✔
117

118
    private void nonExhaustiveParseAndCheckPlausibility(ByteArrayInputStream bufferIn) throws IOException {
119
        // Read the packet header
120
        int hdr = bufferIn.read();
1✔
121
        if (hdr < 0 || (hdr & 0x80) == 0) {
1✔
122
            return;
1✔
123
        }
124

125
        boolean newPacket = (hdr & 0x40) != 0;
1✔
126
        int        tag = 0;
1✔
127
        int        bodyLen = 0;
1✔
128
        boolean    partial = false;
1✔
129

130
        // Determine the packet length
131
        if (newPacket) {
1✔
132
            tag = hdr & 0x3f;
1✔
133

134
            int    l = bufferIn.read();
1✔
135
            if (l < 192) {
1✔
136
                bodyLen = l;
1✔
137
            } else if (l <= 223) {
1✔
138
                int b = bufferIn.read();
1✔
139
                bodyLen = ((l - 192) << 8) + (b) + 192;
1✔
140
            } else if (l == 255) {
1✔
141
                bodyLen = (bufferIn.read() << 24) | (bufferIn.read() << 16) |  (bufferIn.read() << 8)  | bufferIn.read();
×
142
            } else {
143
                partial = true;
×
144
                bodyLen = 1 << (l & 0x1f);
×
145
            }
146
        } else {
1✔
147
            int lengthType = hdr & 0x3;
1✔
148
            tag = (hdr & 0x3f) >> 2;
1✔
149
            switch (lengthType) {
1✔
150
                case 0:
151
                    bodyLen = bufferIn.read();
1✔
152
                    break;
1✔
153
                case 1:
154
                    bodyLen = (bufferIn.read() << 8) | bufferIn.read();
1✔
155
                    break;
1✔
156
                case 2:
157
                    bodyLen = (bufferIn.read() << 24) | (bufferIn.read() << 16) | (bufferIn.read() << 8) | bufferIn.read();
×
158
                    break;
×
159
                case 3:
160
                    partial = true;
1✔
161
                    break;
1✔
162
                default:
163
                    return;
×
164
            }
165
        }
166

167
        // Negative body length -> garbage
168
        if (bodyLen < 0) {
1✔
169
            return;
×
170
        }
171

172
        // Try to unexhaustively parse the first packet bit by bit and check for plausibility
173
        BCPGInputStream bcpgIn = new BCPGInputStream(bufferIn);
1✔
174
        switch (tag) {
1✔
175
            case RESERVED:
176
                // How to handle this? Probably discard as garbage...
177
                return;
×
178

179
            case PUBLIC_KEY_ENC_SESSION:
180
                int pkeskVersion = bcpgIn.read();
1✔
181
                if (pkeskVersion <= 0 || pkeskVersion > 5) {
1✔
182
                    return;
×
183
                }
184

185
                // Skip Key-ID
186
                for (int i = 0; i < 8; i++) {
1✔
187
                    bcpgIn.read();
1✔
188
                }
189

190
                int pkeskAlg = bcpgIn.read();
1✔
191
                if (PublicKeyAlgorithm.fromId(pkeskAlg) == null) {
1✔
192
                    return;
×
193
                }
194

195
                containsOpenPgpPackets = true;
1✔
196
                isLikelyOpenPgpMessage = true;
1✔
197
                break;
1✔
198

199
            case SIGNATURE:
200
                int sigVersion = bcpgIn.read();
1✔
201
                int sigType;
202
                if (sigVersion == 2 || sigVersion == 3) {
1✔
203
                    int l = bcpgIn.read();
1✔
204
                    sigType = bcpgIn.read();
1✔
205
                } else if (sigVersion == 4 || sigVersion == 5) {
1✔
206
                    sigType = bcpgIn.read();
1✔
207
                } else {
208
                    return;
×
209
                }
210

211
                try {
212
                    SignatureType.requireFromCode(sigType);
1✔
213
                } catch (NoSuchElementException e) {
×
214
                    return;
×
215
                }
1✔
216

217
                containsOpenPgpPackets = true;
1✔
218
                break;
1✔
219

220
            case SYMMETRIC_KEY_ENC_SESSION:
221
                int skeskVersion = bcpgIn.read();
1✔
222
                if (skeskVersion == 4) {
1✔
223
                    int skeskAlg = bcpgIn.read();
1✔
224
                    if (SymmetricKeyAlgorithm.fromId(skeskAlg) == null) {
1✔
225
                        return;
×
226
                    }
227
                    // TODO: Parse S2K?
228
                } else {
1✔
229
                    return;
×
230
                }
231
                containsOpenPgpPackets = true;
1✔
232
                isLikelyOpenPgpMessage = true;
1✔
233
                break;
1✔
234

235
            case ONE_PASS_SIGNATURE:
236
                int opsVersion = bcpgIn.read();
1✔
237
                if (opsVersion == 3) {
1✔
238
                    int opsSigType = bcpgIn.read();
1✔
239
                    try {
240
                        SignatureType.requireFromCode(opsSigType);
1✔
241
                    } catch (NoSuchElementException e) {
×
242
                        return;
×
243
                    }
1✔
244
                    int opsHashAlg = bcpgIn.read();
1✔
245
                    if (HashAlgorithm.fromId(opsHashAlg) == null) {
1✔
246
                        return;
×
247
                    }
248
                    int opsKeyAlg = bcpgIn.read();
1✔
249
                    if (PublicKeyAlgorithm.fromId(opsKeyAlg) == null) {
1✔
250
                        return;
×
251
                    }
252
                } else {
1✔
253
                    return;
×
254
                }
255

256
                containsOpenPgpPackets = true;
1✔
257
                isLikelyOpenPgpMessage = true;
1✔
258
                break;
1✔
259

260
            case SECRET_KEY:
261
            case PUBLIC_KEY:
262
            case SECRET_SUBKEY:
263
            case PUBLIC_SUBKEY:
264
                int keyVersion = bcpgIn.read();
1✔
265
                for (int i = 0; i < 4; i++) {
1✔
266
                    // Creation time
267
                    bcpgIn.read();
1✔
268
                }
269
                if (keyVersion == 3) {
1✔
270
                    long validDays = (in.read() << 8) | in.read();
×
271
                    if (validDays < 0) {
×
272
                        return;
×
273
                    }
274
                } else if (keyVersion == 4) {
1✔
275

276
                } else if (keyVersion == 5) {
×
277

278
                } else {
279
                    return;
×
280
                }
281
                int keyAlg = bcpgIn.read();
1✔
282
                if (PublicKeyAlgorithm.fromId(keyAlg) == null) {
1✔
283
                    return;
×
284
                }
285

286
                containsOpenPgpPackets = true;
1✔
287
                break;
1✔
288

289
            case COMPRESSED_DATA:
290
                int compAlg = bcpgIn.read();
1✔
291
                if (CompressionAlgorithm.fromId(compAlg) == null) {
1✔
292
                    return;
×
293
                }
294

295
                containsOpenPgpPackets = true;
1✔
296
                isLikelyOpenPgpMessage = true;
1✔
297
                break;
1✔
298

299
            case SYMMETRIC_KEY_ENC:
300
                // No data to compare :(
301
                containsOpenPgpPackets = true;
×
302
                // While this is a valid OpenPGP message, enabling the line below would lead to too many false positives
303
                // isLikelyOpenPgpMessage = true;
304
                break;
×
305

306
            case MARKER:
307
                byte[] marker = new byte[3];
×
308
                bcpgIn.readFully(marker);
×
309
                if (marker[0] != 0x50 || marker[1] != 0x47 || marker[2] != 0x50) {
×
310
                    return;
×
311
                }
312

313
                containsOpenPgpPackets = true;
×
314
                break;
×
315

316
            case LITERAL_DATA:
317
                int format = bcpgIn.read();
1✔
318
                if (StreamEncoding.fromCode(format) == null) {
1✔
319
                    return;
×
320
                }
321

322
                containsOpenPgpPackets = true;
1✔
323
                isLikelyOpenPgpMessage = true;
1✔
324
                break;
1✔
325

326
            case TRUST:
327
            case USER_ID:
328
            case USER_ATTRIBUTE:
329
                // Not much to compare
330
                containsOpenPgpPackets = true;
×
331
                break;
×
332

333
            case SYM_ENC_INTEGRITY_PRO:
334
                int seipVersion = bcpgIn.read();
×
335
                if (seipVersion != 1) {
×
336
                    return;
×
337
                }
338
                isLikelyOpenPgpMessage = true;
×
339
                containsOpenPgpPackets = true;
×
340
                break;
×
341

342
            case MOD_DETECTION_CODE:
343
                byte[] digest = new byte[20];
×
344
                bcpgIn.readFully(digest);
×
345

346
                containsOpenPgpPackets = true;
×
347
                break;
×
348

349
            case EXPERIMENTAL_1:
350
            case EXPERIMENTAL_2:
351
            case EXPERIMENTAL_3:
352
            case EXPERIMENTAL_4:
353
                return;
×
354
            default:
355
                containsOpenPgpPackets = false;
×
356
                break;
357
        }
358
    }
1✔
359

360
    private boolean startsWithIgnoringWhitespace(byte[] bytes, byte[] subsequence, int bufferLen) {
361
        if (bufferLen == -1) {
1✔
362
            return false;
1✔
363
        }
364

365
        for (int i = 0; i < bufferLen; i++) {
1✔
366
            // Working on bytes is not trivial with unicode data, but its good enough here
367
            if (Character.isWhitespace(bytes[i])) {
1✔
368
                continue;
1✔
369
            }
370

371
            if ((i + subsequence.length) > bytes.length) {
1✔
372
                return false;
×
373
            }
374

375
            for (int j = 0; j < subsequence.length; j++) {
1✔
376
                if (bytes[i + j] != subsequence[j]) {
1✔
377
                    return false;
1✔
378
                }
379
            }
380
            return true;
1✔
381
        }
382
        return false;
×
383
    }
384

385
    public boolean isAsciiArmored() {
386
        return containsArmorHeader;
1✔
387
    }
388

389
    /**
390
     * Return true, if the data is possibly binary OpenPGP.
391
     * The criterion for this are less strict than for {@link #isLikelyOpenPgpMessage()},
392
     * as it also accepts other OpenPGP packets at the beginning of the data stream.
393
     *
394
     * Use with caution.
395
     *
396
     * @return true if data appears to be binary OpenPGP data
397
     */
398
    public boolean isBinaryOpenPgp() {
399
        return containsOpenPgpPackets;
1✔
400
    }
401

402
    /**
403
     * Returns true, if the underlying data is very likely (more than 99,9%) an OpenPGP message.
404
     * OpenPGP Message means here that it starts with either an {@link PGPEncryptedData},
405
     * {@link PGPCompressedData}, {@link PGPOnePassSignature} or {@link PGPLiteralData} packet.
406
     * The plausability of these data packets is checked as far as possible.
407
     *
408
     * @return true if likely OpenPGP message
409
     */
410
    public boolean isLikelyOpenPgpMessage() {
411
        return isLikelyOpenPgpMessage;
1✔
412
    }
413

414
    public boolean isNonOpenPgp() {
415
        return !isAsciiArmored() && !isBinaryOpenPgp();
1✔
416
    }
417
}
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

© 2025 Coveralls, Inc