// Run: npm test (Node strips the TypeScript types itself)
// Pure wire-format tests: no network, no IndexedDB — the reason omemo-wire
// is its own module, separate from omemo.ts.
import { test } from "node:test";
import assert from "node:assert/strict";
import parse from "ltx/lib/parse.js";
import { toBase64 } from "../src/xmpp/push.ts";
import { buildBundle, buildDeviceList, buildEncryptedMessage, parseBundle, parseDeviceList, parseEncryptedMessage } from "../src/crypto/omemo-wire.ts";
const NS = "eu.siacs.conversations.axolotl";
const el = (s: string) => parse(s) as any;
// Distinct key material: every byte pattern different, so a mix-up between
// the parts fails the round-trip loudly.
const key = (byte: number, length = 32) => new Uint8Array(length).fill(byte).buffer;
test("bundle round-trips through build→parse, in the legacy wire format", () => {
const identityKey = key(1, 33);
const signedPreKey = { keyId: 7, publicKey: key(2), signature: key(3, 64) };
const preKeys = [
{ keyId: 1, publicKey: key(4) },
{ keyId: 42, publicKey: key(5) },
];
const built = buildBundle(identityKey, signedPreKey, preKeys);
// Lock the exact wire layout against XEP-0384 (legacy), not just against
// our own parser: a round-trip through our own build+parse pair would
// happily pass if both drifted together away from what Monal reads.
assert.equal(
built.toString(),
`` +
`${toBase64(signedPreKey.publicKey)}` +
`${toBase64(signedPreKey.signature)}` +
`${toBase64(identityKey)}` +
`` +
`${toBase64(preKeys[0].publicKey)}` +
`${toBase64(preKeys[1].publicKey)}` +
`` +
``,
);
const parsed = parseBundle(built)!;
assert.deepEqual(new Uint8Array(parsed.identityKey), new Uint8Array(identityKey));
assert.equal(parsed.signedPreKey.keyId, 7);
assert.deepEqual(new Uint8Array(parsed.signedPreKey.publicKey), new Uint8Array(signedPreKey.publicKey));
assert.deepEqual(new Uint8Array(parsed.signedPreKey.signature), new Uint8Array(signedPreKey.signature));
assert.deepEqual(
parsed.preKeys.map((k) => k.keyId),
[1, 42],
);
assert.deepEqual(new Uint8Array(parsed.preKeys[1].publicKey), new Uint8Array(preKeys[1].publicKey));
});
test("a bundle without one-time prekeys round-trips (all consumed)", () => {
const built = buildBundle(key(1, 33), { keyId: 1, publicKey: key(2), signature: key(3, 64) }, []);
assert.deepEqual(parseBundle(built)!.preKeys, []);
// The prekeys container itself may be absent, not just empty.
const absent = el(
`` +
`${toBase64(key(2))}` +
`${toBase64(key(3, 64))}` +
`${toBase64(key(1, 33))}` +
``,
);
assert.deepEqual(parseBundle(absent)!.preKeys, []);
});
test("a malformed or missing bundle parses to null, never throws", () => {
const B = (inner: string) => el(`${inner}`);
const spk = `${toBase64(key(2))}`;
const sig = `${toBase64(key(3, 64))}`;
const idk = `${toBase64(key(1, 33))}`;
// Not a bundle at all.
assert.equal(parseBundle(el(`
`)), null);
// Missing required parts.
assert.equal(parseBundle(B(`${spk}${sig}`)), null); // no identityKey
assert.equal(parseBundle(B(`${sig}${idk}`)), null); // no signedPreKeyPublic
assert.equal(parseBundle(B(`${spk}${idk}`)), null); // no signature
assert.equal(parseBundle(B("")), null);
// A malformed signed prekey id.
assert.equal(parseBundle(B(`AAA=${sig}${idk}`)), null);
// Undecodable base64 in a required part.
assert.equal(parseBundle(B(`!!!${sig}${idk}`)), null);
assert.equal(parseBundle(B(`${spk}!!!${idk}`)), null);
assert.equal(parseBundle(B(`${spk}${sig}!!!`)), null);
// An empty required part is as malformed as a missing one.
assert.equal(parseBundle(B(`${spk}${sig}`)), null);
});
test("invalid prekey entries are skipped, the rest stay usable", () => {
const bundle = el(
`` +
`${toBase64(key(2))}` +
`${toBase64(key(3, 64))}` +
`${toBase64(key(1, 33))}` +
`` +
`!!!` +
`${toBase64(key(4))}` +
`` +
`${toBase64(key(5))}` +
`` +
``,
);
const parsed = parseBundle(bundle)!;
assert.deepEqual(
parsed.preKeys.map((k) => k.keyId),
[9],
);
});
test("a device list round-trips, deduplicated, in the legacy wire format", () => {
const built = buildDeviceList([42, 7, 42]);
assert.equal(built.toString(), `
`);
assert.deepEqual(parseDeviceList(built), [42, 7]);
});
test("a malformed or missing device list parses to empty, never throws", () => {
assert.deepEqual(
parseDeviceList(el(`
`)),
[1, 2],
);
// Wrong element or wrong namespace: empty — "no OMEMO devices", which
// callers treat as not-OMEMO-capable.
assert.deepEqual(parseDeviceList(el(``)), []);
assert.deepEqual(parseDeviceList(el(`
`)), []);
assert.deepEqual(parseDeviceList(el(`
`)), []);
});
test("an encrypted message round-trips through build→parse, in the legacy wire format", () => {
const msg = {
senderDeviceId: 111,
keys: [
{ deviceId: 222, prekey: true, data: key(6, 32) },
{ deviceId: 333, prekey: false, data: key(7, 32) },
],
iv: key(8, 12),
payload: key(9, 40),
};
const built = buildEncryptedMessage(msg);
// Lock the exact wire layout, not just a round-trip through our own
// parser: `prekey` must be the literal attribute "true", present only
// when true (never prekey="false"), the way deployed clients expect.
assert.equal(
built.toString(),
`` +
`` +
`${toBase64(key(6, 32))}` +
`${toBase64(key(7, 32))}` +
`${toBase64(key(8, 12))}` +
`` +
`${toBase64(key(9, 40))}` +
``,
);
const parsed = parseEncryptedMessage(built)!;
assert.equal(parsed.senderDeviceId, 111);
assert.deepEqual(
parsed.keys.map((k) => [k.deviceId, k.prekey]),
[
[222, true],
[333, false],
],
);
assert.deepEqual(new Uint8Array(parsed.keys[0].data), new Uint8Array(key(6, 32)));
assert.deepEqual(new Uint8Array(parsed.iv), new Uint8Array(key(8, 12)));
assert.deepEqual(new Uint8Array(parsed.payload), new Uint8Array(key(9, 40)));
});
test("an encrypted message with no keys for us still parses (empty keys array)", () => {
const built = buildEncryptedMessage({ senderDeviceId: 1, keys: [], iv: key(1, 12), payload: key(2, 10) });
const parsed = parseEncryptedMessage(built)!;
assert.deepEqual(parsed.keys, []);
});
test("a malformed or missing encrypted message parses to null, never throws", () => {
const E = (inner: string) => el(`${inner}`);
const iv = `${toBase64(key(1, 12))}`;
const payload = `${toBase64(key(2, 10))}`;
// Not an at all.
assert.equal(parseEncryptedMessage(el(``)), null);
// Missing header, missing sid, missing iv, missing payload.
assert.equal(parseEncryptedMessage(E(payload)), null);
assert.equal(parseEncryptedMessage(E(`${payload}`)), null);
assert.equal(parseEncryptedMessage(E(`${payload}`)), null);
assert.equal(parseEncryptedMessage(E(``)), null);
// A malformed sid, and undecodable base64 in iv/payload.
assert.equal(parseEncryptedMessage(E(`${payload}`)), null);
assert.equal(parseEncryptedMessage(E(`${payload}`)), null);
assert.equal(parseEncryptedMessage(E(`!!!`)), null);
});
test("an encrypted message with one malformed key entry skips it, keeps the rest", () => {
const built = el(
`` +
`` +
`${toBase64(key(1, 32))}` +
`${toBase64(key(2, 32))}` + // missing rid
`` + // empty text
`${toBase64(key(3, 12))}` +
`` +
`${toBase64(key(4, 10))}` +
``,
);
const parsed = parseEncryptedMessage(built)!;
assert.deepEqual(parsed.keys.map((k) => k.deviceId), [2]);
});