code wiki / (root) / nx_media_transport.nx

nx_media_transport.nx source

↩ module page · 127 lines · 5664 B

1// nx_media_transport.nx -- sovereign secure-datagram media transport. 2// 3// THE missing layer the rest of the media stack already expects but 4// nobody had written yet: 5// * nx_audio_session.nx: "Playback: caller hands us wire bytes 6// (decoded by TRANSPORT from the remote peer)" <- this module. 7// * nx_call.nx: "zero new transport logic here ... nx_signaling.nx 8// -- SDP exchange + ICE candidates" <- nx_signaling never existed; 9// this is the bytes-on-the-wire half. 10// 11// It is COMPOSED from primitives that already ship -- no new crypto: 12// nx_aead (ChaCha20-Poly1305 seal/open, RFC 8439) 13// nx_udp_rv (native-lane UDP; rv64-numbered so the compiler's 14// syscall translation is correct) 15// NAT traversal is the already-proven nx_turn_relay (Arc 4 A.5.2). 16// 17// Sovereign equivalent of WebRTC's DTLS-SRTP: AEAD-sealed media 18// datagrams over UDP. Payload-agnostic: carries nx_voice_frame audio, 19// video frames, or chat bytes identically. 20// 21// === SENDER-SCOPED NONCE (fixes the multi-party keystream-reuse bug) === 22// Wire datagram: 23// [0..7] seq u64 LE, cleartext, authenticated as AAD 24// [8..11] sender_id u32 LE, cleartext, authenticated as AAD 25// [12..] ciphertext = AEAD-sealed payload (same length as payload) 26// [tail 16] Poly1305 tag 27// 28// nonce (12 B) = seq(8 LE) || sender_id(4 LE). In a multi-sender room 29// with ONE shared room key, two peers both start seq at 0/1 -- WITHOUT 30// the sender_id that is a catastrophic (key, nonce) reuse (ChaCha20 31// keystream + Poly1305 break). Binding the sender_id into the nonce 32// makes every (sender, seq) pair unique, so the E2EE-through-hub 33// property holds for the shared-room-key case. Each sender MUST use a 34// distinct sender_id and a strictly increasing seq. 35// 36// Key exchange is OUT OF SCOPE here: x25519 + ML-KEM (kyber) already 37// ship; this module takes the 32-byte session key they produce. 38// 39// PERF NOTE (honest): aead_seal/aead_open sys_mmap scratch per call. 40// Fine for proofs; sustained real-time should arena-hoist the AEAD 41// scratch (same as the P-256 / X25519 arena hoists). 42 43import "nx_aead.nx" 44import "nx_udp_rv.nx" 45 46const NX_MXPORT_SEQ_BYTES: i64 = 8 47const NX_MXPORT_SID_BYTES: i64 = 4 48const NX_MXPORT_HDR_BYTES: i64 = 12 // seq(8) + sender_id(4) = AAD + nonce material 49const NX_MXPORT_TAG_BYTES: i64 = 16 50const NX_MXPORT_OVERHEAD: i64 = 28 // 12 header + 16 tag 51 52func _mx_put_u64_le(buf: *u8, off: i64, v: i64) -> i64 { 53 var i: i64 = 0 54 while i < 8 { buf[off + i] = (v >> (i * 8)) & 0xff; i = i + 1 } 55 return 0 56} 57func _mx_get_u64_le(buf: *u8, off: i64) -> i64 { 58 var v: i64 = 0 59 var i: i64 = 0 60 while i < 8 { v = v | ((buf[off + i] & 0xff) << (i * 8)); i = i + 1 } 61 return v 62} 63func _mx_put_u32_le(buf: *u8, off: i64, v: i64) -> i64 { 64 buf[off]=v&0xff; buf[off+1]=(v>>8)&0xff; buf[off+2]=(v>>16)&0xff; buf[off+3]=(v>>24)&0xff 65 return 0 66} 67func _mx_get_u32_le(buf: *u8, off: i64) -> i64 { 68 return (buf[off]&0xff)|((buf[off+1]&0xff)<<8)|((buf[off+2]&0xff)<<16)|((buf[off+3]&0xff)<<24) 69} 70 71// nonce (12B) = seq(8 LE) || sender_id(4 LE). Unique per (sender, seq). 72func _mx_nonce(nonce: *u8, sender_id: i64, seq: i64) -> i64 { 73 _mx_put_u64_le(nonce, 0, seq) 74 _mx_put_u32_le(nonce, 8, sender_id) 75 return 0 76} 77 78// Seal `payload` into `wire` (caller buffer >= payload_len + 28). 79// Returns total wire length, or -1 if too small. 80func nx_mxport_seal(key: *u8, sender_id: i64, seq: i64, 81 payload: *u8, payload_len: i64, 82 wire: *u8, wire_cap: i64) -> i64 { 83 let total: i64 = NX_MXPORT_HDR_BYTES + payload_len + NX_MXPORT_TAG_BYTES 84 if total > wire_cap { return 0 - 1 } 85 _mx_put_u64_le(wire, 0, seq) 86 _mx_put_u32_le(wire, 8, sender_id) 87 let nonce: *u8 = sys_mmap(12) 88 _mx_nonce(nonce, sender_id, seq) 89 let ct: *u8 = (wire as i64 + NX_MXPORT_HDR_BYTES) as *u8 90 let tag: *u8 = (wire as i64 + NX_MXPORT_HDR_BYTES + payload_len) as *u8 91 aead_seal(key, nonce, wire, NX_MXPORT_HDR_BYTES, payload, payload_len, ct, tag) 92 return total 93} 94 95// Open a received wire datagram. On success writes plaintext to 96// `payload_out`, sets *payload_len_out, and returns the seq (>= 0). 97// On auth failure returns -1 (payload not written). 98func nx_mxport_open(key: *u8, wire: *u8, wire_len: i64, 99 payload_out: *u8, payload_len_out: *i64) -> i64 { 100 if wire_len < NX_MXPORT_OVERHEAD { return 0 - 1 } 101 let seq: i64 = _mx_get_u64_le(wire, 0) 102 let sender_id: i64 = _mx_get_u32_le(wire, 8) 103 let ct_len: i64 = wire_len - NX_MXPORT_HDR_BYTES - NX_MXPORT_TAG_BYTES 104 let nonce: *u8 = sys_mmap(12) 105 _mx_nonce(nonce, sender_id, seq) 106 let ct: *u8 = (wire as i64 + NX_MXPORT_HDR_BYTES) as *u8 107 let tag: *u8 = (wire as i64 + NX_MXPORT_HDR_BYTES + ct_len) as *u8 108 let rv: i64 = aead_open(key, nonce, wire, NX_MXPORT_HDR_BYTES, ct, ct_len, tag, payload_out) 109 if rv != 0 { return 0 - 1 } 110 *payload_len_out = ct_len 111 return seq 112} 113 114// Read the sender_id from a received wire datagram (cleartext header). 115func nx_mxport_sender(wire: *u8, wire_len: i64) -> i64 { 116 if wire_len < NX_MXPORT_HDR_BYTES { return 0 - 1 } 117 return _mx_get_u32_le(wire, 8) 118} 119 120// Convenience: seal + send over a bound UDP socket to dest16. 121func nx_mxport_send(fd: i64, dest16: *u8, key: *u8, sender_id: i64, seq: i64, 122 payload: *u8, payload_len: i64, 123 scratch: *u8, scratch_cap: i64) -> i64 { 124 let total: i64 = nx_mxport_seal(key, sender_id, seq, payload, payload_len, scratch, scratch_cap) 125 if total < 0 { return 0 - 1 } 126 return nx_udpr_send(fd, scratch, total, dest16) 127}