mtp/test/e2ee.mjs
Alex Emmet e939714557
Some checks failed
CI / checks (push) Failing after 2m54s
[Fix] Version Bump, Checks, tests CQ
2026-08-13 21:18:55 +02:00

2789 lines
88 KiB
JavaScript

import { initSync } from "../dist/raw/index.js";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import assert from "node:assert/strict";
import { describe, it } from "node:test";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const wasmPath = path.resolve(__dirname, "../wasm/pkg/mtp_wasm_bg.wasm");
const wasmBytes = fs.readFileSync(wasmPath);
const wasmModule = new WebAssembly.Module(wasmBytes);
initSync({ module: wasmModule });
const relayCreatedAtMillis = 1720000000123n;
const sdk = await import("../dist/sdk/index.js");
const { MTPRatchet } = await import("../dist/sdk/ratchet.js");
const {
serializeEncryptedMessage,
parseEncryptedMessage,
encryptPayload,
decryptPayload,
FLAG_INIT,
MTP_E2EE_VERSION,
} = await import("../dist/sdk/encrypted-message.js");
const {
MTPSessionManager,
InMemorySessionStorage,
deriveSessionKeys,
derivePeerSessionId,
buildSessionTranscript,
} = await import("../dist/sdk/session.js");
const bindings = sdk.raw;
class MemoryEndpoint {
constructor(pipeId) {
this.pipeId = pipeId;
this.queue = [];
this.waiters = [];
this.peer = null;
this.closed = false;
}
write(data) {
if (this.closed || !this.peer || this.peer.closed) {
return Promise.reject(new Error("memory pipe is closed"));
}
const chunk = data.slice();
const waiter = this.peer.waiters.shift();
if (waiter) waiter(chunk);
else this.peer.queue.push(chunk);
return Promise.resolve();
}
read() {
if (this.queue.length > 0) return Promise.resolve(this.queue.shift());
if (this.closed) return Promise.resolve(null);
return new Promise((resolve) => this.waiters.push(resolve));
}
close() {
this.closed = true;
for (const resolve of this.waiters.splice(0)) resolve(null);
if (this.peer) {
this.peer.closed = true;
for (const resolve of this.peer.waiters.splice(0)) resolve(null);
}
return Promise.resolve();
}
abort() {
this.closed = true;
for (const resolve of this.waiters.splice(0)) resolve(null);
if (this.peer) {
this.peer.closed = true;
for (const resolve of this.peer.waiters.splice(0)) resolve(null);
}
}
}
function memoryDuplexPair(pipeId = 77) {
const left = new MemoryEndpoint(pipeId);
const right = new MemoryEndpoint(pipeId);
left.peer = right;
right.peer = left;
return [left, right];
}
function publicBundle(keyring) {
const keys = sdk.crypto.keyringToKeys(keyring);
return concat(
new Uint8Array([keys.kemPublicKey.length >> 8, keys.kemPublicKey.length & 0xff]),
keys.kemPublicKey,
new Uint8Array([keys.sigPqPublicKey.length >> 8, keys.sigPqPublicKey.length & 0xff]),
keys.sigPqPublicKey,
new Uint8Array([keys.sigClPublicKey.length >> 8, keys.sigClPublicKey.length & 0xff]),
keys.sigClPublicKey,
);
}
function ed25519OnlyEncryptionKeyring(keyring) {
const keys = sdk.crypto.keyringToKeys(keyring);
const fields = [
keys.kemPublicKey,
keys.kemSecretKey,
new Uint8Array(0),
new Uint8Array(0),
keys.sigClPublicKey,
keys.sigClSecretKey,
];
return concat(
...fields.flatMap((field) => [
new Uint8Array([field.length >> 8, field.length & 0xff]),
field,
]),
);
}
function concat(...arrays) {
const totalLen = arrays.reduce((sum, a) => sum + a.length, 0);
const result = new Uint8Array(totalLen);
let offset = 0;
for (const a of arrays) {
result.set(a, offset);
offset += a.length;
}
return result;
}
function setupSessions(sharedSecret, aliceId = 1n, bobId = 2n) {
const aliceStorage = new InMemorySessionStorage();
const aliceManager = new MTPSessionManager(aliceStorage);
const bobStorage = new InMemorySessionStorage();
const bobManager = new MTPSessionManager(bobStorage);
const sessionId = derivePeerSessionId(aliceId, bobId);
return {
aliceManager,
aliceStorage,
bobManager,
bobStorage,
async initSessions() {
const transcript = new Uint8Array([0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e]);
const { initiatorSend, initiatorRecv } = await deriveSessionKeys(
sharedSecret,
transcript,
);
const aliceSession = await aliceManager.createSession({
sessionId,
localId: aliceId,
remoteId: bobId,
remotePublicKey: new Uint8Array(32),
sharedSecret,
role: "initiator",
transcript,
});
const bobSession = await bobManager.createSession({
sessionId,
localId: bobId,
remoteId: aliceId,
remotePublicKey: new Uint8Array(32),
sharedSecret,
role: "receiver",
transcript,
});
return { aliceSession, bobSession, initiatorSend, initiatorRecv };
},
};
}
await describe("E2EE Session Derivation", async () => {
await it("Both sides derive same shared secret", async () => {
const sharedSecret = sdk.crypto.sha256(new Uint8Array([1, 2, 3, 4, 5]));
const transcript = new Uint8Array(0);
const aliceKeys = await deriveSessionKeys(sharedSecret, transcript);
const bobKeys = await deriveSessionKeys(sharedSecret, transcript);
// Deterministic: same inputs → same outputs
assert.deepEqual(aliceKeys.initiatorSend, bobKeys.initiatorSend);
assert.deepEqual(aliceKeys.initiatorRecv, bobKeys.initiatorRecv);
// Init and recv keys are different
assert.notDeepEqual(aliceKeys.initiatorSend, aliceKeys.initiatorRecv);
});
await it("Session manager assigns correct chain keys per role", async () => {
const ss = sdk.crypto.sha256(new Uint8Array([1]));
const { initSessions } = setupSessions(ss);
const { aliceSession, bobSession, initiatorSend, initiatorRecv } =
await initSessions();
// Alice (initiator): send = initiatorSend, recv = initiatorRecv
assert.deepEqual(aliceSession.sendChainKey, initiatorSend);
assert.deepEqual(aliceSession.recvChainKey, initiatorRecv);
// Bob (receiver): send = initiatorRecv, recv = initiatorSend
assert.deepEqual(bobSession.sendChainKey, initiatorRecv);
assert.deepEqual(bobSession.recvChainKey, initiatorSend);
// Alice's send chain = Bob's recv chain
assert.deepEqual(aliceSession.sendChainKey, bobSession.recvChainKey);
// Alice's recv chain = Bob's send chain
assert.deepEqual(aliceSession.recvChainKey, bobSession.sendChainKey);
});
await it("Different transcripts produce different keys", async () => {
const ss = sdk.crypto.sha256(new Uint8Array([99]));
const aliceKeys1 = await deriveSessionKeys(ss, new Uint8Array(0));
const aliceKeys2 = await deriveSessionKeys(
ss,
sdk.crypto.sha256(new Uint8Array([42])),
);
assert.notDeepEqual(aliceKeys1.initiatorSend, aliceKeys2.initiatorSend);
});
});
await describe("MTP Session Transcript", async () => {
const base = {
sessionId: "session-a",
initiatorId: 1n,
recipientId: 2n,
recipientPublicKey: new Uint8Array([1, 2, 3]),
kemCiphertext: new Uint8Array([4, 5, 6]),
};
await it("binds generic session identity and MTP key-establishment values", () => {
const transcript = buildSessionTranscript(base);
assert.notDeepEqual(
transcript,
buildSessionTranscript({ ...base, sessionId: "session-b" }),
);
assert.notDeepEqual(
transcript,
buildSessionTranscript({ ...base, initiatorId: 3n }),
);
assert.notDeepEqual(
transcript,
buildSessionTranscript({
...base,
recipientPublicKey: new Uint8Array([1, 2, 4]),
}),
);
});
await it("hashes opaque application context into the transcript", () => {
const withoutContext = buildSessionTranscript(base);
const withContext = buildSessionTranscript({
...base,
applicationContext: new Uint8Array([0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74]),
});
assert.notDeepEqual(withoutContext, withContext);
});
});
await describe("E2EE Ratchet", async () => {
await it("Repeated sends produce different message keys", async () => {
const chainKey = sdk.crypto.sha256(new Uint8Array([42]));
const step1 = await MTPRatchet.step(chainKey);
const step2 = await MTPRatchet.step(step1.chainKey);
const step3 = await MTPRatchet.step(step2.chainKey);
assert.notDeepEqual(step1.key, step2.key);
assert.notDeepEqual(step2.key, step3.key);
assert.notDeepEqual(step1.key, step3.key);
assert.notDeepEqual(chainKey, step1.chainKey);
});
await it("Receiver can decrypt messages sent by sender in order", async () => {
const chainKey = sdk.crypto.sha256(new Uint8Array([7]));
const send1 = await MTPRatchet.step(chainKey);
const send2 = await MTPRatchet.step(send1.chainKey);
const send3 = await MTPRatchet.step(send2.chainKey);
const recv1 = await MTPRatchet.step(chainKey);
const recv2 = await MTPRatchet.step(recv1.chainKey);
const recv3 = await MTPRatchet.step(recv2.chainKey);
assert.deepEqual(send1.key, recv1.key);
assert.deepEqual(send2.key, recv2.key);
assert.deepEqual(send3.key, recv3.key);
});
});
await describe("E2EE Serialization", async () => {
await it("Roundtrips a basic message", () => {
const msg = {
header: {
version: 1,
flags: 0,
senderId: 0x1234567890abcdefn,
recipientId: 0xfedcba0987654321n,
messageNumber: 42,
},
aeadPayload: new Uint8Array([1, 2, 3, 4, 5]),
};
const bytes = serializeEncryptedMessage(msg);
const parsed = parseEncryptedMessage(bytes);
assert.equal(parsed.header.version, 1);
assert.equal(parsed.header.flags, 0);
assert.equal(parsed.header.senderId, msg.header.senderId);
assert.equal(parsed.header.recipientId, msg.header.recipientId);
assert.equal(parsed.header.messageNumber, 42);
assert.equal(parsed.header.kemCiphertext, undefined);
assert.deepEqual(parsed.aeadPayload, msg.aeadPayload);
});
await it("Roundtrips an init message with KEM ciphertext", () => {
const msg = {
header: {
version: 1,
flags: FLAG_INIT,
senderId: 1n,
recipientId: 2n,
messageNumber: 0,
kemCiphertext: new Uint8Array([0xde, 0xad, 0xbe, 0xef]),
},
aeadPayload: new Uint8Array([10, 20, 30]),
};
const bytes = serializeEncryptedMessage(msg);
const parsed = parseEncryptedMessage(bytes);
assert.equal(parsed.header.flags & FLAG_INIT, FLAG_INIT);
assert.deepEqual(parsed.header.kemCiphertext, msg.header.kemCiphertext);
});
await it("Roundtrip: serialize(parse(x)) === x", () => {
const msg = {
header: {
version: 1,
flags: 0,
senderId: 0xaaaabbbbccccddddn,
recipientId: 0xffff000011112222n,
messageNumber: 65535,
},
aeadPayload: new Uint8Array(100).fill(0x42),
};
const bytes = serializeEncryptedMessage(msg);
const parsed = parseEncryptedMessage(bytes);
const bytes2 = serializeEncryptedMessage(parsed);
assert.deepEqual(bytes, bytes2);
});
await it("Rejects malformed payloads", () => {
assert.throws(() => parseEncryptedMessage(new Uint8Array(0)));
assert.throws(() => parseEncryptedMessage(new Uint8Array([0x01])));
assert.throws(() =>
parseEncryptedMessage(
new Uint8Array([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
),
);
});
await it("Rejects unsupported version", () => {
const msg = {
header: {
version: 1,
flags: 0,
senderId: 0n,
recipientId: 0n,
messageNumber: 0,
},
aeadPayload: new Uint8Array([1]),
};
const bytes = serializeEncryptedMessage(msg);
bytes[0] = 99;
assert.throws(() => parseEncryptedMessage(bytes));
});
await it("Rejects trailing data", () => {
const msg = {
header: {
version: 1,
flags: 0,
senderId: 0n,
recipientId: 0n,
messageNumber: 0,
},
aeadPayload: new Uint8Array([1]),
};
const bytes = concat(
serializeEncryptedMessage(msg),
new Uint8Array([0xff]),
);
assert.throws(() => parseEncryptedMessage(bytes));
});
});
await describe("E2EE Encrypt/Decrypt", async () => {
await it("Alice encrypts and Bob decrypts successfully", async () => {
const keyring = sdk.crypto.generateKeyring();
const bobKeys = sdk.crypto.keyringToKeys(keyring);
// Alice encapsulates to Bob's KEM public key
const enc = sdk.crypto.encapsulate(bobKeys.kemPublicKey);
// Bob decapsulates the ciphertext
const bobSS = sdk.crypto.decapsulate(bobKeys.kemSecretKey, enc.ciphertext);
assert.deepEqual(bobSS, enc.shared_secret);
const ss = enc.shared_secret;
const { initSessions } = setupSessions(ss);
const { aliceSession, bobSession } = await initSessions();
// Alice encrypts a message to Bob
const plaintext = sdk.codec.encode(
"Ping",
{ Version: "hello from Alice" },
{ sender: 1n, receiver: 2n },
);
const { payload, session: aliceNewSession } = await encryptPayload({
plaintext,
session: aliceSession,
kemCiphertext: enc.ciphertext,
});
// Bob decrypts the message
const { plaintext: decrypted, session: bobNewSession } =
await decryptPayload({
payload,
session: bobSession,
});
const frame = sdk.codec.decode(decrypted);
assert.equal(frame.type, "Ping");
assert.equal(frame.data["Version"], "hello from Alice");
// Chain keys advanced correctly
assert.deepEqual(aliceNewSession.sendChainKey, bobNewSession.recvChainKey);
assert.equal(aliceNewSession.sendCount, 1);
assert.equal(bobNewSession.recvCount, 1);
assert.notDeepEqual(
aliceNewSession.sendChainKey,
aliceSession.sendChainKey,
);
});
await it("Encrypt-decrypt multiple messages with chain advance", async () => {
const ss = sdk.crypto.sha256(new Uint8Array([1, 2, 3]));
const { initSessions } = setupSessions(ss);
let { aliceSession, bobSession } = await initSessions();
// Message 1
const { payload: p1, session: aliceAfter1 } = await encryptPayload({
plaintext: sdk.codec.encode("Ping", { Version: "msg1" }),
session: aliceSession,
});
const { plaintext: d1, session: bobAfter1 } = await decryptPayload({
payload: p1,
session: bobSession,
});
assert.equal(sdk.codec.decode(d1).data["Version"], "msg1");
assert.equal(bobAfter1.recvCount, 1);
// Message 2
const { payload: p2, session: aliceAfter2 } = await encryptPayload({
plaintext: sdk.codec.encode("Ping", { Version: "msg2" }),
session: aliceAfter1,
});
const { plaintext: d2, session: bobAfter2 } = await decryptPayload({
payload: p2,
session: bobAfter1,
});
assert.equal(sdk.codec.decode(d2).data["Version"], "msg2");
assert.equal(bobAfter2.recvCount, 2);
// Chain keys match after two messages
assert.deepEqual(aliceAfter2.sendChainKey, bobAfter2.recvChainKey);
assert.equal(aliceAfter2.sendCount, 2);
});
await it("decrypts messages delivered out of order exactly once", async () => {
const ss = sdk.crypto.sha256(new Uint8Array([4, 5, 6]));
const { initSessions } = setupSessions(ss);
let { aliceSession, bobSession } = await initSessions();
const sent = [];
for (const label of ["first", "second", "third"]) {
const encrypted = await encryptPayload({
plaintext: sdk.codec.encode("Ping", { Version: label }),
session: aliceSession,
});
sent.push(encrypted.payload);
aliceSession = encrypted.session;
}
const third = await decryptPayload({ payload: sent[2], session: bobSession });
bobSession = third.session;
assert.equal(sdk.codec.decode(third.plaintext).data["Version"], "third");
assert.equal(bobSession.recvCount, 3);
assert.deepEqual(
bobSession.skippedMessageKeys.map(({ messageNumber }) => messageNumber),
[0, 1],
);
const first = await decryptPayload({ payload: sent[0], session: bobSession });
bobSession = first.session;
assert.equal(sdk.codec.decode(first.plaintext).data["Version"], "first");
assert.deepEqual(
bobSession.skippedMessageKeys.map(({ messageNumber }) => messageNumber),
[1],
);
const second = await decryptPayload({ payload: sent[1], session: bobSession });
bobSession = second.session;
assert.equal(sdk.codec.decode(second.plaintext).data["Version"], "second");
assert.equal(bobSession.skippedMessageKeys.length, 0);
await assert.rejects(
decryptPayload({ payload: sent[0], session: bobSession }),
/replay/,
);
});
});
await describe("E2EE Public Key Bundle", async () => {
await it("parses public key bundles from GetUserData.PublicKey", () => {
const keyring = sdk.crypto.generateKeyring();
const keys = sdk.crypto.keyringToKeys(keyring);
const bundle = concat(
new Uint8Array([keys.kemPublicKey.length >> 8, keys.kemPublicKey.length & 0xff]),
keys.kemPublicKey,
new Uint8Array([keys.sigPqPublicKey.length >> 8, keys.sigPqPublicKey.length & 0xff]),
keys.sigPqPublicKey,
new Uint8Array([keys.sigClPublicKey.length >> 8, keys.sigClPublicKey.length & 0xff]),
keys.sigClPublicKey,
);
const parsed = sdk.crypto.publicKeyBundleToKeys(bundle);
assert.deepEqual(parsed.kemPublicKey, keys.kemPublicKey);
assert.deepEqual(parsed.sigPqPublicKey, keys.sigPqPublicKey);
assert.deepEqual(parsed.sigClPublicKey, keys.sigClPublicKey);
});
});
await describe("Protected-value policy", async () => {
await it("enforces the receiver-selected signature suite", () => {
const keyring = sdk.crypto.generateKeyring();
const bundle = publicBundle(keyring);
const value = bindings.encode_data_value("policy-check");
const edSigned = bindings.sign_data_value_with_keyring(
value,
7n,
0x40,
keyring,
bindings.mtp_protection_signature_suite_ed25519(),
);
assert.doesNotThrow(() =>
bindings.verify_data_value_with_policy(
edSigned,
bundle,
7n,
0x40,
bindings.mtp_protection_signature_suite_ed25519(),
),
);
assert.throws(() =>
bindings.verify_data_value_with_policy(
edSigned,
bundle,
7n,
0x40,
bindings.mtp_protection_signature_suite_dual(),
),
);
const dualSigned = bindings.sign_data_value_with_keyring(
value,
7n,
0x40,
keyring,
bindings.mtp_protection_signature_suite_dual(),
);
assert.doesNotThrow(() =>
bindings.verify_data_value_with_policy(
dualSigned,
bundle,
7n,
0x40,
bindings.mtp_protection_signature_suite_dual(),
),
);
});
});
await describe("Relay recipient separation", async () => {
await it("lets a metadata recipient open metadata but not content", async () => {
const senderKeyring = sdk.crypto.generateKeyring();
const metadataKeyring = sdk.crypto.generateKeyring();
const finalKeyring = sdk.crypto.generateKeyring();
const metadataRecipients = [
publicBundle(metadataKeyring),
publicBundle(finalKeyring),
];
const contentRecipients = [publicBundle(finalKeyring)];
const frameBytes = bindings.build_encrypted_relay_frame_with_keyring(
"ProtectedMessage",
{ ExampleType: "hello" },
11n,
42n,
7n,
"message-1",
123n,
bindings.encode_data_value({ ExampleType: "metadata" }),
senderKeyring,
bindings.mtp_protection_signature_suite_dual(),
metadataRecipients,
contentRecipients,
);
const frame = bindings.parse_frame(frameBytes);
assert.equal(frame.type, "Relay");
assert.equal(frame.sender, undefined);
assert.equal(frame.receiver, 7n);
const metadataBytes = frame.data.encoded;
const openedMetadata = bindings.decrypt_data_value(
metadataBytes,
metadataKeyring,
bindings.mtp_relay_metadata_encryption_purpose(),
);
const metadata = bindings.parse_data_value(openedMetadata);
assert.equal(metadata.kind, "signed");
assert.equal(BigInt(metadata.value.RelayVersion), 1n);
assert.equal(BigInt(metadata.value.FinalRecipientId), 42n);
assert.equal(metadata.value.Metadata.ExampleType, "metadata");
const metadataClient = await sdk.MTPClient.create({
url: "https://example.invalid",
credentials: { clientId: 7n, keyring: metadataKeyring },
pings: false,
});
const verifiedMetadata = await metadataClient.openRelayMetadata(frame, {
expectedSignerId: 11n,
resolveSignerPublicKeys: () => [publicBundle(senderKeyring)],
signaturePolicy: "dual",
});
assert.equal(verifiedMetadata.relayVersion, 1);
assert.equal(verifiedMetadata.finalRecipientId, 42n);
assert.deepEqual(verifiedMetadata.metadata, {
ExampleType: "metadata",
});
await assert.rejects(
() => metadataClient.openRelayContent(verifiedMetadata),
/different final recipient/,
);
const encryptedContent = metadata.value.Content.encoded;
assert.throws(() =>
bindings.decrypt_data_value(
encryptedContent,
metadataKeyring,
bindings.mtp_relay_content_encryption_purpose(),
),
);
const forwardedBytes = bindings.forward_encrypted_relay_frame(frameBytes, 42n);
const forwarded = bindings.parse_frame(forwardedBytes);
assert.deepEqual(forwarded.data.encoded, frame.data.encoded);
const finalMetadata = bindings.parse_data_value(
bindings.decrypt_data_value(
forwarded.data.encoded,
finalKeyring,
bindings.mtp_relay_metadata_encryption_purpose(),
),
);
assert.equal(BigInt(finalMetadata.value.RelayVersion), 1n);
const content = bindings.parse_data_value(
bindings.decrypt_data_value(
finalMetadata.value.Content.encoded,
finalKeyring,
bindings.mtp_relay_content_encryption_purpose(),
),
);
assert.equal(content.kind, "signed");
assert.equal(content.value.MessageType, "ProtectedMessage");
assert.equal(content.value.Content.ExampleType, "hello");
});
await it("opens forwarded relay data with explicit identities", async () => {
const senderKeyring = sdk.crypto.generateKeyring();
const metadataKeyring = sdk.crypto.generateKeyring();
const finalKeyring = sdk.crypto.generateKeyring();
const frameBytes = bindings.build_encrypted_relay_frame_with_keyring(
"ProtectedMessage",
{ ExampleType: "explicit-identity" },
11n,
42n,
7n,
"message-explicit-identity",
456n,
bindings.encode_data_value({ ExampleType: "forwarded" }),
senderKeyring,
bindings.mtp_protection_signature_suite_dual(),
[publicBundle(metadataKeyring), publicBundle(finalKeyring)],
[publicBundle(finalKeyring)],
);
const forwarded = bindings.parse_frame(
bindings.forward_encrypted_relay_frame(frameBytes, 99n),
);
const verification = {
expectedSignerId: 11n,
resolveSignerPublicKeys: () => [publicBundle(senderKeyring)],
signaturePolicy: "dual",
};
const metadataClient = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const metadata = await metadataClient.openRelayMetadata(forwarded, {
...verification,
recipient: { keyring: metadataKeyring },
});
assert.equal(metadata.finalRecipientId, 42n);
assert.deepEqual(metadata.metadata, { ExampleType: "forwarded" });
const wrongKeyring = sdk.crypto.generateKeyring();
await assert.rejects(
() =>
metadataClient.openRelayMetadata(forwarded, {
...verification,
recipient: { keyring: wrongKeyring },
}),
);
const registeredClient = await sdk.MTPClient.create({
url: "https://example.invalid",
credentials: { clientId: 7n, keyring: metadataKeyring },
pings: false,
});
const finalIdentity = { id: 42n, keyring: finalKeyring };
const finalMetadata = await registeredClient.openRelayMetadata(
forwarded,
{
...verification,
recipient: finalIdentity,
},
);
const content = await registeredClient.openRelayContent(finalMetadata, {
...verification,
recipient: finalIdentity,
});
assert.equal(content.signerId, 11n);
assert.equal(content.finalRecipientId, 42n);
assert.equal(content.data.ExampleType, "explicit-identity");
await assert.rejects(
() =>
registeredClient.openRelayContent(finalMetadata, {
...verification,
recipient: finalIdentity,
expectedFinalRecipientId: 99n,
}),
/different final recipient/,
);
});
});
await describe("Relay recipient key history", async () => {
function verificationOptions(recipient) {
return {
recipient,
expectedSignerId: 11n,
resolveSignerPublicKeys: () => [publicBundle(senderKeyring)],
signaturePolicy: "dual",
};
}
let senderKeyring;
await it("uses the current keyring and accepts an empty history", async () => {
senderKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const frame = bindings.parse_frame(
bindings.build_encrypted_relay_frame_with_keyring(
"ProtectedMessage",
{ ExampleType: "current-key" },
11n,
42n,
42n,
"message-current-key",
1n,
null,
senderKeyring,
bindings.mtp_protection_signature_suite_dual(),
[publicBundle(recipientKeyring)],
[publicBundle(recipientKeyring)],
),
);
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const options = verificationOptions({
id: 42n,
keyring: recipientKeyring,
keyringHistory: [],
});
const metadata = await client.openRelayMetadata(frame, options);
const content = await client.openRelayContent(metadata, options);
assert.equal(content.data.ExampleType, "current-key");
});
await it(
"tries rotated metadata and content recipients newest to oldest without duplicates",
async () => {
senderKeyring = sdk.crypto.generateKeyring();
const currentKeyring = sdk.crypto.generateKeyring();
const newerPreviousKeyring = sdk.crypto.generateKeyring();
const previousKeyring = sdk.crypto.generateKeyring();
const history = [
newerPreviousKeyring,
previousKeyring,
previousKeyring,
];
const historySnapshot = history.map((keyring) => keyring.slice());
const frame = bindings.parse_frame(
bindings.build_encrypted_relay_frame_with_keyring(
"ProtectedMessage",
{ ExampleType: "rotated-key" },
11n,
42n,
7n,
"message-rotated-key",
2n,
bindings.encode_data_value({ ExampleType: "rotated-metadata" }),
senderKeyring,
bindings.mtp_protection_signature_suite_dual(),
[publicBundle(previousKeyring)],
[publicBundle(previousKeyring)],
),
);
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const options = verificationOptions({
id: 42n,
keyring: currentKeyring,
keyringHistory: history,
});
const metadata = await client.openRelayMetadata(frame, options);
const content = await client.openRelayContent(metadata, options);
assert.equal(content.data.ExampleType, "rotated-key");
assert.equal(metadata.matchedSignerKeyIndex, 0);
assert.equal(metadata.signerPublicKeys.length, 1);
assert.deepEqual(
metadata.matchedSignerPublicKey,
publicBundle(senderKeyring),
);
assert.deepEqual(history, historySnapshot);
},
);
await it("does not expose individual key failures when all keyrings fail", async () => {
senderKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const wrongCurrentKeyring = sdk.crypto.generateKeyring();
const wrongPreviousKeyring = sdk.crypto.generateKeyring();
const frame = bindings.parse_frame(
bindings.build_encrypted_relay_frame_with_keyring(
"ProtectedMessage",
{ ExampleType: "wrong-key" },
11n,
42n,
42n,
"message-wrong-key",
3n,
null,
senderKeyring,
bindings.mtp_protection_signature_suite_dual(),
[publicBundle(recipientKeyring)],
[publicBundle(recipientKeyring)],
),
);
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
await assert.rejects(
() =>
client.openRelayMetadata(frame, {
...verificationOptions({
keyring: wrongCurrentKeyring,
keyringHistory: [wrongPreviousKeyring],
}),
}),
(error) =>
error.message ===
"Unable to decrypt protected value with supplied recipient keyrings",
);
});
});
await describe("Protected send APIs", async () => {
function captureSend(client) {
const frames = [];
client.raw.client.send = async (frame) => {
frames.push(frame.slice());
};
return frames;
}
await it("round trips with interoperable Ed25519 defaults", async () => {
const signerKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const frames = captureSend(client);
const signerPublicKey = publicBundle(signerKeyring);
const recipientPublicKey = publicBundle(recipientKeyring);
await client.sendProtected(
"ProtectedMessage",
{ ExampleType: "default-direct" },
{
receiverId: 22n,
identity: { signerId: 11n, keyring: signerKeyring },
recipients: [recipientPublicKey],
signaturePurpose: 0x40,
encryptionPurpose: 0x41,
},
);
const directFrame = sdk.codec.decode(frames.shift());
const direct = await client.openProtected(directFrame, {
recipient: { id: 22n, keyring: recipientKeyring },
expectedSignerId: 11n,
expectedReceiverId: 22n,
resolveSignerPublicKeys: () => [signerPublicKey],
signaturePurpose: 0x40,
encryptionPurpose: 0x41,
});
assert.equal(direct.data.ExampleType, "default-direct");
await client.sendSealedRelay(
"ProtectedMessage",
{ ExampleType: "default-relay" },
{
finalRecipientId: 22n,
nextHopId: 7n,
identity: { signerId: 11n, keyring: signerKeyring },
metadataRecipients: [recipientPublicKey],
contentRecipients: [recipientPublicKey],
metadata: { ExampleType: "default-metadata" },
},
);
const relayFrame = sdk.codec.decode(frames.shift());
const relayOptions = {
recipient: { id: 22n, keyring: recipientKeyring },
expectedSignerId: 11n,
resolveSignerPublicKeys: () => [signerPublicKey],
};
const metadata = await client.openRelayMetadata(relayFrame, relayOptions);
const content = await client.openRelayContent(metadata, relayOptions);
assert.equal(metadata.metadata.ExampleType, "default-metadata");
assert.equal(content.data.ExampleType, "default-relay");
metadata.dispose();
});
await it("builds direct protected frames without a Relay hop", async () => {
const signerKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const frames = captureSend(client);
await client.sendProtected(
"ProtectedMessage",
{ ExampleType: "direct" },
{
receiverId: 22n,
id: 123,
identity: { signerId: 11n, keyring: signerKeyring },
recipients: [publicBundle(recipientKeyring)],
signaturePurpose: 0x40,
encryptionPurpose: 0x41,
signatureSuite: "dual",
exposeSender: true,
},
);
assert.equal(frames.length, 1);
const frame = bindings.parse_frame(frames[0]);
assert.equal(frame.type, "ProtectedMessage");
assert.equal(frame.id, 123);
assert.equal(frame.sender, 11n);
assert.equal(frame.receiver, 22n);
assert.equal(frame.data.kind, "encrypted");
const signedBytes = bindings.decrypt_data_value(
frame.data.encoded,
recipientKeyring,
0x41,
);
const signed = bindings.parse_data_value(signedBytes);
assert.equal(signed.kind, "signed");
assert.equal(signed.signerId, 11n);
assert.equal(signed.value.Content.ExampleType, "direct");
assert.equal(signed.value.MessageType, "ProtectedMessage");
assert.equal(BigInt(signed.value.FinalRecipientId), 22n);
assert.equal(typeof signed.value.MessageId, "string");
assert.equal(BigInt(signed.value.CreatedAt) > 0n, true);
bindings.verify_data_value_with_policy(
signedBytes,
publicBundle(signerKeyring),
11n,
0x40,
bindings.mtp_protection_signature_suite_dual(),
);
await assert.rejects(
() =>
client.sendProtected("Ping", { ExampleType: "reserved" }, {
receiverId: 22n,
identity: { signerId: 11n, keyring: signerKeyring },
recipients: [publicBundle(recipientKeyring)],
signaturePurpose: 0x40,
encryptionPurpose: 0x41,
}),
/control communication type/,
);
});
await it("builds sealed relay frames from exact generic recipient sets", async () => {
const signerKeyring = sdk.crypto.generateKeyring();
const metadataKeyring = sdk.crypto.generateKeyring();
const contentKeyring = sdk.crypto.generateKeyring();
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const frames = captureSend(client);
await client.sendSealedRelay(
"ProtectedMessage",
{ ExampleType: "content" },
{
finalRecipientId: 42n,
nextHopId: 7n,
identity: { signerId: 11n, keyring: signerKeyring },
metadataRecipients: [publicBundle(metadataKeyring)],
contentRecipients: [publicBundle(contentKeyring)],
metadata: { ExampleType: "metadata" },
signatureSuite: "dual",
},
);
assert.equal(frames.length, 1);
const frame = bindings.parse_frame(frames[0]);
assert.equal(frame.type, "Relay");
assert.equal(frame.sender, undefined);
assert.equal(frame.receiver, 7n);
const metadataBytes = bindings.decrypt_data_value(
frame.data.encoded,
metadataKeyring,
bindings.mtp_relay_metadata_encryption_purpose(),
);
const metadata = bindings.parse_data_value(metadataBytes);
assert.equal(metadata.kind, "signed");
assert.equal(metadata.signerId, 11n);
assert.equal(BigInt(metadata.value.FinalRecipientId), 42n);
assert.equal(metadata.value.Metadata.ExampleType, "metadata");
assert.equal(metadata.value.Content.kind, "encrypted");
assert.throws(() =>
bindings.decrypt_data_value(
metadata.value.Content.encoded,
metadataKeyring,
bindings.mtp_relay_content_encryption_purpose(),
),
);
const contentBytes = bindings.decrypt_data_value(
metadata.value.Content.encoded,
contentKeyring,
bindings.mtp_relay_content_encryption_purpose(),
);
const content = bindings.parse_data_value(contentBytes);
assert.equal(content.kind, "signed");
assert.equal(content.value.Content.ExampleType, "content");
await assert.rejects(
() =>
client.sendSealedRelay("Relay", { ExampleType: "reserved" }, {
finalRecipientId: 42n,
nextHopId: 7n,
identity: { signerId: 11n, keyring: signerKeyring },
metadataRecipients: [publicBundle(metadataKeyring)],
contentRecipients: [publicBundle(contentKeyring)],
}),
/control communication type/,
);
});
await it("round trips generic relay content and metadata presence", async () => {
const signerKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const frames = captureSend(client);
const openOptions = {
recipient: { id: 42n, keyring: recipientKeyring },
expectedSignerId: 11n,
resolveSignerPublicKeys: () => [publicBundle(signerKeyring)],
signaturePolicy: "dual",
};
const sendAndOpen = async (data, metadata, includeMetadata) => {
const options = {
finalRecipientId: 42n,
nextHopId: 42n,
identity: { signerId: 11n, keyring: signerKeyring },
metadataRecipients: [publicBundle(recipientKeyring)],
contentRecipients: [publicBundle(recipientKeyring)],
signatureSuite: "dual",
...(includeMetadata ? { metadata } : {}),
};
await client.sendSealedRelay("ProtectedMessage", data, options);
const frame = sdk.codec.decode(frames.pop());
const verifiedMetadata = await client.openRelayMetadata(
frame,
openOptions,
);
const content = await client.openRelayContent(
verifiedMetadata,
openOptions,
);
return { verifiedMetadata, content };
};
const explicitNull = await sendAndOpen(
new Uint8Array([1, 2, 3]),
null,
true,
);
assert.equal(explicitNull.verifiedMetadata.metadata, null);
assert.deepEqual(explicitNull.content.data, new Uint8Array([1, 2, 3]));
const absent = await sendAndOpen(
["array", 7, false],
undefined,
false,
);
assert.equal(absent.verifiedMetadata.metadata, undefined);
assert.deepEqual(absent.content.data, ["array", 7, false]);
const scalar = await sendAndOpen("scalar content", "scalar metadata", true);
assert.equal(scalar.verifiedMetadata.metadata, "scalar metadata");
assert.equal(scalar.content.data, "scalar content");
});
await it("opens an unauthenticated connection without discarding credentials", async () => {
const keyring = sdk.crypto.generateKeyring();
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
hostPublicKey: new Uint8Array([1]),
credentials: { clientId: 99n, keyring },
pings: false,
});
let connectCalls = 0;
let authCalls = 0;
client.raw.client.connect = async () => {
connectCalls += 1;
};
client.raw.client.auth_connect = async () => {
authCalls += 1;
return 99n;
};
await client.connectUnauthenticated();
assert.equal(connectCalls, 1);
assert.equal(authCalls, 0);
assert.equal(client.credentials.clientId, 99n);
});
await it("opens an authenticated connection when transport credentials are available", async () => {
const transportKeyring = sdk.crypto.generateKeyring();
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
hostPublicKey: new Uint8Array([1]),
credentials: { clientId: 99n, keyring: transportKeyring },
pings: false,
});
let connectCalls = 0;
let authCalls = 0;
client.raw.client.connect = async () => {
connectCalls += 1;
};
client.raw.client.auth_connect = async (
_config,
hostPublicKey,
keyring,
clientId,
) => {
authCalls += 1;
assert.deepEqual(hostPublicKey, new Uint8Array([1]));
assert.deepEqual(keyring, transportKeyring);
assert.equal(clientId, 99n);
return 99n;
};
await client.connect();
assert.equal(connectCalls, 0);
assert.equal(authCalls, 1);
assert.equal(client.credentials.clientId, 99n);
});
await it("keeps a protected signer independent from transport authentication", async () => {
const transportKeyring = sdk.crypto.generateKeyring();
const signerKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
hostPublicKey: new Uint8Array([1]),
credentials: { clientId: 99n, keyring: transportKeyring },
pings: false,
});
client.raw.client.auth_connect = async () => 99n;
await client.connect();
const frames = captureSend(client);
await client.sendProtected(
"ProtectedMessage",
{ ExampleType: "transport-independent" },
{
receiverId: 22n,
identity: { signerId: 11n, keyring: signerKeyring },
recipients: [publicBundle(recipientKeyring)],
signaturePurpose: 0x40,
encryptionPurpose: 0x41,
signatureSuite: "dual",
},
);
const frame = bindings.parse_frame(frames[0]);
assert.equal(client.credentials.clientId, 99n);
assert.equal(frame.sender, undefined);
const signedBytes = bindings.decrypt_data_value(
frame.data.encoded,
recipientKeyring,
0x41,
);
const signed = bindings.parse_data_value(signedBytes);
assert.equal(signed.kind, "signed");
assert.equal(signed.signerId, 11n);
assert.equal(signed.value.Content.ExampleType, "transport-independent");
});
});
await describe("Protected receive APIs", async () => {
async function buildDirectFrame({
signerKeyring,
recipientKeyring,
data = { ExampleType: "direct-receive" },
exposeSender = true,
receiverId = 22n,
signaturePurpose = 0x40,
encryptionPurpose = 0x41,
signatureSuite = "dual",
}) {
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const frames = [];
client.raw.client.send = async (frame) => frames.push(frame.slice());
await client.sendProtected(
"ProtectedMessage",
data,
{
receiverId,
identity: { signerId: 11n, keyring: signerKeyring },
recipients: [publicBundle(recipientKeyring)],
signaturePurpose,
encryptionPurpose,
signatureSuite,
exposeSender,
},
);
return frames[0];
}
function openOptions(signerKeyring, recipientKeyring, overrides = {}) {
return {
recipient: { id: 22n, keyring: recipientKeyring },
expectedSignerId: 11n,
expectedReceiverId: 22n,
resolveSignerPublicKeys: (signerId) => {
assert.equal(signerId, 11n);
return [publicBundle(signerKeyring)];
},
signaturePolicy: "dual",
signaturePurpose: 0x40,
encryptionPurpose: 0x41,
...overrides,
};
}
await it("opens exposed and hidden outer senders", async () => {
const signerKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const exposed = await client.openProtected(
await buildDirectFrame({ signerKeyring, recipientKeyring }),
openOptions(signerKeyring, recipientKeyring),
);
assert.equal(exposed.type, "ProtectedMessage");
assert.equal(exposed.protectedVersion, 1);
assert.equal(exposed.signerId, 11n);
assert.equal(exposed.finalRecipientId, 22n);
assert.equal(exposed.receiver, 22n);
assert.equal(exposed.outerSender, 11n);
assert.equal(typeof exposed.messageId, "string");
assert.equal(typeof exposed.createdAt, "bigint");
assert.deepEqual(exposed.data, {
ExampleType: "direct-receive",
});
const hidden = await client.openProtected(
await buildDirectFrame({
signerKeyring,
recipientKeyring,
exposeSender: false,
}),
openOptions(signerKeyring, recipientKeyring),
);
assert.equal(hidden.outerSender, undefined);
assert.equal(hidden.signerId, 11n);
});
await it("authenticates direct routing fields and rejects direct replays", async () => {
const signerKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const frame = await buildDirectFrame({ signerKeyring, recipientKeyring });
const changedType = frame.slice();
changedType[4] = 0;
changedType[5] = 33;
await assert.rejects(
() =>
client.openProtected(
changedType,
openOptions(signerKeyring, recipientKeyring),
),
/protected message type does not match outer routing/,
);
const parsed = bindings.parse_frame(frame);
const changedSender = bindings.build_frame_with_payload(
"ProtectedMessage",
parsed.data.encoded,
{ receiver: 22n, sender: 12n },
);
await assert.rejects(
() =>
client.openProtected(
changedSender,
openOptions(signerKeyring, recipientKeyring),
),
/protected frame sender does not match authenticated signer/,
);
const changedReceiver = await buildDirectFrame({
signerKeyring,
recipientKeyring,
exposeSender: false,
});
// The frame has an ID and receiver but no sender, so the receiver's final
// byte is at offset 18 in the MTP wire header.
changedReceiver[18] ^= 1;
await assert.rejects(
() =>
client.openProtected(
changedReceiver,
openOptions(signerKeyring, recipientKeyring, {
recipient: { keyring: recipientKeyring },
expectedReceiverId: undefined,
}),
),
/protected final recipient does not match outer routing receiver/,
);
const replayOptions = openOptions(signerKeyring, recipientKeyring);
await client.openProtected(frame, replayOptions);
await assert.rejects(
() => client.openProtected(frame, replayOptions),
(error) => error instanceof sdk.MTPReplayError,
);
});
await it("snapshots parsed direct frames before async signer resolution", async () => {
const signerKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const serialized = await buildDirectFrame({
signerKeyring,
recipientKeyring,
});
const parsed = bindings.parse_frame(serialized);
delete parsed.raw;
let releaseKeys;
const keysReady = new Promise((resolve) => {
releaseKeys = resolve;
});
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const opening = client.openProtected(parsed, {
recipient: { id: 22n, keyring: recipientKeyring },
resolveSignerPublicKeys: async () => {
await keysReady;
return [publicBundle(signerKeyring)];
},
signaturePolicy: "dual",
signaturePurpose: 0x40,
encryptionPurpose: 0x41,
});
parsed.receiver = 23n;
releaseKeys();
const message = await opening;
assert.equal(message.receiver, 22n);
});
await it("validates the expected signer and outer receiver", async () => {
const signerKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const frame = await buildDirectFrame({ signerKeyring, recipientKeyring });
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
let resolverCalls = 0;
await assert.rejects(
() =>
client.openProtected(
frame,
openOptions(signerKeyring, recipientKeyring, {
expectedSignerId: 99n,
resolveSignerPublicKeys: () => {
resolverCalls += 1;
return [publicBundle(signerKeyring)];
},
}),
),
/protected signer ID mismatch/,
);
assert.equal(
resolverCalls,
0,
"expected signer mismatch must precede signer-key resolution",
);
await assert.rejects(
() =>
client.openProtected(
frame,
openOptions(signerKeyring, recipientKeyring, {
expectedReceiverId: 23n,
}),
),
/protected frame receiver ID mismatch/,
);
});
await it("uses current and historical recipient keyrings", async () => {
const signerKeyring = sdk.crypto.generateKeyring();
const currentRecipientKeyring = sdk.crypto.generateKeyring();
const historicalRecipientKeyring = sdk.crypto.generateKeyring();
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const currentFrame = await buildDirectFrame({
signerKeyring,
recipientKeyring: currentRecipientKeyring,
});
const current = await client.openProtected(
currentFrame,
openOptions(signerKeyring, currentRecipientKeyring),
);
assert.equal(current.data.ExampleType, "direct-receive");
const historicalFrame = await buildDirectFrame({
signerKeyring,
recipientKeyring: historicalRecipientKeyring,
});
const historical = await client.openProtected(
historicalFrame,
openOptions(signerKeyring, currentRecipientKeyring, {
recipient: {
id: 22n,
keyring: currentRecipientKeyring,
keyringHistory: [historicalRecipientKeyring],
},
}),
);
assert.equal(historical.data.ExampleType, "direct-receive");
await assert.rejects(
() =>
client.openProtected(
historicalFrame,
openOptions(signerKeyring, sdk.crypto.generateKeyring()),
),
/Unable to decrypt protected value with supplied recipient keyrings/,
);
});
await it("round trips non-container application DataValues", async () => {
const signerKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const options = openOptions(signerKeyring, recipientKeyring);
const stringMessage = await client.openProtected(
await buildDirectFrame({
signerKeyring,
recipientKeyring,
data: "direct string",
}),
options,
);
assert.equal(stringMessage.data, "direct string");
const arrayMessage = await client.openProtected(
await buildDirectFrame({
signerKeyring,
recipientKeyring,
data: ["direct array", 7, false],
}),
options,
);
assert.deepEqual(arrayMessage.data, ["direct array", 7, false]);
const bytesMessage = await client.openProtected(
await buildDirectFrame({
signerKeyring,
recipientKeyring,
data: new Uint8Array([3, 1, 4]),
}),
options,
);
assert.deepEqual(bytesMessage.data, new Uint8Array([3, 1, 4]));
const largeIntegerMessage = await client.openProtected(
await buildDirectFrame({
signerKeyring,
recipientKeyring,
data: 9_007_199_254_740_992n,
}),
options,
);
assert.equal(largeIntegerMessage.data, 9_007_199_254_740_992n);
const safeIntegerMessage = await client.openProtected(
await buildDirectFrame({
signerKeyring,
recipientKeyring,
data: 9_007_199_254_740_991,
}),
options,
);
assert.equal(safeIntegerMessage.data, 9_007_199_254_740_991);
const largeSignedMessage = await client.openProtected(
await buildDirectFrame({
signerKeyring,
recipientKeyring,
data: -9_007_199_254_740_992n,
}),
options,
);
assert.equal(largeSignedMessage.data, -9_007_199_254_740_992n);
const largeUnsignedMessage = await client.openProtected(
await buildDirectFrame({
signerKeyring,
recipientKeyring,
data: 18_446_744_073_709_551_615n,
}),
options,
);
assert.equal(largeUnsignedMessage.data, 18_446_744_073_709_551_615n);
await assert.rejects(
() =>
client.sendProtected(
"ProtectedMessage",
9_007_199_254_740_992,
{
receiverId: 22n,
identity: { signerId: 11n, keyring: signerKeyring },
recipients: [publicBundle(recipientKeyring)],
signaturePurpose: 0x40,
encryptionPurpose: 0x41,
signatureSuite: "dual",
},
),
/unsafe integral MTP DataValue inputs must use bigint/,
);
});
await it("requires the configured purposes and verifies the protected signature", async () => {
const signerKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const frame = await buildDirectFrame({ signerKeyring, recipientKeyring });
await assert.rejects(
() =>
client.openProtected(
frame,
openOptions(signerKeyring, recipientKeyring, {
signaturePurpose: 0x42,
}),
),
);
await assert.rejects(
() =>
client.openProtected(
frame,
openOptions(signerKeyring, recipientKeyring, {
encryptionPurpose: 0x42,
}),
),
);
const parsed = bindings.parse_frame(frame);
const signedBytes = bindings.decrypt_data_value(
parsed.data.encoded,
recipientKeyring,
0x41,
);
const tamperedSigned = signedBytes.slice();
tamperedSigned[1 + 4 + 1 + 1 + 8] ^= 0xff;
const tamperedPayload = bindings.encrypt_data_value_for_recipients(
tamperedSigned,
[publicBundle(recipientKeyring)],
0x41,
);
const tamperedFrame = bindings.build_frame_with_payload(
"ProtectedMessage",
tamperedPayload,
{ receiver: 22n, sender: 11n },
);
await assert.rejects(() =>
client.openProtected(
tamperedFrame,
openOptions(signerKeyring, recipientKeyring),
),
);
});
await it("resolves policy independently from recipient signing capabilities", async () => {
const signerKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const edOnlyRecipientKeyring = ed25519OnlyEncryptionKeyring(recipientKeyring);
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
defaultSignatureVerificationPolicy: "dual",
pings: false,
});
const dualFrame = await buildDirectFrame({
signerKeyring,
recipientKeyring,
signatureSuite: "dual",
});
const defaultOpened = await client.openProtected(
dualFrame,
openOptions(signerKeyring, edOnlyRecipientKeyring, {
signaturePolicy: undefined,
}),
);
assert.equal(defaultOpened.data.ExampleType, "direct-receive");
const edFrame = await buildDirectFrame({
signerKeyring,
recipientKeyring,
signatureSuite: "ed25519",
});
const overridden = await client.openProtected(
edFrame,
openOptions(signerKeyring, edOnlyRecipientKeyring, {
signaturePolicy: "ed25519",
}),
);
assert.equal(overridden.data.ExampleType, "direct-receive");
await assert.rejects(
() =>
client.openProtected(
edFrame,
openOptions(signerKeyring, edOnlyRecipientKeyring, {
signaturePolicy: undefined,
}),
),
(error) =>
error instanceof sdk.MTPSignatureVerificationError &&
error.code === "policy-rejected",
);
const libraryDefaultClient = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const libraryDefault = await libraryDefaultClient.openProtected(
edFrame,
openOptions(signerKeyring, edOnlyRecipientKeyring, {
signaturePolicy: undefined,
}),
);
assert.equal(libraryDefaultClient.defaultSignatureVerificationPolicy, "ed25519");
assert.equal(libraryDefault.data.ExampleType, "direct-receive");
});
await it("reports unavailable signer keys and invalid signatures separately", async () => {
const signerKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const frame = await buildDirectFrame({ signerKeyring, recipientKeyring });
await assert.rejects(
() =>
client.openProtected(
frame,
openOptions(signerKeyring, recipientKeyring, {
resolveSignerPublicKeys: () => [],
}),
),
(error) =>
error instanceof sdk.MTPSignatureVerificationError &&
error.code === "signer-keys-unavailable",
);
await assert.rejects(
() =>
client.openProtected(
frame,
openOptions(signerKeyring, recipientKeyring, {
resolveSignerPublicKeys: async () => {
throw new Error("signer key store unavailable");
},
}),
),
(error) =>
error instanceof sdk.MTPSignatureVerificationError &&
error.code === "signer-keys-unavailable",
);
const parsed = bindings.parse_frame(frame);
const signedBytes = bindings.decrypt_data_value(
parsed.data.encoded,
recipientKeyring,
0x41,
);
const tamperedSigned = signedBytes.slice();
tamperedSigned[1 + 4 + 1 + 1 + 8] ^= 0xff;
const tamperedPayload = bindings.encrypt_data_value_for_recipients(
tamperedSigned,
[publicBundle(recipientKeyring)],
0x41,
);
const tamperedFrame = bindings.build_frame_with_payload(
"ProtectedMessage",
tamperedPayload,
{ receiver: 22n, sender: 11n },
);
await assert.rejects(
() =>
client.openProtected(
tamperedFrame,
openOptions(signerKeyring, recipientKeyring),
),
(error) =>
error instanceof sdk.MTPSignatureVerificationError &&
error.code === "invalid-signature",
);
});
await it("rejects an unresolved communication type", async () => {
const signerKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const frame = await buildDirectFrame({ signerKeyring, recipientKeyring });
const unknownTypeFrame = frame.slice();
unknownTypeFrame[4] = 0;
unknownTypeFrame[5] = 0xff;
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
await assert.rejects(
() =>
client.openProtected(
unknownTypeFrame,
openOptions(signerKeyring, recipientKeyring),
),
/Unknown communication type/,
);
});
await it("uses the shared opening path for subscriptions", async () => {
const signerKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const frame = bindings.parse_frame(
await buildDirectFrame({ signerKeyring, recipientKeyring }),
);
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const originalSubscribe = client.raw.client.subscribe;
const originalUnsubscribe = client.raw.client.unsubscribe;
let subscribedType;
let subscribedHandler;
let unsubscribedId;
client.raw.client.subscribe = (type, handler) => {
subscribedType = type;
subscribedHandler = handler;
return 23;
};
client.raw.client.unsubscribe = (id) => {
unsubscribedId = id;
return true;
};
try {
const received = [];
const unsubscribe = client.subscribeProtected(
"ProtectedMessage",
(message, receivedFrame) => received.push({ message, receivedFrame }),
openOptions(signerKeyring, recipientKeyring),
);
assert.equal(subscribedType, "ProtectedMessage");
await subscribedHandler(frame);
assert.equal(received.length, 1);
assert.equal(received[0].message.signerId, 11n);
assert.equal(received[0].receivedFrame.type, "ProtectedMessage");
unsubscribe();
assert.equal(unsubscribedId, 23);
} finally {
client.raw.client.subscribe = originalSubscribe;
client.raw.client.unsubscribe = originalUnsubscribe;
}
});
await it("keeps protected subscription replay guards independent", async () => {
const signerKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const frame = bindings.parse_frame(
await buildDirectFrame({
signerKeyring,
recipientKeyring,
signatureSuite: "dual",
}),
);
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const originalSubscribe = client.raw.client.subscribe;
const originalUnsubscribe = client.raw.client.unsubscribe;
const subscriptions = [];
client.raw.client.subscribe = (type, handler) => {
const id = subscriptions.length + 1;
subscriptions.push({ type, handler, id });
return id;
};
client.raw.client.unsubscribe = () => true;
try {
const received = [0, 0];
const options = openOptions(signerKeyring, recipientKeyring);
client.subscribeProtected(
"ProtectedMessage",
() => {
received[0] += 1;
},
options,
);
client.subscribeProtected(
"ProtectedMessage",
() => {
received[1] += 1;
},
options,
);
assert.equal(subscriptions.length, 2);
await subscriptions[0].handler(frame);
await subscriptions[1].handler(frame);
assert.deepEqual(received, [1, 1]);
} finally {
client.raw.client.subscribe = originalSubscribe;
client.raw.client.unsubscribe = originalUnsubscribe;
}
});
});
await describe("E2EE Session Manager", async () => {
await it("derivePeerSessionId is consistent regardless of order", () => {
const id1 = derivePeerSessionId(5n, 10n);
const id2 = derivePeerSessionId(10n, 5n);
assert.equal(id1, id2);
});
await it("stores independent sessions by explicit session ID", async () => {
const ss = sdk.crypto.sha256(new Uint8Array([1, 2, 3]));
const storage = new InMemorySessionStorage();
const manager = new MTPSessionManager(storage);
const sessionId = "independent-session";
assert.equal(await manager.getSession(sessionId), null);
const session = await manager.createSession({
sessionId,
localId: 1n,
remoteId: 2n,
remotePublicKey: new Uint8Array(32),
sharedSecret: ss,
role: "initiator",
transcriptContext: {
sessionId,
initiatorId: 1n,
recipientId: 2n,
recipientPublicKey: new Uint8Array(32),
kemCiphertext: new Uint8Array([1]),
applicationContext: new Uint8Array([2]),
},
});
assert.equal(session.version, 1);
assert.equal(session.sendCount, 0);
assert.equal(session.recvCount, 0);
await manager.saveSession(session);
const retrieved = await manager.getSession(sessionId);
assert.notEqual(retrieved, null);
assert.equal(retrieved.sessionId, session.sessionId);
const otherSession = await manager.createSession({
sessionId: "another-session",
localId: 1n,
remoteId: 2n,
remotePublicKey: new Uint8Array(32),
sharedSecret: ss,
role: "initiator",
transcript: new Uint8Array([2]),
});
await manager.saveSession(otherSession);
assert.notEqual(await manager.getSession(sessionId), null);
assert.notEqual(await manager.getSession(otherSession.sessionId), null);
await manager.deleteSession(sessionId);
assert.equal(await manager.getSession(sessionId), null);
assert.notEqual(await manager.getSession(otherSession.sessionId), null);
});
});
await describe("E2EE Full Flow: KEM + Session + Ratchet + AEAD", async () => {
await it("Alice encapsulates to Bob, both derive matching sessions, encrypt-decrypt works", async () => {
// Bob generates keyring
const bobKeyring = sdk.crypto.generateKeyring();
const bobKeys = sdk.crypto.keyringToKeys(bobKeyring);
// Alice encapsulates to Bob's KEM public key
const enc = sdk.crypto.encapsulate(bobKeys.kemPublicKey);
// Bob decapsulates
const bobSharedSecret = sdk.crypto.decapsulate(
bobKeys.kemSecretKey,
enc.ciphertext,
);
assert.deepEqual(bobSharedSecret, enc.shared_secret);
const ss = enc.shared_secret;
const { initSessions } = setupSessions(ss);
const { aliceSession, bobSession } = await initSessions();
// Alice sends encrypted init message with KEM ciphertext
const msg1 = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // "Hello"
const { payload: p1, session: aliceAfter1 } = await encryptPayload({
plaintext: msg1,
session: aliceSession,
kemCiphertext: enc.ciphertext,
});
// Bob receives and decrypts
const { plaintext: d1, session: bobAfter1 } = await decryptPayload({
payload: p1,
session: bobSession,
});
assert.deepEqual(d1, msg1);
assert.deepEqual(aliceAfter1.sendChainKey, bobAfter1.recvChainKey);
assert.equal(aliceAfter1.sendCount, 1);
assert.equal(bobAfter1.recvCount, 1);
// Second message (no KEM ciphertext)
const msg2 = new Uint8Array([0x57, 0x6f, 0x72, 0x6c, 0x64]); // "World"
const { payload: p2, session: aliceAfter2 } = await encryptPayload({
plaintext: msg2,
session: aliceAfter1,
});
const { plaintext: d2, session: bobAfter2 } = await decryptPayload({
payload: p2,
session: bobAfter1,
});
assert.deepEqual(d2, msg2);
assert.deepEqual(aliceAfter2.sendChainKey, bobAfter2.recvChainKey);
assert.equal(aliceAfter2.sendCount, 2);
assert.equal(bobAfter2.recvCount, 2);
});
});
await describe("E2EE Tamper Detection", async () => {
await it("Rejects modified ciphertext", async () => {
const ss = sdk.crypto.sha256(new Uint8Array([42]));
const { initSessions } = setupSessions(ss);
const { aliceSession, bobSession } = await initSessions();
const plaintext = new Uint8Array([0x01, 0x02, 0x03]);
const { payload } = await encryptPayload({
plaintext,
session: aliceSession,
});
// Tamper with AEAD payload
const tampered = new Uint8Array(payload);
tampered[tampered.length - 1] ^= 0xff;
await assert.rejects(
() => decryptPayload({ payload: tampered, session: bobSession }),
/decrypt failed/,
);
});
await it("Rejects out-of-order message numbers", async () => {
const ss = sdk.crypto.sha256(new Uint8Array([7]));
const { initSessions } = setupSessions(ss);
const { aliceSession, bobSession } = await initSessions();
// Send two messages
const { payload: p1, session: aliceAfter1 } = await encryptPayload({
plaintext: sdk.codec.encode("Ping", { Version: "a" }),
session: aliceSession,
});
await encryptPayload({
plaintext: sdk.codec.encode("Ping", { Version: "b" }),
session: aliceAfter1,
});
// Bob decrypts p1
const { session: bobAfter1 } = await decryptPayload({
payload: p1,
session: bobSession,
});
// Now bob expects msgNumber 1, but we try to replay msgNumber 0
await assert.rejects(
() => decryptPayload({ payload: p1, session: bobAfter1 }),
/replay|out of order/,
);
});
});
await describe("Encrypted Pipe", async () => {
await it("rejects MTP-reserved application purposes", () => {
assert.throws(
() => new sdk.MTPPipeProtectionContext(new Uint8Array([1]), 0x30, 0),
/reserved/,
);
});
await it("authenticates records and requires a final record", async () => {
const [writerPipe, readerPipe] = memoryDuplexPair();
const context = new sdk.MTPPipeProtectionContext(
new Uint8Array([1, 2, 3]),
0x40,
0,
);
const writer = new sdk.MTPEncryptedPipeWriter(
writerPipe,
new Uint8Array(32).fill(7),
context,
);
const reader = new sdk.MTPEncryptedPipeReader(
readerPipe,
new Uint8Array(32).fill(7),
context,
);
await writer.writeRecord(new Uint8Array([1, 2, 3]));
assert.deepEqual(await reader.readRecord(), new Uint8Array([1, 2, 3]));
await writer.close();
assert.equal(await reader.readRecord(), null);
assert.equal(await reader.readRecord(), null);
});
await it("poisons the reader after a truncated stream", async () => {
const [writerPipe, readerPipe] = memoryDuplexPair();
const context = new sdk.MTPPipeProtectionContext(
new Uint8Array([4, 5, 6]),
0x40,
0,
);
const writer = new sdk.MTPEncryptedPipeWriter(
writerPipe,
new Uint8Array(32).fill(8),
context,
);
const reader = new sdk.MTPEncryptedPipeReader(
readerPipe,
new Uint8Array(32).fill(8),
context,
);
await writer.writeRecord(new Uint8Array([9]));
writerPipe.abort();
assert.deepEqual(await reader.readRecord(), new Uint8Array([9]));
await assert.rejects(() => reader.readRecord(), /final|truncated/i);
await assert.rejects(() => reader.readRecord(), /state|readable/i);
});
await it("poisons the writer after a transport write failure", async () => {
const [writerPipe] = memoryDuplexPair();
const context = new sdk.MTPPipeProtectionContext(
new Uint8Array([7, 8, 9]),
0x40,
0,
);
const writer = new sdk.MTPEncryptedPipeWriter(
writerPipe,
new Uint8Array(32).fill(6),
context,
);
writerPipe.abort();
await assert.rejects(() => writer.writeRecord(new Uint8Array([1])), /write|closed/i);
await assert.rejects(() => writer.writeRecord(new Uint8Array([2])), /state|writable/i);
});
await it("establishes a forward-secure authenticated duplex session", async () => {
const senderKeyring = sdk.crypto.generateKeyring();
const currentSenderKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const senderBundle = publicBundle(senderKeyring);
const currentSenderBundle = publicBundle(currentSenderKeyring);
const recipientBundle = publicBundle(recipientKeyring);
const [initiatorPipe, responderPipe] = memoryDuplexPair(91);
const sessionId = new Uint8Array(32).fill(0x42);
const params = {
sessionId,
pipeId: 91,
senderId: 11n,
recipientId: 22n,
purpose: 0x40,
direction: 0,
};
const responder = sdk.acceptMTPForwardSecurePipeSession(
responderPipe,
{
pipeId: params.pipeId,
senderId: params.senderId,
recipientId: params.recipientId,
purpose: params.purpose,
direction: params.direction,
},
recipientKeyring,
[currentSenderBundle, senderBundle],
"dual",
"dual",
);
const initiator = await sdk.initiateMTPForwardSecurePipeSession(
initiatorPipe,
params,
senderKeyring,
recipientBundle,
"dual",
"dual",
);
const receiver = await responder;
await initiator.writeRecord(new Uint8Array([0xaa, 0xbb]));
assert.deepEqual(await receiver.readRecord(), new Uint8Array([0xaa, 0xbb]));
await initiator.close();
assert.equal(await receiver.readRecord(), null);
});
await it("uses interoperable Ed25519 defaults for pipe handshakes", async () => {
const senderKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const [initiatorPipe, responderPipe] = memoryDuplexPair(93);
const params = {
sessionId: new Uint8Array([9, 3]),
pipeId: 93,
senderId: 11n,
recipientId: 22n,
purpose: 0x40,
direction: 0,
};
const responder = sdk.acceptMTPForwardSecurePipeSession(
responderPipe,
params,
recipientKeyring,
publicBundle(senderKeyring),
);
const initiator = await sdk.initiateMTPForwardSecurePipeSession(
initiatorPipe,
params,
senderKeyring,
publicBundle(recipientKeyring),
);
const receiver = await responder;
await initiator.writeRecord(new Uint8Array([0x55]));
assert.deepEqual(await receiver.readRecord(), new Uint8Array([0x55]));
await initiator.close();
assert.equal(await receiver.readRecord(), null);
});
await it("accepts a dual sender with an Ed25519-only recipient keyring", async () => {
const senderKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const recipientEncryptionKeyring = ed25519OnlyEncryptionKeyring(
recipientKeyring,
);
const [writerPipe, readerPipe] = memoryDuplexPair(92);
const params = {
sessionId: new Uint8Array([9, 2]),
pipeId: 92,
senderId: 11n,
recipientId: 22n,
purpose: 0x40,
direction: 0,
};
const readerPromise = sdk.acceptMTPPipeSession(
readerPipe,
params,
recipientEncryptionKeyring,
publicBundle(senderKeyring),
"dual",
);
const writer = await sdk.initiateMTPPipeSession(
writerPipe,
params,
senderKeyring,
publicBundle(recipientKeyring),
"dual",
);
const reader = await readerPromise;
await writer.writeRecord(new Uint8Array([0x11]));
assert.deepEqual(await reader.readRecord(), new Uint8Array([0x11]));
});
});
await describe("Relay API invariants", async () => {
await it("exposes stable codes for raw relay operation errors", () => {
const ping = bindings.build_ping_frame(
7n,
"not-a-relay",
1n,
new Uint8Array(),
);
assert.throws(
() => bindings.forward_encrypted_relay_frame(ping, 9n),
(error) => error?.code === "not-relay",
);
assert.throws(
() => bindings.forward_encrypted_relay_frame(new Uint8Array([0xff]), 9n),
(error) => error?.code === "invalid-frame",
);
});
await it("uses the client default for metadata and content independently of recipient PQ keys", async () => {
const senderKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const frame = sdk.codec.decode(
bindings.build_encrypted_relay_frame_with_keyring(
"ProtectedMessage",
{ ExampleType: "default-policy" },
11n,
42n,
42n,
"message-default-policy",
456n,
null,
senderKeyring,
bindings.mtp_protection_signature_suite_ed25519(),
[publicBundle(recipientKeyring)],
[publicBundle(recipientKeyring)],
),
);
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
defaultSignatureVerificationPolicy: "ed25519",
pings: false,
});
const options = {
recipient: { id: 42n, keyring: recipientKeyring },
expectedSignerId: 11n,
resolveSignerPublicKeys: () => [publicBundle(senderKeyring)],
};
const metadata = await client.openRelayMetadata(frame, options);
assert.equal(metadata.signaturePolicy, "ed25519");
const content = await client.openRelayContent(metadata, options);
assert.equal(content.data.ExampleType, "default-policy");
});
await it("inherits the authenticated metadata policy for relay content", async () => {
const senderKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const frame = sdk.codec.decode(
bindings.build_encrypted_relay_frame_with_keyring(
"ProtectedMessage",
{ ExampleType: "inherited-policy" },
11n,
42n,
42n,
"message-inherited-policy",
456n,
null,
senderKeyring,
bindings.mtp_protection_signature_suite_dual(),
[publicBundle(recipientKeyring)],
[publicBundle(recipientKeyring)],
),
);
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
defaultSignatureVerificationPolicy: "ed25519",
pings: false,
});
const metadataOptions = {
recipient: { id: 42n, keyring: recipientKeyring },
expectedSignerId: 11n,
resolveSignerPublicKeys: () => [publicBundle(senderKeyring)],
signaturePolicy: "dual",
};
const metadata = await client.openRelayMetadata(frame, metadataOptions);
const content = await client.openRelayContent(metadata, {
recipient: metadataOptions.recipient,
expectedSignerId: metadataOptions.expectedSignerId,
resolveSignerPublicKeys: metadataOptions.resolveSignerPublicKeys,
});
assert.equal(content.data.ExampleType, "inherited-policy");
});
await it("preserves relay CreatedAt milliseconds as bigint", async () => {
const senderKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const frame = sdk.codec.decode(
bindings.build_encrypted_relay_frame_with_keyring(
"ProtectedMessage",
{ ExampleType: "generic" },
11n,
42n,
42n,
"message-generic",
relayCreatedAtMillis,
bindings.encode_data_value({ ExampleType: "opaque-to-content-only-relays" }),
senderKeyring,
bindings.mtp_protection_signature_suite_dual(),
[publicBundle(recipientKeyring)],
[publicBundle(recipientKeyring)],
),
);
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const options = {
recipient: { id: 42n, keyring: recipientKeyring },
expectedSignerId: 11n,
resolveSignerPublicKeys: () => [publicBundle(senderKeyring)],
signaturePolicy: "dual",
};
const metadata = await client.openRelayMetadata(frame, options);
assert.ok(metadata);
assert.equal(metadata.signerId, 11n);
assert.equal(metadata.finalRecipientId, 42n);
assert.equal(metadata.messageId, "message-generic");
assert.equal(metadata.createdAt, relayCreatedAtMillis);
assert.deepEqual(metadata.metadata, {
ExampleType: "opaque-to-content-only-relays",
});
const content = await client.openRelayContent(metadata, options);
assert.deepEqual(content, {
type: "ProtectedMessage",
data: { ExampleType: "generic" },
signerId: 11n,
finalRecipientId: 42n,
messageId: "message-generic",
createdAt: relayCreatedAtMillis,
metadata: { ExampleType: "opaque-to-content-only-relays" },
});
});
await it("preserves scalar metadata and byte content values", async () => {
const senderKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const frame = sdk.codec.decode(
bindings.build_encrypted_relay_frame_with_keyring(
"ProtectedMessage",
new Uint8Array([0xde, 0xad, 0xbe, 0xef]),
11n,
42n,
42n,
"message-scalar-values",
456n,
bindings.encode_data_value("opaque metadata"),
senderKeyring,
bindings.mtp_protection_signature_suite_dual(),
[publicBundle(recipientKeyring)],
[publicBundle(recipientKeyring)],
),
);
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const options = {
recipient: { id: 42n, keyring: recipientKeyring },
expectedSignerId: 11n,
resolveSignerPublicKeys: () => [publicBundle(senderKeyring)],
signaturePolicy: "dual",
};
const metadata = await client.openRelayMetadata(frame, options);
assert.equal(metadata.metadata, "opaque metadata");
const content = await client.openRelayContent(metadata, options);
assert.deepEqual(content.data, new Uint8Array([0xde, 0xad, 0xbe, 0xef]));
});
await it("subscribes through the explicit sealed-relay API", async () => {
const senderKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const frame = sdk.codec.decode(
bindings.build_encrypted_relay_frame_with_keyring(
"ProtectedMessage",
{ ExampleType: "subscription" },
11n,
42n,
42n,
"message-subscription",
789n,
bindings.encode_data_value({ ExampleType: "subscription" }),
senderKeyring,
bindings.mtp_protection_signature_suite_dual(),
[publicBundle(recipientKeyring)],
[publicBundle(recipientKeyring)],
),
);
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const originalSubscribe = client.raw.client.subscribe;
const originalUnsubscribe = client.raw.client.unsubscribe;
let subscribedType;
let subscribedHandler;
let unsubscribedId;
client.raw.client.subscribe = (type, handler) => {
subscribedType = type;
subscribedHandler = handler;
return 17;
};
client.raw.client.unsubscribe = (id) => {
unsubscribedId = id;
};
try {
const received = [];
const unsubscribe = client.subscribeSealedRelay(
"ProtectedMessage",
(content, parsedFrame) => {
received.push({ content, parsedFrame });
},
{
recipient: { id: 42n, keyring: recipientKeyring },
expectedSignerId: 11n,
resolveSignerPublicKeys: () => [publicBundle(senderKeyring)],
signaturePolicy: "dual",
},
);
assert.equal(subscribedType, "Relay");
assert.equal(typeof subscribedHandler, "function");
await subscribedHandler(frame);
assert.equal(received.length, 1);
assert.equal(received[0].content.messageId, "message-subscription");
assert.deepEqual(received[0].content.metadata, {
ExampleType: "subscription",
});
assert.equal(received[0].parsedFrame.type, "ProtectedMessage");
assert.deepEqual(received[0].parsedFrame.data, {
ExampleType: "subscription",
});
await subscribedHandler(frame);
assert.equal(received.length, 1);
unsubscribe();
assert.equal(unsubscribedId, 17);
} finally {
client.raw.client.subscribe = originalSubscribe;
client.raw.client.unsubscribe = originalUnsubscribe;
}
});
await it("keeps relay subscription replay guards independent", async () => {
const senderKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const frame = sdk.codec.decode(
bindings.build_encrypted_relay_frame_with_keyring(
"ProtectedMessage",
{ ExampleType: "fanout" },
11n,
42n,
42n,
"message-relay-fanout",
789n,
bindings.encode_data_value({ ExampleType: "fanout-metadata" }),
senderKeyring,
bindings.mtp_protection_signature_suite_dual(),
[publicBundle(recipientKeyring)],
[publicBundle(recipientKeyring)],
),
);
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const originalSubscribe = client.raw.client.subscribe;
const originalUnsubscribe = client.raw.client.unsubscribe;
const subscriptions = [];
client.raw.client.subscribe = (type, handler) => {
const id = subscriptions.length + 1;
subscriptions.push({ type, handler, id });
return id;
};
client.raw.client.unsubscribe = () => true;
try {
const options = {
recipient: { id: 42n, keyring: recipientKeyring },
expectedSignerId: 11n,
resolveSignerPublicKeys: () => [publicBundle(senderKeyring)],
signaturePolicy: "dual",
};
let mismatched = 0;
let matched = 0;
let metadataOnly = 0;
client.subscribeSealedRelay(
"AlternateMessage",
() => {
mismatched += 1;
},
options,
);
client.subscribeSealedRelay(
"ProtectedMessage",
() => {
matched += 1;
},
options,
);
client.subscribeRelayMetadata(
() => {
metadataOnly += 1;
},
options,
);
const relaySubscriptions = subscriptions.filter(
({ type }) => type === "Relay",
);
assert.equal(relaySubscriptions.length, 3);
for (const subscription of relaySubscriptions) {
await subscription.handler(frame);
}
assert.equal(mismatched, 0);
assert.equal(matched, 1);
assert.equal(metadataOnly, 1);
} finally {
client.raw.client.subscribe = originalSubscribe;
client.raw.client.unsubscribe = originalUnsubscribe;
}
});
await it("consumes authenticated relay IDs through the caller's replay guard", async () => {
const senderKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const frame = sdk.codec.decode(
bindings.build_encrypted_relay_frame_with_keyring(
"ProtectedMessage",
{ ExampleType: "replay" },
11n,
42n,
42n,
"message-replay",
123n,
null,
senderKeyring,
bindings.mtp_protection_signature_suite_dual(),
[publicBundle(recipientKeyring)],
[publicBundle(recipientKeyring)],
),
);
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
credentials: { clientId: 42n, keyring: recipientKeyring },
pings: false,
});
const accepted = new Set();
const replayGuard = {
accept(signerId, messageId) {
const key = `${signerId}:${messageId}`;
if (accepted.has(key)) return false;
accepted.add(key);
return true;
},
};
const options = {
expectedSignerId: 11n,
resolveSignerPublicKeys: () => [publicBundle(senderKeyring)],
signaturePolicy: "dual",
replayGuard,
};
const metadata = await client.openRelayMetadata(frame, options);
assert.equal(metadata.messageId, "message-replay");
await assert.rejects(
() => client.openRelayMetadata(frame, options),
(error) => error instanceof sdk.MTPReplayError,
);
metadata.dispose();
metadata.free();
await assert.rejects(
() => client.openRelayContent(metadata, options),
/disposed/,
);
});
await it("disposes metadata after relay metadata subscription handlers complete", async () => {
const senderKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const frame = sdk.codec.decode(
bindings.build_encrypted_relay_frame_with_keyring(
"ProtectedMessage",
{ ExampleType: "metadata-subscription" },
11n,
42n,
42n,
"message-metadata-subscription",
123n,
null,
senderKeyring,
bindings.mtp_protection_signature_suite_dual(),
[publicBundle(recipientKeyring)],
[publicBundle(recipientKeyring)],
),
);
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const originalSubscribe = client.raw.client.subscribe;
let subscribedHandler;
client.raw.client.subscribe = (_type, handler) => {
subscribedHandler = handler;
return 19;
};
try {
let receivedMetadata;
client.subscribeRelayMetadata(
(metadata) => {
receivedMetadata = metadata;
assert.equal(metadata.messageId, "message-metadata-subscription");
},
{
recipient: { id: 42n, keyring: recipientKeyring },
expectedSignerId: 11n,
resolveSignerPublicKeys: () => [publicBundle(senderKeyring)],
signaturePolicy: "dual",
},
);
await subscribedHandler(frame);
assert.throws(() => receivedMetadata.messageId, /disposed/);
} finally {
client.raw.client.subscribe = originalSubscribe;
}
});
await it("rejects relay content opening when metadata is disposed during resolution", async () => {
const senderKeyring = sdk.crypto.generateKeyring();
const recipientKeyring = sdk.crypto.generateKeyring();
const frame = sdk.codec.decode(
bindings.build_encrypted_relay_frame_with_keyring(
"ProtectedMessage",
{ ExampleType: "dispose-race" },
11n,
42n,
42n,
"message-dispose-race",
123n,
null,
senderKeyring,
bindings.mtp_protection_signature_suite_dual(),
[publicBundle(recipientKeyring)],
[publicBundle(recipientKeyring)],
),
);
const client = await sdk.MTPClient.create({
url: "https://example.invalid",
pings: false,
});
const signerPublicKey = publicBundle(senderKeyring);
const metadata = await client.openRelayMetadata(frame, {
recipient: { id: 42n, keyring: recipientKeyring },
expectedSignerId: 11n,
resolveSignerPublicKeys: () => [signerPublicKey],
signaturePolicy: "dual",
});
let signalResolverStarted;
let releaseResolver;
const resolverStarted = new Promise((resolve) => {
signalResolverStarted = resolve;
});
const resolverRelease = new Promise((resolve) => {
releaseResolver = resolve;
});
const opening = client.openRelayContent(metadata, {
recipient: { id: 42n, keyring: recipientKeyring },
expectedSignerId: 11n,
resolveSignerPublicKeys: async () => {
signalResolverStarted();
await resolverRelease;
return [signerPublicKey];
},
signaturePolicy: "dual",
});
await resolverStarted;
metadata.dispose();
releaseResolver();
await assert.rejects(opening, /relay metadata has been disposed/);
});
await it("does not permit callers to construct verified metadata", () => {
assert.throws(
() => new sdk.MTPVerifiedRelayMetadata(Symbol(), {}),
/authenticated opening/,
);
});
});