nx_media_crypt.nx source
↩ module page · 97 lines · 5108 B
1// nx_media_crypt.nx -- SOVEREIGN E2EE media cipher = ChaCha20 (RFC 8439), pure Nishi. Chosen over a
2// hash-CTR construction because ChaCha20 is (a) a NAMED SOTA stream cipher (TLS 1.3, WireGuard, SFrame-
3// adjacent), (b) KAT-verifiable against RFC 8439 test vectors, and (c) WASM-VIABLE by construction --
4// flat 16 x u32 state, only add/xor/rotate, NO sha256, NO struct-pointer alloca (the pattern the WAT
5// backend can't yet lower; see nx_sha256_wasm.nx). Same SSOT compiles native (this gate) + into the client.
6//
7// E2EE role: block counter + nonce make each frame's keystream unique WITHOUT reusing the wire seq (which
8// the SFU rewrites). LENGTH-PRESERVING (XOR) -> the relay's dominant-speaker byte-energy still works and
9// the wire is unchanged. The 32-byte key is the invite link's #e= fragment (hash = never sent to a server;
10// relay has room_secret but not e2ee_secret -> cannot derive it -> genuinely relay-blind). license_tier: ORIGINAL
11import "nx_syscalls.nx"
12
13const MC_SCRATCH: i64 = 320 // mc_xform working set: s(128) + w(128) + keystream(64)
14
15func mc_rotl(x: i64, n: i64) -> i64 { return ((x << n) | (x >> (32 - n))) & 0xffffffff }
16func mc_u32le(b: *u8, o: i64) -> i64 {
17 return (b[o] & 0xff) | ((b[o+1] & 0xff) << 8) | ((b[o+2] & 0xff) << 16) | ((b[o+3] & 0xff) << 24) }
18func mc_wr32le(b: *u8, o: i64, v: i64) -> i64 {
19 b[o] = (v & 0xff) as u8
20 b[o+1] = ((v >> 8) & 0xff) as u8
21 b[o+2] = ((v >> 16) & 0xff) as u8
22 b[o+3] = ((v >> 24) & 0xff) as u8
23 return 0 }
24// one ChaCha quarter-round on state words a,b,c,d (in place)
25func mc_qr(w: *i64, a: i64, b: i64, c: i64, d: i64) -> i64 {
26 w[a] = (w[a] + w[b]) & 0xffffffff; w[d] = mc_rotl(w[d] ^ w[a], 16)
27 w[c] = (w[c] + w[d]) & 0xffffffff; w[b] = mc_rotl(w[b] ^ w[c], 12)
28 w[a] = (w[a] + w[b]) & 0xffffffff; w[d] = mc_rotl(w[d] ^ w[a], 8)
29 w[c] = (w[c] + w[d]) & 0xffffffff; w[b] = mc_rotl(w[b] ^ w[c], 7)
30 return 0 }
31// 64-byte keystream block for (key32, nonce12, block counter). s,w are caller scratch (16 i64 each).
32func mc_block(key32: *u8, nonce12: *u8, counter: i64, out64: *u8, s: *i64, w: *i64) -> i64 {
33 s[0] = 0x61707865; s[1] = 0x3320646e; s[2] = 0x79622d32; s[3] = 0x6b206574 // "expand 32-byte k"
34 var i: i64 = 0
35 while i < 8 { s[4 + i] = mc_u32le(key32, i * 4); i = i + 1 }
36 s[12] = counter & 0xffffffff
37 s[13] = mc_u32le(nonce12, 0); s[14] = mc_u32le(nonce12, 4); s[15] = mc_u32le(nonce12, 8)
38 i = 0
39 while i < 16 { w[i] = s[i]; i = i + 1 }
40 var r: i64 = 0
41 while r < 10 {
42 mc_qr(w, 0, 4, 8, 12); mc_qr(w, 1, 5, 9, 13); mc_qr(w, 2, 6, 10, 14); mc_qr(w, 3, 7, 11, 15)
43 mc_qr(w, 0, 5, 10, 15); mc_qr(w, 1, 6, 11, 12); mc_qr(w, 2, 7, 8, 13); mc_qr(w, 3, 4, 9, 14)
44 r = r + 1
45 }
46 i = 0
47 while i < 16 { mc_wr32le(out64, i * 4, (w[i] + s[i]) & 0xffffffff); i = i + 1 }
48 return 0 }
49
50// XOR data[0..len) in place with the ChaCha20 keystream for (key32, nonce12), starting at block ctr0.
51// Symmetric (twice = identity). Real use: ctr0 = 0. NO sys_mmap (WASM-viable) -- caller provides `scratch`
52// (>= MC_SCRATCH bytes): native passes a heap buffer, the wasm client passes a fixed linear-memory offset.
53func mc_xform(key32: *u8, nonce12: *u8, ctr0: i64, data: *u8, len: i64, scratch: *u8) -> i64 {
54 if len <= 0 { return 0 }
55 let s: *i64 = scratch as *i64 // [0..128) 16 i64
56 let w: *i64 = ((scratch as i64) + 128) as *i64 // [128..256) 16 i64
57 let ks: *u8 = ((scratch as i64) + 256) as *u8 // [256..320) keystream block
58 var off: i64 = 0
59 var ctr: i64 = ctr0
60 while off < len {
61 mc_block(key32, nonce12, ctr, ks, s, w)
62 var k: i64 = 0
63 while k < 64 { if off + k < len { data[off + k] = data[off + k] ^ ks[k] } k = k + 1 }
64 off = off + 64
65 ctr = ctr + 1
66 }
67 return 0 }
68
69// key = hex-decode of the link #e= fragment (64 hex chars -> 32 bytes; short input zero-pads). No hash ->
70// wasm-viable. The invite tool already produced hex(sha256(room||e2ee_secret)), so #e= IS 256-bit material.
71func mc_hexval(c: i64) -> i64 {
72 if c >= 48 { if c <= 57 { return c - 48 } } // 0-9
73 if c >= 97 { if c <= 102 { return c - 87 } } // a-f
74 if c >= 65 { if c <= 70 { return c - 55 } } // A-F
75 return 0 }
76func mc_key(hexk: *u8, hlen: i64, out32: *u8) -> i64 {
77 var i: i64 = 0
78 while i < 32 { out32[i] = 0 as u8; i = i + 1 }
79 i = 0
80 while i < 32 {
81 if i * 2 + 1 < hlen {
82 out32[i] = ((mc_hexval(hexk[i*2] & 0xff) << 4) | mc_hexval(hexk[i*2+1] & 0xff)) as u8
83 }
84 i = i + 1
85 }
86 return 0 }
87
88// 12-byte ChaCha nonce = sender_id(8) || le32(frame_counter). frame_counter is the SENDER's own monotonic
89// value (NOT the SFU-rewritten wire seq). out needs 12 bytes.
90func mc_nonce(sender: *u8, fctr: i64, out: *u8) -> i64 {
91 var i: i64 = 0
92 while i < 8 { out[i] = sender[i]; i = i + 1 }
93 out[8] = (fctr & 0xff) as u8
94 out[9] = ((fctr >> 8) & 0xff) as u8
95 out[10] = ((fctr >> 16) & 0xff) as u8
96 out[11] = ((fctr >> 24) & 0xff) as u8
97 return 12 }