// SPDX-FileCopyrightText: 2023 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 package org.pgpainless.encryption_signing import java.util.* 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.encryption_signing.EncryptionOptions.EncryptionKeySelector 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 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[keyId] = info _keyViews[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[keyId] = info _keyViews[keyId] = KeyAccessor.ViaKeyId(info, keyId) addRecipientKey(key, subkey, wildcardKeyId) } } private fun addRecipientKey( certificate: PGPPublicKeyRing, key: PGPPublicKey, wildcardKeyId: Boolean ) { _encryptionKeyIdentifiers.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.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 } } }