Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fb50568
refactor(crypto): rename `Text` variant of `DecryptedMessage` to `App…
SimonThormeyer Aug 3, 2026
8893ed3
refactor(crypto-ffi): rename `Text` variant of `DecryptedMessage` to …
SimonThormeyer Aug 3, 2026
c04caef
chore: `DecryptedMessage.Text` -> `DecryptedMessage.ApplicationMessag…
SimonThormeyer Aug 3, 2026
b907770
docs: update migration guide for `DecryptedMessage.ApplicationMessage`
SimonThormeyer Aug 3, 2026
0c5b3d0
feat(crypto-provider): support decrypting via hpke psk mode
SimonThormeyer Aug 18, 2026
acf3bcc
refactor(conversation/decrypt): add targeted message variants to `Dec…
SimonThormeyer Aug 18, 2026
2ec05f7
feat(keystore): add `tnt_secrets` table and corresponding struct
SimonThormeyer Aug 19, 2026
dd8ff50
feat(keystore): add `targeted_message_rx_counters` table and correspo…
SimonThormeyer Aug 19, 2026
502a0d4
refactor: rename `decrypt_message_inner()` to `decrypt_mls_message()`
SimonThormeyer Aug 3, 2026
25fc1a1
refactor(crypto): `ConversationMut::decrypt_message` delegates to mls…
SimonThormeyer Aug 18, 2026
f1155fc
chore(conversation): persist new and remove old tnt secrets when epoc…
SimonThormeyer Aug 19, 2026
98e2d56
chore(tnt): add `TntWireFormat`
SimonThormeyer Aug 3, 2026
7506ff5
refactor(tnt): extracting hpke context data is shared between encrypt…
SimonThormeyer Aug 18, 2026
c67cd46
feat(tnt): add `ConversationMut::decrypt_tnt_message()` [WPB-27214]
SimonThormeyer Aug 19, 2026
b1ae7bf
feat(tnt): add `ConversationMut::decrypt_targeted()` [WPB-27214]
SimonThormeyer Aug 18, 2026
9283ed2
feat(tnt): enable buffering targeted messages
SimonThormeyer Aug 19, 2026
3ec0236
test(tnt): test decryption of targeted messages
SimonThormeyer Aug 19, 2026
24f023a
refactor(crypto-ffi): add targeted message variants to `DecryptedMess…
SimonThormeyer Aug 19, 2026
34c7860
chore(ffi/ts): add ts test for targeted messages
SimonThormeyer Aug 19, 2026
7a56ca5
chore(ffi/kotlin): add kotlin test for targeted messages
SimonThormeyer Aug 19, 2026
09eb4dc
chore(ffi/swift): add swift test for targeted messages
SimonThormeyer Aug 19, 2026
839364d
chore(interop): resolve compile errors caused by new decrypted messag…
SimonThormeyer Aug 19, 2026
b4d95c9
fixup! feat(keystore): add `tnt_secrets` table and corresponding struct
SimonThormeyer Aug 19, 2026
db89d37
fixup! feat(keystore): add `targeted_message_rx_counters` table and c…
SimonThormeyer Aug 19, 2026
ffa32fa
wip pk ref. todo: impl EntityGetBorrowed
SimonThormeyer Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions cc-book/src/unreleased/migration_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@

## DecryptedMessage

`DecryptedMessage` is now an enum with `Text`, `Commit`, and `Proposal` variants. Data that was previously exposed
through optional properties is now carried by the corresponding variant. Match on the variant before accessing its data:
`DecryptedMessage` is now an enum with `ApplicationMessage`, `Commit`, and `Proposal` variants. Data that was previously
exposed through optional properties is now carried by the corresponding variant. Match on the variant before accessing
its data:

<!-- langtabs-start -->

