diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/EncryptionMechanismNegotiator.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/EncryptionMechanismNegotiator.kt new file mode 100644 index 00000000..c8476910 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/algorithm/negotiation/EncryptionMechanismNegotiator.kt @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.algorithm.negotiation + +import org.bouncycastle.openpgp.api.MessageEncryptionMechanism +import org.pgpainless.algorithm.AEADAlgorithm +import org.pgpainless.algorithm.AEADCipherMode +import org.pgpainless.algorithm.Feature +import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.policy.Policy + +fun interface EncryptionMechanismNegotiator { + + fun negotiate( + policy: Policy, + override: MessageEncryptionMechanism?, + features: List>, + aeadAlgorithmPreferences: List>, + symmetricAlgorithmPreferences: List> + ): MessageEncryptionMechanism + + companion object { + @JvmStatic + fun modificationDetectionOrBetter( + symmetricKeyAlgorithmNegotiator: SymmetricKeyAlgorithmNegotiator + ): EncryptionMechanismNegotiator = + object : EncryptionMechanismNegotiator { + + override fun negotiate( + policy: Policy, + override: MessageEncryptionMechanism?, + features: List>, + aeadAlgorithmPreferences: List>, + symmetricAlgorithmPreferences: List> + ): MessageEncryptionMechanism { + + // If the user supplied an override, use that + if (override != null) { + return override + } + + // If all support SEIPD2, use SEIPD2 + if (features.all { it.contains(Feature.MODIFICATION_DETECTION_2) }) { + // Find best supported algorithm combination + val counted = mutableMapOf() + for (pref in aeadAlgorithmPreferences) { + for (mode in pref) { + counted[mode] = counted.getOrDefault(mode, 0) + 1 + } + } + // filter for supported combinations and find most widely supported + val bestSupportedMode: AEADCipherMode = + counted + .filter { + policy.messageEncryptionAlgorithmPolicy.isAcceptable( + MessageEncryptionMechanism.aead( + it.key.ciphermode.algorithmId, + it.key.aeadAlgorithm.algorithmId)) + } + .maxByOrNull { it.value } + ?.key + ?: AEADCipherMode(AEADAlgorithm.OCB, SymmetricKeyAlgorithm.AES_128) + + // return best supported mode or symmetric key fallback mechanism + return MessageEncryptionMechanism.aead( + bestSupportedMode.ciphermode.algorithmId, + bestSupportedMode.aeadAlgorithm.algorithmId) + } + // If all support SEIPD1, negotiate SEIPD1 using symmetricKeyAlgorithmNegotiator + else if (features.all { it.contains(Feature.MODIFICATION_DETECTION) }) { + return MessageEncryptionMechanism.integrityProtected( + symmetricKeyAlgorithmNegotiator + .negotiate( + policy.messageEncryptionAlgorithmPolicy + .symmetricAlgorithmPolicy, + null, + symmetricAlgorithmPreferences) + .algorithmId) + } + // Else fall back to fallback mechanism from policy + else { + return policy.messageEncryptionAlgorithmPolicy.asymmetricFallbackMechanism + } + } + } + } +} 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 index 45a8ba4e..980a2278 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionOptions.kt @@ -11,15 +11,15 @@ import org.bouncycastle.openpgp.api.OpenPGPCertificate import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator import org.pgpainless.PGPainless -import org.pgpainless.algorithm.AEADAlgorithm -import org.pgpainless.algorithm.AEADCipherMode import org.pgpainless.algorithm.EncryptionPurpose -import org.pgpainless.algorithm.Feature import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.algorithm.negotiation.EncryptionMechanismNegotiator import org.pgpainless.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator.Companion.byPopularity import org.pgpainless.authentication.CertificateAuthority import org.pgpainless.encryption_signing.EncryptionOptions.EncryptionKeySelector -import org.pgpainless.exception.KeyException.* +import org.pgpainless.exception.KeyException.ExpiredKeyException +import org.pgpainless.exception.KeyException.UnacceptableEncryptionKeyException +import org.pgpainless.exception.KeyException.UnacceptableSelfSignatureException import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.key.info.KeyAccessor import org.pgpainless.key.info.KeyRingInfo @@ -427,42 +427,26 @@ class EncryptionOptions(private val purpose: EncryptionPurpose, private val api: fun hasEncryptionMethod() = _encryptionMethods.isNotEmpty() - internal fun negotiateSymmetricEncryptionAlgorithm(): SymmetricKeyAlgorithm { - val preferences = - keysAndAccessors.values.map { it.preferredSymmetricKeyAlgorithms }.toList() - val algorithm = - byPopularity() - .negotiate( - api.algorithmPolicy.messageEncryptionAlgorithmPolicy.symmetricAlgorithmPolicy, - encryptionAlgorithmOverride, - preferences) - return algorithm - } - internal fun negotiateEncryptionMechanism(): MessageEncryptionMechanism { if (encryptionMechanismOverride != null) { return encryptionMechanismOverride!! } val features = keysAndAccessors.values.map { it.features }.toList() + val aeadAlgorithms = keysAndAccessors.values.map { it.preferredAEADCipherSuites }.toList() + val symmetricKeyAlgorithms = + keysAndAccessors.values.map { it.preferredSymmetricKeyAlgorithms }.toList() - if (features.all { it.contains(Feature.MODIFICATION_DETECTION_2) }) { - val aeadPrefs = keysAndAccessors.values.map { it.preferredAEADCipherSuites }.toList() - val counted = mutableMapOf() - for (pref in aeadPrefs) { - for (mode in pref) { - counted[mode] = counted.getOrDefault(mode, 0) + 1 - } - } - val max: AEADCipherMode = - counted.maxByOrNull { it.value }?.key - ?: AEADCipherMode(AEADAlgorithm.OCB, SymmetricKeyAlgorithm.AES_128) - return MessageEncryptionMechanism.aead( - max.ciphermode.algorithmId, max.aeadAlgorithm.algorithmId) - } else { - return MessageEncryptionMechanism.integrityProtected( - negotiateSymmetricEncryptionAlgorithm().algorithmId) - } + val mechanism = + EncryptionMechanismNegotiator.modificationDetectionOrBetter(byPopularity()) + .negotiate( + api.algorithmPolicy, + encryptionMechanismOverride, + features, + aeadAlgorithms, + symmetricKeyAlgorithms) + + return mechanism } fun interface EncryptionKeySelector { diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MechanismNegotiationTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MechanismNegotiationTest.java index e55b08fd..5f32e678 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MechanismNegotiationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/MechanismNegotiationTest.java @@ -79,11 +79,15 @@ public class MechanismNegotiationTest { .build())); } + + /** + * Here, we fall back to SEIPD1(AES128), as that is the policy fallback mechanism. + */ @TestTemplate @ExtendWith(TestAllImplementations.class) public void testEncryptToV6SEIPD1CertAndV6SEIPD2Cert() throws IOException, PGPException { testEncryptDecryptAndCheckExpectations( - MessageEncryptionMechanism.integrityProtected(SymmetricKeyAlgorithm.AES_192.getAlgorithmId()), + MessageEncryptionMechanism.integrityProtected(SymmetricKeyAlgorithm.AES_128.getAlgorithmId()), new KeySpecification(OpenPGPKeyVersion.v6, AlgorithmSuite.emptyBuilder() .overrideAeadAlgorithms(new AEADCipherMode(AEADAlgorithm.OCB, SymmetricKeyAlgorithm.AES_256))