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

pgpainless / pgpainless / #1037

pending completion
#1037

push

github-actions

vanitasvitae
EncryptionOptions: Allow overriding evaluation date for recipient keys

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

7081 of 7950 relevant lines covered (89.07%)

0.89 hits per line

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

84.68
/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java
1
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
2
//
3
// SPDX-License-Identifier: Apache-2.0
4

5
package org.pgpainless.encryption_signing;
6

7
import java.util.Collections;
8
import java.util.Date;
9
import java.util.HashMap;
10
import java.util.HashSet;
11
import java.util.LinkedHashSet;
12
import java.util.List;
13
import java.util.Map;
14
import java.util.NoSuchElementException;
15
import java.util.Set;
16
import javax.annotation.Nonnull;
17

18
import org.bouncycastle.openpgp.PGPPublicKey;
19
import org.bouncycastle.openpgp.PGPPublicKeyRing;
20
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
21
import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator;
22
import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator;
23
import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator;
24
import org.pgpainless.algorithm.EncryptionPurpose;
25
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
26
import org.pgpainless.authentication.CertificateAuthenticity;
27
import org.pgpainless.authentication.CertificateAuthority;
28
import org.pgpainless.exception.KeyException;
29
import org.pgpainless.implementation.ImplementationFactory;
30
import org.pgpainless.key.OpenPgpFingerprint;
31
import org.pgpainless.key.SubkeyIdentifier;
32
import org.pgpainless.key.info.KeyAccessor;
33
import org.pgpainless.key.info.KeyRingInfo;
34
import org.pgpainless.util.Passphrase;
35

36
/**
37
 * Options for the encryption process.
38
 * This class can be used to set encryption parameters, like encryption keys and passphrases, algorithms etc.
39
 * <p>
40
 * A typical use might look like follows:
41
 * <pre>
42
 * {@code
43
 * EncryptionOptions opt = new EncryptionOptions();
44
 * opt.addRecipient(aliceKey, "Alice <alice@wonderland.lit>");
45
 * opt.addPassphrase(Passphrase.fromPassword("AdditionalDecryptionPassphrase123"));
46
 * }
47
 * </pre>
48
 *<p>
49
 * To use a custom symmetric encryption algorithm, use {@link #overrideEncryptionAlgorithm(SymmetricKeyAlgorithm)}.
50
 * This will cause PGPainless to use the provided algorithm for message encryption, instead of negotiating an algorithm
51
 * by inspecting the provided recipient keys.
52
 * <p>
53
 * By default, PGPainless will encrypt to all suitable, encryption capable subkeys on each recipient's certificate.
54
 * This behavior can be changed per recipient, e.g. by calling
55
 * <pre>
56
 * {@code
57
 * opt.addRecipient(aliceKey, EncryptionOptions.encryptToFirstSubkey());
58
 * }
59
 * </pre>
60
 * when adding the recipient key.
61
 */
