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
This commit is contained in:
Claude
2026-04-03 20:46:22 +00:00
parent 9d34bb8b2e
commit 9c94344399
6 changed files with 72 additions and 20 deletions
@@ -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)
}
}
@@ -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)
}
@@ -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 ---
@@ -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
}
@@ -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"
}
@@ -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)
}
}