code wiki / (root) / nx_chacha20_pure.nx

nx_chacha20_pure.nx source

↩ module page · 215 lines · 7305 B

1// nx_chacha20_pure.nx -- architecture-neutral ChaCha20 (arena-based). 2// 3// Arena-refactor arc piece 1 per 4// docs/NISHI_LANG_FRICTION_CATALOG.md#F5. Parallel to the original 5// nx_chacha20.nx (RV64-pinned via `import "nx_syscalls.nx"` for 6// sys_mmap). This variant takes `*NxArena` for all internal 7// scratch buffers and imports NO syscall layer -- composes 8// cleanly with x86_64-native sockets in one binary, proven by 9// bench/nx_chacha20_pure_x86_64_smoke.nx. 10// 11// Why parallel rather than in-place: existing downstream consumers 12// (nx_chacha20_poly1305, nx_tls13_record, nx_tls13_loopback_test) 13// pass through ~6 RV64+qemu smokes today. A single-turn in-place 14// refactor would break all of them simultaneously. The parallel- 15// file pattern lets each downstream consumer migrate one at a time 16// in a separate turn, with no smoke regression at any point. Once 17// all consumers migrate, the original nx_chacha20.nx becomes a 18// thin backwards-compat wrapper (or deletable). 19// 20// API differs from nx_chacha20 by adding `arena: *NxArena` as the 21// first parameter and renaming functions with `_pure` suffix so 22// the two files can be co-imported without symbol collision: 23// 24// chacha20_block(key, counter, nonce, out) 25// -> chacha20_block_pure(arena, key, counter, nonce, out) 26// 27// chacha20_encrypt(key, counter, nonce, in, n, out) 28// -> chacha20_encrypt_pure(arena, key, counter, nonce, in, n, out) 29// 30// Helpers (load_u32_le / store_u32_le / u32_mask / rotl32 / qr) 31// are duplicated with `pure_` prefix to avoid collision with the 32// original module's same-named helpers. 33// 34// Arena budget: chacha20_block_pure uses 2 * 128 = 256 bytes per 35// call (state + init snapshot). chacha20_encrypt_pure uses an 36// additional 64 bytes (per-block keystream output). Caller should 37// size the arena accordingly (256 + 64 = 320 bytes per encryption 38// call; for back-to-back blocks the arena can be reset between 39// calls). 40// 41// KAT verified: 42// RFC 8439 ยง2.3.2 keystream block byte-exact, identical to the 43// original nx_chacha20's KAT. Smoke at 44// bench/nx_chacha20_pure_x86_64_smoke.nx. 45// 46// Composes with: 47// - nx_arena_types (NxArena + nx_arena_alloc) 48// - Any architecture's syscall layer at the orchestrator boundary 49// (nx_syscalls.nx for RV64+qemu, nx_syscalls_x86_64.nx for 50// x86_64 native, etc.) 51// 52// nx_capability_claims: 53// needs: [arena_alloc, pointer_arithmetic] 54// provides: [chacha20_block_pure, chacha20_encrypt_pure] 55// safety: [no_syscall, no_floating_point, target_agnostic, 56// bit_equal_reproducible, 57// RFC_8439_Appendix_A1_KAT_VERIFIED] 58// verdict: [no_silent_failure] 59// license: INDEPENDENT_REDERIVE 60// kind: racing_crew_specialist 61// 62// license_tier: INDEPENDENT_REDERIVE 63// genealogy_id: international-research-sources/ietf/rfc_8439 64// lineage_id: nishi_chacha20_pure_arena_q10 65 66import "nx_arena_types.nx" 67 68// ---- Prefixed pure helpers (no collision with nx_chacha20.nx) ---- 69 70func pure_u32_mask(x: i64) -> i64 { 71 return x & 0xFFFFFFFF 72} 73 74func pure_rotl32(x: i64, r: i64) -> i64 { 75 let a: i64 = (x << r) & 0xFFFFFFFF 76 let b: i64 = (x & 0xFFFFFFFF) >> (32 - r) 77 return a | b 78} 79 80func pure_qr(state: *i64, a: i64, b: i64, c: i64, d: i64) -> i64 { 81 let va1: i64 = pure_u32_mask(state[a] + state[b]) 82 state[a] = va1 83 state[d] = pure_rotl32(state[d] ^ va1, 16) 84 85 let vc1: i64 = pure_u32_mask(state[c] + state[d]) 86 state[c] = vc1 87 state[b] = pure_rotl32(state[b] ^ vc1, 12) 88 89 let va2: i64 = pure_u32_mask(state[a] + state[b]) 90 state[a] = va2 91 state[d] = pure_rotl32(state[d] ^ va2, 8) 92 93 let vc2: i64 = pure_u32_mask(state[c] + state[d]) 94 state[c] = vc2 95 state[b] = pure_rotl32(state[b] ^ vc2, 7) 96 return 0 97} 98 99func pure_load_u32_le(buf: *u8, off: i64) -> i64 { 100 let b0: i64 = buf[off + 0] 101 let b1: i64 = buf[off + 1] 102 let b2: i64 = buf[off + 2] 103 let b3: i64 = buf[off + 3] 104 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24) 105} 106 107func pure_store_u32_le(buf: *u8, off: i64, v: i64) -> i64 { 108 buf[off + 0] = v & 0xFF 109 buf[off + 1] = (v >> 8) & 0xFF 110 buf[off + 2] = (v >> 16) & 0xFF 111 buf[off + 3] = (v >> 24) & 0xFF 112 return 0 113} 114 115// ---- One 64-byte ChaCha20 keystream block (arena-based) ---- 116// 117// Allocates two 128-byte buffers from the arena (state + init 118// snapshot). Returns 0 on success, -1 on arena OOM. 119func chacha20_block_pure( 120 arena: *NxArena, 121 key: *u8, counter: i64, nonce: *u8, 122 out: *u8 123) -> i64 { 124 let state_raw: *u8 = nx_arena_alloc(arena, 128, 8) 125 if state_raw == (0 as *u8) { return 0 - 1 } 126 let state: *i64 = state_raw as *i64 127 128 // "expand 32-byte k" constants. 129 state[0] = 0x61707865 130 state[1] = 0x3320646e 131 state[2] = 0x79622d32 132 state[3] = 0x6b206574 133 134 // Key: 8 x u32 LE. 135 state[4] = pure_load_u32_le(key, 0) 136 state[5] = pure_load_u32_le(key, 4) 137 state[6] = pure_load_u32_le(key, 8) 138 state[7] = pure_load_u32_le(key, 12) 139 state[8] = pure_load_u32_le(key, 16) 140 state[9] = pure_load_u32_le(key, 20) 141 state[10] = pure_load_u32_le(key, 24) 142 state[11] = pure_load_u32_le(key, 28) 143 144 state[12] = counter & 0xFFFFFFFF 145 146 state[13] = pure_load_u32_le(nonce, 0) 147 state[14] = pure_load_u32_le(nonce, 4) 148 state[15] = pure_load_u32_le(nonce, 8) 149 150 let init_raw: *u8 = nx_arena_alloc(arena, 128, 8) 151 if init_raw == (0 as *u8) { return 0 - 1 } 152 let init: *i64 = init_raw as *i64 153 var i: i64 = 0 154 while i < 16 { init[i] = state[i]; i = i + 1 } 155 156 // 10 double-rounds (20 total rounds). 157 var r: i64 = 0 158 while r < 10 { 159 pure_qr(state, 0, 4, 8, 12) 160 pure_qr(state, 1, 5, 9, 13) 161 pure_qr(state, 2, 6, 10, 14) 162 pure_qr(state, 3, 7, 11, 15) 163 pure_qr(state, 0, 5, 10, 15) 164 pure_qr(state, 1, 6, 11, 12) 165 pure_qr(state, 2, 7, 8, 13) 166 pure_qr(state, 3, 4, 9, 14) 167 r = r + 1 168 } 169 170 var j: i64 = 0 171 while j < 16 { 172 state[j] = pure_u32_mask(state[j] + init[j]) 173 j = j + 1 174 } 175 176 var k: i64 = 0 177 while k < 16 { 178 pure_store_u32_le(out, k * 4, state[k]) 179 k = k + 1 180 } 181 return 0 182} 183 184// ---- Stream-encrypt n bytes (XOR with keystream) ---- 185// 186// Each 64-byte chunk allocates fresh state from the arena via 187// chacha20_block_pure + a 64-byte keystream buffer. For long 188// messages the arena will fill quickly; callers should EITHER 189// pre-size the arena (~320 bytes per block) OR reset the arena 190// between chunks (nx_arena_reset). Returns 0 on success. 191func chacha20_encrypt_pure( 192 arena: *NxArena, 193 key: *u8, counter: i64, nonce: *u8, 194 in_bytes: *u8, n: i64, out: *u8 195) -> i64 { 196 var block_idx: i64 = 0 197 var pos: i64 = 0 198 while pos < n { 199 let ks_raw: *u8 = nx_arena_alloc(arena, 64, 8) 200 if ks_raw == (0 as *u8) { return 0 - 1 } 201 let rc: i64 = chacha20_block_pure(arena, key, counter + block_idx, nonce, ks_raw) 202 if rc < 0 { return rc } 203 let remain: i64 = n - pos 204 var take: i64 = 64 205 if remain < 64 { take = remain } 206 var i: i64 = 0 207 while i < take { 208 out[pos + i] = in_bytes[pos + i] ^ ks_raw[i] 209 i = i + 1 210 } 211 pos = pos + take 212 block_idx = block_idx + 1 213 } 214 return 0 215}