sueta / client / src / lib / selftest.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/** Dev-only crypto self-test: roundtrip, cross-key and AAD-mismatch must fail. */

import { newRoomSecret, RoomCrypto } from "./crypto";

export async function runSelfTest(): Promise<void> {
  const secret = newRoomSecret();
  const a = await RoomCrypto.create(secret, "selftest/v1");
  const b = await RoomCrypto.create(secret, "selftest/v1");
  const other = await RoomCrypto.create(newRoomSecret(), "selftest/v1");
  const otherApp = await RoomCrypto.create(secret, "selftest/v2");

  const assert = (cond: boolean, what: string) => {
    if (!cond) throw new Error(`crypto self-test FAILED: ${what}`);
  };

  assert(a.roomId === b.roomId, "same secret ⇒ same roomId");
  assert(a.roomId !== other.roomId, "different secret ⇒ different roomId");
  assert(a.roomId !== otherApp.roomId, "different app salt ⇒ different roomId");

  // signaling roundtrip a → b
  const sig = await a.sealSignal(b.peerId, { kind: "offer", sdp: "x", name: "n" });
  assert(((await b.openSignal(a.peerId, sig)) as { sdp: string }).sdp === "x", "signal roundtrip");
  assert((await b.openSignal(b.peerId, sig)) === null, "wrong `from` must fail (AAD)");
  const c = await RoomCrypto.create(secret, "selftest/v1");
  assert((await c.openSignal(a.peerId, sig)) === null, "wrong `to` must fail (AAD)");
  assert((await other.openSignal(a.peerId, sig)) === null, "wrong room must fail");

  // datachannel JSON roundtrip + binary
  const msg = await a.sealMsg({ kind: "chat", text: "hi" });
  assert(((await b.openMsg(a.peerId, msg)) as { text: string }).text === "hi", "msg roundtrip");
  assert((await b.openMsg(b.peerId, msg)) === null, "msg wrong sender must fail");

  const data = crypto.getRandomValues(new Uint8Array(1000));
  const bin = await a.sealMsgBinary(data);
  const openedBin = await b.openMsgBinary(a.peerId, bin);
  assert(!!openedBin && openedBin.every((v, i) => v === data[i]), "binary roundtrip");
  assert((await b.openMsgBinary(b.peerId, bin)) === null, "binary wrong sender must fail");

  // IVs must never repeat for one sealer
  const ivs = new Set<string>();
  for (let i = 0; i < 200; i++) ivs.add((await a.sealMsg({ i })).iv);
  assert(ivs.size === 200, "IV uniqueness");

  // identity: deterministic from seed, assertion verifies, wrong context fails
  const { Identity, verifyAssertion } = await import("./identity");
  const seed = Identity.newSeed();
  const id1 = await Identity.fromSeed(seed);
  const id2 = await Identity.fromSeed(seed);
  assert(id1.publicKeyB64 === id2.publicKeyB64, "identity deterministic from seed");
  assert(id1.fingerprint.emoji.length > 0 && id1.fingerprint.hex.length === 64, "fingerprint shape");
  const idSig = await id1.assert("selftest/v1", a.roomId, a.peerId);
  assert((await verifyAssertion("selftest/v1", a.roomId, a.peerId, id1.publicKeyB64, idSig)) !== null, "assertion verifies");
  assert((await verifyAssertion("selftest/v1", a.roomId, b.peerId, id1.publicKeyB64, idSig)) === null, "assertion bound to peerId");
  assert((await verifyAssertion("selftest/v1", other.roomId, a.peerId, id1.publicKeyB64, idSig)) === null, "assertion bound to room");

  console.info("✅ crypto self-test passed");
}