game2 / client / test / room-crypto.test.mjs
 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
// Cross-implementation room-crypto vectors: the CLJS port must derive the same
// room ids and OPEN envelopes sealed by the deployed TS implementation (fixtures
// generated while the TS was canon), plus seal/open round trips of its own
// (randomized-IV sealing can't be byte-fixed).
//
// The net/ layer is vendored byte-identically from the game1 CLJS canon, so this
// suite is really a guard that the vendoring and game2's APP_SALT still derive
// the room ids the deployed build speaks.
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import * as lib from "../test-dist/testlib.js";

const v = JSON.parse(readFileSync(new URL("./vectors/room.json", import.meta.url), "utf8"));

test("room id derivation matches TS (HKDF roomid, game2's salt)", async () => {
  assert.equal(v.appSalt, "game2.ardegazu.ro/v2");
  const rc1 = await lib.createRoomCrypto(v.secret1, v.appSalt);
  assert.equal(rc1.roomId, v.roomId1);
  const rc2 = await lib.createRoomCrypto(v.secret2, v.appSalt);
  assert.equal(rc2.roomId, v.roomId2);
});

test("the room-scoped protocol id is the DEPLOYED /game2/2/ prefix", () => {
  // valley-blocks is the one game whose prefix is its repo name, not its
  // codename — the wire must keep speaking exactly this.
  assert.equal(v.protocolPrefix, "/game2/2/");
  assert.equal(lib.protocolOf(v.roomId1), v.protocol1);
  assert.ok(lib.protocolOf(v.roomId1).startsWith(v.protocolPrefix));
});

test("opens TS-sealed hellos (plain and with an identity block)", async () => {
  const rc = await lib.createRoomCrypto(v.secret1, v.appSalt);
  const plain = await rc.openHello(v.peerA, v.peerB, v.envAtoB);
  assert.ok(plain, "A->B hello must open");
  assert.equal(plain.v, 2);
  const withId = await rc.openHello(v.peerB, v.peerA, v.envBtoA);
  assert.ok(withId, "B->A hello must open");
  assert.equal(withId.v, 2);
  assert.deepEqual(withId.id, v.envBtoAId);
});

test("rejects swapped direction, wrong peer, wrong room", async () => {
  const rc = await lib.createRoomCrypto(v.secret1, v.appSalt);
  assert.equal(await rc.openHello(v.peerB, v.peerA, v.envAtoB), null, "AAD direction");
  assert.equal(await rc.openHello(v.peerA, v.peerA, v.envAtoB), null, "wrong recipient");
  const other = await lib.createRoomCrypto(v.secret2, v.appSalt);
  assert.equal(await other.openHello(v.peerA, v.peerB, v.envAtoB), null, "wrong room");
});

test("seal/open round trip with extra fields riding next to v", async () => {
  const rc = await lib.createRoomCrypto(v.secret1, v.appSalt);
  const env = await rc.sealHello(v.peerA, v.peerB, { id: { pub: "p", sig: "s" } });
  assert.equal(env.v, 2);
  assert.equal(typeof env.iv, "string");
  assert.equal(typeof env.ct, "string");
  const pt = await rc.openHello(v.peerA, v.peerB, env);
  assert.ok(pt);
  assert.equal(pt.v, 2);
  assert.deepEqual(pt.id, { pub: "p", sig: "s" });
  // and sequential seals never reuse an IV
  const env2 = await rc.sealHello(v.peerA, v.peerB, undefined);
  assert.notEqual(env2.iv, env.iv);
  assert.ok(await rc.openHello(v.peerA, v.peerB, env2));
});

test("malformed envelopes and short secrets are rejected", async () => {
  const rc = await lib.createRoomCrypto(v.secret1, v.appSalt);
  for (const bad of [null, {}, { v: 1, iv: "", ct: "" }, { v: 2, iv: 42, ct: "" }]) {
    assert.equal(await rc.openHello(v.peerA, v.peerB, bad), null);
  }
  await assert.rejects(() => lib.createRoomCrypto(v.tooShortSecret, v.appSalt));
});

test("base64 primitives match the TS byte for byte", () => {
  for (const { bytes, b64, b64url } of v.b64) {
    const u8 = new Uint8Array(bytes);
    assert.equal(lib.toB64(u8), b64, JSON.stringify(bytes));
    assert.equal(lib.toB64url(u8), b64url, JSON.stringify(bytes));
    assert.deepEqual([...lib.fromB64url(b64url)], bytes, JSON.stringify(bytes));
  }
});

test("newRoomSecret mints 32-byte b64url secrets", () => {
  const s = lib.newRoomSecret();
  assert.match(s, /^[A-Za-z0-9_-]{43}$/);
  assert.equal(lib.fromB64url(s).length, 32);
  assert.equal(lib.toB64url(lib.fromB64url(s)), s);
  assert.deepEqual(
    { length: s.length, bytes: lib.fromB64url(s).length, roundTrips: lib.toB64url(lib.fromB64url(s)) === s },
    v.newRoomSecretShape,
  );
});

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/game2.git