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

pgpainless / pgpainless / #1032

pending completion
#1032

push

github-actions

vanitasvitae
Make use of new ArmoredOutputStream.Builder

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

7063 of 7937 relevant lines covered (88.99%)

0.89 hits per line

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

93.22
/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java
1
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
2
//
3
// SPDX-License-Identifier: Apache-2.0
4

5
package org.pgpainless.util;
6

7
import java.io.ByteArrayInputStream;
8
import java.io.ByteArrayOutputStream;
9
import java.io.IOException;
10
import java.io.InputStream;
11
import java.io.OutputStream;
12
import java.util.ArrayList;
13
import java.util.Iterator;
14
import java.util.List;
15
import java.util.regex.Pattern;
16
import javax.annotation.Nonnull;
17
import javax.annotation.Nullable;
18

19
import org.bouncycastle.bcpg.ArmoredInputStream;
20
import org.bouncycastle.bcpg.ArmoredOutputStream;
21
import org.bouncycastle.openpgp.PGPKeyRing;
22
import org.bouncycastle.openpgp.PGPPublicKey;
23
import org.bouncycastle.openpgp.PGPPublicKeyRing;
24
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
25
import org.bouncycastle.openpgp.PGPSecretKey;
26
import org.bouncycastle.openpgp.PGPSecretKeyRing;
27
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
28
import org.bouncycastle.openpgp.PGPSignature;
29
import org.bouncycastle.openpgp.PGPUtil;
30
import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator;
31
import org.bouncycastle.util.io.Streams;
32
import org.pgpainless.algorithm.HashAlgorithm;
33
import org.pgpainless.decryption_verification.OpenPgpInputStream;
34
import org.pgpainless.key.OpenPgpFingerprint;
35
import org.pgpainless.key.util.KeyRingUtils;
36

37
/**
38
 * Utility class for dealing with ASCII armored OpenPGP data.
39
 */
