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

pgpainless / pgpainless / #1046

04 Sep 2023 12:18PM CUT coverage: 89.061% (-0.03%) from 89.086%
#1046

push

github-actions

vanitasvitae
Pad long KeyIDs with zeros to 16 chars

1 of 1 new or added line in 1 file covered. (100.0%)

7083 of 7953 relevant lines covered (89.06%)

0.89 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

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
/**
49
 * InputStream used to determine the nature of potential OpenPGP data.
50
 */
51
public class OpenPgpInputStream extends BufferedInputStream {
52

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

210
                try {
211
                    SignatureType.valueOf(sigType);
1✔
212
                } catch (IllegalArgumentException e) {
×
213
                    return;
×
214
                }
1✔
215

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

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

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

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

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

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

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

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

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

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

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

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

312
                containsOpenPgpPackets = true;
×
313
                break;
×
314

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

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

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

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

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

345
                containsOpenPgpPackets = true;
×
346
                break;
×
347

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

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

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

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

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

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

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

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

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