From b41fb2c468090cc7e6939ffb07ede0123e0dab9a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 5 May 2025 12:17:47 +0200 Subject: [PATCH] First draft for SEIPD2 negotiation --- .../OpenPGPImplementationExtensions.kt | 25 ++++++ .../encryption_signing/EncryptionOptions.kt | 83 ++++++++++++++----- .../encryption_signing/EncryptionResult.kt | 18 ++-- .../encryption_signing/EncryptionStream.kt | 38 ++++----- .../org/pgpainless/key/info/KeyAccessor.kt | 6 +- .../EncryptDecryptTest.java | 3 +- 6 files changed, 124 insertions(+), 49 deletions(-) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/OpenPGPImplementationExtensions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/OpenPGPImplementationExtensions.kt index 5a33b609..18a0a737 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/OpenPGPImplementationExtensions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/bouncycastle/extensions/OpenPGPImplementationExtensions.kt @@ -5,9 +5,34 @@ package org.pgpainless.bouncycastle.extensions import org.bouncycastle.bcpg.HashAlgorithmTags +import org.bouncycastle.openpgp.api.EncryptedDataPacketType +import org.bouncycastle.openpgp.api.MessageEncryptionMechanism import org.bouncycastle.openpgp.api.OpenPGPImplementation +import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder import org.bouncycastle.openpgp.operator.PGPDigestCalculator fun OpenPGPImplementation.checksumCalculator(): PGPDigestCalculator { return pgpDigestCalculatorProvider().get(HashAlgorithmTags.SHA1) } + +fun OpenPGPImplementation.pgpDataEncryptorBuilder( + mechanism: MessageEncryptionMechanism +): PGPDataEncryptorBuilder { + require(mechanism.isEncrypted) { "Cannot create PGPDataEncryptorBuilder for NULL algorithm." } + return pgpDataEncryptorBuilder(mechanism.symmetricKeyAlgorithm).also { + when (mechanism.mode!!) { + EncryptedDataPacketType.SED -> it.setWithIntegrityPacket(false) + EncryptedDataPacketType.SEIPDv1 -> it.setWithIntegrityPacket(true) + EncryptedDataPacketType.SEIPDv2 -> { + it.setWithAEAD(mechanism.aeadAlgorithm, mechanism.symmetricKeyAlgorithm) + it.setUseV6AEAD() + } + EncryptedDataPacketType.LIBREPGP_OED -> { + it.setWithAEAD(mechanism.aeadAlgorithm, mechanism.symmetricKeyAlgorithm) + it.setUseV5AEAD() + } + } + + return it + } +} 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 7dd6b3fb..fb7881b4 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 @@ -6,11 +6,15 @@ package org.pgpainless.encryption_signing import java.util.* import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.api.MessageEncryptionMechanism 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.SymmetricKeyAlgorithmNegotiator.Companion.byPopularity import org.pgpainless.authentication.CertificateAuthority @@ -23,32 +27,34 @@ import org.pgpainless.util.Passphrase class EncryptionOptions(private val purpose: EncryptionPurpose, private val api: PGPainless) { private val _encryptionMethods: MutableSet = mutableSetOf() - private val _encryptionKeys: MutableSet = mutableSetOf() + private val keysAndAccessors: MutableMap = mutableMapOf() 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 + private var _encryptionMechanismOverride: MessageEncryptionMechanism? = null val encryptionMethods get() = _encryptionMethods.toSet() val encryptionKeyIdentifiers - get() = _encryptionKeys.map { SubkeyIdentifier(it) } + get() = keysAndAccessors.keys.map { SubkeyIdentifier(it) } val encryptionKeys - get() = _encryptionKeys.toSet() - - val keyRingInfo - get() = _keyRingInfo.toMap() - - val keyViews - get() = _keyViews.toMap() + get() = keysAndAccessors.keys.toSet() + @Deprecated( + "Deprecated in favor of encryptionMechanismOverride", + replaceWith = ReplaceWith("encryptionMechanismOverride")) val encryptionAlgorithmOverride - get() = _encryptionAlgorithmOverride + get() = + _encryptionMechanismOverride?.let { + SymmetricKeyAlgorithm.requireFromId(it.symmetricKeyAlgorithm) + } + + val encryptionMechanismOverride + get() = _encryptionMechanismOverride constructor(api: PGPainless) : this(EncryptionPurpose.ANY, api) @@ -200,8 +206,8 @@ class EncryptionOptions(private val purpose: EncryptionPurpose, private val api: for (subkey in subkeys) { val keyId = SubkeyIdentifier(subkey) _keyRingInfo[keyId] = info - _keyViews[keyId] = KeyAccessor.ViaUserId(subkey, cert.getUserId(userId.toString())) - addRecipientKey(subkey, false) + val accessor = KeyAccessor.ViaUserId(subkey, cert.getUserId(userId.toString())) + addRecipientKey(subkey, accessor, false) } } @@ -319,13 +325,17 @@ class EncryptionOptions(private val purpose: EncryptionPurpose, private val api: for (subkey in encryptionSubkeys) { val keyId = SubkeyIdentifier(subkey) _keyRingInfo[keyId] = info - _keyViews[keyId] = KeyAccessor.ViaKeyIdentifier(subkey) - addRecipientKey(subkey, wildcardKeyId) + val accessor = KeyAccessor.ViaKeyIdentifier(subkey) + addRecipientKey(subkey, accessor, wildcardKeyId) } } - private fun addRecipientKey(key: OpenPGPComponentKey, wildcardRecipient: Boolean) { - _encryptionKeys.add(key) + private fun addRecipientKey( + key: OpenPGPComponentKey, + accessor: KeyAccessor, + wildcardRecipient: Boolean + ) { + keysAndAccessors[key] = accessor addEncryptionMethod( api.implementation.publicKeyKeyEncryptionMethodGenerator(key.pgpPublicKey).also { it.setUseWildcardRecipient(wildcardRecipient) @@ -381,11 +391,21 @@ class EncryptionOptions(private val purpose: EncryptionPurpose, private val api: * @param encryptionAlgorithm encryption algorithm override * @return this */ + @Deprecated( + "Deprecated in favor of overrideEncryptionMechanism", + replaceWith = + ReplaceWith( + "overrideEncryptionMechanism(MessageEncryptionMechanism.integrityProtected(encryptionAlgorithm.algorithmId))")) fun overrideEncryptionAlgorithm(encryptionAlgorithm: SymmetricKeyAlgorithm) = apply { require(encryptionAlgorithm != SymmetricKeyAlgorithm.NULL) { "Encryption algorithm override cannot be NULL." } - _encryptionAlgorithmOverride = encryptionAlgorithm + overrideEncryptionMechanism( + MessageEncryptionMechanism.integrityProtected(encryptionAlgorithm.algorithmId)) + } + + fun overrideEncryptionMechanism(encryptionMechanism: MessageEncryptionMechanism) = apply { + _encryptionMechanismOverride = encryptionMechanism } /** @@ -403,7 +423,8 @@ class EncryptionOptions(private val purpose: EncryptionPurpose, private val api: fun hasEncryptionMethod() = _encryptionMethods.isNotEmpty() internal fun negotiateSymmetricEncryptionAlgorithm(): SymmetricKeyAlgorithm { - val preferences = keyViews.values.map { it.preferredSymmetricKeyAlgorithms }.toList() + val preferences = + keysAndAccessors.values.map { it.preferredSymmetricKeyAlgorithms }.toList() val algorithm = byPopularity() .negotiate( @@ -413,6 +434,28 @@ class EncryptionOptions(private val purpose: EncryptionPurpose, private val api: return algorithm } + internal fun negotiateEncryptionMechanism(): MessageEncryptionMechanism { + val features = keysAndAccessors.values.map { it.features }.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) + } + } + fun interface EncryptionKeySelector { fun selectEncryptionSubkeys( encryptionCapableKeys: List diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt index 626c1de4..e86b2d90 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionResult.kt @@ -8,6 +8,7 @@ import java.util.* import org.bouncycastle.openpgp.PGPLiteralData import org.bouncycastle.openpgp.PGPPublicKeyRing import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.api.MessageEncryptionMechanism import org.bouncycastle.openpgp.api.OpenPGPCertificate import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature import org.pgpainless.algorithm.CompressionAlgorithm @@ -18,7 +19,7 @@ import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.util.MultiMap data class EncryptionResult( - val encryptionAlgorithm: SymmetricKeyAlgorithm, + val encryptionMechanism: MessageEncryptionMechanism, val compressionAlgorithm: CompressionAlgorithm, val detachedDocumentSignatures: OpenPGPSignatureSet, val recipients: Set, @@ -27,6 +28,11 @@ data class EncryptionResult( val fileEncoding: StreamEncoding ) { + @Deprecated( + "Use encryptionMechanism instead.", replaceWith = ReplaceWith("encryptionMechanism")) + val encryptionAlgorithm: SymmetricKeyAlgorithm? + get() = SymmetricKeyAlgorithm.fromId(encryptionMechanism.symmetricKeyAlgorithm) + @Deprecated( "Use detachedSignatures instead", replaceWith = ReplaceWith("detachedDocumentSignatures")) // TODO: Remove in 2.1 @@ -69,7 +75,8 @@ data class EncryptionResult( } class Builder { - var _encryptionAlgorithm: SymmetricKeyAlgorithm? = null + var _encryptionMechanism: MessageEncryptionMechanism = + MessageEncryptionMechanism.unencrypted() var _compressionAlgorithm: CompressionAlgorithm? = null val detachedSignatures: MutableList = mutableListOf() @@ -78,8 +85,8 @@ data class EncryptionResult( private var _modificationDate = Date(0) private var _encoding = StreamEncoding.BINARY - fun setEncryptionAlgorithm(encryptionAlgorithm: SymmetricKeyAlgorithm) = apply { - _encryptionAlgorithm = encryptionAlgorithm + fun setEncryptionMechanism(mechanism: MessageEncryptionMechanism): Builder = apply { + _encryptionMechanism = mechanism } fun setCompressionAlgorithm(compressionAlgorithm: CompressionAlgorithm) = apply { @@ -103,11 +110,10 @@ data class EncryptionResult( } fun build(): EncryptionResult { - checkNotNull(_encryptionAlgorithm) { "Encryption algorithm not set." } checkNotNull(_compressionAlgorithm) { "Compression algorithm not set." } return EncryptionResult( - _encryptionAlgorithm!!, + _encryptionMechanism, _compressionAlgorithm!!, OpenPGPSignatureSet(detachedSignatures), recipients, diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt index 4c913a62..d2a9a63c 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/EncryptionStream.kt @@ -13,11 +13,12 @@ import org.bouncycastle.openpgp.PGPCompressedDataGenerator import org.bouncycastle.openpgp.PGPEncryptedDataGenerator import org.bouncycastle.openpgp.PGPException import org.bouncycastle.openpgp.PGPLiteralDataGenerator +import org.bouncycastle.openpgp.api.MessageEncryptionMechanism import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature import org.pgpainless.PGPainless import org.pgpainless.algorithm.CompressionAlgorithm import org.pgpainless.algorithm.StreamEncoding -import org.pgpainless.algorithm.SymmetricKeyAlgorithm +import org.pgpainless.bouncycastle.extensions.pgpDataEncryptorBuilder import org.pgpainless.util.ArmoredOutputStreamFactory // 1 << 8 causes wrong partial body length encoding @@ -74,34 +75,29 @@ class EncryptionStream( @Throws(IOException::class, PGPException::class) private fun prepareEncryption() { if (options.encryptionOptions == null) { - // No encryption options -> no encryption - resultBuilder.setEncryptionAlgorithm(SymmetricKeyAlgorithm.NULL) return } require(options.encryptionOptions.encryptionMethods.isNotEmpty()) { "If EncryptionOptions are provided, at least one encryption method MUST be provided as well." } - options.encryptionOptions.negotiateSymmetricEncryptionAlgorithm().let { - resultBuilder.setEncryptionAlgorithm(it) - val encryptedDataGenerator = - PGPEncryptedDataGenerator( - api.implementation.pgpDataEncryptorBuilder(it.algorithmId).apply { - setWithIntegrityPacket(true) - }) - options.encryptionOptions.encryptionMethods.forEach { m -> - encryptedDataGenerator.addMethod(m) - } - options.encryptionOptions.encryptionKeyIdentifiers.forEach { r -> - resultBuilder.addRecipient(r) - } + val mechanism: MessageEncryptionMechanism = + options.encryptionOptions.negotiateEncryptionMechanism() + resultBuilder.setEncryptionMechanism(mechanism) + val encryptedDataGenerator = + PGPEncryptedDataGenerator(api.implementation.pgpDataEncryptorBuilder(mechanism)) - publicKeyEncryptedStream = - encryptedDataGenerator.open(outermostStream, ByteArray(BUFFER_SIZE)).also { stream - -> - outermostStream = stream - } + options.encryptionOptions.encryptionMethods.forEach { m -> + encryptedDataGenerator.addMethod(m) } + options.encryptionOptions.encryptionKeyIdentifiers.forEach { r -> + resultBuilder.addRecipient(r) + } + + publicKeyEncryptedStream = + encryptedDataGenerator.open(outermostStream, ByteArray(BUFFER_SIZE)).also { stream -> + outermostStream = stream + } } @Throws(IOException::class) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt index a5c2627e..838fd222 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/info/KeyAccessor.kt @@ -4,6 +4,7 @@ package org.pgpainless.key.info +import org.bouncycastle.bcpg.sig.PreferredAEADCiphersuites.Combination import java.util.* import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPCertificateComponent import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey @@ -40,7 +41,10 @@ abstract class KeyAccessor( val preferredAEADCipherSuites: Set get() = - component.getAEADCipherSuitePreferences(referenceTime)?.toAEADCipherModes() ?: setOf() + component.getAEADCipherSuitePreferences(referenceTime) + ?.rawAlgorithms + ?.map { AEADCipherMode(it) } + ?.toSet() ?: setOf() val features: Set get() = diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java index b5713345..11b5d9cc 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/EncryptDecryptTest.java @@ -376,6 +376,7 @@ public class EncryptDecryptTest { eOut.write(testMessage.getBytes(StandardCharsets.UTF_8)); eOut.close(); + ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray()); DecryptionStream dIn = PGPainless.decryptAndOrVerify() .onInputStream(bIn) @@ -389,7 +390,7 @@ public class EncryptDecryptTest { MessageMetadata metadata = dIn.getMetadata(); MessageEncryptionMechanism encryptionMechanism = metadata.getEncryptionMechanism(); assertEquals( - MessageEncryptionMechanism.aead(SymmetricKeyAlgorithm.AES_128.getAlgorithmId(), AEADAlgorithm.OCB.getAlgorithmId()), + MessageEncryptionMechanism.aead(SymmetricKeyAlgorithm.AES_192.getAlgorithmId(), AEADAlgorithm.OCB.getAlgorithmId()), encryptionMechanism); } }