From f3ea9f62e18a4f5518e25ae8a37c25aeec180ea3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 3 Sep 2023 17:55:46 +0200 Subject: [PATCH] Kotlin conversion: EncryptionOptions --- .../encryption_signing/EncryptionOptions.java | 473 ------------------ .../encryption_signing/EncryptionOptions.kt | 294 +++++++++++ .../EncryptionOptionsTest.java | 9 +- 3 files changed, 300 insertions(+), 476 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java deleted file mode 100644 index bb937a1a..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionOptions.java +++ /dev/null @@ -1,473 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.encryption_signing; - -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; -import javax.annotation.Nonnull; - -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.operator.PBEKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; -import org.bouncycastle.openpgp.operator.PublicKeyKeyEncryptionMethodGenerator; -import org.pgpainless.algorithm.EncryptionPurpose; -import org.pgpainless.algorithm.SymmetricKeyAlgorithm; -import org.pgpainless.authentication.CertificateAuthenticity; -import org.pgpainless.authentication.CertificateAuthority; -import org.pgpainless.exception.KeyException; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.OpenPgpFingerprint; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.info.KeyAccessor; -import org.pgpainless.key.info.KeyRingInfo; -import org.pgpainless.util.Passphrase; - -/** - * Options for the encryption process. - * This class can be used to set encryption parameters, like encryption keys and passphrases, algorithms etc. - *

- * A typical use might look like follows: - *

- * {@code
- * EncryptionOptions opt = new EncryptionOptions();
- * opt.addRecipient(aliceKey, "Alice ");
- * opt.addPassphrase(Passphrase.fromPassword("AdditionalDecryptionPassphrase123"));
- * }
- * 
- *

- * To use a custom symmetric encryption algorithm, use {@link #overrideEncryptionAlgorithm(SymmetricKeyAlgorithm)}. - * This will cause PGPainless to use the provided algorithm for message encryption, instead of negotiating an algorithm - * by inspecting the provided recipient keys. - *

- * By default, PGPainless will encrypt to all suitable, encryption capable subkeys on each recipient's certificate. - * This behavior can be changed per recipient, e.g. by calling - *

- * {@code
- * opt.addRecipient(aliceKey, EncryptionOptions.encryptToFirstSubkey());
- * }
- * 
- * when adding the recipient key. - */ -public class EncryptionOptions { - - private final EncryptionPurpose purpose; - private final Set encryptionMethods = new LinkedHashSet<>(); - private final Set encryptionKeys = new LinkedHashSet<>(); - private final Map keyRingInfo = new HashMap<>(); - private final Map keyViews = new HashMap<>(); - private final EncryptionKeySelector encryptionKeySelector = encryptToAllCapableSubkeys(); - private boolean allowEncryptionWithMissingKeyFlags = false; - private Date evaluationDate = new Date(); - - private SymmetricKeyAlgorithm encryptionAlgorithmOverride = null; - - /** - * Encrypt to keys both carrying the key flag {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS} - * or {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE}. - */ - public EncryptionOptions() { - this(EncryptionPurpose.ANY); - } - - public EncryptionOptions(@Nonnull EncryptionPurpose purpose) { - this.purpose = purpose; - } - - /** - * Factory method to create an {@link EncryptionOptions} object which will encrypt for keys - * which carry either the {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS} or - * {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE} flag. - *

- * Use this if you are not sure. - * - * @return encryption options - */ - public static EncryptionOptions get() { - return new EncryptionOptions(); - } - - /** - * Override the evaluation date for recipient keys with the given date. - * - * @param evaluationDate new evaluation date - * @return this - */ - public EncryptionOptions setEvaluationDate(@Nonnull Date evaluationDate) { - this.evaluationDate = evaluationDate; - return this; - } - - /** - * Factory method to create an {@link EncryptionOptions} object which will encrypt for keys - * which carry the flag {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS}. - * - * @return encryption options - */ - public static EncryptionOptions encryptCommunications() { - return new EncryptionOptions(EncryptionPurpose.COMMUNICATIONS); - } - - /** - * Factory method to create an {@link EncryptionOptions} object which will encrypt for keys - * which carry the flag {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_STORAGE}. - * - * @return encryption options - */ - public static EncryptionOptions encryptDataAtRest() { - return new EncryptionOptions(EncryptionPurpose.STORAGE); - } - - /** - * Identify authenticatable certificates for the given user-ID by querying the {@link CertificateAuthority} for - * identifiable bindings. - * Add all acceptable bindings, whose trust amount is larger or equal to the target amount to the list of recipients. - * @param userId userId - * @param email if true, treat the user-ID as an email address and match all user-IDs containing the mail address - * @param authority certificate authority - * @return encryption options - */ - public EncryptionOptions addAuthenticatableRecipients(String userId, boolean email, CertificateAuthority authority) { - return addAuthenticatableRecipients(userId, email, authority, 120); - } - - /** - * Identify authenticatable certificates for the given user-ID by querying the {@link CertificateAuthority} for - * identifiable bindings. - * Add all acceptable bindings, whose trust amount is larger or equal to the target amount to the list of recipients. - * @param userId userId - * @param email if true, treat the user-ID as an email address and match all user-IDs containing the mail address - * @param authority certificate authority - * @param targetAmount target amount (120 = fully authenticated, 240 = doubly authenticated, - * 60 = partially authenticated...) - * @return encryption options - */ - public EncryptionOptions addAuthenticatableRecipients(String userId, boolean email, CertificateAuthority authority, int targetAmount) { - List identifiedCertificates = authority.lookupByUserId(userId, email, evaluationDate, targetAmount); - boolean foundAcceptable = false; - for (CertificateAuthenticity candidate : identifiedCertificates) { - if (candidate.isAuthenticated()) { - addRecipient(candidate.getCertificate()); - foundAcceptable = true; - } - } - if (!foundAcceptable) { - throw new IllegalArgumentException("Could not identify any trust-worthy certificates for '" + userId + "' and target trust amount " + targetAmount); - } - return this; - } - - /** - * Add all key rings in the provided {@link Iterable} (e.g. {@link PGPPublicKeyRingCollection}) as recipients. - * - * @param keys keys - * @return this - */ - public EncryptionOptions addRecipients(@Nonnull Iterable keys) { - if (!keys.iterator().hasNext()) { - throw new IllegalArgumentException("Set of recipient keys cannot be empty."); - } - for (PGPPublicKeyRing key : keys) { - addRecipient(key); - } - return this; - } - - /** - * Add all key rings in the provided {@link Iterable} (e.g. {@link PGPPublicKeyRingCollection}) as recipients. - * Per key ring, the selector is applied to select one or more encryption subkeys. - * - * @param keys keys - * @param selector encryption key selector - * @return this - */ - public EncryptionOptions addRecipients(@Nonnull Iterable keys, @Nonnull EncryptionKeySelector selector) { - if (!keys.iterator().hasNext()) { - throw new IllegalArgumentException("Set of recipient keys cannot be empty."); - } - for (PGPPublicKeyRing key : keys) { - addRecipient(key, selector); - } - return this; - } - - /** - * Add a recipient by providing a key and recipient user-id. - * The user-id is used to determine the recipients preferences (algorithms etc.). - * - * @param key key ring - * @param userId user id - * @return this - */ - public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key, @Nonnull CharSequence userId) { - return addRecipient(key, userId, encryptionKeySelector); - } - - /** - * Add a recipient by providing a key and recipient user-id, as well as a strategy for selecting one or multiple - * encryption capable subkeys from the key. - * - * @param key key - * @param userId user-id - * @param encryptionKeySelectionStrategy strategy to select one or more encryption subkeys to encrypt to - * @return this - */ - public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key, - @Nonnull CharSequence userId, - @Nonnull EncryptionKeySelector encryptionKeySelectionStrategy) { - KeyRingInfo info = new KeyRingInfo(key, evaluationDate); - - List encryptionSubkeys = encryptionKeySelectionStrategy - .selectEncryptionSubkeys(info.getEncryptionSubkeys(userId.toString(), purpose)); - if (encryptionSubkeys.isEmpty()) { - throw new KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)); - } - - for (PGPPublicKey encryptionSubkey : encryptionSubkeys) { - SubkeyIdentifier keyId = new SubkeyIdentifier(key, encryptionSubkey.getKeyID()); - keyRingInfo.put(keyId, info); - keyViews.put(keyId, new KeyAccessor.ViaUserId(info, keyId, userId.toString())); - addRecipientKey(key, encryptionSubkey, false); - } - - return this; - } - - /** - * Add a recipient by providing a key. - * - * @param key key ring - * @return this - */ - public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key) { - return addRecipient(key, encryptionKeySelector); - } - - /** - * Add a recipient by providing a key and an encryption key selection strategy. - * - * @param key key ring - * @param encryptionKeySelectionStrategy strategy used to select one or multiple encryption subkeys. - * @return this - */ - public EncryptionOptions addRecipient(@Nonnull PGPPublicKeyRing key, - @Nonnull EncryptionKeySelector encryptionKeySelectionStrategy) { - return addAsRecipient(key, encryptionKeySelectionStrategy, false); - } - - /** - * Add a certificate as hidden recipient. - * The recipients key-id will be obfuscated by setting a wildcard key ID. - * - * @param key recipient key - * @return this - */ - public EncryptionOptions addHiddenRecipient(@Nonnull PGPPublicKeyRing key) { - return addHiddenRecipient(key, encryptionKeySelector); - } - - /** - * Add a certificate as hidden recipient, using the provided {@link EncryptionKeySelector} to select recipient subkeys. - * The recipients key-ids will be obfuscated by setting a wildcard key ID instead. - * - * @param key recipient key - * @param encryptionKeySelectionStrategy strategy to select recipient (sub) keys. - * @return this - */ - public EncryptionOptions addHiddenRecipient(PGPPublicKeyRing key, EncryptionKeySelector encryptionKeySelectionStrategy) { - return addAsRecipient(key, encryptionKeySelectionStrategy, true); - } - - private EncryptionOptions addAsRecipient(PGPPublicKeyRing key, EncryptionKeySelector encryptionKeySelectionStrategy, boolean wildcardKeyId) { - KeyRingInfo info = new KeyRingInfo(key, evaluationDate); - - Date primaryKeyExpiration; - try { - primaryKeyExpiration = info.getPrimaryKeyExpirationDate(); - } catch (NoSuchElementException e) { - throw new KeyException.UnacceptableSelfSignatureException(OpenPgpFingerprint.of(key)); - } - if (primaryKeyExpiration != null && primaryKeyExpiration.before(evaluationDate)) { - throw new KeyException.ExpiredKeyException(OpenPgpFingerprint.of(key), primaryKeyExpiration); - } - - List encryptionSubkeys = encryptionKeySelectionStrategy - .selectEncryptionSubkeys(info.getEncryptionSubkeys(purpose)); - - // There are some legacy keys around without key flags. - // If we allow encryption for those keys, we add valid keys without any key flags, if they are - // capable of encryption by means of their algorithm - if (encryptionSubkeys.isEmpty() && allowEncryptionWithMissingKeyFlags) { - List validSubkeys = info.getValidSubkeys(); - for (PGPPublicKey validSubkey : validSubkeys) { - if (!validSubkey.isEncryptionKey()) { - continue; - } - // only add encryption keys with no key flags. - if (info.getKeyFlagsOf(validSubkey.getKeyID()).isEmpty()) { - encryptionSubkeys.add(validSubkey); - } - } - } - - if (encryptionSubkeys.isEmpty()) { - throw new KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)); - } - - for (PGPPublicKey encryptionSubkey : encryptionSubkeys) { - SubkeyIdentifier keyId = new SubkeyIdentifier(key, encryptionSubkey.getKeyID()); - keyRingInfo.put(keyId, info); - keyViews.put(keyId, new KeyAccessor.ViaKeyId(info, keyId)); - addRecipientKey(key, encryptionSubkey, wildcardKeyId); - } - - return this; - } - - private void addRecipientKey(@Nonnull PGPPublicKeyRing keyRing, - @Nonnull PGPPublicKey key, - boolean wildcardKeyId) { - encryptionKeys.add(new SubkeyIdentifier(keyRing, key.getKeyID())); - PublicKeyKeyEncryptionMethodGenerator encryptionMethod = ImplementationFactory - .getInstance().getPublicKeyKeyEncryptionMethodGenerator(key); - encryptionMethod.setUseWildcardKeyID(wildcardKeyId); - addEncryptionMethod(encryptionMethod); - } - - /** - * Add a symmetric passphrase which the message will be encrypted to. - * - * @param passphrase passphrase - * @return this - */ - public EncryptionOptions addPassphrase(@Nonnull Passphrase passphrase) { - if (passphrase.isEmpty()) { - throw new IllegalArgumentException("Passphrase must not be empty."); - } - PBEKeyEncryptionMethodGenerator encryptionMethod = ImplementationFactory - .getInstance().getPBEKeyEncryptionMethodGenerator(passphrase); - return addEncryptionMethod(encryptionMethod); - } - - /** - * Add an {@link PGPKeyEncryptionMethodGenerator} which will be used to encrypt the message. - * Method generators are either {@link PBEKeyEncryptionMethodGenerator} (passphrase) - * or {@link PGPKeyEncryptionMethodGenerator} (public key). - * - * This method is intended for advanced users to allow encryption for specific subkeys. - * This can come in handy for example if data needs to be encrypted to a subkey that's ignored by PGPainless. - * - * @param encryptionMethod encryption method - * @return this - */ - public EncryptionOptions addEncryptionMethod(@Nonnull PGPKeyEncryptionMethodGenerator encryptionMethod) { - encryptionMethods.add(encryptionMethod); - return this; - } - - Set getEncryptionMethods() { - return new HashSet<>(encryptionMethods); - } - - Map getKeyRingInfo() { - return new HashMap<>(keyRingInfo); - } - - Set getEncryptionKeyIdentifiers() { - return new HashSet<>(encryptionKeys); - } - - Map getKeyViews() { - return new HashMap<>(keyViews); - } - - SymmetricKeyAlgorithm getEncryptionAlgorithmOverride() { - return encryptionAlgorithmOverride; - } - - /** - * Override the used symmetric encryption algorithm. - * The symmetric encryption algorithm is used to encrypt the message itself, - * while the used symmetric key will be encrypted to all recipients using public key - * cryptography. - * - * If the algorithm is not overridden, a suitable algorithm will be negotiated. - * - * @param encryptionAlgorithm encryption algorithm override - * @return this - */ - public EncryptionOptions overrideEncryptionAlgorithm(@Nonnull SymmetricKeyAlgorithm encryptionAlgorithm) { - if (encryptionAlgorithm == SymmetricKeyAlgorithm.NULL) { - throw new IllegalArgumentException("Plaintext encryption can only be used to denote unencrypted secret keys."); - } - this.encryptionAlgorithmOverride = encryptionAlgorithm; - return this; - } - - /** - * If this method is called, subsequent calls to {@link #addRecipient(PGPPublicKeyRing)} will allow encryption - * for subkeys that do not carry any {@link org.pgpainless.algorithm.KeyFlag} subpacket. - * This is a workaround for dealing with legacy keys that have no key flags subpacket but rely on the key algorithm - * type to convey the subkeys use. - * - * @return this - */ - public EncryptionOptions setAllowEncryptionWithMissingKeyFlags() { - this.allowEncryptionWithMissingKeyFlags = true; - return this; - } - - /** - * Return

true
iff the user specified at least one encryption method, - *
false
otherwise. - * - * @return encryption methods is not empty - */ - public boolean hasEncryptionMethod() { - return !encryptionMethods.isEmpty(); - } - - public interface EncryptionKeySelector { - List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys); - } - - /** - * Only encrypt to the first valid encryption capable subkey we stumble upon. - * - * @return encryption key selector - */ - public static EncryptionKeySelector encryptToFirstSubkey() { - return new EncryptionKeySelector() { - @Override - public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { - return encryptionCapableKeys.isEmpty() ? Collections.emptyList() : Collections.singletonList(encryptionCapableKeys.get(0)); - } - }; - } - - /** - * Encrypt to any valid, encryption capable subkey on the key ring. - * - * @return encryption key selector - */ - public static EncryptionKeySelector encryptToAllCapableSubkeys() { - return new EncryptionKeySelector() { - @Override - public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { - return encryptionCapableKeys; - } - }; - } - - // TODO: Create encryptToBestSubkey() method -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt new file mode 100644 index 00000000..6a8c0125 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt @@ -0,0 +1,294 @@ +package org.pgpainless.encryption_signing + +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator +import org.pgpainless.algorithm.EncryptionPurpose +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.authentication.CertificateAuthority +import org.pgpainless.exception.KeyException +import org.pgpainless.exception.KeyException.* +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.SubkeyIdentifier +import org.pgpainless.key.info.KeyAccessor +import org.pgpainless.key.info.KeyRingInfo +import org.pgpainless.util.Passphrase +import java.util.* +import javax.annotation.Nonnull + + +class EncryptionOptions( + private val purpose: EncryptionPurpose +) { + private val _encryptionMethods: MutableSet = mutableSetOf() + private val _encryptionKeyIdentifiers: MutableSet = mutableSetOf() + private val _keyRingInfo: MutableMap = mutableMapOf() + private val _keyViews: MutableMap = mutableMapOf() + private val encryptionKeySelector: EncryptionKeySelector = encryptToAllCapableSubkeys() + + private var allowEncryptionWithMissingKeyFlags = false + private var evaluationDate = Date() + private var _encryptionAlgorithmOverride: SymmetricKeyAlgorithm? = null + + val encryptionMethods + get() = _encryptionMethods.toSet() + val encryptionKeyIdentifiers + get() = _encryptionKeyIdentifiers.toSet() + val keyRingInfo + get() = _keyRingInfo.toMap() + val keyViews + get() = _keyViews.toMap() + val encryptionAlgorithmOverride + get() = _encryptionAlgorithmOverride + + constructor(): this(EncryptionPurpose.ANY) + + /** + * Factory method to create an {@link EncryptionOptions} object which will encrypt for keys + * which carry the flag {@link org.pgpainless.algorithm.KeyFlag#ENCRYPT_COMMS}. + * + * @return encryption options + */ + fun setEvaluationDate(evaluationDate: Date) = apply { + this.evaluationDate = evaluationDate + } + + /** + * Identify authenticatable certificates for the given user-ID by querying the {@link CertificateAuthority} for + * identifiable bindings. + * Add all acceptable bindings, whose trust amount is larger or equal to the target amount to the list of recipients. + * @param userId userId + * @param email if true, treat the user-ID as an email address and match all user-IDs containing the mail address + * @param authority certificate authority + * @param targetAmount target amount (120 = fully authenticated, 240 = doubly authenticated, + * 60 = partially authenticated...) + * @return encryption options + */ + @JvmOverloads + fun addAuthenticatableRecipients(userId: String, email: Boolean, authority: CertificateAuthority, targetAmount: Int = 120) = apply { + var foundAcceptable = false + authority.lookupByUserId(userId, email, evaluationDate, targetAmount) + .filter { it.isAuthenticated() } + .forEach { addRecipient(it.certificate) + .also { + foundAcceptable = true + } + } + require(foundAcceptable) { + "Could not identify any trust-worthy certificates for '$userId' and target trust amount $targetAmount." + } + } + + /** + * Add all key rings in the provided {@link Iterable} (e.g. {@link PGPPublicKeyRingCollection}) as recipients. + * + * @param keys keys + * @return this + */ + fun addRecipients(keys: Iterable) = apply { + keys.toList().let { + require(it.isNotEmpty()) { + "Set of recipient keys cannot be empty." + } + it.forEach { key -> addRecipient(key) } + } + } + + /** + * Add all key rings in the provided {@link Iterable} (e.g. {@link PGPPublicKeyRingCollection}) as recipients. + * Per key ring, the selector is applied to select one or more encryption subkeys. + * + * @param keys keys + * @param selector encryption key selector + * @return this + */ + fun addRecipients(keys: Iterable, selector: EncryptionKeySelector) = apply { + keys.toList().let { + require(it.isNotEmpty()) { + "Set of recipient keys cannot be empty." + } + it.forEach { key -> addRecipient(key, selector) } + } + } + + /** + * Add a recipient by providing a key. + * + * @param key key ring + * @return this + */ + fun addRecipient(key: PGPPublicKeyRing) = addRecipient(key, encryptionKeySelector) + + /** + * Add a recipient by providing a key and recipient user-id. + * The user-id is used to determine the recipients preferences (algorithms etc.). + * + * @param key key ring + * @param userId user id + * @return this + */ + fun addRecipient(key: PGPPublicKeyRing, userId: CharSequence) = + addRecipient(key, userId, encryptionKeySelector) + + fun addRecipient(key: PGPPublicKeyRing, userId: CharSequence, encryptionKeySelector: EncryptionKeySelector) = apply { + val info = KeyRingInfo(key, evaluationDate) + val subkeys = encryptionKeySelector.selectEncryptionSubkeys(info.getEncryptionSubkeys(userId, purpose)) + if (subkeys.isEmpty()) { + throw KeyException.UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)) + } + + for (subkey in subkeys) { + val keyId = SubkeyIdentifier(key, subkey.keyID) + (_keyRingInfo as MutableMap)[keyId] = info + (_keyViews as MutableMap)[keyId] = KeyAccessor.ViaUserId(info, keyId, userId.toString()) + addRecipientKey(key, subkey, false) + } + } + + fun addRecipient(key: PGPPublicKeyRing, encryptionKeySelector: EncryptionKeySelector) = apply { + addAsRecipient(key, encryptionKeySelector, false) + } + + @JvmOverloads + fun addHiddenRecipient(key: PGPPublicKeyRing, selector: EncryptionKeySelector = encryptionKeySelector) = apply { + addAsRecipient(key, selector, true) + } + + private fun addAsRecipient(key: PGPPublicKeyRing, selector: EncryptionKeySelector, wildcardKeyId: Boolean) = apply { + val info = KeyRingInfo(key, evaluationDate) + val primaryKeyExpiration = try { + info.primaryKeyExpirationDate + } catch (e: NoSuchElementException) { + throw UnacceptableSelfSignatureException(OpenPgpFingerprint.of(key)) + } + + if (primaryKeyExpiration != null && primaryKeyExpiration < evaluationDate) { + throw ExpiredKeyException(OpenPgpFingerprint.of(key), primaryKeyExpiration) + } + + var encryptionSubkeys = selector.selectEncryptionSubkeys(info.getEncryptionSubkeys(purpose)) + + // There are some legacy keys around without key flags. + // If we allow encryption for those keys, we add valid keys without any key flags, if they are + // capable of encryption by means of their algorithm + if (encryptionSubkeys.isEmpty() && allowEncryptionWithMissingKeyFlags) { + encryptionSubkeys = info.validSubkeys + .filter { it.isEncryptionKey } + .filter { info.getKeyFlagsOf(it.keyID).isEmpty() } + } + + if (encryptionSubkeys.isEmpty()) { + throw UnacceptableEncryptionKeyException(OpenPgpFingerprint.of(key)) + } + + for (subkey in encryptionSubkeys) { + val keyId = SubkeyIdentifier(key, subkey.keyID) + (_keyRingInfo as MutableMap)[keyId] = info + (_keyViews as MutableMap)[keyId] = KeyAccessor.ViaKeyId(info, keyId) + addRecipientKey(key, subkey, wildcardKeyId) + } + } + + private fun addRecipientKey(certificate: PGPPublicKeyRing, + key: PGPPublicKey, + wildcardKeyId: Boolean) { + (_encryptionKeyIdentifiers as MutableSet).add(SubkeyIdentifier(certificate, key.keyID)) + addEncryptionMethod(ImplementationFactory.getInstance() + .getPublicKeyKeyEncryptionMethodGenerator(key) + .also { it.setUseWildcardKeyID(wildcardKeyId) }) + } + + /** + * Add a symmetric passphrase which the message will be encrypted to. + * + * @param passphrase passphrase + * @return this + */ + fun addPassphrase(passphrase: Passphrase) = apply { + require(!passphrase.isEmpty) { + "Passphrase MUST NOT be empty." + } + addEncryptionMethod(ImplementationFactory.getInstance().getPBEKeyEncryptionMethodGenerator(passphrase)) + } + + /** + * Add an {@link PGPKeyEncryptionMethodGenerator} which will be used to encrypt the message. + * Method generators are either {@link PBEKeyEncryptionMethodGenerator} (passphrase) + * or {@link PGPKeyEncryptionMethodGenerator} (public key). + * + * This method is intended for advanced users to allow encryption for specific subkeys. + * This can come in handy for example if data needs to be encrypted to a subkey that's ignored by PGPainless. + * + * @param encryptionMethod encryption method + * @return this + */ + fun addEncryptionMethod(encryptionMethod: PGPKeyEncryptionMethodGenerator) = apply { + (_encryptionMethods as MutableSet).add(encryptionMethod) + } + + /** + * Override the used symmetric encryption algorithm. + * The symmetric encryption algorithm is used to encrypt the message itself, + * while the used symmetric key will be encrypted to all recipients using public key + * cryptography. + * + * If the algorithm is not overridden, a suitable algorithm will be negotiated. + * + * @param encryptionAlgorithm encryption algorithm override + * @return this + */ + fun overrideEncryptionAlgorithm(encryptionAlgorithm: SymmetricKeyAlgorithm) = apply { + require(encryptionAlgorithm != SymmetricKeyAlgorithm.NULL) { + "Encryption algorithm override cannot be NULL." + } + _encryptionAlgorithmOverride = encryptionAlgorithm + } + + /** + * If this method is called, subsequent calls to {@link #addRecipient(PGPPublicKeyRing)} will allow encryption + * for subkeys that do not carry any {@link org.pgpainless.algorithm.KeyFlag} subpacket. + * This is a workaround for dealing with legacy keys that have no key flags subpacket but rely on the key algorithm + * type to convey the subkeys use. + * + * @return this + */ + fun setAllowEncryptionWithMissingKeyFlags() = apply { + this.allowEncryptionWithMissingKeyFlags = true + } + + fun hasEncryptionMethod() = _encryptionMethods.isNotEmpty() + + + fun interface EncryptionKeySelector { + fun selectEncryptionSubkeys(encryptionCapableKeys: List): List + } + + companion object { + @JvmStatic + fun get() = EncryptionOptions() + + @JvmStatic + fun encryptCommunications() = EncryptionOptions(EncryptionPurpose.COMMUNICATIONS) + + @JvmStatic + fun encryptDataAtRest() = EncryptionOptions(EncryptionPurpose.STORAGE) + + /** + * Only encrypt to the first valid encryption capable subkey we stumble upon. + * + * @return encryption key selector + */ + @JvmStatic + fun encryptToFirstSubkey() = EncryptionKeySelector { encryptionCapableKeys -> + encryptionCapableKeys.firstOrNull()?.let { listOf(it) } ?: listOf() } + + /** + * Encrypt to any valid, encryption capable subkey on the key ring. + * + * @return encryption key selector + */ + @JvmStatic + fun encryptToAllCapableSubkeys() = EncryptionKeySelector { encryptionCapableKeys -> encryptionCapableKeys } + } +} \ No newline at end of file diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java index 7d2fa453..87dab34a 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptionOptionsTest.java @@ -11,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; @@ -22,6 +23,7 @@ import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.pgpainless.PGPainless; @@ -122,7 +124,7 @@ public class EncryptionOptionsTest { EncryptionOptions options = new EncryptionOptions(); assertThrows(IllegalArgumentException.class, () -> options.addRecipients(Collections.emptyList())); assertThrows(IllegalArgumentException.class, () -> options.addRecipients(Collections.emptyList(), - encryptionCapableKeys -> encryptionCapableKeys)); + ArrayList::new)); } @Test @@ -150,8 +152,9 @@ public class EncryptionOptionsTest { assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> options.addRecipient(publicKeys, new EncryptionOptions.EncryptionKeySelector() { + @NotNull @Override - public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { + public List selectEncryptionSubkeys(@NotNull List encryptionCapableKeys) { return Collections.emptyList(); } })); @@ -159,7 +162,7 @@ public class EncryptionOptionsTest { assertThrows(KeyException.UnacceptableEncryptionKeyException.class, () -> options.addRecipient(publicKeys, "test@pgpainless.org", new EncryptionOptions.EncryptionKeySelector() { @Override - public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { + public List selectEncryptionSubkeys(@Nonnull List encryptionCapableKeys) { return Collections.emptyList(); } }));