code wiki / (root) / nx_media_crypt.nx

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 }