From 9c943443994c04b70fc060a381451f34e4e59dc5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 20:46:22 +0000 Subject: [PATCH] feat: add AAD support to AESGCM for MLS AEAD compliance Added encrypt(data, aad) and decrypt(data, aad) overloads to the AESGCM expect/actual class across all platforms: - JVM/Android: Uses Cipher.updateAAD() with AES/GCM/NoPadding - Apple: Uses whyoleg.cryptography encryptWithIvBlocking(iv, data, aad) - Linux: Same as Apple via whyoleg.cryptography Updated HPKE aeadSeal/aeadOpen and MlsCryptoProvider aeadEncrypt/aeadDecrypt to use the AAD-aware methods instead of ignoring the AAD parameter. The EncryptWithLabel test still fails due to an X25519 DH computation discrepancy between Python reference and the Quartz JVM implementation. The HPKE implementation is internally consistent (MlsGroupTest passes). https://claude.ai/code/session_01NocQDWj2Y92FugjfgazzL3 --- .../quartz/utils/ciphers/AESGCM.apple.kt | 18 +++++++++++++++++ .../quartz/marmot/mls/crypto/Hpke.kt | 12 ++--------- .../marmot/mls/crypto/MlsCryptoProvider.kt | 12 ++--------- .../quartz/utils/ciphers/AESGCM.kt | 12 +++++++++++ .../quartz/utils/ciphers/AESGCM.jvmAndroid.kt | 20 +++++++++++++++++++ .../quartz/utils/ciphers/AESGCM.linux.kt | 18 +++++++++++++++++ 6 files changed, 72 insertions(+), 20 deletions(-) diff --git a/quartz/src/appleMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.apple.kt b/quartz/src/appleMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.apple.kt index eb065059a..9536b80e5 100644 --- a/quartz/src/appleMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.apple.kt +++ b/quartz/src/appleMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.apple.kt @@ -63,4 +63,22 @@ actual class AESGCM actual constructor( Log.w("AESGCM", "Failed to decrypt", e) null } + + @OptIn(DelicateCryptographyApi::class) + actual fun encrypt( + bytesToEncrypt: ByteArray, + aad: ByteArray, + ): ByteArray = + with(cipher()) { + encryptWithIvBlocking(nonce, bytesToEncrypt, aad) + } + + @OptIn(DelicateCryptographyApi::class) + actual fun decrypt( + bytesToDecrypt: ByteArray, + aad: ByteArray, + ): ByteArray = + with(cipher()) { + decryptWithIvBlocking(nonce, bytesToDecrypt, aad) + } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mls/crypto/Hpke.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mls/crypto/Hpke.kt index 23d328d97..67508522d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mls/crypto/Hpke.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mls/crypto/Hpke.kt @@ -197,20 +197,12 @@ object Hpke { nonce: ByteArray, aad: ByteArray, plaintext: ByteArray, - ): ByteArray { - val cipher = AESGCM(key, nonce) - // Note: Current AESGCM doesn't support AAD. For HPKE base mode with empty AAD this works. - // TODO: Add AAD support to AESGCM for full compliance - return cipher.encrypt(plaintext) - } + ): ByteArray = AESGCM(key, nonce).encrypt(plaintext, aad) private fun aeadOpen( key: ByteArray, nonce: ByteArray, aad: ByteArray, ciphertext: ByteArray, - ): ByteArray { - val cipher = AESGCM(key, nonce) - return cipher.decrypt(ciphertext) - } + ): ByteArray = AESGCM(key, nonce).decrypt(ciphertext, aad) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mls/crypto/MlsCryptoProvider.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mls/crypto/MlsCryptoProvider.kt index 2d22a54ef..c2eca8b0f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mls/crypto/MlsCryptoProvider.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mls/crypto/MlsCryptoProvider.kt @@ -139,22 +139,14 @@ object MlsCryptoProvider { nonce: ByteArray, aad: ByteArray, plaintext: ByteArray, - ): ByteArray { - val cipher = AESGCM(key, nonce) - // AESGCM doesn't support AAD directly; for MLS we need raw AES-GCM with AAD - // TODO: extend AESGCM to support AAD or use platform-specific AES-GCM with AAD - return cipher.encrypt(plaintext) - } + ): ByteArray = AESGCM(key, nonce).encrypt(plaintext, aad) fun aeadDecrypt( key: ByteArray, nonce: ByteArray, aad: ByteArray, ciphertext: ByteArray, - ): ByteArray { - val cipher = AESGCM(key, nonce) - return cipher.decrypt(ciphertext) - } + ): ByteArray = AESGCM(key, nonce).decrypt(ciphertext, aad) // --- Random --- diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.kt index 8b1b00578..831100a90 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.kt @@ -36,4 +36,16 @@ expect class AESGCM( override fun decrypt(bytesToDecrypt: ByteArray): ByteArray override fun decryptOrNull(bytesToDecrypt: ByteArray): ByteArray? + + /** AES-GCM encrypt with Additional Authenticated Data (AAD). */ + fun encrypt( + bytesToEncrypt: ByteArray, + aad: ByteArray, + ): ByteArray + + /** AES-GCM decrypt with Additional Authenticated Data (AAD). */ + fun decrypt( + bytesToDecrypt: ByteArray, + aad: ByteArray, + ): ByteArray } diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.jvmAndroid.kt index 986104fdb..a48ee39c6 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.jvmAndroid.kt @@ -65,6 +65,26 @@ actual class AESGCM actual constructor( null } + actual fun encrypt( + bytesToEncrypt: ByteArray, + aad: ByteArray, + ): ByteArray = + with(newCipher()) { + init(Cipher.ENCRYPT_MODE, keySpec(), param()) + updateAAD(aad) + doFinal(bytesToEncrypt) + } + + actual fun decrypt( + bytesToDecrypt: ByteArray, + aad: ByteArray, + ): ByteArray = + with(newCipher()) { + init(Cipher.DECRYPT_MODE, keySpec(), param()) + updateAAD(aad) + doFinal(bytesToDecrypt) + } + companion object { const val NAME = "aes-gcm" } diff --git a/quartz/src/linuxMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.linux.kt b/quartz/src/linuxMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.linux.kt index eb065059a..9536b80e5 100644 --- a/quartz/src/linuxMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.linux.kt +++ b/quartz/src/linuxMain/kotlin/com/vitorpamplona/quartz/utils/ciphers/AESGCM.linux.kt @@ -63,4 +63,22 @@ actual class AESGCM actual constructor( Log.w("AESGCM", "Failed to decrypt", e) null } + + @OptIn(DelicateCryptographyApi::class) + actual fun encrypt( + bytesToEncrypt: ByteArray, + aad: ByteArray, + ): ByteArray = + with(cipher()) { + encryptWithIvBlocking(nonce, bytesToEncrypt, aad) + } + + @OptIn(DelicateCryptographyApi::class) + actual fun decrypt( + bytesToDecrypt: ByteArray, + aad: ByteArray, + ): ByteArray = + with(cipher()) { + decryptWithIvBlocking(nonce, bytesToDecrypt, aad) + } }