cautpartener / 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
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
/** Dev-only crypto self-test: roundtrip, cross-key and AAD-mismatch must fail. */

import { signerMatchesIdentity } from "./access";
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");

  // v2 at-rest layer: dbName must be identical across instances (this is what
  // makes every member open the SAME OrbitDB address), ciphers must roundtrip
  // across instances of the same room and reject tampering + foreign rooms
  assert((await a.dbName()) === (await b.dbName()), "same secret ⇒ same dbName");
  assert((await a.dbName()) !== (await other.dbName()), "different secret ⇒ different dbName");
  const ea = await a.logEntryCipher();
  const eb = await b.logEntryCipher();
  const eOther = await other.logEntryCipher();
  const plain = crypto.getRandomValues(new Uint8Array(500));
  const sealed = await ea.seal(plain);
  const openedAtRest = await eb.open(sealed);
  assert(!!openedAtRest && openedAtRest.every((v, i) => v === plain[i]), "at-rest cross-instance roundtrip");
  assert((await eOther.open(sealed)) === null, "at-rest foreign room must fail");
  const tampered = sealed.slice();
  tampered[tampered.length - 1] ^= 0x01;
  assert((await eb.open(tampered)) === null, "at-rest tamper must fail");
  const payloadCipher = await a.logPayloadCipher();
  assert((await payloadCipher.open(sealed)) === null, "entry/payload key domains must differ");
  const iv2 = new Set<string>();
  for (let i = 0; i < 100; i++) iv2.add(toHex((await ea.seal(plain)).subarray(1, 13)));
  assert(iv2.size === 100, "at-rest IV uniqueness (random)");

  // authorship binding (lib/access.ts): the entry's signing key must BE the
  // referenced identity's key — a member signing with their own key while
  // referencing a victim's identity must be rejected
  const identities = {
    getIdentity: async (hash: string) =>
      hash === "victim-ref" ? { publicKey: "victim-pub" } : { publicKey: "attacker-pub" },
  };
  assert(
    await signerMatchesIdentity(identities, { key: "victim-pub", identity: "victim-ref" }),
    "matching signer/identity must pass",
  );
  assert(
    !(await signerMatchesIdentity(identities, { key: "attacker-pub", identity: "victim-ref" })),
    "forged authorship (foreign identity ref) must fail",
  );
  assert(!(await signerMatchesIdentity(identities, { identity: "victim-ref" })), "missing key must fail");
  assert(!(await signerMatchesIdentity(identities, { key: "victim-pub" })), "missing identity ref must fail");

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

function toHex(b: Uint8Array): string {
  return Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
}

static mirror of HEAD · about · clone: git clone https://cod-sursa.cautpartener.ro/cautpartener.git