mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
feat(marmot): send and return authenticated_data on application messages
MLS application messages carry an authenticated_data field (RFC 9420 §6.3.2): sent in the clear and bound to the message by the AEAD and the signature. Quartz verified it on receive but always sent it empty and never returned it, so an application could not use it, e.g. to bind the sender's account key to the message. encrypt() takes an optional authenticatedData, and DecryptedMessage returns it. Without the argument nothing changes on the wire.
This commit is contained in:
@@ -1116,8 +1116,15 @@ class MlsGroup private constructor(
|
||||
*
|
||||
* The signature is computed with `SignWithLabel(., "FramedContentTBS",
|
||||
* FramedContentTBS)` using the member's signature private key.
|
||||
*
|
||||
* [authenticatedData] is sent in the clear as the message's
|
||||
* `authenticated_data` (RFC 9420 §6.3.2). It is bound to the message by the
|
||||
* AEAD and the signature, and returned by [decrypt].
|
||||
*/
|
||||
fun encrypt(plaintext: ByteArray): ByteArray {
|
||||
fun encrypt(
|
||||
plaintext: ByteArray,
|
||||
authenticatedData: ByteArray = ByteArray(0),
|
||||
): ByteArray {
|
||||
// Trim sentKeys if it grows too large
|
||||
if (sentKeys.size > MAX_SENT_KEYS) {
|
||||
val sortedKeys = sentKeys.keys.sorted()
|
||||
@@ -1146,7 +1153,7 @@ class MlsGroup private constructor(
|
||||
groupId = groupId,
|
||||
epoch = epoch,
|
||||
senderLeafIndex = myLeafIndex,
|
||||
authenticatedData = ByteArray(0),
|
||||
authenticatedData = authenticatedData,
|
||||
applicationData = plaintext,
|
||||
groupContext = groupContext,
|
||||
),
|
||||
@@ -1162,7 +1169,7 @@ class MlsGroup private constructor(
|
||||
val pmcPlaintext = pmcWriter.toByteArray()
|
||||
|
||||
// Build PrivateContentAAD (RFC 9420 §6.3.2)
|
||||
val contentAad = buildPrivateContentAAD(groupId, epoch, ContentType.APPLICATION, ByteArray(0))
|
||||
val contentAad = buildPrivateContentAAD(groupId, epoch, ContentType.APPLICATION, authenticatedData)
|
||||
val ciphertext = MlsCryptoProvider.aeadEncrypt(kng.key, guardedNonce, contentAad, pmcPlaintext)
|
||||
|
||||
// Build sender data plaintext: leaf_index || generation || reuse_guard
|
||||
@@ -1200,7 +1207,7 @@ class MlsGroup private constructor(
|
||||
groupId = groupId,
|
||||
epoch = epoch,
|
||||
contentType = ContentType.APPLICATION,
|
||||
authenticatedData = ByteArray(0),
|
||||
authenticatedData = authenticatedData,
|
||||
encryptedSenderData = encryptedSenderData,
|
||||
ciphertext = ciphertext,
|
||||
)
|
||||
@@ -1442,6 +1449,7 @@ class MlsGroup private constructor(
|
||||
contentType = privMsg.contentType,
|
||||
content = applicationData,
|
||||
epoch = privMsg.epoch,
|
||||
authenticatedData = privMsg.authenticatedData,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1475,6 +1483,7 @@ class MlsGroup private constructor(
|
||||
contentType = privMsg.contentType,
|
||||
content = commitBytes,
|
||||
epoch = privMsg.epoch,
|
||||
authenticatedData = privMsg.authenticatedData,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1562,6 +1571,7 @@ class MlsGroup private constructor(
|
||||
contentType = privMsg.contentType,
|
||||
content = proposalBytes,
|
||||
epoch = privMsg.epoch,
|
||||
authenticatedData = privMsg.authenticatedData,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4563,19 +4573,23 @@ data class DecryptedMessage(
|
||||
val contentType: ContentType,
|
||||
val content: ByteArray,
|
||||
val epoch: Long,
|
||||
/** The message's `authenticated_data` (RFC 9420 §6.3.2), verified by the AEAD. */
|
||||
val authenticatedData: ByteArray = ByteArray(0),
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is DecryptedMessage) return false
|
||||
return senderLeafIndex == other.senderLeafIndex &&
|
||||
content.contentEquals(other.content) &&
|
||||
epoch == other.epoch
|
||||
epoch == other.epoch &&
|
||||
authenticatedData.contentEquals(other.authenticatedData)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = senderLeafIndex
|
||||
result = 31 * result + content.contentHashCode()
|
||||
result = 31 * result + epoch.hashCode()
|
||||
result = 31 * result + authenticatedData.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.marmot.mls.group
|
||||
|
||||
import com.vitorpamplona.quartz.marmot.mls.codec.TlsReader
|
||||
import com.vitorpamplona.quartz.marmot.mls.framing.MlsMessage
|
||||
import com.vitorpamplona.quartz.marmot.mls.framing.PrivateMessage
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFails
|
||||
|
||||
/** `authenticated_data` on application messages (RFC 9420 §6.3.2): sent in the clear, bound by the AEAD. */
|
||||
class AuthenticatedDataTest {
|
||||
private val aad = "sender-binding".encodeToByteArray()
|
||||
|
||||
@Test
|
||||
fun theReceiverGetsTheSendersAuthenticatedData() {
|
||||
val (alice, bob) = twoMemberGroup()
|
||||
|
||||
val received = bob.decrypt(alice.encrypt("hi".encodeToByteArray(), aad))
|
||||
|
||||
assertContentEquals("hi".encodeToByteArray(), received.content)
|
||||
assertContentEquals(aad, received.authenticatedData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun withoutItTheAuthenticatedDataIsEmpty() {
|
||||
val (alice, bob) = twoMemberGroup()
|
||||
|
||||
val received = bob.decrypt(alice.encrypt("hi".encodeToByteArray()))
|
||||
|
||||
assertEquals(0, received.authenticatedData.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun alteredAuthenticatedDataFailsToDecrypt() {
|
||||
val (alice, bob) = twoMemberGroup()
|
||||
|
||||
val sent = MlsMessage.decodeTls(TlsReader(alice.encrypt("hi".encodeToByteArray(), aad)))
|
||||
val message = PrivateMessage.decodeTls(TlsReader(sent.payload))
|
||||
val altered = MlsMessage.fromPrivateMessage(message.copy(authenticatedData = "someone-else".encodeToByteArray()))
|
||||
|
||||
assertFails { bob.decrypt(altered.toTlsBytes()) }
|
||||
}
|
||||
|
||||
private fun twoMemberGroup(): Pair<MlsGroup, MlsGroup> {
|
||||
val alice = MlsGroup.create("11".repeat(32).hexToByteArray())
|
||||
val bobBundle = alice.createKeyPackage("22".repeat(32).hexToByteArray(), ByteArray(0))
|
||||
val result = alice.addMember(bobBundle.keyPackage.toTlsBytes())
|
||||
val bob = MlsGroup.processWelcome(result.welcomeBytes!!, bobBundle)
|
||||
return alice to bob
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user