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}