nx_argon2id.nx source
↩ module page · 436 lines · 17348 B
1// nx_argon2id.nx -- RFC 9106 Argon2id orchestrator (p=1, single-lane).
2//
3// The substrate's memory-hard password hashing primitive. Composes:
4//
5// - nx_blake2b.nx BLAKE2b-512 core hash
6// - nx_blake2b_long.nx RFC 9106 sec 3.4 variable-length H'
7// - nx_argon2_block.nx RFC 9106 sec 3.5/3.6 G compression + P
8//
9// ===== Why Argon2id =============================================
10//
11// Argon2id (RFC 9106) is the WINNER of the Password Hashing
12// Competition (PHC). Intentionally hostile to AI / GPU / ASIC
13// attackers via memory hardness (each G call reads two 1024-byte
14// blocks from a configurably-sized matrix), integer-multiply
15// within GB (punishes weak 32-bit-mul hardware), and hybrid
16// Argon2i (first half pass) + Argon2d (rest) indexing for
17// side-channel + tradeoff resistance both.
18//
19// ===== Scope of this version (v1) ================================
20//
21// - Parallelism: p = 1 ONLY (single-lane). Multi-lane queued.
22// - Type: Argon2id only (Argon2d / Argon2i variants share 90%
23// of code; will land as a single flag if needed).
24// - Version: 0x13 (RFC 9106) only.
25// - Memory: m_kib >= 8, multiple of 4 (sync-point alignment).
26// - Iterations: t >= 1.
27// - Output: tag_len in [4, 2^32 - 1].
28//
29// ===== Verdict scope ============================================
30//
31// - All component bricks (BLAKE2b, H', G) are KAT-verified
32// or algebraic-property-verified.
33// - Argon2id END-TO-END output is NOT yet cross-validated
34// against argon2_ref C reference. Sensitivity tests prove
35// orchestrator wiring is correct + every parameter influences
36// the tag.
37// - Per cardinal feedback-honest-perf-verdict-no-aspirational-
38// claims: verdict = NOT_YET_EVALUATED until cross-validation
39// lands. Don't yet trust for production password storage.
40//
41// ===== Caller-owns-memory: 16-arg NishiLang limit ===============
42//
43// The function-arg limit forces all scratch through a single
44// NxArgon2idCtx struct. Caller allocates the struct (144 bytes)
45// + the 18 buffer pointers it carries, then passes ONE pointer.
46//
47// license_tier: ORIGINAL
48
49// nx_safety_envelope:
50// intended_use: memory-hard password hashing
51// sil_target: SIL3 (security-critical primitive)
52// evidence: [determinism_two_runs_same_tag,
53// salt_sensitivity_one_byte_changes_tag,
54// password_sensitivity_one_byte_changes_tag,
55// memory_param_sensitivity,
56// iteration_param_sensitivity]
57// verdict: NOT_YET_EVALUATED (cross-validate vs
58// argon2_ref C output)
59
60import "nx_blake2b.nx"
61import "nx_blake2b_long.nx"
62import "nx_argon2_block.nx"
63const NX_MAGIC_1024: i64 = 1024
64const NX_MAGIC_1100: i64 = 1100
65const NX_MAGIC_4294967295: i64 = 4294967295
66
67// ===== Verdict ====================================================
68
69const NX_AR2_OK: i64 = 0
70const NX_AR2_BAD_PARALLELISM: i64 = 1
71const NX_AR2_BAD_MEMORY: i64 = 2
72const NX_AR2_BAD_ITERATIONS: i64 = 3
73const NX_AR2_BAD_TAG_LEN: i64 = 4
74const NX_AR2_BAD_ARG: i64 = 5
75const NX_AR2_VERDICT_N: i64 = 6
76
77func nx_argon2id_verdict_name(v: i64) -> *u8 {
78 if v == NX_AR2_OK { return "OK" }
79 if v == NX_AR2_BAD_PARALLELISM { return "BAD_PARALLELISM" }
80 if v == NX_AR2_BAD_MEMORY { return "BAD_MEMORY" }
81 if v == NX_AR2_BAD_ITERATIONS { return "BAD_ITERATIONS" }
82 if v == NX_AR2_BAD_TAG_LEN { return "BAD_TAG_LEN" }
83 if v == NX_AR2_BAD_ARG { return "BAD_ARG" }
84 return "UNKNOWN"
85}
86
87// ===== Constants ==================================================
88
89const NX_AR2_BLOCK_BYTES: i64 = 1024
90const NX_AR2_VERSION: i64 = 0x13 // RFC 9106
91const NX_AR2_TYPE_ID: i64 = 2
92
93const NX_AR2_SYNC_POINTS: i64 = 4 // slices per lane
94
95// ===== Context bundle ============================================
96//
97// The 16-arg NishiLang function limit forces all scratch through
98// one struct. Caller allocates each buffer at the size noted in
99// the comment.
100
101struct NxArgon2idCtx {
102 memory_blocks: *u8, // m_kib * NX_MAGIC_1024 bytes
103 h0_buf: *u8, // 64 bytes
104 prepend_buf: *u8, // NX_MAGIC_1024 + 4 + spare (>=NX_MAGIC_1100)
105 prev_buf: *u8, // 64
106 curr_buf: *u8, // 64
107 zero_block: *u8, // NX_MAGIC_1024 (pre-zeroed; mmap default)
108 z_buf: *u8, // NX_MAGIC_1024 (working area for Z)
109 tmp_block: *u8, // NX_MAGIC_1024
110 addr_block: *u8, // NX_MAGIC_1024
111 final_block: *u8, // NX_MAGIC_1024
112 h0_input: *u8, // p_len + s_len + 32 + spare (>=NX_MAGIC_1024)
113 b2b_ctx: *NxBlake2b, // NX_BLAKE2B_CTX_BYTES (104)
114 b2b_buf: *u8, // 128
115 b2b_sv: *i64, // 16 i64
116 b2b_sm: *i64, // 16 i64
117 g_r: *i64, // 128 i64 (NX_MAGIC_1024 bytes)
118 g_rs: *i64, // 128 i64
119 g_col: *i64, // 16 i64
120}
121
122const NX_ARGON2ID_CTX_BYTES: i64 = 144 // 18 * 8
123
124// ===== Helpers ====================================================
125
126func _ar2_write_le32(buf: *u8, off: i64, v: i64) -> i64 {
127 buf[off + 0] = (v & 255) as u8
128 buf[off + 1] = ((v >> 8) & 255) as u8
129 buf[off + 2] = ((v >> 16) & 255) as u8
130 buf[off + 3] = ((v >> 24) & 255) as u8
131 return 0
132}
133
134func _ar2_write_le64(buf: *u8, off: i64, v: i64) -> i64 {
135 buf[off + 0] = (v & 255) as u8
136 buf[off + 1] = ((v >> 8) & 255) as u8
137 buf[off + 2] = ((v >> 16) & 255) as u8
138 buf[off + 3] = ((v >> 24) & 255) as u8
139 buf[off + 4] = ((v >> 32) & 255) as u8
140 buf[off + 5] = ((v >> 40) & 255) as u8
141 buf[off + 6] = ((v >> 48) & 255) as u8
142 buf[off + 7] = ((v >> 56) & 255) as u8
143 return 0
144}
145
146func _ar2_load_u64_le(buf: *u8, off: i64) -> i64 {
147 let b0: i64 = (buf[off + 0] as i64) & 255
148 let b1: i64 = (buf[off + 1] as i64) & 255
149 let b2: i64 = (buf[off + 2] as i64) & 255
150 let b3: i64 = (buf[off + 3] as i64) & 255
151 let b4: i64 = (buf[off + 4] as i64) & 255
152 let b5: i64 = (buf[off + 5] as i64) & 255
153 let b6: i64 = (buf[off + 6] as i64) & 255
154 let b7: i64 = (buf[off + 7] as i64) & 255
155 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
156 | (b4 << 32) | (b5 << 40) | (b6 << 48) | (b7 << 56)
157}
158
159func _ar2_block_xor_into(dst: *u8, src: *u8) -> i64 {
160 var i: i64 = 0
161 while i < NX_AR2_BLOCK_BYTES {
162 dst[i] = (dst[i] as i64 ^ src[i] as i64) as u8
163 i = i + 1
164 }
165 return 0
166}
167
168func _ar2_block_copy(dst: *u8, src: *u8) -> i64 {
169 var i: i64 = 0
170 while i < NX_AR2_BLOCK_BYTES {
171 dst[i] = src[i]
172 i = i + 1
173 }
174 return 0
175}
176
177func _ar2_block_at(memory: *u8, block_idx: i64) -> *u8 {
178 return ((memory as i64) + block_idx * NX_AR2_BLOCK_BYTES) as *u8
179}
180
181// ===== Map J_1 -> reference offset (RFC 9106 sec 3.3) ============
182//
183// NishiLang i64 `>>` is ARITHMETIC shift (sign-extending). For
184// 32x32 multiplication of unsigned u32s, the 64-bit product can
185// overflow signed i64 to negative; an arithmetic `>>` then fills
186// with 1s instead of 0s, yielding the wrong x. Use a logical
187// shift via mask.
188
189func _ar2_lshr32(x: i64) -> i64 {
190 // Unsigned right-shift by 32: keep low 64 bits, mask away
191 // sign-extended 1s. Equivalent to (x as u64) >> 32.
192 let lo32_mask: i64 = NX_MAGIC_4294967295
193 return (x >> 32) & lo32_mask
194}
195
196func _ar2_map_index(j1: i64, pool_size: i64) -> i64 {
197 let j1_mask: i64 = j1 & NX_MAGIC_4294967295
198 let prod: i64 = j1_mask * j1_mask
199 let x: i64 = _ar2_lshr32(prod)
200 let prod2: i64 = pool_size * x
201 let y: i64 = _ar2_lshr32(prod2)
202 return pool_size - 1 - y
203}
204
205// ===== H_0 derivation (uses ctx for blake scratch) ===============
206
207func _ar2_h0(ctx: *NxArgon2idCtx,
208 password: *u8, p_len: i64,
209 salt: *u8, s_len: i64,
210 parallelism: i64, tag_len: i64,
211 m_kib: i64, t_iters: i64) -> i64 {
212 var off: i64 = 0
213 _ar2_write_le32(ctx.h0_input, off, parallelism); off = off + 4
214 _ar2_write_le32(ctx.h0_input, off, tag_len); off = off + 4
215 _ar2_write_le32(ctx.h0_input, off, m_kib); off = off + 4
216 _ar2_write_le32(ctx.h0_input, off, t_iters); off = off + 4
217 _ar2_write_le32(ctx.h0_input, off, NX_AR2_VERSION); off = off + 4
218 _ar2_write_le32(ctx.h0_input, off, NX_AR2_TYPE_ID); off = off + 4
219
220 _ar2_write_le32(ctx.h0_input, off, p_len); off = off + 4
221 var i: i64 = 0
222 while i < p_len { ctx.h0_input[off + i] = password[i]; i = i + 1 }
223 off = off + p_len
224
225 _ar2_write_le32(ctx.h0_input, off, s_len); off = off + 4
226 i = 0
227 while i < s_len { ctx.h0_input[off + i] = salt[i]; i = i + 1 }
228 off = off + s_len
229
230 // Empty K + X (v1 scope).
231 _ar2_write_le32(ctx.h0_input, off, 0); off = off + 4
232 _ar2_write_le32(ctx.h0_input, off, 0); off = off + 4
233
234 return nx_blake2b_hash(ctx.h0_input, off,
235 0 as *u8, 0,
236 ctx.h0_buf, 64,
237 ctx.b2b_ctx, ctx.b2b_buf, ctx.b2b_sv, ctx.b2b_sm)
238}
239
240// ===== Address-block generator (Argon2i indexing) ================
241
242func _ar2_gen_address_block(ctx: *NxArgon2idCtx,
243 pass_r: i64, slice_sl: i64,
244 m_kib: i64, t_iters: i64,
245 address_ctr: i64) -> i64 {
246 // Build Z (1024 bytes), pre-zero it.
247 var i: i64 = 0
248 while i < NX_AR2_BLOCK_BYTES { ctx.z_buf[i] = 0 as u8; i = i + 1 }
249 _ar2_write_le64(ctx.z_buf, 0, pass_r)
250 _ar2_write_le64(ctx.z_buf, 8, 0) // lane = 0 (p=1)
251 _ar2_write_le64(ctx.z_buf, 16, slice_sl)
252 _ar2_write_le64(ctx.z_buf, 24, m_kib)
253 _ar2_write_le64(ctx.z_buf, 32, t_iters)
254 _ar2_write_le64(ctx.z_buf, 40, NX_AR2_TYPE_ID)
255 _ar2_write_le64(ctx.z_buf, 48, address_ctr)
256
257 // tmp = G(zero_block, z_buf)
258 let v1: i64 = nx_argon2_g(ctx.zero_block, ctx.z_buf, ctx.tmp_block,
259 ctx.g_r, ctx.g_rs, ctx.g_col)
260 if v1 != NX_A2B_OK { return NX_AR2_BAD_ARG }
261 // addr_block = G(zero_block, tmp)
262 let v2: i64 = nx_argon2_g(ctx.zero_block, ctx.tmp_block, ctx.addr_block,
263 ctx.g_r, ctx.g_rs, ctx.g_col)
264 if v2 != NX_A2B_OK { return NX_AR2_BAD_ARG }
265 return NX_AR2_OK
266}
267
268// ===== Main entry point ===========================================
269//
270// nx_argon2id_hash(ctx, password, p_len, salt, s_len, parallelism,
271// tag_len, m_kib, t_iters, out_tag) -> verdict
272//
273// 10 args -- under the 16-arg limit.
274
275func nx_argon2id_hash(ctx: *NxArgon2idCtx,
276 password: *u8, p_len: i64,
277 salt: *u8, s_len: i64,
278 parallelism: i64,
279 tag_len: i64,
280 m_kib: i64,
281 t_iters: i64,
282 out_tag: *u8) -> i64 {
283 // ---- validate ----
284 if ctx == (0 as *NxArgon2idCtx) { return NX_AR2_BAD_ARG }
285 if parallelism != 1 { return NX_AR2_BAD_PARALLELISM }
286 if m_kib < 8 { return NX_AR2_BAD_MEMORY }
287 if (m_kib % 4) != 0 { return NX_AR2_BAD_MEMORY }
288 if t_iters < 1 { return NX_AR2_BAD_ITERATIONS }
289 if tag_len < 4 { return NX_AR2_BAD_TAG_LEN }
290 if p_len < 0 { return NX_AR2_BAD_ARG }
291 if s_len < 0 { return NX_AR2_BAD_ARG }
292 if out_tag == (0 as *u8) { return NX_AR2_BAD_ARG }
293
294 // Ensure zero_block is 1024 zero bytes (mmap-default but
295 // explicit re-zero for callers reusing the buffer).
296 var zz: i64 = 0
297 while zz < NX_AR2_BLOCK_BYTES { ctx.zero_block[zz] = 0 as u8; zz = zz + 1 }
298
299 // ---- 1. Compute H_0 ----
300 let v_h0: i64 = _ar2_h0(ctx, password, p_len, salt, s_len,
301 parallelism, tag_len, m_kib, t_iters)
302 if v_h0 != NX_BLAKE2B_OK { return NX_AR2_BAD_ARG }
303
304 // ---- 2. B[0] = H'(1024, H_0 || le32(0) || le32(0)) ----
305 // ---- 2. B[1] = H'(1024, H_0 || le32(1) || le32(0)) ----
306 // Overwrite h0_input with H_0 || le32(i) || le32(lane).
307 var kk: i64 = 0
308 while kk < 64 {
309 ctx.h0_input[kk] = ctx.h0_buf[kk]
310 kk = kk + 1
311 }
312 // Zero bytes 64..71 explicitly (mmap default but be defensive
313 // since ctx may be re-used across calls).
314 var zk: i64 = 64
315 while zk < 72 { ctx.h0_input[zk] = 0 as u8; zk = zk + 1 }
316
317 let b00_ptr: *u8 = _ar2_block_at(ctx.memory_blocks, 0)
318 if nx_blake2b_long(ctx.h0_input, 72,
319 b00_ptr, NX_AR2_BLOCK_BYTES,
320 ctx.prepend_buf, ctx.prev_buf, ctx.curr_buf,
321 ctx.b2b_ctx, ctx.b2b_buf,
322 ctx.b2b_sv, ctx.b2b_sm) != NX_B2BL_OK {
323 return NX_AR2_BAD_ARG
324 }
325
326 // Flip byte 64 to 1 for B[1]; bytes 65..71 stay 0.
327 ctx.h0_input[64] = 1 as u8
328
329 let b01_ptr: *u8 = _ar2_block_at(ctx.memory_blocks, 1)
330 if nx_blake2b_long(ctx.h0_input, 72,
331 b01_ptr, NX_AR2_BLOCK_BYTES,
332 ctx.prepend_buf, ctx.prev_buf, ctx.curr_buf,
333 ctx.b2b_ctx, ctx.b2b_buf,
334 ctx.b2b_sv, ctx.b2b_sm) != NX_B2BL_OK {
335 return NX_AR2_BAD_ARG
336 }
337
338 // ---- 3. Fill loop ----
339 let segment_len: i64 = m_kib / NX_AR2_SYNC_POINTS
340 let lane_len: i64 = m_kib
341
342 var pass: i64 = 0
343 while pass < t_iters {
344 var slice: i64 = 0
345 while slice < NX_AR2_SYNC_POINTS {
346 // Argon2id: i-indexing for pass 0 / slice in {0,1};
347 // d-indexing otherwise.
348 var use_i_indexing: i64 = 0
349 if pass == 0 {
350 if slice < 2 { use_i_indexing = 1 }
351 }
352
353 var address_ctr: i64 = 1
354 if use_i_indexing == 1 {
355 _ar2_gen_address_block(ctx, pass, slice, m_kib, t_iters,
356 address_ctr)
357 }
358
359 var start_offset: i64 = 0
360 if pass == 0 {
361 if slice == 0 { start_offset = 2 }
362 }
363
364 var i: i64 = start_offset
365 while i < segment_len {
366 let cur_idx: i64 = slice * segment_len + i
367 // prev_idx wraps to lane_len - 1 at lane-start
368 // (pass > 0, slice 0, position 0). In pass 0 we never
369 // reach cur_idx == 0 because start_offset = 2 in that
370 // slice; the wrap is purely a pass > 0 affordance.
371 var prev_idx: i64 = cur_idx - 1
372 if prev_idx < 0 { prev_idx = lane_len - 1 }
373
374 var j1: i64 = 0
375 if use_i_indexing == 1 {
376 let pos_in_block: i64 = i % 128
377 if i > 0 {
378 if pos_in_block == 0 {
379 address_ctr = address_ctr + 1
380 _ar2_gen_address_block(ctx, pass, slice,
381 m_kib, t_iters,
382 address_ctr)
383 }
384 }
385 let w: i64 = _ar2_load_u64_le(ctx.addr_block, pos_in_block * 8)
386 j1 = w & NX_MAGIC_4294967295
387 } else {
388 let prev_ptr: *u8 = _ar2_block_at(ctx.memory_blocks, prev_idx)
389 let w: i64 = _ar2_load_u64_le(prev_ptr, 0)
390 j1 = w & NX_MAGIC_4294967295
391 }
392
393 // Map j1 to reference block.
394 var pool_size: i64 = 0
395 var pool_start: i64 = 0
396 if pass == 0 {
397 pool_size = cur_idx - 1
398 pool_start = 0
399 } else {
400 pool_size = lane_len - segment_len + i - 1
401 pool_start = ((slice + 1) % NX_AR2_SYNC_POINTS) * segment_len
402 }
403 if pool_size < 1 { pool_size = 1 }
404 let z_rel: i64 = _ar2_map_index(j1, pool_size)
405 var ref_idx: i64 = pool_start + z_rel
406 if ref_idx >= lane_len { ref_idx = ref_idx - lane_len }
407
408 let prev_ptr: *u8 = _ar2_block_at(ctx.memory_blocks, prev_idx)
409 let ref_ptr: *u8 = _ar2_block_at(ctx.memory_blocks, ref_idx)
410 let cur_ptr: *u8 = _ar2_block_at(ctx.memory_blocks, cur_idx)
411 if pass == 0 {
412 nx_argon2_g(prev_ptr, ref_ptr, cur_ptr,
413 ctx.g_r, ctx.g_rs, ctx.g_col)
414 } else {
415 nx_argon2_g(prev_ptr, ref_ptr, ctx.tmp_block,
416 ctx.g_r, ctx.g_rs, ctx.g_col)
417 _ar2_block_xor_into(cur_ptr, ctx.tmp_block)
418 }
419
420 i = i + 1
421 }
422 slice = slice + 1
423 }
424 pass = pass + 1
425 }
426
427 // ---- 4. Final block = B[m_kib - 1] (single-lane) ----
428 let last_ptr: *u8 = _ar2_block_at(ctx.memory_blocks, m_kib - 1)
429 _ar2_block_copy(ctx.final_block, last_ptr)
430
431 // ---- 5. Tag = H'(tag_len, final_block) ----
432 return nx_blake2b_long(ctx.final_block, NX_AR2_BLOCK_BYTES,
433 out_tag, tag_len,
434 ctx.prepend_buf, ctx.prev_buf, ctx.curr_buf,
435 ctx.b2b_ctx, ctx.b2b_buf, ctx.b2b_sv, ctx.b2b_sm)
436}