62
public class EncryptionOptions {
63

64
    private final EncryptionPurpose purpose;
65
    private final Set<PGPKeyEncryptionMethodGenerator> encryptionMethods = new LinkedHashSet<>();
1✔
66
    private final Set<SubkeyIdentifier> encryptionKeys = new LinkedHashSet<>();
1✔
67
    private final Map<SubkeyIdentifier, KeyRingInfo> keyRingInfo = new HashMap<>();
1✔
68
    private final Map<SubkeyIdentifier, KeyAccessor> keyViews = new HashMap<>();
1✔
69
    private final EncryptionKeySelector encryptionKeySelector = encryptToAllCapableSubkeys();
1✔
70
    private boolean allowEncryptionWithMissingKeyFlags = false;
1✔
71
    private Date evaluationDate = new Date();
1✔
72

73
    private SymmetricKeyAlgorithm encryptionAlgorithmOverride = null;
1✔
74

75
    /**
76
     * Encrypt to keys both carrying the key flag {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS}
77
     * or {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE}.
78
     */
79
    public EncryptionOptions() {
80
        this(EncryptionPurpose.ANY);
1✔
81
    }
1✔
82

83
    public EncryptionOptions(@Nonnull EncryptionPurpose purpose) {
1✔
84
        this.purpose = purpose;
1✔
85
    }
1✔
86

87
    /**
88
     * Factory method to create an {@link EncryptionOptions} object which will encrypt for keys
89
     * which carry either the {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS} or
90
     * {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE} flag.
91
     * <p>
92
     * Use this if you are not sure.
93
     *
94
     * @return encryption options
95
     */
96
    public static EncryptionOptions get() {
97
        return new EncryptionOptions();
1✔
98
    }
99

100
    /**
101
     * Override the evaluation date for recipient keys with the given date.
102
     *
103
     * @param evaluationDate new evaluation date
104
     * @return this
105
     */
106
    public EncryptionOptions setEvaluationDate(@Nonnull Date evaluationDate) {
107
        this.evaluationDate = evaluationDate;
×
108
        return this;
×
109
    }
110

111
    /**
112
     * Factory method to create an {@link EncryptionOptions} object which will encrypt for keys
113
     * which carry the flag {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS}.
114
     *
115
     * @return encryption options
116
     */
117
    public static EncryptionOptions encryptCommunications() {
118
        return new EncryptionOptions(EncryptionPurpose.COMMUNICATIONS);
1✔
119
    }
120

121
    /**
122
     * Factory method to create an {@link EncryptionOptions} object which will encrypt for keys
123
     * which carry the flag {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE}.
124
     *
125
     * @return encryption options
126
     */
127
    public static EncryptionOptions encryptDataAtRest() {
128
        return new EncryptionOptions(EncryptionPurpose.STORAGE);
1✔
129
    }
130

131
    /**
132
     * Identify authenticatable certificates for the given user-ID by querying the {@link CertificateAuthority} for
133
     * identifiable bindings.
134
     * Add all acceptable bindings, whose trust amount is larger or equal to the target amount to the list of recipients.
135
     * @param userId userId
136
     * @param email if true, treat the user-ID as an email address and match all user-IDs containing the mail address
137
     * @param authority certificate authority
138
     * @return encryption options
139
     */
140
    public EncryptionOptions addAuthenticatableRecipients(String userId, boolean email, CertificateAuthority authority) {
141
        return addAuthenticatableRecipients(userId, email, authority, 120);
×
142
    }
143

144
    /**
145
     * Identify authenticatable certificates for the given user-ID by querying the {@link CertificateAuthority} for
146
     * identifiable bindings.
147
     * Add all acceptable bindings, whose trust amount is larger or equal to the target amount to the list of recipients.
148
     * @param userId userId
149
     * @param email if true, treat the user-ID as an email address and match all user-IDs containing the mail address
150
     * @param authority certificate authority
151
     * @param targetAmount target amount (120 = fully authenticated, 240 = doubly authenticated,
152
     *                    60 = partially authenticated...)
153
     * @return encryption options
154
     */
155
    public EncryptionOptions addAuthenticatableRecipients(String userId, boolean email, CertificateAuthority authority, int targetAmount) {
156
        List<CertificateAuthenticity> identifiedCertificates = authority.lookupByUserId(userId, email, evaluationDate, targetAmount);
×
157
        boolean foundAcceptable = false;
×
158
        for (CertificateAuthenticity candidate : identifiedCertificates) {
×
159
            if (candidate.isAuthenticated()) {
×
160
                addRecipient(candidate.getCertificate());
×
161
                foundAcceptable = true;
×
162
            }
163
        }
×
164
        if (!foundAcceptable) {
×
165
            throw new IllegalArgumentException("Could not identify any trust-worthy certificates for '" + userId + "' and target trust amount " + targetAmount);
×
166
        }
167
        return this;
×
168
    }
169

170
    /**
171
     * Add all key rings in the provided {@link Iterable} (e.g. {@link PGPPublicKeyRingCollection}) as recipients.
172
     *
173
     * @param keys keys
174
     * @return this
175
     */
176
    public EncryptionOptions addRecipients(@Nonnull Iterable<PGPPublicKeyRing> keys) {
177
        if (!keys.iterator().hasNext()) {
1✔
178
            throw new IllegalArgumentException("Set of recipient keys cannot be empty.");
1✔
179
        }
180
        for (PGPPublicKeyRing key : keys) {
1✔
181
            addRecipient(key);
1✔
182
        }
1✔
183
        return this;
1✔
184
    }
185

186
    /**
187
     * Add all key rings in the provided {@link Iterable} (e.g. {@link PGPPublicKeyRingCollection}) as recipients.
188
     * Per key ring, the selector is applied to select one or more encryption subkeys.
189
     *
190
     * @param keys keys
191
     * @param selector encryption key selector
192
     * @return this
193
     */
194
    public EncryptionOptions addRecipients(@Nonnull Iterable<PGPPublicKeyRing> keys, @Nonnull EncryptionKeySelector selector) {
195
        if (!keys.iterator().hasNext()) {
1✔
196
            throw new IllegalArgumentException("Set of recipient keys cannot be empty.");
1✔
197
        }
198
        for (PGPPublicKeyRing key : keys) {
1✔
199
            addRecipient(key, selector);
1✔
200
        }
1✔
201
        return this;
1✔
202
    }
203

204
    /**
205
     * Add a recipient by providing a key and recipient user-id.
206
     * The user-id is used to determine the recipients preferences (algorithms etc.).
207
     *
208
     * @param key key ring
209
     * @param userId user id
210
     * @return this
211
     */
212
    public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key, @Nonnull CharSequence userId) {
213
        return addRecipient(key, userId, encryptionKeySelector);
1✔
214
    }