```typescript
if (DecryptedMessage.Text.instanceOf(decryptedMessage)) {
if (DecryptedMessage.ApplicationMessage.instanceOf(decryptedMessage)) {
const { plaintext, senderClientId, identity } = decryptedMessage.inner;
// Handle the application message.
} else if (DecryptedMessage.Commit.instanceOf(decryptedMessage)) {
Expand All @@ -31,7 +32,7 @@ if (DecryptedMessage.Text.instanceOf(decryptedMessage)) {

```swift
switch decryptedMessage {
case let .text(plaintext, senderClientId, identity):
case let .applicationMessage(plaintext, senderClientId, identity):
// Handle the application message.
case let .commit(isActive, bufferedMessages, identity):
// Handle the commit.
Expand All @@ -42,7 +43,7 @@ case let .proposal(delay, identity):

```kotlin
when (decryptedMessage) {
is DecryptedMessage.Text -> {
is DecryptedMessage.ApplicationMessage -> {
val (plaintext, senderClientId, identity) = decryptedMessage
// Handle the application message.
}
Expand Down
4 changes: 2 additions & 2 deletions crypto-ffi/bindings/js/shared/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,10 +423,10 @@ async function setHelpers() {
);

if (
ccModule.DecryptedMessage.Text.instanceOf(
ccModule.DecryptedMessage.ApplicationMessage.instanceOf(
decryptedByClient1
) &&
ccModule.DecryptedMessage.Text.instanceOf(
ccModule.DecryptedMessage.ApplicationMessage.instanceOf(
decryptedByClient2
)
) {
Expand Down
1 change: 1 addition & 0 deletions crypto-ffi/bindings/js/shared/src/CoreCrypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,6 @@ export {
HttpMethod,
X509CredentialAcquisitionConfiguration,
type Timestamp,
TargetedMessagePolicy,
type MlsTransport,
} from "#core-crypto-ffi";
62 changes: 62 additions & 0 deletions crypto-ffi/bindings/js/shared/test/conversation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,66 @@ describe("conversation", () => {
expect(decryptedByAlice).to.equal(messageText);
expect(decryptedByBob).to.equal(messageText);
});

it("should allow decrypting targeted messages", async () => {
const results = await runOnPlatform(async () => {
const alice = await helpers.ccInit();
const bobId = helpers.newClientId();
const bob = await helpers.ccInit({ clientId: bobId });
const conversationId = await helpers.createConversation(alice);
await helpers.invite(alice, bob, conversationId);

const persistedMessage = new TextEncoder().encode(
"This persisted message targets Bob"
);
const persistedCiphertext = await alice.transaction((ctx) =>
ctx.encryptTargetedMessage(
conversationId,
bobId,
ccModule.TargetedMessagePolicy.Persisted,
persistedMessage
)
);
const persistedDecrypted = await bob.transaction((ctx) =>
ctx.decryptMessage(conversationId, persistedCiphertext)
);

const transientMessage = new TextEncoder().encode(
"This transient message targets Bob"
);
const transientCiphertext = await alice.transaction((ctx) =>
ctx.encryptTargetedMessage(
conversationId,
bobId,
ccModule.TargetedMessagePolicy.Transient,
transientMessage
)
);
const transientDecrypted = await bob.transaction((ctx) =>
ctx.decryptMessage(conversationId, transientCiphertext)
);

const decoder = new TextDecoder();

return {
persistedMessage: decoder.decode(persistedMessage),
persistedPlaintext:
ccModule.DecryptedMessage.PersistedTargeted.instanceOf(
persistedDecrypted
)
? decoder.decode(persistedDecrypted.inner.plaintext)
: "wrong decrypted variant",
transientMessage: decoder.decode(transientMessage),
transientPlaintext:
ccModule.DecryptedMessage.TransientTargeted.instanceOf(
transientDecrypted
)
? decoder.decode(transientDecrypted.inner.plaintext)
: "wrong decrypted variant",
};
});

expect(results.persistedPlaintext).to.equal(results.persistedMessage);
expect(results.transientPlaintext).to.equal(results.transientMessage);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ class MLSTest {

val plaintextMsg =
bob.transaction { ctx ->
assertIs<DecryptedMessage.Text>(ctx.decryptMessage(groupId, ciphertextMsg)).plaintext
assertIs<DecryptedMessage.ApplicationMessage>(ctx.decryptMessage(groupId, ciphertextMsg)).plaintext
}
assertThat(plaintextMsg).isNotEmpty().isEqualTo(msg)

Expand All @@ -233,6 +233,52 @@ class MLSTest {
assertIs<MlsException.DuplicateMessage>(expectedException.mlsError)
}

@Test
fun encryptTargetedMessage_should_encrypt_then_receiver_should_decrypt() = runTest {
val alice = ccInit()
val bobId = genClientId()
val bob = ccInit(CcInitOptions(clientId = bobId))

val conversationId = createConversation(alice)
invite(alice, bob, conversationId)

val persistedMessage = "This persisted message targets Bob".toByteArray()
val persistedCiphertext = alice.transaction { ctx ->
ctx.encryptTargetedMessage(
conversationId,
bobId,
TargetedMessagePolicy.PERSISTED,
persistedMessage
)
}
assertThat(persistedCiphertext).isNotEqualTo(persistedMessage)

val persistedPlaintext = bob.transaction { ctx ->
assertIs<DecryptedMessage.PersistedTargeted>(
ctx.decryptMessage(conversationId, persistedCiphertext)
).plaintext
}
assertThat(persistedPlaintext).isEqualTo(persistedMessage)

val transientMessage = "This transient message targets Bob".toByteArray()
val transientCiphertext = alice.transaction { ctx ->
ctx.encryptTargetedMessage(
conversationId,
bobId,
TargetedMessagePolicy.TRANSIENT,
transientMessage
)
}
assertThat(transientCiphertext).isNotEqualTo(transientMessage)

val transientPlaintext = bob.transaction { ctx ->
assertIs<DecryptedMessage.TransientTargeted>(
ctx.decryptMessage(conversationId, transientCiphertext)
).plaintext
}
assertThat(transientPlaintext).isEqualTo(transientMessage)
}

@Test
fun addClientsToConversation_should_add_members_to_the_MLS_group() = runTest {
val aliceId = genClientId()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ final class WireCoreCryptoTests: XCTestCase {
let decrypted = try await bob.transaction { ctx in
try await ctx.decryptMessage(conversationId: conversationId, payload: ciphertext)
}
guard case .text(let plaintext, _, _) = decrypted else {
guard case .applicationMessage(let plaintext, _, _) = decrypted else {
XCTFail("Expected decrypted text")
return
}
Expand All @@ -612,6 +612,61 @@ final class WireCoreCryptoTests: XCTestCase {
}
}

func testTargetedMessagesCanBeDecryptedByReceiver() async throws {
let alice = try await ccInit()
let bobId = genClientId()
let bob = try await ccInit(options: CcInitOptions(clientId: bobId))

let conversationId = try await createConversation(coreCrypto: alice)
_ = try await invite(cc1: alice, cc2: bob, conversationId: conversationId)

let persistedMessage = Data("This persisted message targets Bob".utf8)
let persistedCiphertext = try await alice.transaction { ctx in
try await ctx.encryptTargetedMessage(
conversationId: conversationId,
recipient: bobId,
policy: .persisted,
message: persistedMessage
)
}
XCTAssertNotEqual(persistedCiphertext, persistedMessage)

let persistedDecrypted = try await bob.transaction { ctx in
try await ctx.decryptMessage(
conversationId: conversationId,
payload: persistedCiphertext
)
}
guard case .persistedTargeted(let plaintext, _, _) = persistedDecrypted else {
XCTFail("Expected a decrypted persisted targeted message")
return
}
XCTAssertEqual(plaintext, persistedMessage)

let transientMessage = Data("This transient message targets Bob".utf8)
let transientCiphertext = try await alice.transaction { ctx in
try await ctx.encryptTargetedMessage(
conversationId: conversationId,
recipient: bobId,
policy: .transient,
message: transientMessage
)
}
XCTAssertNotEqual(transientCiphertext, transientMessage)

let transientDecrypted = try await bob.transaction { ctx in
try await ctx.decryptMessage(
conversationId: conversationId,
payload: transientCiphertext
)
}
guard case .transientTargeted(let plaintext, _, _) = transientDecrypted else {
XCTFail("Expected a decrypted transient targeted message")
return
}
XCTAssertEqual(plaintext, transientMessage)
}

func testRegisterEpochObserverShouldNotifyObserverOnNewEpoch() async throws {
struct Epoch: Equatable {
let conversationId: ConversationId
Expand Down
80 changes: 68 additions & 12 deletions crypto-ffi/src/decrypted_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use crate::{ClientId, WireIdentity};
/// Represents the items a consumer might require after decrypting a message.
#[derive(Debug, uniffi::Enum)]
pub enum DecryptedMessage {
/// The decrypted message is a text message.
Text {
/// The decrypted message is an MLS application message.
ApplicationMessage {
/// Decrypted text message.
plaintext: Vec<u8>,
/// The sender's `ClientId`.
Expand Down Expand Up @@ -37,15 +37,33 @@ pub enum DecryptedMessage {
/// Identity claims present in the sender credential.
identity: WireIdentity,
},
/// The decrypted message is a transient targeted message.
TransientTargeted {
/// Decrypted text message.
plaintext: Vec<u8>,
/// The sender's `ClientId`.
sender_client_id: Arc<ClientId>,
/// Identity claims present in the sender credential.
identity: WireIdentity,
},
/// The decrypted message is a persisted targeted message.
PersistedTargeted {
/// Decrypted text message.
plaintext: Vec<u8>,
/// The sender's `ClientId`.
sender_client_id: Arc<ClientId>,
/// Identity claims present in the sender credential.
identity: WireIdentity,
},
}

impl From<CcDecryptedMessage> for DecryptedMessage {
fn from(from: CcDecryptedMessage) -> Self {
match from {
CcDecryptedMessage::Text(text) => Self::Text {
plaintext: text.plaintext,
sender_client_id: Arc::new(text.sender_client_id.into()),
identity: text.identity.into(),
CcDecryptedMessage::ApplicationMessage(message) => Self::ApplicationMessage {
plaintext: message.plaintext,
sender_client_id: Arc::new(message.sender_client_id.into()),
identity: message.identity.into(),
},
CcDecryptedMessage::Commit(commit) => Self::Commit {
is_active: commit.is_active,
Expand All @@ -58,6 +76,16 @@ impl From<CcDecryptedMessage> for DecryptedMessage {
delay: proposal.delay,
identity: proposal.identity.into(),
},
CcDecryptedMessage::TransientTargeted(message) => Self::TransientTargeted {
plaintext: message.plaintext,
sender_client_id: Arc::new(message.sender_client_id.into()),
identity: message.identity.into(),
},
CcDecryptedMessage::PersistedTargeted(message) => Self::PersistedTargeted {
plaintext: message.plaintext,
sender_client_id: Arc::new(message.sender_client_id.into()),
identity: message.identity.into(),
},
}
}
}
Expand All @@ -66,8 +94,8 @@ impl From<CcDecryptedMessage> for DecryptedMessage {
/// It represents messages for the new epoch that arrived before the commit that created it.
#[derive(Debug, uniffi::Enum)]
pub enum BufferedDecryptedMessage {
/// The decrypted message is a text message.
Text {
/// The decrypted message is an MLS application message.
ApplicationMessage {
/// Decrypted text message.
plaintext: Vec<u8>,
/// The sender's `ClientId`.
Expand All @@ -93,15 +121,33 @@ pub enum BufferedDecryptedMessage {
/// Identity claims present in the sender credential.
identity: WireIdentity,
},
/// The decrypted message is a transient targeted message.
TransientTargeted {
/// Decrypted text message.
plaintext: Vec<u8>,
/// The sender's `ClientId`.
sender_client_id: Arc<ClientId>,
/// Identity claims present in the sender credential.
identity: WireIdentity,
},
/// The decrypted message is a persisted targeted message.
PersistedTargeted {
/// Decrypted text message.
plaintext: Vec<u8>,
/// The sender's `ClientId`.
sender_client_id: Arc<ClientId>,
/// Identity claims present in the sender credential.
identity: WireIdentity,
},
}

impl From<CcBufferedDecryptedMessage> for BufferedDecryptedMessage {
fn from(from: CcBufferedDecryptedMessage) -> Self {
match from {
CcBufferedDecryptedMessage::Text(text) => Self::Text {
plaintext: text.plaintext,
sender_client_id: Arc::new(text.sender_client_id.into()),
identity: text.identity.into(),
CcBufferedDecryptedMessage::ApplicationMessage(message) => Self::ApplicationMessage {
plaintext: message.plaintext,
sender_client_id: Arc::new(message.sender_client_id.into()),
identity: message.identity.into(),
},
CcBufferedDecryptedMessage::Commit(commit) => Self::Commit {
is_active: commit.is_active,
Expand All @@ -111,6 +157,16 @@ impl From<CcBufferedDecryptedMessage> for BufferedDecryptedMessage {
delay: proposal.delay,
identity: proposal.identity.into(),
},
CcBufferedDecryptedMessage::TransientTargeted(message) => Self::TransientTargeted {
plaintext: message.plaintext,
sender_client_id: Arc::new(message.sender_client_id.into()),
identity: message.identity.into(),
},
CcBufferedDecryptedMessage::PersistedTargeted(message) => Self::PersistedTargeted {
plaintext: message.plaintext,
sender_client_id: Arc::new(message.sender_client_id.into()),
identity: message.identity.into(),
},
}
}
}
4 changes: 2 additions & 2 deletions crypto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ pub use crate::{
cipher_suite::CipherSuite,
conversation::{
BufferedCommit, BufferedDecryptedMessage, Commit, CommitBundle, ConversationConfiguration, ConversationId,
CustomConfiguration, DecryptedMessage, GroupInfoBundle, GroupInfoEncryptionType, GroupInfoPayload,
Proposal, RatchetTreeType, Text, WirePolicy,
CustomConfiguration, DecryptedBytes, DecryptedMessage, GroupInfoBundle, GroupInfoEncryptionType,
GroupInfoPayload, Proposal, RatchetTreeType, WirePolicy,
},
credential::{
Credential, CredentialRef, CredentialType, FindFilters as CredentialFindFilters, x509::CertificateBundle,
Expand Down
Loading
Loading