code wiki / (root) / nx_argon2id.nx

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}