215

216
    /**
217
     * Add a recipient by providing a key and recipient user-id, as well as a strategy for selecting one or multiple
218
     * encryption capable subkeys from the key.
219
     *
220
     * @param key key
221
     * @param userId user-id
222
     * @param encryptionKeySelectionStrategy strategy to select one or more encryption subkeys to encrypt to
223
     * @return this
224
     */
225
    public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key,
226
                                          @Nonnull CharSequence userId,
227
                                          @Nonnull EncryptionKeySelector encryptionKeySelectionStrategy) {
228
        KeyRingInfo info = new KeyRingInfo(key, evaluationDate);
1✔
229

230
        List<PGPPublicKey> encryptionSubkeys = encryptionKeySelectionStrategy
1✔
231
                .selectEncryptionSubkeys(info.getEncryptionSubkeys(userId.toString(), purpose));
1✔
232
        if (encryptionSubkeys.isEmpty()) {
1✔
233
            throw new KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key));
1✔
234
        }
235

236
        for (PGPPublicKey encryptionSubkey : encryptionSubkeys) {
1✔
237
            SubkeyIdentifier keyId = new SubkeyIdentifier(key, encryptionSubkey.getKeyID());
1✔
238
            keyRingInfo.put(keyId, info);
1✔
239
            keyViews.put(keyId, new KeyAccessor.ViaUserId(info, keyId, userId.toString()));
1✔
240
            addRecipientKey(key, encryptionSubkey, false);
1✔
241
        }
1✔
242

243
        return this;
1✔
244
    }
245

246
    /**
247
     * Add a recipient by providing a key.
248
     *
249
     * @param key key ring
250
     * @return this
251
     */
252
    public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key) {
253
        return addRecipient(key, encryptionKeySelector);
1✔
254
    }
255

256
    /**
257
     * Add a recipient by providing a key and an encryption key selection strategy.
258
     *
259
     * @param key key ring
260
     * @param encryptionKeySelectionStrategy strategy used to select one or multiple encryption subkeys.
261
     * @return this
262
     */
263
    public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key,
264
                                          @Nonnull EncryptionKeySelector encryptionKeySelectionStrategy) {
265
        return addAsRecipient(key, encryptionKeySelectionStrategy, false);
1✔
266
    }
267

268
    /**
269
     * Add a certificate as hidden recipient.
270
     * The recipients key-id will be obfuscated by setting a wildcard key ID.
271
     *
272
     * @param key recipient key
273
     * @return this
274
     */
275
    public EncryptionOptions addHiddenRecipient(@Nonnull PGPPublicKeyRing key) {
276
        return addHiddenRecipient(key, encryptionKeySelector);
1✔
277
    }
278

279
    /**
280
     * Add a certificate as hidden recipient, using the provided {@link EncryptionKeySelector} to select recipient subkeys.
281
     * The recipients key-ids will be obfuscated by setting a wildcard key ID instead.
282
     *
283
     * @param key recipient key
284
     * @param encryptionKeySelectionStrategy strategy to select recipient (sub) keys.
285
     * @return this
286
     */
287
    public EncryptionOptions addHiddenRecipient(PGPPublicKeyRing key, EncryptionKeySelector encryptionKeySelectionStrategy) {
288
        return addAsRecipient(key, encryptionKeySelectionStrategy, true);
1✔
289
    }
290

