/* package argon2id implements the Argon2id password hashing algorithm. See: [[ https://datatracker.ietf.org/doc/rfc9106/ ]] */ package argon2id import "core:crypto/blake2b" import "core:encoding/endian" import "core:math/bits" import "core:mem" // Implementation based on the RFC, Monocypher (CC0-1.0), and the reference // code (CC0-1.0). // MAX_INPUT_SIZE is the mamximum size of the various inputs (password, // salt, secret, ad) in bytes. MAX_INPUT_SIZE :: (1 << 32) - 1 // MIN_PARALLELISM is the minimum allowed parallelism. MIN_PARALLELISM :: 1 // MAX_PARALLELISM is the maximum allowed parallelism. MAX_PARALLELISM :: (1 << 24) - 1 // MIN_TAG_SIZE is the minimum digest size in bytes. MIN_TAG_SIZE :: 4 // MAX_TAG_SIZE is the maximum digest size in bytes. MAX_TAG_SIZE :: (1 << 32) - 1 // RECOMMENDED_TAG_SIZE is the recommended tag size in bytes. RECOMMENTED_TAG_SIZE :: 32 // 256-bits // RECOMMENDNED_SALT_SIZE is the recommended salt size in bytes. RECOMMENDED_SALT_SIZE :: 16 // 128-bits @(private) V_RFC9106 :: 0x13 @(private) Y_ID :: 0x02 @(private) BLOCK_SIZE_BYTES :: 1024 @(private) BLOCK_SIZE_U64 :: 128 // PARAMS_RFC9106 is the first recommended "uniformly safe" parameter set // per RFC 9106. @(rodata) PARAMS_RFC9106 := Parameters{ memory_size = 2 * 1024 * 1024, // 2 GiB passes = 1, parallelism = 4, } // PARAMS_RFC9106_SMALL is the second recommended "uniformly safe" parameter // set per RFC 9106 tailored for memory constrained environments. @(rodata) PARAMS_RFC9106_SMALL := Parameters{ memory_size = 64 * 1024, // 64 MiB passes = 3, parallelism = 4, } // PARAMS_OWASP is one of the recommended parameter set from the OWASP // Password Storage Cheat Sheet (as of 2026/02). The cheat sheet contains // additional variations to this parameter set with various trade-offs // between `memory_size` and `passes` that are intended to provide // equivalent security. // // See: [[ https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html ]] @(rodata) PARAMS_OWASP := Parameters{ memory_size = 19 * 1024, // 19 MiB passes = 2, parallelism = 1, } // PARAMS_OWASP_SMALL is equivalent in strength to PARAMS_OWASP, but // trades off less memory use for more CPU usage. @(rodata) PARAMS_OWASP_SMALL := Parameters{ memory_size = 7 * 1024, // 7 MiB passes = 5, parallelism = 1, } // Parameters is an Argon2id parameter set. Parameters :: struct { memory_size: u32, // m (KiB) passes: u32, // t parallelism: u32, // p } @(private) Block :: [BLOCK_SIZE_U64]u64 // derive invokes Argon2id with the specified parameter set and inputs, // and outputs the derived key to dst. @(require_results) derive :: proc( parameters: ^Parameters, password: []byte, // P salt: []byte, // S dst: []byte, secret: []byte = nil, // K (aka `pepper`) ad: []byte = nil, // X sanitize := true, allocator := context.allocator, // Not temp as this can be large. ) -> mem.Allocator_Error #no_bounds_check { if u64(len(password)) > MAX_INPUT_SIZE { panic("crypto/argon2id: invalid password size") } if u64(len(salt)) > MAX_INPUT_SIZE { panic("crypto/argon2id: invalid salt size") } if u64(len(secret)) > MAX_INPUT_SIZE { panic("crypto/argon2id: invalid secret size") } if u64(len(ad)) > MAX_INPUT_SIZE { panic("crypto/argon2id: invalid ad size") } if l := u64(len(dst)); l > MAX_TAG_SIZE || l < MIN_TAG_SIZE { panic("crypto/argon2id: invalid dst size") } p, t, m := parameters.parallelism, parameters.passes, u64(parameters.memory_size) if p < MIN_PARALLELISM || p > MAX_PARALLELISM { panic("crypto/argon2id: invalid parallelism") } if t < 1 { panic("crypto/argon2id: invalid passes") } if m < 8 * u64(p) { panic("crypto/argon2id: insufficient memory size") } if m * BLOCK_SIZE_BYTES > u64(max(int)) { panic("crypto/argon2id: excessive memory size") } // Allocate the memory as m' 1024-byte blocks, where m' is derived as: // m' = 4 * p * floor (m / 4p) // // For p lanes, the memory is organized in a matrix B[i][j] of // blocks with p rows (lanes) and q = m' / p columns. m_ := 4 * u64(p) * (m / u64(4 * p)) b := mem.alloc_bytes_non_zeroed( int(m_) * BLOCK_SIZE_BYTES, alignment = mem.DEFAULT_PAGE_SIZE, allocator = allocator, ) or_return defer delete(b, allocator) block_buf: [BLOCK_SIZE_BYTES]byte = --- blocks := ([^]Block)(raw_data(b))[:m_] segment_size := u32(m_ / u64(p) / 4) lane_size := segment_size * 4 // Establish H_0 as the 64-byte value as shown below. If K, X, or S // has zero length, it is just absent, but its length field remains. // // H_0 = H^(64)(LE32(p) || LE32(T) || LE32(m) || LE32(t) || // LE32(v) || LE32(y) || LE32(length(P)) || P || // LE32(length(S)) || S || LE32(length(K)) || K || // LE32(length(X)) || X) { ctx: blake2b.Context blake2b.init(&ctx) blake2b_update_u32le(&ctx, u32(p)) blake2b_update_u32le(&ctx, u32(len(dst))) blake2b_update_u32le(&ctx, parameters.memory_size) blake2b_update_u32le(&ctx, t) blake2b_update_u32le(&ctx, V_RFC9106) blake2b_update_u32le(&ctx, Y_ID) blake2b_update_u32le(&ctx, u32(len(password))) blake2b.update(&ctx, password) blake2b_update_u32le(&ctx, u32(len(salt))) blake2b.update(&ctx, salt) blake2b_update_u32le(&ctx, u32(len(secret))) blake2b.update(&ctx, secret) blake2b_update_u32le(&ctx, u32(len(ad))) blake2b.update(&ctx, ad) h_0: [blake2b.DIGEST_SIZE+8]byte blake2b.final(&ctx, h_0[:blake2b.DIGEST_SIZE]) // Compute B[i][0] for all i ranging from (and including) 0 to (not // including) p. // // B[i][0] = H'^(1024)(H_0 || LE32(0) || LE32(i)) // // Compute B[i][1] for all i ranging from (and including) 0 to (not // including) p. // // B[i][1] = H'^(1024)(H_0 || LE32(1) || LE32(i)) for l in u32(0) ..< p { for i in u32(0) ..< 2 { endian.unchecked_put_u32le(h_0[blake2b.DIGEST_SIZE:], i) // LE32({0,1}) endian.unchecked_put_u32le(h_0[blake2b.DIGEST_SIZE+4:], l) // LE32(i) h_prime(block_buf[:], h_0[:]) blk := &blocks[l * lane_size + i] for j in 0 ..< BLOCK_SIZE_U64 { blk[j] = endian.unchecked_get_u64le(block_buf[j*8:]) } } } mem.zero_explicit(&h_0, size_of(h_0)) // No longer needed. } // Compute B[i][j] for all i ranging from (and including) 0 to (not // including) p and for all j ranging from (and including) 2 to (not // including) q. The computation MUST proceed slicewise // (Section 3.4): first, blocks from slice 0 are computed for all // lanes (in an arbitrary order of lanes), then blocks from slice 1 // are computed, etc. The block indices l and z are determined for // each i, j differently for Argon2d, Argon2i, and Argon2id. // // B[i][j] = G(B[i][j-1], B[l][z]) // // If the number of passes t is larger than 1, we repeat step 5. We // compute B[i][0] and B[i][j] for all i raging from (and including) // 0 to (not including) p and for all j ranging from (and including) // 1 to (not including) q. However, blocks are computed differently // as the old value is XORed with the new one: // // B[i][0] = G(B[i][q-1], B[l][z]) XOR B[i][0]; // B[i][j] = G(B[i][j-1], B[l][z]) XOR B[i][j]. constant_time := true // Start with constant time indexing. tmp, index_block: Block = ---, --- for pass in u32(0) ..< t { for slice in u32(0) ..< 4 { // The first slice of the first pass has blocks 0 and 1 // pre-filled. pass_offset: u32 = pass == 0 && slice == 0 ? 2 : 0 slice_offset := slice * segment_size // 3.4.1.3. Argon2id // // If the pass number is 0 and the slice number is 0 or 1, then compute // J_1 and J_2 as for Argon2i, else compute J_1 and J_2 as for Argon2d. if slice == 2 { constant_time = false } // Each segment can be processed in parallel, as long as // each iteration of the loop completes before proceeding // to the next. For simplicity we do this in serial // instead of using threads. for segment in u32(0) ..< u32(p) { index_ctr: u64 = 1 for block in pass_offset ..< segment_size { // Current and previous blocks (indexes, not pointers) lane_offset := segment * lane_size segment_start := lane_offset + slice_offset current := segment_start + block previous := segment_start - 1 switch { case block == 0 && slice_offset == 0: previous += lane_size case: previous += block } index_seed: u64 if constant_time { // 3.4.1.2. Argon2i // // For each segment, we do the following. First, we compute the value Z // as: // // Z= ( LE64(r) || LE64(l) || LE64(sl) || LE64(m') || // LE64(t) || LE64(y) ) // // Figure 11: Input to Compute J1,J2 in Argon2i // // where // // r: the pass number // l: the lane number // sl: the slice number // m': the total number of memory blocks // t: the total number of passes // y: the Argon2 type (0 for Argon2d, 1 for Argon2i, 2 for Argon2id) // // Then we compute: // // q/(128*SL) 1024-byte values // G(ZERO(1024),G(ZERO(1024), // Z || LE64(1) || ZERO(968) )), // G(ZERO(1024),G(ZERO(1024), // Z || LE64(2) || ZERO(968) )),... , // G(ZERO(1024),G(ZERO(1024), // Z || LE64(q/(128*SL)) || ZERO(968) )), // // which are partitioned into q/(SL) 8-byte values X, which are viewed // as X1||X2 and converted to J_1=int32(X1) and J_2=int32(X2). // // The values r, l, sl, m', t, y, and i are represented as 8 bytes in // little endian. if block == pass_offset || (block % 128) == 0 { mem.zero(&index_block, size_of(index_block)) index_block[0] = u64(pass) index_block[1] = u64(segment) index_block[2] = u64(slice) index_block[3] = u64(lane_size * p) index_block[4] = u64(t) // passes index_block[5] = Y_ID index_block[6] = index_ctr index_ctr += 1 copy(tmp[:], index_block[:]) g_rounds(&index_block) xor_block(&index_block, &tmp) copy(tmp[:], index_block[:]) g_rounds(&index_block) xor_block(&index_block, &tmp) } index_seed = index_block[block % 128] } else { // 3.4.1.1. Argon2d // // J_1 is given by the first 32 bits of block B[i][j-1], while J_2 is // given by the next 32 bits of block B[i][j-1]: // // J_1 = int32(extract(B[i][j-1], 0)) // J_2 = int32(extract(B[i][j-1], 1)) // // Figure 10: Deriving J1,J2 in Argon2d index_seed = blocks[previous][0] } // 3.4.2. Mapping J_1 and J_2 to Reference Block Index [l][z] // // The value of l = J_2 mod p gives the index of the lane from which the // block will be taken. For the first pass (r=0) and the first slice // (sl=0), the block is taken from the current lane. // // The set W contains the indices that are referenced according to the // following rules: // // 1. If l is the current lane, then W includes the indices of all // blocks in the last SL - 1 = 3 segments computed and finished, as // well as the blocks computed in the current segment in the current // pass excluding B[i][j-1]. // // 2. If l is not the current lane, then W includes the indices of all // blocks in the last SL - 1 = 3 segments computed and finished in // lane l. If B[i][j] is the first block of a segment, then the // very last index from W is excluded. // // Then take a block from W with a nonuniform distribution over [0, |W|) // using the following mapping: // // J_1 -> |W|(1 - J_1^2 / 2^(64)) // // Figure 12: Computing J1 // // To avoid floating point computation, the following approximation is // used: // // x = J_1^2 / 2^(32) // y = (|W| * x) / 2^(32) // zz = |W| - 1 - y // // Figure 13: Computing J1, Part 2 // // Then take the zz-th index from W; it will be the z value for the // reference block index [l][z]. next_slice: u32 = ((slice + 1) % 4) * segment_size window_start, nb_segments: u32 lane := u32(index_seed >> 32) % p switch { case pass == 0: nb_segments = slice if slice == 0 { lane = segment } case: window_start = next_slice nb_segments = 3 } window_size := nb_segments * segment_size if lane == segment { window_size += block - 1 } else if block == 0 { window_size += ~u32(0) } j1 := index_seed & 0xffffffff x := (j1 * j1) >> 32 y := (u64(window_size) * x) >> 32 z := (u64(window_size) - 1) - y ref := u32((u64(window_start) + z) % u64(lane_size)) reference: u32 = lane * lane_size + ref copy(tmp[:], blocks[previous][:]) xor_block(&tmp, &blocks[reference]) if pass == 0 { copy(blocks[current][:], tmp[:]) } else { xor_block(&blocks[current], &tmp) } g_rounds(&tmp) xor_block(&blocks[current], &tmp) } } } } mem.zero_explicit(&tmp, size_of(tmp)) mem.zero_explicit(&index_block, size_of(index_block)) // After t steps have been iterated, the final block C is computed // as the XOR of the last column: // // C = B[0][q-1] XOR B[1][q-1] XOR ... XOR B[p-1][q-1] idx := lane_size - 1 last_block := &blocks[idx] for _ in 1 ..< p { idx += lane_size next_block := &blocks[idx] xor_block(next_block, last_block) last_block = next_block } for v, i in last_block { endian.unchecked_put_u64le(block_buf[i*8:], v) } // The output tag is computed as H'^T(C). h_prime(dst, block_buf[:]) mem.zero_explicit(&block_buf, size_of(block_buf)) // Sanitize the working memory. While the RFC implies that this is // optional ("enable the memory-wiping option in the library call"), // the reference code defaults to enabling it. // // An opt-out is provided, as this can get somewhat expensive when // m gets large. if sanitize { mem.zero_explicit(raw_data(b), len(b)) } return nil } @(private) xor_block :: #force_inline proc(dst, src: ^Block) { for v, i in src { dst[i] ~= v } } @(private) blake2b_update_u32le :: #force_inline proc(ctx: ^blake2b.Context, i: u32) { tmp: [4]byte = --- endian.unchecked_put_u32le(tmp[:], i) blake2b.update(ctx, tmp[:]) mem.zero_explicit(&tmp, size_of(tmp)) // Probably overkill. } // 3.3. Variable-Length Hash Function H' // // Let V_i be a 64-byte block and W_i be its first 32 bytes. Then we // define function H' as follows: // // if T <= 64 // H'^T(A) = H^T(LE32(T)||A) // else // r = ceil(T/32)-2 // V_1 = H^(64)(LE32(T)||A) // V_2 = H^(64)(V_1) // ... // V_r = H^(64)(V_{r-1}) // V_{r+1} = H^(T-32*r)(V_{r}) // H'^T(X) = W_1 || W_2 || ... || W_r || V_{r+1} // // Figure 8: Function H' for Tag and Initial Block Computations @(private) h_prime :: proc(dst, src: []byte) { t := len(dst) ctx: blake2b.Context blake2b.init(&ctx, min(t, blake2b.DIGEST_SIZE)) blake2b_update_u32le(&ctx, u32(t)) blake2b.update(&ctx, src) blake2b.final(&ctx, dst) if t > 64 { r := u32((u64(t) + 31) >> 5) - 2 i: u32 = 1 off_in := 0 off_out := 32 for i < r { blake2b.init(&ctx, blake2b.DIGEST_SIZE) blake2b.update(&ctx, dst[off_in:off_in+64]) blake2b.final(&ctx, dst[off_out:]) i += 1 off_in += 32 off_out += 32 } blake2b.init(&ctx, t - int(32 * r)) blake2b.update(&ctx, dst[off_in:off_in+64]) blake2b.final(&ctx, dst[off_out:]) } } // GB(a, b, c, d) is defined as follows: // // a = (a + b + 2 * trunc(a) * trunc(b)) mod 2^(64) // d = (d XOR a) >>> 32 // c = (c + d + 2 * trunc(c) * trunc(d)) mod 2^(64) // b = (b XOR c) >>> 24 // // a = (a + b + 2 * trunc(a) * trunc(b)) mod 2^(64) // d = (d XOR a) >>> 16 // c = (c + d + 2 * trunc(c) * trunc(d)) mod 2^(64) // b = (b XOR c) >>> 63 // // Figure 19: Details of GB // // The modular additions in GB are combined with 64-bit multiplications. // Multiplications are the only difference from the original BLAKE2b // design. This choice is done to increase the circuit depth and thus // the running time of ASIC implementations, while having roughly the // same running time on CPUs thanks to parallelism and pipelining. @(private,require_results) gb :: #force_inline proc(a, b, c, d: u64) -> (u64, u64, u64, u64) { a, b, c, d := a, b, c, d trunc := #force_inline proc(v: u64) -> u64 { return u64(u32(v)) } a += b + ((trunc(a) * trunc(b)) << 1) d = bits.rotate_left64(d ~ a, 32) // >>> 32 c += d + ((trunc(c) * trunc(d)) << 1) b = bits.rotate_left64((b ~ c), 40) // >>> 24 a += b + ((trunc(a) * trunc(b)) << 1) d = bits.rotate_left64(d ~ a, 48) // >>> 16 c += d + ((trunc(c) * trunc(d)) << 1) b = bits.rotate_left64((b ~ c), 1) // >>> 63 return a, b, c, d } // 3.6. Permutation P // // Permutation P is based on the round function of BLAKE2b. The eight // 16-byte inputs S_0, S_1, ... , S_7 are viewed as a 4x4 matrix of // 64-bit words, where S_i = (v_{2*i+1} || v_{2*i}): // // v_0 v_1 v_2 v_3 // v_4 v_5 v_6 v_7 // v_8 v_9 v_10 v_11 // v_12 v_13 v_14 v_15 // // Figure 17: Matrix Element Labeling // // It works as follows: // // GB(v_0, v_4, v_8, v_12) // GB(v_1, v_5, v_9, v_13) // GB(v_2, v_6, v_10, v_14) // GB(v_3, v_7, v_11, v_15) // // GB(v_0, v_5, v_10, v_15) // GB(v_1, v_6, v_11, v_12) // GB(v_2, v_7, v_8, v_13) // GB(v_3, v_4, v_9, v_14) // // Figure 18: Feeding Matrix Elements to GB @(private,require_results) perm_p :: #force_inline proc(v_0, v_1, v_2, v_3, v_4, v_5, v_6, v_7, v_8, v_9, v_10, v_11, v_12, v_13, v_14, v_15: u64) -> (u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64, u64) { v_0, v_1, v_2, v_3, v_4, v_5, v_6, v_7, v_8, v_9, v_10, v_11, v_12, v_13, v_14, v_15 := v_0, v_1, v_2, v_3, v_4, v_5, v_6, v_7, v_8, v_9, v_10, v_11, v_12, v_13, v_14, v_15 v_0, v_4, v_8, v_12 = gb(v_0, v_4, v_8, v_12) v_1, v_5, v_9, v_13 = gb(v_1, v_5, v_9, v_13) v_2, v_6, v_10, v_14 = gb(v_2, v_6, v_10, v_14) v_3, v_7, v_11, v_15 = gb(v_3, v_7, v_11, v_15) v_0, v_5, v_10, v_15 = gb(v_0, v_5, v_10, v_15) v_1, v_6, v_11, v_12 = gb(v_1, v_6, v_11, v_12) v_2, v_7, v_8, v_13 = gb(v_2, v_7, v_8, v_13) v_3, v_4, v_9, v_14 = gb(v_3, v_4, v_9, v_14) return v_0, v_1, v_2, v_3, v_4, v_5, v_6, v_7, v_8, v_9, v_10, v_11, v_12, v_13, v_14, v_15 } // 3.5. Compression Function G // // The compression function G is built upon the BLAKE2b-based // transformation P. P operates on the 128-byte input, which can be // viewed as eight 16-byte registers: // // P(A_0, A_1, ... ,A_7) = (B_0, B_1, ... ,B_7) // // Figure 14: Blake Round Function P // // The compression function G(X, Y) operates on two 1024-byte blocks X // and Y. It first computes R = X XOR Y. Then R is viewed as an 8x8 // matrix of 16-byte registers R_0, R_1, ... , R_63. Then P is first // applied to each row, and then to each column to get Z: // // ( Q_0, Q_1, Q_2, ... , Q_7) <- P( R_0, R_1, R_2, ... , R_7) // ( Q_8, Q_9, Q_10, ... , Q_15) <- P( R_8, R_9, R_10, ... , R_15) // ... // (Q_56, Q_57, Q_58, ... , Q_63) <- P(R_56, R_57, R_58, ... , R_63) // ( Z_0, Z_8, Z_16, ... , Z_56) <- P( Q_0, Q_8, Q_16, ... , Q_56) // ( Z_1, Z_9, Z_17, ... , Z_57) <- P( Q_1, Q_9, Q_17, ... , Q_57) // ... // ( Z_7, Z_15, Z 23, ... , Z_63) <- P( Q_7, Q_15, Q_23, ... , Q_63) // // Figure 15: Core of Compression Function G @(private) g_rounds :: proc(b: ^Block) { for i := 0; i < 128; i += 16 { b[i], b[i+1], b[i+2], b[i+3], b[i+4], b[i+5], b[i+6], b[i+7], b[i+8], b[i+9], b[i+10], b[i+11], b[i+12], b[i+13], b[i+14], b[i+15] = perm_p(b[i], b[i+1], b[i+2], b[i+3], b[i+4], b[i+5], b[i+6], b[i+7], b[i+8], b[i+9], b[i+10], b[i+11], b[i+12], b[i+13], b[i+14], b[i+15]) } for i := 0; i < 16; i += 2 { b[i], b[i+1], b[i+16], b[i+17], b[i+32], b[i+33], b[i+48], b[i+49], b[i+64], b[i+65], b[i+80], b[i+81], b[i+96], b[i+97], b[i+112], b[i+113] = perm_p(b[i], b[i+1], b[i+16], b[i+17], b[i+32], b[i+33], b[i+48], b[i+49], b[i+64], b[i+65], b[i+80], b[i+81], b[i+96], b[i+97], b[i+112], b[i+113]) } }