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}