291
    private EncryptionOptions addAsRecipient(PGPPublicKeyRing key, EncryptionKeySelector encryptionKeySelectionStrategy, boolean wildcardKeyId) {
292
        KeyRingInfo info = new KeyRingInfo(key, evaluationDate);
1✔
293

294
        Date primaryKeyExpiration;
295
        try {
296
            primaryKeyExpiration = info.getPrimaryKeyExpirationDate();
1✔
297
        } catch (NoSuchElementException e) {
×
298
            throw new KeyException.UnacceptableSelfSignatureException(OpenPgpFingerprint.of(key));
×
299
        }
1✔
300
        if (primaryKeyExpiration != null && primaryKeyExpiration.before(evaluationDate)) {
1✔
301
            throw new KeyException.ExpiredKeyException(OpenPgpFingerprint.of(key), primaryKeyExpiration);
1✔
302
        }
303

304
        List<PGPPublicKey> encryptionSubkeys = encryptionKeySelectionStrategy
1✔
305
                .selectEncryptionSubkeys(info.getEncryptionSubkeys(purpose));
1✔
306

307
        // There are some legacy keys around without key flags.
308
        // If we allow encryption for those keys, we add valid keys without any key flags, if they are
309
        // capable of encryption by means of their algorithm
310
        if (encryptionSubkeys.isEmpty() && allowEncryptionWithMissingKeyFlags) {
1✔
311
            List<PGPPublicKey> validSubkeys = info.getValidSubkeys();
1✔
312
            for (PGPPublicKey validSubkey : validSubkeys) {
1✔
313
                if (!validSubkey.isEncryptionKey()) {
1✔
314
                    continue;
×
315
                }
316
                // only add encryption keys with no key flags.
317
                if (info.getKeyFlagsOf(validSubkey.getKeyID()).isEmpty()) {
1✔
318
                    encryptionSubkeys.add(validSubkey);
1✔
319
                }
320
            }
1✔
321
        }
322

323
        if (encryptionSubkeys.isEmpty()) {
1✔
324
            throw new KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key));
1✔
325
        }
326

327
        for (PGPPublicKey encryptionSubkey : encryptionSubkeys) {
1✔
328
            SubkeyIdentifier keyId = new SubkeyIdentifier(key, encryptionSubkey.getKeyID());
1✔
329
            keyRingInfo.put(keyId, info);
1✔
330
            keyViews.put(keyId, new KeyAccessor.ViaKeyId(info, keyId));
1✔
331
            addRecipientKey(key, encryptionSubkey, wildcardKeyId);
1✔
332
        }
1✔
333

334
        return this;
1✔
335
    }
336

337
    private void addRecipientKey(@Nonnull PGPPublicKeyRing keyRing,
338
                                 @Nonnull PGPPublicKey key,
339
                                 boolean wildcardKeyId) {
340
        encryptionKeys.add(new SubkeyIdentifier(keyRing, key.getKeyID()));
1✔
341
        PublicKeyKeyEncryptionMethodGenerator encryptionMethod = ImplementationFactory
342
                .getInstance().getPublicKeyKeyEncryptionMethodGenerator(key);
1✔
343
        encryptionMethod.setUseWildcardKeyID(wildcardKeyId);
1✔
344
        addEncryptionMethod(encryptionMethod);
1✔
345
    }
1✔
346

347
    /**
348
     * Add a symmetric passphrase which the message will be encrypted to.
349
     *
350
     * @param passphrase passphrase
351
     * @return this
352
     */
353
    public EncryptionOptions addPassphrase(@Nonnull Passphrase passphrase) {
354
        if (passphrase.isEmpty()) {
1✔
355
            throw new IllegalArgumentException("Passphrase must not be empty.");
1✔
356
        }
357
        PBEKeyEncryptionMethodGenerator encryptionMethod = ImplementationFactory
358
                .getInstance().getPBEKeyEncryptionMethodGenerator(passphrase);
1✔
359
        return addEncryptionMethod(encryptionMethod);
1✔
360
    }
361

362
    /**
363
     * Add an {@link PGPKeyEncryptionMethodGenerator} which will be used to encrypt the message.
364
     * Method generators are either {@link PBEKeyEncryptionMethodGenerator} (passphrase)
365
     * or {@link PGPKeyEncryptionMethodGenerator} (public key).
366
     *
367
     * This method is intended for advanced users to allow encryption for specific subkeys.
368
     * This can come in handy for example if data needs to be encrypted to a subkey that's ignored by PGPainless.
369
     *
370
     * @param encryptionMethod encryption method
371
     * @return this
372
     */