40
public final class ArmorUtils {
41

42
    // MessageIDs are 32 printable characters
43
    private static final Pattern PATTERN_MESSAGE_ID = Pattern.compile("^\\S{32}$");
1✔
44

45
    /**
46
     * Constant armor key for comments.
47
     */
48
    public static final String HEADER_COMMENT = "Comment";
49
    /**
50
     * Constant armor key for program versions.
51
     */
52
    public static final String HEADER_VERSION = "Version";
53
    /**
54
     * Constant armor key for message IDs. Useful for split messages.
55
     */
56
    public static final String HEADER_MESSAGEID = "MessageID";
57
    /**
58
     * Constant armor key for used hash algorithms in clearsigned messages.
59
     */
60
    public static final String HEADER_HASH = "Hash";
61
    /**
62
     * Constant armor key for message character sets.
63
     */
64
    public static final String HEADER_CHARSET = "Charset";
65

66
    private ArmorUtils() {
67

68
    }
69

70
    /**
71
     * Return the ASCII armored encoding of the given {@link PGPSecretKey}.
72
     *
73
     * @param secretKey secret key
74
     * @return ASCII armored encoding
75
     *
76
     * @throws IOException in case of an io error
77
     */
78
    @Nonnull
79
    public static String toAsciiArmoredString(@Nonnull PGPSecretKey secretKey)
80
            throws IOException {
81
        MultiMap<String, String> header = keyToHeader(secretKey.getPublicKey());
×
82
        return toAsciiArmoredString(secretKey.getEncoded(), header);
×
83
    }
84

85
    /**
86
     * Return the ASCII armored encoding of the given {@link PGPPublicKey}.
87
     *
88
     * @param publicKey public key
89
     * @return ASCII armored encoding
90
     *
91
     * @throws IOException in case of an io error
92
     */
93
    @Nonnull
94
    public static String toAsciiArmoredString(@Nonnull PGPPublicKey publicKey)
95
            throws IOException {
96
        MultiMap<String, String> header = keyToHeader(publicKey);
×
97
        return toAsciiArmoredString(publicKey.getEncoded(), header);
×
98
    }
99

100
    /**
101
     * Return the ASCII armored encoding of the given {@link PGPSecretKeyRing}.
102
     *
103
     * @param secretKeys secret key ring
104
     * @return ASCII armored encoding
105
     *
106
     * @throws IOException in case of an io error
107
     */
108
    @Nonnull
109
    public static String toAsciiArmoredString(@Nonnull PGPSecretKeyRing secretKeys)
110
            throws IOException {
111
        MultiMap<String, String> header = keysToHeader(secretKeys);
1✔
112
        return toAsciiArmoredString(secretKeys.getEncoded(), header);
1✔
113
    }
114

115
    /**
116
     * Return the ASCII armored encoding of the given {@link PGPPublicKeyRing}.
117
     *
118
     * @param publicKeys public key ring
119
     * @return ASCII armored encoding
120
     *
121
     * @throws IOException in case of an io error
122
     */
123
    @Nonnull
124
    public static String toAsciiArmoredString(@Nonnull PGPPublicKeyRing publicKeys)
125
            throws IOException {
126
        MultiMap<String, String> header = keysToHeader(publicKeys);
1✔
127
        return toAsciiArmoredString(publicKeys.getEncoded(), header);
1✔
128
    }
129

130
    /**
131
     * Return the ASCII armored encoding of the given {@link PGPSecretKeyRingCollection}.
132
     * The encoding will use per-key ASCII armors protecting each {@link PGPSecretKeyRing} individually.
133
     * Those armors are then concatenated with newlines in between.
134
     *
135
     * @param secretKeyRings secret key ring collection
136
     * @return ASCII armored encoding
137
     *
138
     * @throws IOException in case of an io error
139
     */
140
    @Nonnull
141
    public static String toAsciiArmoredString(@Nonnull PGPSecretKeyRingCollection secretKeyRings)
142
            throws IOException {
143
        StringBuilder sb = new StringBuilder();
1✔
144
        for (Iterator<PGPSecretKeyRing> iterator = secretKeyRings.iterator(); iterator.hasNext(); ) {
1✔
145
            PGPSecretKeyRing secretKeyRing = iterator.next();
1✔
146
            sb.append(toAsciiArmoredString(secretKeyRing));
1✔
147
            if (iterator.hasNext()) {
1✔
148
                sb.append('\n');
1✔
149
            }
150
        }
1✔
151
        return sb.toString();
1✔
152
    }
153

154
    /**
155
     * Return the ASCII armored encoding of the given {@link PGPPublicKeyRingCollection}.
156
     * The encoding will use per-key ASCII armors protecting each {@link PGPPublicKeyRing} individually.
157
     * Those armors are then concatenated with newlines in between.
158
     *
159
     * @param publicKeyRings public key ring collection
160
     * @return ascii armored encoding
161
     *
162
     * @throws IOException in case of an io error
163
     */
164
    @Nonnull
165
    public static String toAsciiArmoredString(@Nonnull PGPPublicKeyRingCollection publicKeyRings)
166
            throws IOException {
167
        StringBuilder sb = new StringBuilder();
1✔
168
        for (Iterator<PGPPublicKeyRing> iterator = publicKeyRings.iterator(); iterator.hasNext(); ) {
1✔
169
            PGPPublicKeyRing publicKeyRing = iterator.next();
1✔
170
            sb.append(toAsciiArmoredString(publicKeyRing));
1✔
171
            if (iterator.hasNext()) {
1✔
172
                sb.append('\n');
1✔
173
            }
174
        }
1✔
175
        return sb.toString();
1✔
176
    }
177

178
    /**
179
     * Return the ASCII armored representation of the given detached signature.
180
     * The signature will not be stripped of non-exportable subpackets or trust-packets.
181
     * If you need to strip those (e.g. because the signature is intended to be sent to a third party), use
182
     * {@link #toAsciiArmoredString(PGPSignature, boolean)} and provide <pre>true</pre> as boolean value.
183
     *
184
     * @param signature signature
185
     * @return ascii armored string
186
     *
187
     * @throws IOException in case of an error in the {@link ArmoredOutputStream}
188
     */
189
    @Nonnull
190
    public static String toAsciiArmoredString(@Nonnull PGPSignature signature) throws IOException {
191
        return toAsciiArmoredString(signature, false);
1✔
192
    }
193

194
    /**
195
     * Return the ASCII armored representation of the given detached signature.
196
     * If <pre>export</pre> is true, the signature will be stripped of non-exportable subpackets or trust-packets.
197
     * If it is <pre>false</pre>, the signature will be encoded as-is.
198
     *
199
     * @param signature signature
200
     * @param export whether to exclude non-exportable subpackets or trust-packets.
201
     * @return ascii armored string
202
     *
203
     * @throws IOException in case of an error in the {@link ArmoredOutputStream}
204
     */
205
    @Nonnull
206
    public static String toAsciiArmoredString(@Nonnull PGPSignature signature, boolean export)
207
            throws IOException {
208
        return toAsciiArmoredString(signature.getEncoded(export));
1✔
209
    }
210

211
    /**
212
     * Return the ASCII armored encoding of the given OpenPGP data bytes.
213
     *
214
     * @param bytes openpgp data
215
     * @return ASCII armored encoding
216
     *
217
     * @throws IOException in case of an io error
218
     */
219
    @Nonnull
220
    public static String toAsciiArmoredString(@Nonnull byte[] bytes)
221
            throws IOException {
222
        return toAsciiArmoredString(bytes, null);
1✔
223
    }
224

225
    /**
226
     * Return the ASCII armored encoding of the given OpenPGP data bytes.
227
     * The ASCII armor will include headers from the header map.
228
     *
229
     * @param bytes OpenPGP data
230
     * @param additionalHeaderValues header map
231
     * @return ASCII armored encoding
232
     *
233
     * @throws IOException in case of an io error
234
     */
235
    @Nonnull
236
    public static String toAsciiArmoredString(@Nonnull byte[] bytes,
237
                                              @Nullable MultiMap<String, String> additionalHeaderValues)
238
            throws IOException {
239
        return toAsciiArmoredString(new ByteArrayInputStream(bytes), additionalHeaderValues);
1✔
240
    }
241

242
    /**
243
     * Return the ASCII armored encoding of the {@link InputStream} containing OpenPGP data.
244
     *
245
     * @param inputStream input stream of OpenPGP data
246
     * @return ASCII armored encoding
247
     *
248
     * @throws IOException in case of an io error
249
     */
250
    @Nonnull
251
    public static String toAsciiArmoredString(@Nonnull InputStream inputStream)
252
            throws IOException {
253
        return toAsciiArmoredString(inputStream, null);
1✔
254
    }
255

256
    /**
257
     * Return the ASCII armored encoding of the OpenPGP data from the given {@link InputStream}.
258
     * The ASCII armor will include armor headers from the given header map.
259
     *
260
     * @param inputStream input stream of OpenPGP data
261
     * @param additionalHeaderValues ASCII armor header map
262
     * @return ASCII armored encoding
263
     *
264
     * @throws IOException in case of an io error
265
     */
266
    @Nonnull
267
    public static String toAsciiArmoredString(@Nonnull InputStream inputStream,
268
                                              @Nullable MultiMap<String, String> additionalHeaderValues)
269
            throws IOException {
270
        ByteArrayOutputStream out = new ByteArrayOutputStream();
1✔
271
        ArmoredOutputStream armor = toAsciiArmoredStream(out, additionalHeaderValues);
1✔
272
        Streams.pipeAll(inputStream, armor);
1✔
273
        armor.close();
1✔
274

275
        return out.toString();
1✔
276
    }
277

278
    /**
279
     * Return an {@link ArmoredOutputStream} prepared with headers for the given key ring, which wraps the given
280
     * {@link OutputStream}.
281
     *
282
     * The armored output stream can be used to encode the key ring by calling {@link PGPKeyRing#encode(OutputStream)}
283
     * with the armored output stream as an argument.
284
     *
285
     * @param keyRing key ring
286
     * @param outputStream wrapped output stream
287
     * @return armored output stream
288
     */
289
    @Nonnull
290
    public static ArmoredOutputStream toAsciiArmoredStream(@Nonnull PGPKeyRing keyRing,
291
                                                           @Nonnull OutputStream outputStream) {
292
        MultiMap<String, String> header = keysToHeader(keyRing);
1✔
293
        return toAsciiArmoredStream(outputStream, header);
1✔
294
    }
295

296
    /**
297
     * Create an {@link ArmoredOutputStream} wrapping the given {@link OutputStream}.
298
     * The armored output stream will be prepared with armor headers given by header.
299
     *
300
     * Note: Since the armored output stream is retrieved from {@link ArmoredOutputStreamFactory#get(OutputStream)},
301
     * it may already come with custom headers. Hence, the header entries given by header are appended below those
302
     * already populated headers.
303
     *
304
     * @param outputStream output stream to wrap
305
     * @param header map of header entries
306
     * @return armored output stream
307
     */
308
    @Nonnull
309
    public static ArmoredOutputStream toAsciiArmoredStream(@Nonnull OutputStream outputStream,
310
                                                           @Nullable MultiMap<String, String> header) {
311
        ArmoredOutputStream armoredOutputStream = ArmoredOutputStreamFactory.get(outputStream);
1✔
312
        if (header != null) {
1✔
313
            for (String headerKey : header.keySet()) {
1✔
314
                for (String headerValue : header.get(headerKey)) {
1✔
315
                    armoredOutputStream.addHeader(headerKey, headerValue);
1✔
316
                }
1✔
317
            }
1✔
318
        }
319
        return armoredOutputStream;
1✔
320
    }
321

322
    /**
323
     * Generate a header map for ASCII armor from the given {@link PGPKeyRing}.
324
     *
325
     * @param keyRing key ring
326
     * @return header map
327
     */
328
    @Nonnull
329
    private static MultiMap<String, String> keysToHeader(@Nonnull PGPKeyRing keyRing) {
330
        PGPPublicKey publicKey = keyRing.getPublicKey();
1✔
331
        return keyToHeader(publicKey);
1✔
332
    }
333

334
    /**
335
     * Generate a header map for ASCII armor from the given {@link PGPPublicKey}.
336
     * The header map consists of a comment field of the keys pretty-printed fingerprint,
337
     * as well as some optional user-id information (see {@link #setUserIdInfoOnHeader(MultiMap, PGPPublicKey)}.
338
     *
339
     * @param publicKey public key
340
     * @return header map
341
     */
342
    @Nonnull
343
    private static MultiMap<String, String> keyToHeader(@Nonnull PGPPublicKey publicKey) {
344
        MultiMap<String, String> header = new MultiMap<>();
1✔
345
        OpenPgpFingerprint fingerprint = OpenPgpFingerprint.of(publicKey);
1✔
346

347
        header.put(HEADER_COMMENT, fingerprint.prettyPrint());
1✔
348
        setUserIdInfoOnHeader(header, publicKey);
1✔
349
        return header;
1✔
350
    }
351

352
    /**
353
     * Add user-id information to the header map.
354
     * If the key is carrying at least one user-id, we add a comment for the probable primary user-id.
355
     * If the key carries more than one user-id, we further add a comment stating how many further identities
356
     * the key has.
357
     *
358
     * @param header header map
359
     * @param publicKey public key
360
     */
361
    private static void setUserIdInfoOnHeader(@Nonnull MultiMap<String, String> header,
362
                                              @Nonnull PGPPublicKey publicKey) {
363
        Tuple<String, Integer> idCount = getPrimaryUserIdAndUserIdCount(publicKey);
1✔
364
        String primary = idCount.getA();
1✔
365
        int totalCount = idCount.getB();
1✔
366
        if (primary != null) {
1✔
367
            header.put(HEADER_COMMENT, primary);
1✔
368
        }
369
        if (totalCount == 2) {
1✔
370
            header.put(HEADER_COMMENT, "1 further identity");
1✔
371
        } else if (totalCount > 2) {
1✔
372
            header.put(HEADER_COMMENT, String.format("%d further identities", totalCount - 1));
1✔
373
        }
374
    }
1✔
375

376
    /**
377
     * Determine a probable primary user-id, as well as the total number of user-ids on the given {@link PGPPublicKey}.
378
     * This method is trimmed for efficiency and does not do any cryptographic validation of signatures.
379
     *
380
     * The key might not have any user-id at all, in which case {@link Tuple#getA()} will return null.
381
     * The key might have some user-ids, but none of it marked as primary, in which case {@link Tuple#getA()}
382
     * will return the first user-id of the key.
383
     *
384
     * @param publicKey public key
385
     * @return tuple consisting of a primary user-id candidate, and the total number of user-ids on the key.
386
     */
387
    @Nonnull
388
    private static Tuple<String, Integer> getPrimaryUserIdAndUserIdCount(@Nonnull PGPPublicKey publicKey) {
389
        // Quickly determine the primary user-id + number of total user-ids
390
        // NOTE: THIS METHOD DOES NOT CRYPTOGRAPHICALLY VERIFY THE SIGNATURES
391
        // DO NOT RELY ON IT!
392
        List<String> userIds = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(publicKey);
1✔
393
        int countIdentities = 0;
1✔
394
        String first = null;
1✔
395
        String primary = null;
1✔
396
        for (String userId : userIds) {
1✔
397
            countIdentities++;
1✔
398
            // remember the first user-id
399
            if (first == null) {
1✔
400
                first = userId;
1✔
401
            }
402

403
            if (primary == null) {
1✔
404
                Iterator<PGPSignature> signatures = publicKey.getSignaturesForID(userId);
1✔
405
                while (signatures.hasNext()) {
1✔
406
                    PGPSignature signature = signatures.next();
1✔
407
                    if (signature.getHashedSubPackets().isPrimaryUserID()) {
1✔
408
                        primary = userId;
1✔
409
                        break;
1✔
410
                    }
411
                }
1✔
412
            }
413
        }
1✔
414
        // It may happen that no user-id is marked as primary
415
        // in that case print the first one
416
        String printed = primary != null ? primary : first;
1✔
417
        return new Tuple<>(printed, countIdentities);
1✔
418
    }
419

420
    /**
421
     * Set the version header entry in the ASCII armor.
422
     * If the version info is null or only contains whitespace characters, then the version header will be removed.
423
     *
424
     * @param armor armored output stream
425
     * @param version version header.
426
     */
427
    public static void setVersionHeader(@Nonnull ArmoredOutputStream armor,
428
                                        @Nullable String version) {
429
        if (version == null || version.trim().isEmpty()) {
×
430
            armor.setHeader(HEADER_VERSION, null);
×
431
        } else {
432
            armor.setHeader(HEADER_VERSION, version);
×
433
        }
434
    }
×
435

436
    /**
437
     * Add an ASCII armor header entry about the used hash algorithm into the {@link ArmoredOutputStream}.
438
     *
439
     * @param armor armored output stream
440
     * @param hashAlgorithm hash algorithm
441
     *
442
     * @see <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-6.2">
443
     *     RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor</a>
444
     */
445
    public static void addHashAlgorithmHeader(@Nonnull ArmoredOutputStream armor,
446
                                              @Nonnull HashAlgorithm hashAlgorithm) {
447
        armor.addHeader(HEADER_HASH, hashAlgorithm.getAlgorithmName());
1✔
448
    }
1✔
449

450
    /**
451
     * Add an ASCII armor comment header entry into the {@link ArmoredOutputStream}.
452
     *
453
     * @param armor armored output stream
454
     * @param comment free-text comment
455
     *
456
     * @see <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-6.2">
457
     *     RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor</a>
458
     */
459
    public static void addCommentHeader(@Nonnull ArmoredOutputStream armor,
460
                                        @Nonnull String comment) {
461
        armor.addHeader(HEADER_COMMENT, comment);
1✔
462
    }
1✔
463

464
    /**
465
     * Add an ASCII armor message-id header entry into the {@link ArmoredOutputStream}.
466
     *
467
     * @param armor armored output stream
468
     * @param messageId message id
469
     *
470
     * @see <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-6.2">
471
     *     RFC 4880 - OpenPGP Message Format §6.2. Forming ASCII Armor</a>
472
     */
473
    public static void addMessageIdHeader(@Nonnull ArmoredOutputStream armor,
474
                                          @Nonnull String messageId) {
475
        if (!PATTERN_MESSAGE_ID.matcher(messageId).matches()) {
1✔
476
            throw new IllegalArgumentException("MessageIDs MUST consist of 32 printable characters.");
1✔
477
        }
478
        armor.addHeader(HEADER_MESSAGEID, messageId);
1✔
479
    }
1✔
480

481
    /**
482
     * Extract all ASCII armor header values of type comment from the given {@link ArmoredInputStream}.
483
     *
484
     * @param armor armored input stream
485
     * @return list of comment headers
486
     */
487
    @Nonnull
488
    public static List<String> getCommentHeaderValues(@Nonnull ArmoredInputStream armor) {
489
        return getArmorHeaderValues(armor, HEADER_COMMENT);
1✔
490
    }
491

492
    /**
493
     * Extract all ASCII armor header values of type message id from the given {@link ArmoredInputStream}.
494
     *
495
     * @param armor armored input stream
496
     * @return list of message-id headers
497
     */
498
    @Nonnull
499
    public static List<String> getMessageIdHeaderValues(@Nonnull ArmoredInputStream armor) {
500
        return getArmorHeaderValues(armor, HEADER_MESSAGEID);
1✔
501
    }
502

503
    /**
504
     * Return all ASCII armor header values of type hash-algorithm from the given {@link ArmoredInputStream}.
505
     *
506
     * @param armor armored input stream
507
     * @return list of hash headers
508
     */
509
    @Nonnull
510
    public static List<String> getHashHeaderValues(@Nonnull ArmoredInputStream armor) {
511
        return getArmorHeaderValues(armor, HEADER_HASH);
1✔
512
    }
513

514
    /**
515
     * Return a list of {@link HashAlgorithm} enums extracted from the hash header entries of the given
516
     * {@link ArmoredInputStream}.
517
     *
518
     * @param armor armored input stream
519
     * @return list of hash algorithms from the ASCII header
520
     */
521
    @Nonnull
522
    public static List<HashAlgorithm> getHashAlgorithms(@Nonnull ArmoredInputStream armor) {
523
        List<String> algorithmNames = getHashHeaderValues(armor);
1✔
524
        List<HashAlgorithm> algorithms = new ArrayList<>();
1✔
525
        for (String name : algorithmNames) {
1✔
526
            HashAlgorithm algorithm = HashAlgorithm.fromName(name);
1✔
527
            if (algorithm != null) {
1✔
528
                algorithms.add(algorithm);
1✔
529
            }
530
        }
1✔
531
        return algorithms;
1✔
532
    }
533

534
    /**
535
     * Return all ASCII armor header values of type version from the given {@link ArmoredInputStream}.
536
     *
537
     * @param armor armored input stream
538
     * @return list of version headers
539
     */
540
    @Nonnull
541
    public static List<String> getVersionHeaderValues(@Nonnull ArmoredInputStream armor) {
542
        return getArmorHeaderValues(armor, HEADER_VERSION);
1✔
543
    }
544

545
    /**
546
     * Return all ASCII armor header values of type charset from the given {@link ArmoredInputStream}.
547
     *
548
     * @param armor armored input stream
549
     * @return list of charset headers
550
     */
551
    @Nonnull
552
    public static List<String> getCharsetHeaderValues(@Nonnull ArmoredInputStream armor) {
553
        return getArmorHeaderValues(armor, HEADER_CHARSET);
1✔
554
    }
555

556
    /**
557
     * Return all ASCII armor header values of the given headerKey from the given {@link ArmoredInputStream}.
558
     *
559
     * @param armor armored input stream
560
     * @param headerKey ASCII armor header key
561
     * @return list of values for the header key
562
     */
563
    @Nonnull
564
    public static List<String> getArmorHeaderValues(@Nonnull ArmoredInputStream armor,
565
                                                    @Nonnull String headerKey) {
566
        String[] header = armor.getArmorHeaders();
1✔
567
        String key = headerKey + ": ";
1✔
568
        List<String> values = new ArrayList<>();
1✔
569
        for (String line : header) {
1✔
570
            if (line.startsWith(key)) {
1✔
571
                values.add(line.substring(key.length()));
1✔
572
            }
573
        }
574
        return values;
1✔
575
    }
576

577
    /**
578
     * Hacky workaround for #96.
579
     * For {@link PGPPublicKeyRingCollection#PGPPublicKeyRingCollection(InputStream, KeyFingerPrintCalculator)}
580
     * or {@link PGPSecretKeyRingCollection#PGPSecretKeyRingCollection(InputStream, KeyFingerPrintCalculator)}
581
     * to read all PGPKeyRings properly, we apparently have to make sure that the {@link InputStream} that is given
582
     * as constructor argument is a PGPUtil.BufferedInputStreamExt.
583
     * Since {@link PGPUtil#getDecoderStream(InputStream)} will return an {@link org.bouncycastle.bcpg.ArmoredInputStream}
584
     * if the underlying input stream contains armored data, we first dearmor the data ourselves to make sure that the
585
     * end-result is a PGPUtil.BufferedInputStreamExt.
586
     *
587
     * @param inputStream input stream
588
     * @return BufferedInputStreamExt
589
     *
590
     * @throws IOException in case of an IO error
591
     */
592
    @Nonnull
593
    public static InputStream getDecoderStream(@Nonnull InputStream inputStream)
594
            throws IOException {
595
        OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream);
1✔
596
        if (openPgpIn.isAsciiArmored()) {
1✔
597
            ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn);
1✔
598
            return PGPUtil.getDecoderStream(armorIn);
1✔
599
        }
600

601
        return openPgpIn;
1✔
602
    }
603
}
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