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

pgpainless / pgpainless / #1038

pending completion
#1038

push

github-actions

vanitasvitae
Override evaluation date in test with expiring key

7083 of 7950 relevant lines covered (89.09%)

0.89 hits per line

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

71.51
/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

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

48
public class OpenPgpInputStream extends BufferedInputStream {
49

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

53
    // Buffer beginning bytes of the data
54
    public static final int MAX_BUFFER_SIZE = 8192 * 2;
55

56
    private final byte[] buffer;
57
    private final int bufferLen;
58

59
    private boolean containsArmorHeader;
60
    private boolean containsOpenPgpPackets;
61
    private boolean isLikelyOpenPgpMessage;
62

63
    public OpenPgpInputStream(InputStream in, boolean check) throws IOException {
64
        super(in, MAX_BUFFER_SIZE);
1✔
65

66
        mark(MAX_BUFFER_SIZE);
1✔
67
        buffer = new byte[MAX_BUFFER_SIZE];
1✔
68
        bufferLen = read(buffer);
1✔
69
        reset();
1✔
70

71
        if (check) {
1✔
72
            inspectBuffer();
1✔
73
        }
74
    }
1✔
75

76
    public OpenPgpInputStream(InputStream in) throws IOException {
77
        this(in, true);
1✔
78
    }
1✔
79

80
    private void inspectBuffer() throws IOException {
81
        if (checkForAsciiArmor()) {
1✔
82
            return;
1✔
83
        }
84

85
        checkForBinaryOpenPgp();
1✔
86
    }
1✔
87

88
    private boolean checkForAsciiArmor() {
89
        if (startsWithIgnoringWhitespace(buffer, ARMOR_HEADER, bufferLen)) {
1✔
90
            containsArmorHeader = true;
1✔
91
            return true;
1✔
92
        }
93
        return false;
1✔
94
    }
95

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

110
        ByteArrayInputStream bufferIn = new ByteArrayInputStream(buffer, 0, bufferLen);
1✔
111
        nonExhaustiveParseAndCheckPlausibility(bufferIn);
1✔
112
    }
1✔
113

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

121
        boolean newPacket = (hdr & 0x40) != 0;
1✔
122
        int        tag = 0;
1✔
123
        int        bodyLen = 0;
1✔
124
        boolean    partial = false;
1✔
125

126
        // Determine the packet length
127
        if (newPacket) {
1✔
128
            tag = hdr & 0x3f;
1✔
129

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

163
        // Negative body length -> garbage
164
        if (bodyLen < 0) {
1✔
165
            return;
×
166
        }
167

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

175
            case PUBLIC_KEY_ENC_SESSION:
176
                int pkeskVersion = bcpgIn.read();
1✔
177
                if (pkeskVersion <= 0 || pkeskVersion > 5) {
1✔
178
                    return;
×
179
                }
180

181
                // Skip Key-ID
182
                for (int i = 0; i < 8; i++) {
1✔
183
                    bcpgIn.read();
1✔
184
                }
185

186
                int pkeskAlg = bcpgIn.read();
1✔
187
                if (PublicKeyAlgorithm.fromId(pkeskAlg) == null) {
1✔
188
                    return;
×
189
                }
190

191
                containsOpenPgpPackets = true;
1✔
192
                isLikelyOpenPgpMessage = true;
1✔
193
                break;
1✔
194

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

207
                try {
208
                    SignatureType.valueOf(sigType);
1✔
209
                } catch (IllegalArgumentException e) {
×
210
                    return;
×
211
                }
1✔
212

213
                containsOpenPgpPackets = true;
1✔
214
                break;
1✔
215

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

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

252
                containsOpenPgpPackets = true;
1✔
253
                isLikelyOpenPgpMessage = true;
1✔
254
                break;
1✔
255

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

272
                } else if (keyVersion == 5) {
×
273

274
                } else {
275
                    return;
×
276
                }
277
                int keyAlg = bcpgIn.read();
1✔
278
                if (PublicKeyAlgorithm.fromId(keyAlg) == null) {
1✔
279
                    return;
×
280
                }
281

282
                containsOpenPgpPackets = true;
1✔
283
                break;
1✔
284

285
            case COMPRESSED_DATA:
286
                int compAlg = bcpgIn.read();
1✔
287
                if (CompressionAlgorithm.fromId(compAlg) == null) {
1✔
288
                    return;
×
289
                }
290

291
                containsOpenPgpPackets = true;
1✔
292
                isLikelyOpenPgpMessage = true;
1✔
293
                break;
1✔
294

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

302
            case MARKER:
303
                byte[] marker = new byte[3];
×
304
                bcpgIn.readFully(marker);
×
305
                if (marker[0] != 0x50 || marker[1] != 0x47 || marker[2] != 0x50) {
×
306
                    return;
×
307
                }
308

309
                containsOpenPgpPackets = true;
×
310
                break;
×
311

312
            case LITERAL_DATA:
313
                int format = bcpgIn.read();
1✔
314
                if (StreamEncoding.fromCode(format) == null) {
1✔
315
                    return;
×
316
                }
317

318
                containsOpenPgpPackets = true;
1✔
319
                isLikelyOpenPgpMessage = true;
1✔
320
                break;
1✔
321

322
            case TRUST:
323
            case USER_ID:
324
            case USER_ATTRIBUTE:
325
                // Not much to compare
326
                containsOpenPgpPackets = true;
×
327
                break;
×
328

329
            case SYM_ENC_INTEGRITY_PRO:
330
                int seipVersion = bcpgIn.read();
×
331
                if (seipVersion != 1) {
×
332
                    return;
×
333
                }
334
                isLikelyOpenPgpMessage = true;
×
335
                containsOpenPgpPackets = true;
×
336
                break;
×
337

338
            case MOD_DETECTION_CODE:
339
                byte[] digest = new byte[20];
×
340
                bcpgIn.readFully(digest);
×
341

342
                containsOpenPgpPackets = true;
×
343
                break;
×
344

345
            case EXPERIMENTAL_1:
346
            case EXPERIMENTAL_2:
347
            case EXPERIMENTAL_3:
348
            case EXPERIMENTAL_4:
349
                return;
1✔
350
            default:
351
                containsOpenPgpPackets = false;
×
352
                break;
353
        }
354
    }
1✔
355

356
    private boolean startsWithIgnoringWhitespace(byte[] bytes, byte[] subsequence, int bufferLen) {
357
        if (bufferLen == -1) {
1✔
358
            return false;
1✔
359
        }
360

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

367
            if ((i + subsequence.length) > bytes.length) {
1✔
368
                return false;
×
369
            }
370

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

381
    public boolean isAsciiArmored() {
382
        return containsArmorHeader;
1✔
383
    }
384

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

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

410
    public boolean isNonOpenPgp() {
411
        return !isAsciiArmored() && !isBinaryOpenPgp();
1✔
412
    }
413
}
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