From c2958b2a9c2981675812046b40de5a687c3080a9 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Tue, 14 Apr 2026 12:45:31 +0300 Subject: [PATCH] crypto: argon2 (fixes #24389) --- vlib/crypto/README.md | 15 +- vlib/crypto/argon2/argon2.v | 687 +++++++++++++++++++++++++++++++ vlib/crypto/argon2/argon2_test.v | 164 ++++++++ 3 files changed, 865 insertions(+), 1 deletion(-) create mode 100644 vlib/crypto/argon2/argon2.v create mode 100644 vlib/crypto/argon2/argon2_test.v diff --git a/vlib/crypto/README.md b/vlib/crypto/README.md index 7c14535dd..2461ad866 100644 --- a/vlib/crypto/README.md +++ b/vlib/crypto/README.md @@ -102,4 +102,17 @@ fn decode_payload(token string) !JwtPayload { payload := json.decode(JwtPayload, base64.url_decode_str(token_split[1]))! return payload } -``` \ No newline at end of file +``` + +### Argon2 Password Hashing + +```v +import crypto.argon2 + +fn main() { + hash := argon2.generate_from_password('correct horse battery staple'.bytes())! + println(hash) + + argon2.compare_hash_and_password('correct horse battery staple'.bytes(), hash.bytes())! +} +``` diff --git a/vlib/crypto/argon2/argon2.v b/vlib/crypto/argon2/argon2.v new file mode 100644 index 000000000..110ede8b6 --- /dev/null +++ b/vlib/crypto/argon2/argon2.v @@ -0,0 +1,687 @@ +// Copyright (c) 2026 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +// Based off: https://github.com/golang/crypto/tree/master/argon2 +// Package argon2 implements the Argon2 password hashing and key derivation +// functions described in RFC 9106. +module argon2 + +import crypto.blake2b +import crypto.hmac +import crypto.rand +import encoding.base64 +import encoding.binary +import math.bits + +pub const version = u32(0x13) + +pub const default_time = u32(3) +pub const default_memory = u32(64 * 1024) +pub const default_threads = u8(4) +pub const default_key_len = u32(32) +pub const default_salt_len = 16 + +const argon2_d = 0 +const argon2_i = 1 +const argon2_id = 2 +const block_length = 128 +const block_bytes = 1024 +const sync_points = u32(4) + +// Params controls Argon2 password hashing output. +pub struct Params { +pub: + time u32 + memory u32 + threads u8 + key_len u32 + salt_len int +} + +struct InvalidTimeError { + Error + time u32 +} + +fn (err InvalidTimeError) msg() string { + return 'the number of rounds ${err.time} must be greater than 0' +} + +struct InvalidThreadsError { + Error + threads u8 +} + +fn (err InvalidThreadsError) msg() string { + return 'the parallelism degree ${err.threads} must be greater than 0' +} + +struct InvalidKeyLengthError { + Error + length u32 +} + +fn (err InvalidKeyLengthError) msg() string { + return 'the output length ${err.length} must be greater than 0' +} + +struct InvalidSaltLengthError { + Error + length int +} + +fn (err InvalidSaltLengthError) msg() string { + return 'the salt length ${err.length} must be greater than 0' +} + +struct InvalidHashError { + Error + reason string +} + +fn (err InvalidHashError) msg() string { + return err.reason +} + +// default_params returns the recommended default Argon2id password hashing parameters. +pub fn default_params() Params { + return Params{ + time: default_time + memory: default_memory + threads: default_threads + key_len: default_key_len + salt_len: default_salt_len + } +} + +// key derives a key from `password` and `salt` using Argon2i. +pub fn key(password []u8, salt []u8, time u32, memory u32, threads u8, key_len u32) ![]u8 { + return derive_key(argon2_i, password, salt, []u8{}, []u8{}, time, memory, threads, + key_len) +} + +// d_key derives a key from `password` and `salt` using Argon2d. +pub fn d_key(password []u8, salt []u8, time u32, memory u32, threads u8, key_len u32) ![]u8 { + return derive_key(argon2_d, password, salt, []u8{}, []u8{}, time, memory, threads, + key_len) +} + +// id_key derives a key from `password` and `salt` using Argon2id. +pub fn id_key(password []u8, salt []u8, time u32, memory u32, threads u8, key_len u32) ![]u8 { + return derive_key(argon2_id, password, salt, []u8{}, []u8{}, time, memory, threads, + key_len) +} + +// generate_from_password hashes `password` with default Argon2id parameters +// and returns a PHC-formatted encoded string. +pub fn generate_from_password(password []u8) !string { + return generate_from_password_with_params(password, default_params()) +} + +// generate_from_password_with_params hashes `password` with Argon2id using `params` +// and returns a PHC-formatted encoded string. +pub fn generate_from_password_with_params(password []u8, params Params) !string { + normalized := normalize_params(params) + if normalized.salt_len <= 0 { + return InvalidSaltLengthError{ + length: normalized.salt_len + } + } + salt := rand.bytes(normalized.salt_len)! + hash := derive_key(argon2_id, password, salt, []u8{}, []u8{}, normalized.time, normalized.memory, + normalized.threads, normalized.key_len)! + return encode_hash(argon2_id, salt, hash, normalized.time, normalized.memory, normalized.threads) +} + +// compare_hash_and_password verifies a PHC-formatted Argon2 hash against `password`. +pub fn compare_hash_and_password(password []u8, encoded_hash []u8) ! { + decoded := decode_hash(encoded_hash.bytestr())! + other_hash := derive_key(decoded.mode, password, decoded.salt, []u8{}, []u8{}, decoded.time, + decoded.memory, decoded.threads, u32(decoded.hash.len))! + if !hmac.equal(other_hash, decoded.hash) { + return error('mismatched hash and password') + } +} + +struct DecodedHash { + mode int + time u32 + memory u32 + threads u8 + salt []u8 + hash []u8 +} + +fn derive_key(mode int, password []u8, salt []u8, secret []u8, data []u8, time u32, memory u32, threads u8, key_len u32) ![]u8 { + if time < 1 { + return InvalidTimeError{ + time: time + } + } + if threads < 1 { + return InvalidThreadsError{ + threads: threads + } + } + if key_len < 1 { + return InvalidKeyLengthError{ + length: key_len + } + } + threads_u32 := u32(threads) + effective_memory := normalize_memory(memory, threads_u32) + h0 := init_hash(password, salt, secret, data, time, effective_memory, threads_u32, + key_len, mode)! + mut blocks := init_blocks(h0, effective_memory, threads_u32)! + process_blocks(mut blocks, time, effective_memory, threads_u32, mode) + return extract_key(mut blocks, effective_memory, threads_u32, key_len) +} + +fn normalize_params(params Params) Params { + threads := if params.threads == 0 { default_threads } else { params.threads } + memory := if params.memory == 0 { default_memory } else { params.memory } + return Params{ + time: if params.time == 0 { default_time } else { params.time } + memory: normalize_memory(memory, u32(threads)) + threads: threads + key_len: if params.key_len == 0 { default_key_len } else { params.key_len } + salt_len: if params.salt_len == 0 { default_salt_len } else { params.salt_len } + } +} + +@[inline] +fn normalize_memory(memory u32, threads u32) u32 { + mut effective_memory := memory / (sync_points * threads) * (sync_points * threads) + if effective_memory < 2 * sync_points * threads { + effective_memory = 2 * sync_points * threads + } + return effective_memory +} + +fn init_hash(password []u8, salt []u8, secret []u8, data []u8, time u32, memory u32, threads u32, key_len u32, mode int) ![]u8 { + mut params := []u8{len: 24, init: 0} + mut tmp := []u8{len: 4, init: 0} + mut d := blake2b.new512()! + binary.little_endian_put_u32_at(mut params, threads, 0) + binary.little_endian_put_u32_at(mut params, key_len, 4) + binary.little_endian_put_u32_at(mut params, memory, 8) + binary.little_endian_put_u32_at(mut params, time, 12) + binary.little_endian_put_u32_at(mut params, version, 16) + binary.little_endian_put_u32_at(mut params, u32(mode), 20) + d.write(params)! + binary.little_endian_put_u32(mut tmp, u32(password.len)) + d.write(tmp)! + d.write(password)! + binary.little_endian_put_u32(mut tmp, u32(salt.len)) + d.write(tmp)! + d.write(salt)! + binary.little_endian_put_u32(mut tmp, u32(secret.len)) + d.write(tmp)! + d.write(secret)! + binary.little_endian_put_u32(mut tmp, u32(data.len)) + d.write(tmp)! + d.write(data)! + mut h0 := []u8{len: blake2b.size512 + 8, init: 0} + sum := d.checksum() + copy(mut h0[..blake2b.size512], sum) + return h0 +} + +fn init_blocks(h0 []u8, memory u32, threads u32) ![]u64 { + mut block := []u8{len: block_bytes, init: 0} + mut blocks := []u64{len: int(memory) * block_length, init: u64(0)} + mut h0_block := h0.clone() + for lane := u32(0); lane < threads; lane++ { + start := lane * (memory / threads) + binary.little_endian_put_u32_at(mut h0_block, lane, blake2b.size512 + 4) + binary.little_endian_put_u32_at(mut h0_block, 0, blake2b.size512) + block = blake2b_hash(block_bytes, h0_block)! + set_block(mut blocks, start, block) + binary.little_endian_put_u32_at(mut h0_block, 1, blake2b.size512) + block = blake2b_hash(block_bytes, h0_block)! + set_block(mut blocks, start + 1, block) + } + return blocks +} + +fn process_blocks(mut blocks []u64, time u32, memory u32, threads u32, mode int) { + lanes := memory / threads + segments := lanes / sync_points + for pass := u32(0); pass < time; pass++ { + for slice := u32(0); slice < sync_points; slice++ { + for lane := u32(0); lane < threads; lane++ { + process_segment(mut blocks, pass, slice, lane, time, memory, lanes, segments, + threads, mode) + } + } + } +} + +fn process_segment(mut blocks []u64, pass u32, slice u32, lane u32, time u32, memory u32, lanes u32, segments u32, threads u32, mode int) { + mut addresses := [block_length]u64{} + mut input := [block_length]u64{} + zero := [block_length]u64{} + if mode == argon2_i || (mode == argon2_id && pass == 0 && slice < sync_points / 2) { + input[0] = u64(pass) + input[1] = u64(lane) + input[2] = u64(slice) + input[3] = u64(memory) + input[4] = u64(time) + input[5] = u64(mode) + } + mut index := u32(0) + if pass == 0 && slice == 0 { + index = 2 + if mode == argon2_i || mode == argon2_id { + input[6]++ + process_block(mut addresses, input, zero) + process_block(mut addresses, addresses, zero) + } + } + mut offset := lane * lanes + slice * segments + index + for index < segments { + mut prev := offset - 1 + if index == 0 && slice == 0 { + prev += lanes + } + mut random := u64(0) + if mode == argon2_i || (mode == argon2_id && pass == 0 && slice < sync_points / 2) { + if index % u32(block_length) == 0 { + input[6]++ + process_block(mut addresses, input, zero) + process_block(mut addresses, addresses, zero) + } + random = addresses[int(index % u32(block_length))] + } else { + random = blocks[block_offset(prev)] + } + new_offset := index_alpha(random, lanes, segments, threads, pass, slice, lane, + index) + process_block_xor_in_place(mut blocks, offset, prev, new_offset) + index++ + offset++ + } +} + +fn extract_key(mut blocks []u64, memory u32, threads u32, key_len u32) ![]u8 { + lanes := memory / threads + last_block := memory - 1 + last_base := block_offset(last_block) + for lane := u32(0); lane < threads - 1; lane++ { + base := block_offset(lane * lanes + lanes - 1) + for i := 0; i < block_length; i++ { + blocks[last_base + i] ^= blocks[base + i] + } + } + mut block := []u8{len: block_bytes, init: 0} + for i := 0; i < block_length; i++ { + binary.little_endian_put_u64_at(mut block, blocks[last_base + i], i * 8) + } + return blake2b_hash(int(key_len), block) +} + +@[inline] +fn block_offset(index u32) int { + return int(index) * block_length +} + +fn set_block(mut blocks []u64, index u32, data []u8) { + base := block_offset(index) + for i := 0; i < block_length; i++ { + blocks[base + i] = binary.little_endian_u64_at(data, i * 8) + } +} + +fn blake2b_hash(out_len int, input []u8) ![]u8 { + mut prefix := []u8{len: 4, init: 0} + binary.little_endian_put_u32(mut prefix, u32(out_len)) + mut initial := prefix.clone() + initial << input + if out_len <= blake2b.size512 { + mut d := blake2b.new_digest(u8(out_len), []u8{})! + d.write(initial)! + return d.checksum() + } + mut out := []u8{len: out_len, init: 0} + mut block := blake2b.sum512(initial) + copy(mut out[..32], block[..32]) + mut pos := 32 + for out_len - pos > blake2b.size512 { + block = blake2b.sum512(block) + copy(mut out[pos..pos + 32], block[..32]) + pos += 32 + } + remaining := out_len - pos + mut d := blake2b.new_digest(u8(remaining), []u8{})! + d.write(block)! + copy(mut out[pos..], d.checksum()) + return out +} + +fn process_block(mut out [block_length]u64, in1 [block_length]u64, in2 [block_length]u64) { + process_block_generic(mut out, in1, in2, false) +} + +fn process_block_generic(mut out [block_length]u64, in1 [block_length]u64, in2 [block_length]u64, xor bool) { + mut t := [block_length]u64{} + for i := 0; i < block_length; i++ { + t[i] = in1[i] ^ in2[i] + } + for i := 0; i < block_length; i += 16 { + blamka_generic(mut t, i + 0, i + 1, i + 2, i + 3, i + 4, i + 5, i + 6, i + 7, + i + 8, i + 9, i + 10, i + 11, i + 12, i + 13, i + 14, i + 15) + } + for i := 0; i < block_length / 8; i += 2 { + blamka_generic(mut t, i, i + 1, 16 + i, 16 + i + 1, 32 + i, 32 + i + 1, 48 + i, + 48 + i + 1, 64 + i, 64 + i + 1, 80 + i, 80 + i + 1, 96 + i, 96 + i + 1, 112 + i, + 112 + i + 1) + } + for i := 0; i < block_length; i++ { + if xor { + out[i] ^= in1[i] ^ in2[i] ^ t[i] + } else { + out[i] = in1[i] ^ in2[i] ^ t[i] + } + } +} + +fn process_block_xor_in_place(mut blocks []u64, out_index u32, in1_index u32, in2_index u32) { + mut t := [block_length]u64{} + out_base := block_offset(out_index) + in1_base := block_offset(in1_index) + in2_base := block_offset(in2_index) + for i := 0; i < block_length; i++ { + t[i] = blocks[in1_base + i] ^ blocks[in2_base + i] + } + for i := 0; i < block_length; i += 16 { + blamka_generic(mut t, i + 0, i + 1, i + 2, i + 3, i + 4, i + 5, i + 6, i + 7, + i + 8, i + 9, i + 10, i + 11, i + 12, i + 13, i + 14, i + 15) + } + for i := 0; i < block_length / 8; i += 2 { + blamka_generic(mut t, i, i + 1, 16 + i, 16 + i + 1, 32 + i, 32 + i + 1, 48 + i, + 48 + i + 1, 64 + i, 64 + i + 1, 80 + i, 80 + i + 1, 96 + i, 96 + i + 1, 112 + i, + 112 + i + 1) + } + for i := 0; i < block_length; i++ { + blocks[out_base + i] ^= blocks[in1_base + i] ^ blocks[in2_base + i] ^ t[i] + } +} + +fn blamka_generic(mut block [block_length]u64, a int, b int, c int, d int, e int, f int, g int, h int, i int, j int, k int, l int, m int, n int, o int, p int) { + mut v00 := block[a] + mut v01 := block[b] + mut v02 := block[c] + mut v03 := block[d] + mut v04 := block[e] + mut v05 := block[f] + mut v06 := block[g] + mut v07 := block[h] + mut v08 := block[i] + mut v09 := block[j] + mut v10 := block[k] + mut v11 := block[l] + mut v12 := block[m] + mut v13 := block[n] + mut v14 := block[o] + mut v15 := block[p] + + v00 += v04 + 2 * u64(u32(v00)) * u64(u32(v04)) + v12 ^= v00 + v12 = bits.rotate_left_64(v12, -32) + v08 += v12 + 2 * u64(u32(v08)) * u64(u32(v12)) + v04 ^= v08 + v04 = bits.rotate_left_64(v04, -24) + + v00 += v04 + 2 * u64(u32(v00)) * u64(u32(v04)) + v12 ^= v00 + v12 = bits.rotate_left_64(v12, -16) + v08 += v12 + 2 * u64(u32(v08)) * u64(u32(v12)) + v04 ^= v08 + v04 = bits.rotate_left_64(v04, -63) + + v01 += v05 + 2 * u64(u32(v01)) * u64(u32(v05)) + v13 ^= v01 + v13 = bits.rotate_left_64(v13, -32) + v09 += v13 + 2 * u64(u32(v09)) * u64(u32(v13)) + v05 ^= v09 + v05 = bits.rotate_left_64(v05, -24) + + v01 += v05 + 2 * u64(u32(v01)) * u64(u32(v05)) + v13 ^= v01 + v13 = bits.rotate_left_64(v13, -16) + v09 += v13 + 2 * u64(u32(v09)) * u64(u32(v13)) + v05 ^= v09 + v05 = bits.rotate_left_64(v05, -63) + + v02 += v06 + 2 * u64(u32(v02)) * u64(u32(v06)) + v14 ^= v02 + v14 = bits.rotate_left_64(v14, -32) + v10 += v14 + 2 * u64(u32(v10)) * u64(u32(v14)) + v06 ^= v10 + v06 = bits.rotate_left_64(v06, -24) + + v02 += v06 + 2 * u64(u32(v02)) * u64(u32(v06)) + v14 ^= v02 + v14 = bits.rotate_left_64(v14, -16) + v10 += v14 + 2 * u64(u32(v10)) * u64(u32(v14)) + v06 ^= v10 + v06 = bits.rotate_left_64(v06, -63) + + v03 += v07 + 2 * u64(u32(v03)) * u64(u32(v07)) + v15 ^= v03 + v15 = bits.rotate_left_64(v15, -32) + v11 += v15 + 2 * u64(u32(v11)) * u64(u32(v15)) + v07 ^= v11 + v07 = bits.rotate_left_64(v07, -24) + + v03 += v07 + 2 * u64(u32(v03)) * u64(u32(v07)) + v15 ^= v03 + v15 = bits.rotate_left_64(v15, -16) + v11 += v15 + 2 * u64(u32(v11)) * u64(u32(v15)) + v07 ^= v11 + v07 = bits.rotate_left_64(v07, -63) + + v00 += v05 + 2 * u64(u32(v00)) * u64(u32(v05)) + v15 ^= v00 + v15 = bits.rotate_left_64(v15, -32) + v10 += v15 + 2 * u64(u32(v10)) * u64(u32(v15)) + v05 ^= v10 + v05 = bits.rotate_left_64(v05, -24) + + v00 += v05 + 2 * u64(u32(v00)) * u64(u32(v05)) + v15 ^= v00 + v15 = bits.rotate_left_64(v15, -16) + v10 += v15 + 2 * u64(u32(v10)) * u64(u32(v15)) + v05 ^= v10 + v05 = bits.rotate_left_64(v05, -63) + + v01 += v06 + 2 * u64(u32(v01)) * u64(u32(v06)) + v12 ^= v01 + v12 = bits.rotate_left_64(v12, -32) + v11 += v12 + 2 * u64(u32(v11)) * u64(u32(v12)) + v06 ^= v11 + v06 = bits.rotate_left_64(v06, -24) + + v01 += v06 + 2 * u64(u32(v01)) * u64(u32(v06)) + v12 ^= v01 + v12 = bits.rotate_left_64(v12, -16) + v11 += v12 + 2 * u64(u32(v11)) * u64(u32(v12)) + v06 ^= v11 + v06 = bits.rotate_left_64(v06, -63) + + v02 += v07 + 2 * u64(u32(v02)) * u64(u32(v07)) + v13 ^= v02 + v13 = bits.rotate_left_64(v13, -32) + v08 += v13 + 2 * u64(u32(v08)) * u64(u32(v13)) + v07 ^= v08 + v07 = bits.rotate_left_64(v07, -24) + + v02 += v07 + 2 * u64(u32(v02)) * u64(u32(v07)) + v13 ^= v02 + v13 = bits.rotate_left_64(v13, -16) + v08 += v13 + 2 * u64(u32(v08)) * u64(u32(v13)) + v07 ^= v08 + v07 = bits.rotate_left_64(v07, -63) + + v03 += v04 + 2 * u64(u32(v03)) * u64(u32(v04)) + v14 ^= v03 + v14 = bits.rotate_left_64(v14, -32) + v09 += v14 + 2 * u64(u32(v09)) * u64(u32(v14)) + v04 ^= v09 + v04 = bits.rotate_left_64(v04, -24) + + v03 += v04 + 2 * u64(u32(v03)) * u64(u32(v04)) + v14 ^= v03 + v14 = bits.rotate_left_64(v14, -16) + v09 += v14 + 2 * u64(u32(v09)) * u64(u32(v14)) + v04 ^= v09 + v04 = bits.rotate_left_64(v04, -63) + + block[a] = v00 + block[b] = v01 + block[c] = v02 + block[d] = v03 + block[e] = v04 + block[f] = v05 + block[g] = v06 + block[h] = v07 + block[i] = v08 + block[j] = v09 + block[k] = v10 + block[l] = v11 + block[m] = v12 + block[n] = v13 + block[o] = v14 + block[p] = v15 +} + +fn index_alpha(random u64, lanes u32, segments u32, threads u32, pass u32, slice u32, lane u32, index u32) u32 { + mut ref_lane := u32(random >> 32) % threads + if pass == 0 && slice == 0 { + ref_lane = lane + } + mut m := u32(3) * segments + mut s := ((slice + 1) % sync_points) * segments + if lane == ref_lane { + m += index + } + if pass == 0 { + m = slice * segments + s = 0 + if slice == 0 || lane == ref_lane { + m += index + } + } + if index == 0 || lane == ref_lane { + m-- + } + return phi(random, u64(m), u64(s), ref_lane, lanes) +} + +fn phi(random u64, m u64, s u64, lane u32, lanes u32) u32 { + mut p := random & u64(0xffffffff) + p = (p * p) >> 32 + p = (p * m) >> 32 + return lane * lanes + u32((s + m - (p + 1)) % u64(lanes)) +} + +fn encode_hash(mode int, salt []u8, hash []u8, time u32, memory u32, threads u8) string { + salt_b64 := base64.encode(salt).replace_each(['=', '']) + hash_b64 := base64.encode(hash).replace_each(['=', '']) + return '\$${mode_name(mode)}\$v=${version}\$m=${memory},t=${time},p=${threads}\$${salt_b64}\$${hash_b64}' +} + +fn decode_hash(encoded string) !DecodedHash { + parts := encoded.split('\$') + if parts.len != 6 || parts[0] != '' { + return InvalidHashError{ + reason: 'invalid argon2 hash format' + } + } + if parts[2] != 'v=${version}' { + return InvalidHashError{ + reason: 'unsupported argon2 hash version' + } + } + mode := parse_mode(parts[1])! + mut memory := u32(0) + mut time := u32(0) + mut threads := u8(0) + for param in parts[3].split(',') { + name := param.all_before('=') + value := param.all_after('=') + match name { + 'm' { + memory = value.u32() + } + 't' { + time = value.u32() + } + 'p' { + threads = u8(value.int()) + } + else { + return InvalidHashError{ + reason: 'unsupported argon2 parameter ${name}' + } + } + } + } + if time == 0 || threads == 0 { + return InvalidHashError{ + reason: 'invalid argon2 parameters' + } + } + salt := decode_base64(parts[4]) + hash := decode_base64(parts[5]) + if salt.len == 0 || hash.len == 0 { + return InvalidHashError{ + reason: 'invalid argon2 hash payload' + } + } + return DecodedHash{ + mode: mode + time: time + memory: normalize_memory(memory, u32(threads)) + threads: threads + salt: salt + hash: hash + } +} + +fn decode_base64(value string) []u8 { + mut padded := value + match padded.len % 4 { + 2 { padded += '==' } + 3 { padded += '=' } + else {} + } + return base64.decode(padded) +} + +fn mode_name(mode int) string { + return match mode { + argon2_d { 'argon2d' } + argon2_i { 'argon2i' } + else { 'argon2id' } + } +} + +fn parse_mode(name string) !int { + return match name { + 'argon2d' { + argon2_d + } + 'argon2i' { + argon2_i + } + 'argon2id' { + argon2_id + } + else { + return InvalidHashError{ + reason: 'unsupported argon2 algorithm ${name}' + } + } + } +} diff --git a/vlib/crypto/argon2/argon2_test.v b/vlib/crypto/argon2/argon2_test.v new file mode 100644 index 000000000..d828fe29a --- /dev/null +++ b/vlib/crypto/argon2/argon2_test.v @@ -0,0 +1,164 @@ +// Test vectors adapted from the Go x/crypto argon2 package. +module argon2 + +import encoding.hex + +const gen_kat_password = [ + u8(0x01), + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, + 0x01, +]! +const gen_kat_salt = [ + u8(0x02), + 0x02, + 0x02, + 0x02, + 0x02, + 0x02, + 0x02, + 0x02, + 0x02, + 0x02, + 0x02, + 0x02, + 0x02, + 0x02, + 0x02, + 0x02, +]! +const gen_kat_secret = [u8(0x03), 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03]! +const gen_kat_aad = [u8(0x04), 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04]! + +struct TestVector { + mode int + time u32 + memory u32 + threads u8 + hash string +} + +const test_vectors = [ + TestVector{ + mode: argon2_i + time: 1 + memory: 64 + threads: 1 + hash: 'b9c401d1844a67d50eae3967dc28870b22e508092e861a37' + }, + TestVector{ + mode: argon2_d + time: 1 + memory: 64 + threads: 1 + hash: '8727405fd07c32c78d64f547f24150d3f2e703a89f981a19' + }, + TestVector{ + mode: argon2_id + time: 1 + memory: 64 + threads: 1 + hash: '655ad15eac652dc59f7170a7332bf49b8469be1fdb9c28bb' + }, + TestVector{ + mode: argon2_id + time: 2 + memory: 64 + threads: 2 + hash: '350ac37222f436ccb5c0972f1ebd3bf6b958bf2071841362' + }, + TestVector{ + mode: argon2_i + time: 3 + memory: 256 + threads: 2 + hash: 'f5bbf5d4c3836af13193053155b73ec7476a6a2eb93fd5e6' + }, + TestVector{ + mode: argon2_id + time: 4 + memory: 1024 + threads: 8 + hash: '8dafa8e004f8ea96bf7c0f93eecf67a6047476143d15577f' + }, +]! + +fn test_argon2_known_answer_vectors() { + want_d := hex.decode('512b391b6f1162975371d30919734294f868e3be3984f3c1a13a4db9fabe4acb')! + got_d := derive_key(argon2_d, gen_kat_password[..], gen_kat_salt[..], gen_kat_secret[..], + gen_kat_aad[..], 3, 32, 4, u32(want_d.len))! + assert got_d == want_d + + want_i := hex.decode('c814d9d1dc7f37aa13f0d77f2494bda1c8de6b016dd388d29952a4c4672b6ce8')! + got_i := derive_key(argon2_i, gen_kat_password[..], gen_kat_salt[..], gen_kat_secret[..], + gen_kat_aad[..], 3, 32, 4, u32(want_i.len))! + assert got_i == want_i + + want_id := hex.decode('0d640df58d78766c08c037a34a8b53c9d01ef0452d75b65eb52520e96b01e659')! + got_id := derive_key(argon2_id, gen_kat_password[..], gen_kat_salt[..], gen_kat_secret[..], + gen_kat_aad[..], 3, 32, 4, u32(want_id.len))! + assert got_id == want_id +} + +fn test_argon2_reference_vectors() { + password := 'password'.bytes() + salt := 'somesalt'.bytes() + for vector in test_vectors { + want := hex.decode(vector.hash)! + got := derive_key(vector.mode, password, salt, []u8{}, []u8{}, vector.time, vector.memory, + vector.threads, u32(want.len))! + assert got == want + } +} + +fn test_generate_from_password_and_compare() { + params := Params{ + time: 2 + memory: 64 + threads: 1 + key_len: 24 + salt_len: 16 + } + password := 'correct horse battery staple'.bytes() + hash := generate_from_password_with_params(password, params)! + assert hash.starts_with('\$argon2id\$v=19\$m=64,t=2,p=1\$') + compare_hash_and_password(password, hash.bytes())! + compare_hash_and_password('incorrect'.bytes(), hash.bytes()) or { + assert err.msg() == 'mismatched hash and password' + return + } + assert false +} + +fn test_generate_from_password_uses_defaults() { + hash := generate_from_password('hunter2'.bytes())! + assert hash.starts_with('\$argon2id\$v=19\$m=65536,t=3,p=4\$') +} -- 2.39.5