373
    public EncryptionOptions addEncryptionMethod(@Nonnull PGPKeyEncryptionMethodGenerator encryptionMethod) {
374
        encryptionMethods.add(encryptionMethod);
1✔
375
        return this;
1✔
376
    }
377

378
    Set<PGPKeyEncryptionMethodGenerator> getEncryptionMethods() {
379
        return new HashSet<>(encryptionMethods);
1✔
380
    }
381

382
    Map<SubkeyIdentifier, KeyRingInfo> getKeyRingInfo() {
383
        return new HashMap<>(keyRingInfo);
×
384
    }
385

386
    Set<SubkeyIdentifier> getEncryptionKeyIdentifiers() {
387
        return new HashSet<>(encryptionKeys);
1✔
388
    }
389

390
    Map<SubkeyIdentifier, KeyAccessor> getKeyViews() {
391
        return new HashMap<>(keyViews);
1✔
392
    }
393

394
    SymmetricKeyAlgorithm getEncryptionAlgorithmOverride() {
395
        return encryptionAlgorithmOverride;
1✔
396
    }
397

398
    /**
399
     * Override the used symmetric encryption algorithm.
400
     * The symmetric encryption algorithm is used to encrypt the message itself,
401
     * while the used symmetric key will be encrypted to all recipients using public key
402
     * cryptography.
403
     *
404
     * If the algorithm is not overridden, a suitable algorithm will be negotiated.
405
     *
406
     * @param encryptionAlgorithm encryption algorithm override
407
     * @return this
408
     */
409
    public EncryptionOptions overrideEncryptionAlgorithm(@Nonnull SymmetricKeyAlgorithm encryptionAlgorithm) {
410
        if (encryptionAlgorithm == SymmetricKeyAlgorithm.NULL) {
1✔
411
            throw new IllegalArgumentException("Plaintext encryption can only be used to denote unencrypted secret keys.");
1✔
412
        }
413
        this.encryptionAlgorithmOverride = encryptionAlgorithm;
1✔
414
        return this;
1✔
415
    }
416

417
    /**
418
     * If this method is called, subsequent calls to {@link #addRecipient(PGPPublicKeyRing)} will allow encryption
419
     * for subkeys that do not carry any {@link org.pgpainless.algorithm.KeyFlag} subpacket.
420
     * This is a workaround for dealing with legacy keys that have no key flags subpacket but rely on the key algorithm
421
     * type to convey the subkeys use.
422
     *
423
     * @return this
424
     */
425
    public EncryptionOptions setAllowEncryptionWithMissingKeyFlags() {
426
        this.allowEncryptionWithMissingKeyFlags = true;
1✔
427
        return this;
1✔
428
    }
429

430
    /**
431
     * Return <pre>true</pre> iff the user specified at least one encryption method,
432
     * <pre>false</pre> otherwise.
433
     *
434
     * @return encryption methods is not empty
435
     */
436
    public boolean hasEncryptionMethod() {
437
        return !encryptionMethods.isEmpty();
1✔
438
    }
439

440
    public interface EncryptionKeySelector {
441
        List<PGPPublicKey> selectEncryptionSubkeys(@Nonnull List<PGPPublicKey> encryptionCapableKeys);
442
    }
443

444
    /**
445
     * Only encrypt to the first valid encryption capable subkey we stumble upon.
446
     *
447
     * @return encryption key selector
448
     */
449
    public static EncryptionKeySelector encryptToFirstSubkey() {
450
        return new EncryptionKeySelector() {
1✔
451
            @Override
452
            public List<PGPPublicKey> selectEncryptionSubkeys(@Nonnull List<PGPPublicKey> encryptionCapableKeys) {
453
                return encryptionCapableKeys.isEmpty() ? Collections.emptyList() : Collections.singletonList(encryptionCapableKeys.get(0));
1✔
454
            }
455
        };
456
    }
457

458
    /**
459
     * Encrypt to any valid, encryption capable subkey on the key ring.
460
     *
461
     * @return encryption key selector
462
     */
463
    public static EncryptionKeySelector encryptToAllCapableSubkeys() {
464
        return new EncryptionKeySelector() {
1✔
465
            @Override
466
            public List<PGPPublicKey> selectEncryptionSubkeys(@Nonnull List<PGPPublicKey> encryptionCapableKeys) {
467
                return encryptionCapableKeys;
1✔
468
            }
469
        };
470
    }
471

472
    // TODO: Create encryptToBestSubkey() method
473
}
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