Skip to content

Commit dcc71e0

Browse files
SSD DDDclaude
authored andcommitted
test(verify): commit the phone/ verification suite as one reproducible gate
Forty waves of correctness proofs — every codec, every crypto property — lived in throwaway swiftc harnesses under the session scratchpad. Scratchpad is wiped between sessions, so a fresh checkout could re-prove NOTHING. "PROVEN requires reproduction" (SOUL Art. VIII) — a repo that cannot reproduce its own proofs is itself a defect. Fix: the six harnesses now live in smoke/harness/*.swift and smoke/verify.sh compiles each against the REAL production file (never a copy, so it cannot drift) and reports a single PASS/FAIL. 81 checks + the dispatcher-parity guard: AudioRED 23, VideoFEC 12, MeshCrypto room 12 / replay 11 / identity 15 / keychain 8, dispatcher parity. verify: 7 passed, 0 failed. Two defects surfaced while making it hermetic (the point of a real gate): * bare MeshCrypto() in the room/replay harnesses loads the device key from the Keychain; a freshly-built unsigned binary is not in the item's ACL, so SecItemCopyMatching blocks on a GUI SecurityAgent prompt that never arrives headless — it wedged the gate for 8 minutes with no output. Construct with an explicit ephemeral identity (room/replay are orthogonal to identity). * added a per-harness watchdog (macOS has no timeout(1)): a hung harness now reports FAIL(timeout) instead of a silent wedge — a probe against invisible state is the broken-ruler error. The two-endpoint rigs need two running apps and a camera, which a compile-and-run harness cannot stage; verify.sh names them in its footer so the split is documented, not forgotten. phone/ is hand-written, outside the golden pipeline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent bb97911 commit dcc71e0

8 files changed

Lines changed: 1169 additions & 0 deletions

File tree

.claude/skills/tri-net-development/SKILL.md

Lines changed: 593 additions & 0 deletions
Large diffs are not rendered by default.

smoke/harness/audio_red.swift

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Standalone verification for AudioRED (adaptive depth). Compiles the ACTUAL
2+
// production file (AudioRED.swift) plus this main — no re-implementation, so it
3+
// cannot drift. Round-trips through naked wire bytes exactly like the app.
4+
// swiftc AudioRED.swift main.swift -o /tmp/audio_red && /tmp/audio_red
5+
import Foundation
6+
7+
func frame(_ i: Int) -> Data { Data("OPUS_FRAME_\(i)".utf8) }
8+
9+
var failures = 0
10+
func check(_ cond: Bool, _ label: String) {
11+
print("\(cond ? "PASS" : "FAIL") \(label)")
12+
if !cond { failures += 1 }
13+
}
14+
15+
// Sender keeps a newest-first ring of the last `depth` frames and packs them;
16+
// `drop` is the set of dropped sequence indices; returns the frames the receiver played.
17+
func run(n: Int, drop: Set<Int>, depth: Int) -> [Data] {
18+
var rx = AudioREDReceiver()
19+
var ring: [Data] = []
20+
var played: [Data] = []
21+
for i in 0..<n {
22+
ring.insert(frame(i), at: 0)
23+
if ring.count > depth { ring.removeLast() }
24+
let pkt = AudioRED.pack(seq: UInt8(i & 0xFF), frames: ring)
25+
if drop.contains(i) { continue }
26+
guard let (s, frames) = AudioRED.parse(pkt) else { played.append(Data("PARSE_FAIL".utf8)); continue }
27+
played.append(contentsOf: rx.receive(seq: s, frames: frames))
28+
}
29+
return played
30+
}
31+
32+
print("== pack/parse round-trip (variable depth, incl max-len) ==")
33+
for frames in [[frame(1)], [frame(9), frame(8)], [frame(5), frame(4), frame(3)],
34+
[Data(repeating: 0xAB, count: 300), frame(2)]] {
35+
let (s, fs) = AudioRED.parse(AudioRED.pack(seq: 200, frames: frames))!
36+
check(s == 200 && fs == Array(frames.prefix(AudioRED.maxFrames)), "roundtrip depth=\(frames.count)")
37+
}
38+
check(AudioRED.parse(Data([0x01])) == nil, "truncated header rejected")
39+
check(AudioRED.parse(Data([0x01, 0x02, 0xFF, 0x00])) == nil, "declared frame past end rejected")
40+
check(AudioRED.parse(Data([0x01, 0x09])) == nil, "count > maxFrames rejected")
41+
42+
print("\n== depth 2 (baseline: survive 1 isolated loss) ==")
43+
check(run(n: 10, drop: [], depth: 2) == (0..<10).map(frame), "no loss -> all in order")
44+
check(run(n: 10, drop: [4], depth: 2) == (0..<10).map(frame), "1 isolated loss recovered")
45+
check(run(n: 10, drop: [4, 5], depth: 2) == [0,1,2,3,5,6,7,8,9].map(frame), "2 CONSECUTIVE -> only #5 recovered (1-deep)")
46+
check(run(n: 12, drop: [3, 7], depth: 2) == (0..<12).map(frame), "2 spaced losses both recovered")
47+
48+
print("\n== depth 3 (survive a 2-loss BURST — the new capability) ==")
49+
check(run(n: 10, drop: [], depth: 3) == (0..<10).map(frame), "no loss -> all in order")
50+
check(run(n: 10, drop: [4], depth: 3) == (0..<10).map(frame), "1 isolated loss recovered")
51+
check(run(n: 10, drop: [4, 5], depth: 3) == (0..<10).map(frame), "2 CONSECUTIVE losses BOTH recovered")
52+
check(run(n: 12, drop: [4, 5, 6], depth: 3) == [0,1,2,3,5,6,7,8,9,10,11].map(frame), "3 consecutive -> #5,#6 recovered, #4 lost (2-deep ceiling)")
53+
54+
print("\n== adaptive: depth changes MID-STREAM without breaking order ==")
55+
do {
56+
var rx = AudioREDReceiver(); var ring: [Data] = []; var played: [Data] = []
57+
for i in 0..<12 {
58+
let depth = i < 6 ? 2 : 3 // sender bumps depth at frame 6
59+
ring.insert(frame(i), at: 0); if ring.count > depth { ring.removeLast() }
60+
let pkt = AudioRED.pack(seq: UInt8(i), frames: ring)
61+
if i == 8 || i == 9 { continue } // a 2-burst while at depth 3
62+
let (s, fs) = AudioRED.parse(pkt)!
63+
played += rx.receive(seq: s, frames: fs)
64+
}
65+
check(played == (0..<12).map(frame), "depth 2->3 mid-call, a 2-burst at depth 3 fully recovered")
66+
}
67+
68+
print("\n== duplicate not played twice; late/reordered dropped ==")
69+
do {
70+
var rx = AudioREDReceiver(); var played: [Data] = []
71+
played += rx.receive(seq: 0, frames: [frame(0)])
72+
played += rx.receive(seq: 1, frames: [frame(1), frame(0)])
73+
played += rx.receive(seq: 1, frames: [frame(1), frame(0)]) // duplicate
74+
check(played == [frame(0), frame(1)], "seq 1 twice -> played once")
75+
let before = played.count
76+
for i in 2...5 { played += rx.receive(seq: UInt8(i), frames: [frame(i), frame(i-1)]) }
77+
let afterFwd = played.count
78+
played += rx.receive(seq: 3, frames: [frame(3), frame(2)]) // arrives late
79+
check(played.count == afterFwd, "seq 3 after seq 5 -> ignored")
80+
_ = before
81+
}
82+
83+
print("\n== u8 wraparound with a loss across 255->0 (depth 2) ==")
84+
do {
85+
var rx = AudioREDReceiver(); var played: [Data] = []
86+
played += rx.receive(seq: 254, frames: [frame(254), frame(253)])
87+
played += rx.receive(seq: 255, frames: [frame(255), frame(254)])
88+
// seq 0 dropped
89+
played += rx.receive(seq: 1, frames: [frame(257), frame(256)]) // 256==seq0, 257==seq1
90+
check(played == [frame(254), frame(255), frame(256), frame(257)], "loss at wrap recovered")
91+
}
92+
93+
print("\n== long outage resyncs immediately at any depth ==")
94+
func outageDrop(_ outage: Int, _ depth: Int) -> Int {
95+
var rx = AudioREDReceiver(); var ring: [Data] = []
96+
for i in 0..<20 { ring.insert(frame(i), at: 0); if ring.count > depth { ring.removeLast() }; _ = rx.receive(seq: UInt8(i & 0xFF), frames: ring) }
97+
for i in 20..<(20+outage) { ring.insert(frame(i), at: 0); if ring.count > depth { ring.removeLast() } }
98+
var dropped = 0
99+
for k in 0..<300 {
100+
let i = 20 + outage + k
101+
ring.insert(frame(i), at: 0); if ring.count > depth { ring.removeLast() }
102+
if rx.receive(seq: UInt8(i & 0xFF), frames: ring).isEmpty { dropped += 1 } else { break }
103+
}
104+
return dropped
105+
}
106+
for o in [130, 200, 255] { check(outageDrop(o, 3) <= 1, "outage \(o) (depth 3) resumes within 1 packet") }
107+
108+
print("\n== exhaustive: every interior single-loss position recovered, depth 2 AND 3 ==")
109+
var ok2 = true, ok3 = true
110+
for k in 1..<19 {
111+
if run(n: 20, drop: [k], depth: 2) != (0..<20).map(frame) { ok2 = false }
112+
if run(n: 20, drop: [k], depth: 3) != (0..<20).map(frame) { ok3 = false }
113+
}
114+
check(ok2 && ok3, "all interior single-loss positions recovered at depth 2 and depth 3")
115+
116+
print("\n\(failures == 0 ? "ALL PASS" : "\(failures) FAILURE(S)")")
117+
exit(failures == 0 ? 0 : 1)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Verifies identity signing + TOFU pinning + MITM detection in the real MeshCrypto.swift.
2+
// swiftc MeshCrypto.swift crypto_identity_main.swift -o /tmp/id && /tmp/id
3+
import Foundation
4+
import CryptoKit
5+
6+
UserDefaults.standard.removeObject(forKey: "trinetPeerPins") // clean slate per run
7+
8+
var fails = 0
9+
func check(_ c: Bool, _ l: String) { print("\(c ? "PASS" : "FAIL") \(l)"); if !c { fails += 1 } }
10+
11+
func peer() -> MeshCrypto { MeshCrypto(identity: Curve25519.Signing.PrivateKey()) }
12+
13+
print("== handshake is 162B and carries a valid identity signature ==")
14+
do {
15+
let a = peer()
16+
let hs = a.handshakePacket()
17+
check(hs.count == 162, "handshake is 162 bytes")
18+
check(a.isHandshake(hs), "recognized as a handshake")
19+
check(!a.isHandshake(hs.prefix(66)), "the old 66B format is NOT accepted")
20+
}
21+
22+
print("== two identity-bearing peers establish + pin each other ==")
23+
do {
24+
let a = peer(), b = peer()
25+
a.consumeHandshake(b.handshakePacket(), from: "10.0.0.2")
26+
b.consumeHandshake(a.handshakePacket(), from: "10.0.0.1")
27+
check(a.established && b.established, "both sessions up")
28+
check(a.peerIdentity == b.identityPub, "A pinned B's real identity")
29+
check(b.peerIdentity == a.identityPub, "B pinned A's real identity")
30+
check(!a.mitmDetected && !b.mitmDetected, "no MITM flagged")
31+
}
32+
33+
print("== MITM: a changed identity at a pinned peer is refused ==")
34+
do {
35+
let victimReceiver = peer()
36+
let honest = peer(), attacker = peer()
37+
victimReceiver.consumeHandshake(honest.handshakePacket(), from: "10.0.0.9") // TOFU-pin honest
38+
check(victimReceiver.established && !victimReceiver.mitmDetected, "first (honest) call pins + establishes")
39+
let v2 = peer() // simulate a later fresh session obj sharing the pin store
40+
v2.consumeHandshake(attacker.handshakePacket(), from: "10.0.0.9") // attacker at the SAME peer IP
41+
check(v2.mitmDetected, "different identity at pinned IP -> MITM detected")
42+
check(!v2.established, "MITM session refused")
43+
_ = honest
44+
}
45+
46+
print("== a tampered signature / wrong room is rejected ==")
47+
do {
48+
let a = peer(), b = peer()
49+
var hs = b.handshakePacket()
50+
hs[100] ^= 0xFF // flip a byte inside the signature
51+
a.consumeHandshake(hs, from: "10.0.1.1")
52+
check(!a.established, "bad signature -> no session")
53+
let c = peer(); c.room = "OFFICE"; let d = peer(); d.room = "HOME"
54+
c.consumeHandshake(d.handshakePacket(), from: "10.0.1.2")
55+
check(!c.established, "wrong room -> no session (room gate still holds)")
56+
}
57+
58+
print("== safety number is symmetric, deterministic, and 11 digits ==")
59+
do {
60+
let a = peer(), b = peer()
61+
let sab = MeshCrypto.safetyNumber(a.identityPub, b.identityPub)
62+
let sba = MeshCrypto.safetyNumber(b.identityPub, a.identityPub)
63+
check(sab == sba, "safety number is order-independent (both parties see the same)")
64+
check(sab.count == 11 && sab.allSatisfy { $0.isNumber }, "11-digit numeric code")
65+
let cKey = peer()
66+
check(MeshCrypto.safetyNumber(a.identityPub, cKey.identityPub) != sab, "different peer -> different code")
67+
}
68+
69+
print("\n\(fails == 0 ? "ALL PASS" : "\(fails) FAILURE(S)")")
70+
UserDefaults.standard.removeObject(forKey: "trinetPeerPins")
71+
exit(fails == 0 ? 0 : 1)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Verifies Keychain-backed identity storage + UserDefaults->Keychain migration in the
2+
// real MeshCrypto.swift. swiftc MeshCrypto.swift keychain_main.swift -o /tmp/kc && /tmp/kc
3+
import Foundation
4+
import CryptoKit
5+
import Security
6+
7+
func wipe() {
8+
SecItemDelete([kSecClass: kSecClassGenericPassword,
9+
kSecAttrService: MeshCrypto.kcService, kSecAttrAccount: MeshCrypto.kcAccount] as CFDictionary)
10+
UserDefaults.standard.removeObject(forKey: "trinetIdentityKeyV1")
11+
}
12+
13+
var fails = 0
14+
func check(_ c: Bool, _ l: String) { print("\(c ? "PASS" : "FAIL") \(l)"); if !c { fails += 1 } }
15+
16+
wipe()
17+
18+
print("== first run generates + stores in the Keychain; not in UserDefaults ==")
19+
let k1 = MeshCrypto.deviceIdentity()
20+
check(MeshCrypto.keychainLoad() == k1.rawRepresentation, "the key is now in the Keychain")
21+
check(UserDefaults.standard.string(forKey: "trinetIdentityKeyV1") == nil, "the key is NOT written to the plist")
22+
23+
print("== the identity PERSISTS: a second load returns the same key ==")
24+
let k2 = MeshCrypto.deviceIdentity()
25+
check(k1.rawRepresentation == k2.rawRepresentation, "deviceIdentity() is stable across calls (loaded from Keychain)")
26+
27+
print("== raw Keychain save/load round-trip ==")
28+
wipe()
29+
let blob = Data((0..<32).map { UInt8($0 &* 7 &+ 3) })
30+
check(MeshCrypto.keychainSave(blob), "SecItemAdd succeeds")
31+
check(MeshCrypto.keychainLoad() == blob, "SecItemCopyMatching returns the same bytes")
32+
33+
print("== MIGRATION: a legacy UserDefaults key moves to the Keychain and the plist is scrubbed ==")
34+
wipe()
35+
let legacy = Curve25519.Signing.PrivateKey()
36+
UserDefaults.standard.set(legacy.rawRepresentation.base64EncodedString(), forKey: "trinetIdentityKeyV1")
37+
let migrated = MeshCrypto.deviceIdentity()
38+
check(migrated.rawRepresentation == legacy.rawRepresentation, "the SAME identity is returned (no new key -> pins stay valid)")
39+
check(MeshCrypto.keychainLoad() == legacy.rawRepresentation, "it now lives in the Keychain")
40+
check(UserDefaults.standard.string(forKey: "trinetIdentityKeyV1") == nil, "the plaintext plist copy is removed")
41+
42+
wipe()
43+
print("\n\(fails == 0 ? "ALL PASS" : "\(fails) FAILURE(S)")")
44+
exit(fails == 0 ? 0 : 1)

smoke/harness/crypto_replay.swift

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Verifies anti-replay in the ACTUAL MeshCrypto.swift.
2+
// swiftc MeshCrypto.swift crypto_replay_main.swift -o /tmp/rp && /tmp/rp
3+
// Construct with an EXPLICIT ephemeral identity, never bare MeshCrypto(identity: Curve25519.Signing.PrivateKey()): the default
4+
// initializer loads the device key from the Keychain, and a freshly-built unsigned binary
5+
// is not in the item's ACL, so SecItemCopyMatching blocks on a GUI SecurityAgent prompt
6+
// forever in a headless run. Replay is orthogonal to identity, so this weakens nothing.
7+
import Foundation
8+
import CryptoKit
9+
10+
var fails = 0
11+
func check(_ c: Bool, _ l: String) { print("\(c ? "PASS" : "FAIL") \(l)"); if !c { fails += 1 } }
12+
13+
// A live session between two peers (same room).
14+
func pair() -> (MeshCrypto, MeshCrypto) {
15+
let a = MeshCrypto(identity: Curve25519.Signing.PrivateKey()); let b = MeshCrypto(identity: Curve25519.Signing.PrivateKey())
16+
a.consumeHandshake(b.handshakePacket()); b.consumeHandshake(a.handshakePacket())
17+
return (a, b)
18+
}
19+
20+
print("== a replayed datagram is rejected; fresh ones pass ==")
21+
do {
22+
let (a, b) = pair()
23+
let w1 = a.seal(Data("chat: hello".utf8))!
24+
check(b.unseal(w1) == Data("chat: hello".utf8), "first delivery decodes")
25+
check(b.unseal(w1) == nil, "SAME datagram replayed -> rejected")
26+
check(b.unseal(w1) == nil, "replayed again -> still rejected")
27+
let w2 = a.seal(Data("chat: world".utf8))!
28+
check(b.unseal(w2) == Data("chat: world".utf8), "a different datagram still passes")
29+
}
30+
31+
print("== no false positives across a long distinct stream ==")
32+
do {
33+
let (a, b) = pair()
34+
var ok = true
35+
for i in 0..<5000 {
36+
let w = a.seal(Data("frame \(i)".utf8))!
37+
if b.unseal(w) != Data("frame \(i)".utf8) { ok = false; break }
38+
}
39+
check(ok, "5000 distinct sealed frames all delivered (each nonce unique -> no false drop)")
40+
}
41+
42+
print("== acceptNonce is a pure fresh/replay predicate ==")
43+
do {
44+
let c = MeshCrypto(identity: Curve25519.Signing.PrivateKey())
45+
let n = Data((0..<12).map { UInt8($0) })
46+
check(c.acceptNonce(n) == true, "novel nonce -> fresh")
47+
check(c.acceptNonce(n) == false, "same nonce -> replay")
48+
check(c.acceptNonce(Data((0..<12).map { UInt8($0 + 1) })) == true, "another novel nonce -> fresh")
49+
}
50+
51+
print("== HONEST bound: the window is finite; a replay after eviction succeeds ==")
52+
do {
53+
let c = MeshCrypto(identity: Curve25519.Signing.PrivateKey())
54+
let victim = Data((100..<112).map { UInt8($0) })
55+
check(c.acceptNonce(victim) == true, "victim nonce recorded")
56+
check(c.acceptNonce(victim) == false, "immediate replay caught")
57+
for i in 0..<9000 { _ = c.acceptNonce(Data([UInt8(i & 0xFF), UInt8((i >> 8) & 0xFF)] + (0..<10).map { UInt8($0) })) }
58+
check(c.acceptNonce(victim) == true, "after > window fresh nonces, the victim is EVICTED -> replay no longer caught (documented bound)")
59+
}
60+
61+
print("\n\(fails == 0 ? "ALL PASS" : "\(fails) FAILURE(S)")")
62+
exit(fails == 0 ? 0 : 1)

smoke/harness/crypto_room.swift

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Verifies room-bound auth in the ACTUAL MeshCrypto.swift.
2+
// swiftc MeshCrypto.swift crypto_room_main.swift -o /tmp/cr && /tmp/cr
3+
// Construct with an EXPLICIT ephemeral identity, never bare MeshCrypto(identity: Curve25519.Signing.PrivateKey()): the default
4+
// initializer loads the device key from the Keychain, and a freshly-built unsigned binary
5+
// is not in the item's ACL, so SecItemCopyMatching blocks on a GUI SecurityAgent prompt
6+
// forever in a headless run. Room auth is orthogonal to identity, so this weakens nothing.
7+
import Foundation
8+
import CryptoKit
9+
10+
var fails = 0
11+
func check(_ c: Bool, _ l: String) { print("\(c ? "PASS" : "FAIL") \(l)"); if !c { fails += 1 } }
12+
13+
// Two peers exchange handshakes; returns whether BOTH installed a session.
14+
func handshakeEstablishes(roomA: String, roomB: String) -> Bool {
15+
let a = MeshCrypto(identity: Curve25519.Signing.PrivateKey()); a.room = roomA
16+
let b = MeshCrypto(identity: Curve25519.Signing.PrivateKey()); b.room = roomB
17+
let pa = a.handshakePacket(), pb = b.handshakePacket()
18+
a.consumeHandshake(pb)
19+
b.consumeHandshake(pa)
20+
return a.established && b.established
21+
}
22+
23+
print("== handshake auth is bound to the room secret ==")
24+
check(handshakeEstablishes(roomA: "OFFICE", roomB: "OFFICE"), "same room -> session established")
25+
check(!handshakeEstablishes(roomA: "OFFICE", roomB: "HOME"), "different room -> NO session (HMAC rejected)")
26+
check(!handshakeEstablishes(roomA: "OFFICE", roomB: ""), "room vs open-lobby -> NO session")
27+
check(handshakeEstablishes(roomA: "", roomB: ""), "empty room both -> session (open lobby)")
28+
29+
print("\n== round-trip seal/open works once a same-room session is up ==")
30+
do {
31+
let a = MeshCrypto(identity: Curve25519.Signing.PrivateKey()); a.room = "TEAM"; let b = MeshCrypto(identity: Curve25519.Signing.PrivateKey()); b.room = "TEAM"
32+
a.consumeHandshake(b.handshakePacket()); b.consumeHandshake(a.handshakePacket())
33+
let msg = Data("hello mesh".utf8)
34+
let sealed = a.seal(msg)
35+
check(sealed != nil && b.unseal(sealed!) == msg, "A seals -> B opens the same bytes")
36+
}
37+
38+
print("\n== BACKWARD COMPAT: empty room reproduces the legacy keys BIT-FOR-BIT ==")
39+
let psk = SymmetricKey(data: SHA256.hash(data: Data("tri-net-psk-v1".utf8)))
40+
// legacy handshake key was the PSK directly:
41+
do {
42+
let pub = Data((0..<32).map { UInt8($0) })
43+
let legacy = HMAC<SHA256>.authenticationCode(for: pub, using: psk)
44+
let now = HMAC<SHA256>.authenticationCode(for: pub, using: MeshCrypto.handshakeAuthKey(room: ""))
45+
check(Data(legacy) == Data(now), "handshakeAuthKey(\"\") == legacy PSK key")
46+
check(Data(HMAC<SHA256>.authenticationCode(for: pub, using: MeshCrypto.handshakeAuthKey(room: "X"))) != Data(legacy),
47+
"handshakeAuthKey(room) != legacy (room actually changes the key)")
48+
}
49+
// legacy invite key: HKDF(psk, salt="trios-mesh/v1/invite", info="invite-auth")
50+
do {
51+
let legacy = SymmetricKey(data: HKDF<SHA256>.deriveKey(
52+
inputKeyMaterial: psk, salt: Data("trios-mesh/v1/invite".utf8),
53+
info: Data("invite-auth".utf8), outputByteCount: 32))
54+
let payload = Data("name\n1.2.3.4\n\n123".utf8)
55+
let lm = HMAC<SHA256>.authenticationCode(for: payload, using: legacy)
56+
let nm = HMAC<SHA256>.authenticationCode(for: payload, using: MeshCrypto.inviteAuthKey(room: ""))
57+
check(Data(lm) == Data(nm), "inviteAuthKey(\"\") == legacy invite key")
58+
check(Data(HMAC<SHA256>.authenticationCode(for: payload, using: MeshCrypto.inviteAuthKey(room: "OFFICE"))) != Data(lm),
59+
"inviteAuthKey(room) != legacy")
60+
check(Data(HMAC<SHA256>.authenticationCode(for: payload, using: MeshCrypto.inviteAuthKey(room: "A"))) !=
61+
Data(HMAC<SHA256>.authenticationCode(for: payload, using: MeshCrypto.inviteAuthKey(room: "B"))),
62+
"different rooms -> different invite MAC")
63+
}
64+
// legacy group key: HKDF(psk, salt="trios-mesh/v1/conference", info="group-aead")
65+
do {
66+
let legacy = SymmetricKey(data: HKDF<SHA256>.deriveKey(
67+
inputKeyMaterial: psk, salt: Data("trios-mesh/v1/conference".utf8),
68+
info: Data("group-aead".utf8), outputByteCount: 32))
69+
let msg = Data("group frame".utf8)
70+
let sealed = try! ChaChaPoly.seal(msg, using: MeshCrypto.groupAuthKey(room: "")).combined
71+
let opened = try? ChaChaPoly.open(try! ChaChaPoly.SealedBox(combined: sealed), using: legacy)
72+
check(opened == msg, "groupAuthKey(\"\") == legacy conference key (legacy peer can still decrypt)")
73+
// a room-bound group key cannot be opened with the legacy key
74+
let sealedRoom = try! ChaChaPoly.seal(msg, using: MeshCrypto.groupAuthKey(room: "SECRET")).combined
75+
let openedRoom = try? ChaChaPoly.open(try! ChaChaPoly.SealedBox(combined: sealedRoom), using: legacy)
76+
check(openedRoom == nil, "room-bound group key is NOT decryptable with the legacy key")
77+
}
78+
79+
print("\n\(fails == 0 ? "ALL PASS" : "\(fails) FAILURE(S)")")
80+
exit(fails == 0 ? 0 : 1)

0 commit comments

Comments
 (0)