// https://datatracker.ietf.org/doc/rfc9106
// https://github.com/golang/crypto/tree/master/argon2
// https://github.com/P-H-C/phc-winner-argon2

const builtin = @import("builtin");

const std = @import("std");
const blake2 = crypto.hash.blake2;
const crypto = std.crypto;
const Io = std.Io;
const math = std.math;
const mem = std.mem;
const phc_format = pwhash.phc_format;
const pwhash = crypto.pwhash;
const Blake2b512 = blake2.Blake2b512;
const Blocks = std.array_list.AlignedManaged([block_length]u64, .@"16");
const H0 = [Blake2b512.digest_length + 8]u8;

const EncodingError = crypto.errors.EncodingError;
const KdfError = pwhash.KdfError;
const HasherError = pwhash.HasherError;
const Error = pwhash.Error;

const version = 0x13;
const block_length = 128;
const sync_points = 4;
const max_int = 0xffff_ffff;

const default_salt_len = 32;
const default_hash_len = 32;
const max_salt_len = 64;
const max_hash_len = 64;

/// Argon2 type
pub const Mode = enum {
    /// Argon2d is faster and uses data-depending memory access, which makes it highly resistant
    /// against GPU cracking attacks and suitable for applications with no threats from side-channel
    /// timing attacks (eg. cryptocurrencies).
    argon2d,

    /// Argon2i instead uses data-independent memory access, which is preferred for password
    /// hashing and password-based key derivation, but it is slower as it makes more passes over
    /// the memory to protect from tradeoff attacks.
    argon2i,

    /// Argon2id is a hybrid of Argon2i and Argon2d, using a combination of data-depending and
    /// data-independent memory accesses, which gives some of Argon2i's resistance to side-channel
    /// cache timing attacks and much of Argon2d's resistance to GPU cracking attacks.
    argon2id,
};

/// Argon2 parameters
pub const Params = struct {
    const Self = @This();

    /// Time cost, which defines the amount of computation realized and therefore the execution
    /// time, given in number of iterations.
    t: u32,

    /// Memory cost, which defines the memory usage, given in kibibytes.
    m: u32,

    /// Parallelism degree, which defines the number of independent tasks,
    /// to be multiplexed onto threads when possible.
    p: u24,

    /// The secret parameter, which is used for keyed hashing. This allows a secret key to be input
    /// at hashing time (from some external location) and be folded into the value of the hash. This
    /// means that even if your salts and hashes are compromised, an attacker cannot brute-force to
    /// find the password without the key.
    secret: ?[]const u8 = null,

    /// The ad parameter, which is used to fold any additional data into the hash value. Functionally,
    /// this behaves almost exactly like the secret or salt parameters; the ad parameter is folding
    /// into the value of the hash. However, this parameter is used for different data. The salt
    /// should be a random string stored alongside your password. The secret should be a random key
    /// only usable at hashing time. The ad is for any other data.
    ad: ?[]const u8 = null,

    /// Baseline parameters for interactive logins using argon2i type
    pub const interactive_2i = Self.fromLimits(4, 33554432);
    /// Baseline parameters for normal usage using argon2i type
    pub const moderate_2i = Self.fromLimits(6, 134217728);
    /// Baseline parameters for offline usage using argon2i type
    pub const sensitive_2i = Self.fromLimits(8, 536870912);

    /// Baseline parameters for interactive logins using argon2id type
    pub const interactive_2id = Self.fromLimits(2, 67108864);
    /// Baseline parameters for normal usage using argon2id type
    pub const moderate_2id = Self.fromLimits(3, 268435456);
    /// Baseline parameters for offline usage using argon2id type
    pub const sensitive_2id = Self.fromLimits(4, 1073741824);

    /// Recommended parameters for argon2id type according to the
    /// [OWASP cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html).
    pub const owasp_2id = Self{ .t = 2, .m = 19 * 1024, .p = 1 };

    /// Create parameters from ops and mem limits, where mem_limit given in bytes
    pub fn fromLimits(ops_limit: u32, mem_limit: usize) Self {
        const m = mem_limit / 1024;
        std.debug.assert(m <= max_int);
        return .{ .t = ops_limit, .m = @as(u32, @intCast(m)), .p = 1 };
    }
};

fn initHash(
    password: []const u8,
    salt: []const u8,
    params: Params,
    dk_len: usize,
    mode: Mode,
) H0 {
    var h0: H0 = undefined;
    var parameters: [24]u8 = undefined;
    var tmp: [4]u8 = undefined;
    var b2 = Blake2b512.init(.{});
    mem.writeInt(u32, parameters[0..4], params.p, .little);
    mem.writeInt(u32, parameters[4..8], @as(u32, @intCast(dk_len)), .little);
    mem.writeInt(u32, parameters[8..12], params.m, .little);
    mem.writeInt(u32, parameters[12..16], params.t, .little);
    mem.writeInt(u32, parameters[16..20], version, .little);
    mem.writeInt(u32, parameters[20..24], @intFromEnum(mode), .little);
    b2.update(&parameters);
    mem.writeInt(u32, &tmp, @as(u32, @intCast(password.len)), .little);
    b2.update(&tmp);
    b2.update(password);
    mem.writeInt(u32, &tmp, @as(u32, @intCast(salt.len)), .little);
    b2.update(&tmp);
    b2.update(salt);
    const secret = params.secret orelse "";
    std.debug.assert(secret.len <= max_int);
    mem.writeInt(u32, &tmp, @as(u32, @intCast(secret.len)), .little);
    b2.update(&tmp);
    b2.update(secret);
    const ad = params.ad orelse "";
    std.debug.assert(ad.len <= max_int);
    mem.writeInt(u32, &tmp, @as(u32, @intCast(ad.len)), .little);
    b2.update(&tmp);
    b2.update(ad);
    b2.final(h0[0..Blake2b512.digest_length]);
    return h0;
}

fn blake2bLong(out: []u8, in: []const u8) void {
    const H = Blake2b512;
    var outlen_bytes: [4]u8 = undefined;
    mem.writeInt(u32, &outlen_bytes, @as(u32, @intCast(out.len)), .little);

    var out_buf: [H.digest_length]u8 = undefined;

    if (out.len <= H.digest_length) {
        var h = H.init(.{ .expected_out_bits = out.len * 8 });
        h.update(&outlen_bytes);
        h.update(in);
        h.final(&out_buf);
        @memcpy(out, out_buf[0..out.len]);
        return;
    }

    var h = H.init(.{});
    h.update(&outlen_bytes);
    h.update(in);
    h.final(&out_buf);
    var out_slice = out;
    out_slice[0 .. H.digest_length / 2].* = out_buf[0 .. H.digest_length / 2].*;
    out_slice = out_slice[H.digest_length / 2 ..];

    var in_buf: [H.digest_length]u8 = undefined;
    while (out_slice.len > H.digest_length) {
        in_buf = out_buf;
        H.hash(&in_buf, &out_buf, .{});
        out_slice[0 .. H.digest_length / 2].* = out_buf[0 .. H.digest_length / 2].*;
        out_slice = out_slice[H.digest_length / 2 ..];
    }
    in_buf = out_buf;
    H.hash(&in_buf, &out_buf, .{ .expected_out_bits = out_slice.len * 8 });
    @memcpy(out_slice, out_buf[0..out_slice.len]);
}

fn initBlocks(
    blocks: *Blocks,
    h0: *H0,
    memory: u32,
    threads: u24,
) void {
    var block0: [1024]u8 = undefined;
    var lane: u24 = 0;
    while (lane < threads) : (lane += 1) {
        const j = lane * (memory / threads);
        mem.writeInt(u32, h0[Blake2b512.digest_length + 4 ..][0..4], lane, .little);

        mem.writeInt(u32, h0[Blake2b512.digest_length..][0..4], 0, .little);
        blake2bLong(&block0, h0);
        for (&blocks.items[j + 0], 0..) |*v, i| {
            v.* = mem.readInt(u64, block0[i * 8 ..][0..8], .little);
        }

        mem.writeInt(u32, h0[Blake2b512.digest_length..][0..4], 1, .little);
        blake2bLong(&block0, h0);
        for (&blocks.items[j + 1], 0..) |*v, i| {
            v.* = mem.readInt(u64, block0[i * 8 ..][0..8], .little);
        }
    }
}

fn processBlocks(
    blocks: *Blocks,
    time: u32,
    memory: u32,
    threads: u24,
    mode: Mode,
    io: Io,
) Io.Cancelable!void {
    const lanes = memory / threads;
    const segments = lanes / sync_points;

    if (builtin.single_threaded or threads == 1) {
        processBlocksSync(blocks, time, memory, threads, mode, lanes, segments);
    } else {
        try processBlocksAsync(blocks, time, memory, threads, mode, lanes, segments, io);
    }
}

fn processBlocksSync(
    blocks: *Blocks,
    time: u32,
    memory: u32,
    threads: u24,
    mode: Mode,
    lanes: u32,
    segments: u32,
) void {
    var n: u32 = 0;
    while (n < time) : (n += 1) {
        var slice: u32 = 0;
        while (slice < sync_points) : (slice += 1) {
            var lane: u24 = 0;
            while (lane < threads) : (lane += 1) {
                processSegment(blocks, time, memory, threads, mode, lanes, segments, n, slice, lane);
            }
        }
    }
}

fn processBlocksAsync(
    blocks: *Blocks,
    time: u32,
    memory: u32,
    threads: u24,
    mode: Mode,
    lanes: u32,
    segments: u32,
    io: Io,
) Io.Cancelable!void {
    var n: u32 = 0;
    while (n < time) : (n += 1) {
        var slice: u32 = 0;
        while (slice < sync_points) : (slice += 1) {
            var group: Io.Group = .init;
            defer group.cancel(io);
            var lane: u24 = 0;
            while (lane < threads) : (lane += 1) {
                group.async(io, processSegment, .{
                    blocks, time, memory, threads, mode, lanes, segments, n, slice, lane,
                });
            }
            try group.await(io);
        }
    }
}

fn processSegment(
    blocks: *Blocks,
    passes: u32,
    memory: u32,
    threads: u24,
    mode: Mode,
    lanes: u32,
    segments: u32,
    n: u32,
    slice: u32,
    lane: u24,
) void {
    var addresses align(16) = [_]u64{0} ** block_length;
    var in align(16) = [_]u64{0} ** block_length;
    const zero align(16) = [_]u64{0} ** block_length;
    if (mode == .argon2i or (mode == .argon2id and n == 0 and slice < sync_points / 2)) {
        in[0] = n;
        in[1] = lane;
        in[2] = slice;
        in[3] = memory;
        in[4] = passes;
        in[5] = @intFromEnum(mode);
    }
    var index: u32 = 0;
    if (n == 0 and slice == 0) {
        index = 2;
        if (mode == .argon2i or mode == .argon2id) {
            in[6] += 1;
            processBlock(&addresses, &in, &zero);
            processBlock(&addresses, &addresses, &zero);
        }
    }
    var offset = lane * lanes + slice * segments + index;
    var random: u64 = 0;
    while (index < segments) : ({
        index += 1;
        offset += 1;
    }) {
        var prev = offset -% 1;
        if (index == 0 and slice == 0) {
            prev +%= lanes;
        }
        if (mode == .argon2i or (mode == .argon2id and n == 0 and slice < sync_points / 2)) {
            if (index % block_length == 0) {
                in[6] += 1;
                processBlock(&addresses, &in, &zero);
                processBlock(&addresses, &addresses, &zero);
            }
            random = addresses[index % block_length];
        } else {
            random = blocks.items[prev][0];
        }
        const new_offset = indexAlpha(random, lanes, segments, threads, n, slice, lane, index);
        processBlockXor(&blocks.items[offset], &blocks.items[prev], &blocks.items[new_offset]);
    }
}

fn processBlock(
    out: *align(16) [block_length]u64,
    in1: *align(16) const [block_length]u64,
    in2: *align(16) const [block_length]u64,
) void {
    processBlockGeneric(out, in1, in2, false);
}

fn processBlockXor(
    out: *[block_length]u64,
    in1: *const [block_length]u64,
    in2: *const [block_length]u64,
) void {
    processBlockGeneric(out, in1, in2, true);
}

fn processBlockGeneric(
    out: *[block_length]u64,
    in1: *const [block_length]u64,
    in2: *const [block_length]u64,
    comptime xor: bool,
) void {
    var t: [block_length]u64 = undefined;
    for (&t, 0..) |*v, i| {
        v.* = in1[i] ^ in2[i];
    }
    var i: usize = 0;
    while (i < block_length) : (i += 16) {
        blamkaGeneric(t[i..][0..16]);
    }
    i = 0;
    var buffer: [16]u64 = undefined;
    while (i < block_length / 8) : (i += 2) {
        var j: usize = 0;
        while (j < block_length / 8) : (j += 2) {
            buffer[j] = t[j * 8 + i];
            buffer[j + 1] = t[j * 8 + i + 1];
        }
        blamkaGeneric(&buffer);
        j = 0;
        while (j < block_length / 8) : (j += 2) {
            t[j * 8 + i] = buffer[j];
            t[j * 8 + i + 1] = buffer[j + 1];
        }
    }
    if (xor) {
        for (t, 0..) |v, j| {
            out[j] ^= in1[j] ^ in2[j] ^ v;
        }
    } else {
        for (t, 0..) |v, j| {
            out[j] = in1[j] ^ in2[j] ^ v;
        }
    }
}

const QuarterRound = struct { a: usize, b: usize, c: usize, d: usize };

fn Rp(a: usize, b: usize, c: usize, d: usize) QuarterRound {
    return .{ .a = a, .b = b, .c = c, .d = d };
}

fn fBlaMka(x: u64, y: u64) u64 {
    const xy = @as(u64, @as(u32, @truncate(x))) * @as(u64, @as(u32, @truncate(y)));
    return x +% y +% 2 *% xy;
}

fn blamkaGeneric(x: *[16]u64) void {
    const rounds = comptime [_]QuarterRound{
        Rp(0, 4, 8, 12),
        Rp(1, 5, 9, 13),
        Rp(2, 6, 10, 14),
        Rp(3, 7, 11, 15),
        Rp(0, 5, 10, 15),
        Rp(1, 6, 11, 12),
        Rp(2, 7, 8, 13),
        Rp(3, 4, 9, 14),
    };
    inline for (rounds) |r| {
        x[r.a] = fBlaMka(x[r.a], x[r.b]);
        x[r.d] = math.rotr(u64, x[r.d] ^ x[r.a], 32);
        x[r.c] = fBlaMka(x[r.c], x[r.d]);
        x[r.b] = math.rotr(u64, x[r.b] ^ x[r.c], 24);
        x[r.a] = fBlaMka(x[r.a], x[r.b]);
        x[r.d] = math.rotr(u64, x[r.d] ^ x[r.a], 16);
        x[r.c] = fBlaMka(x[r.c], x[r.d]);
        x[r.b] = math.rotr(u64, x[r.b] ^ x[r.c], 63);
    }
}

fn finalize(
    blocks: *Blocks,
    memory: u32,
    threads: u24,
    out: []u8,
) void {
    const lanes = memory / threads;
    var lane: u24 = 0;
    while (lane < threads - 1) : (lane += 1) {
        for (blocks.items[(lane * lanes) + lanes - 1], 0..) |v, i| {
            blocks.items[memory - 1][i] ^= v;
        }
    }
    var block: [1024]u8 = undefined;
    for (blocks.items[memory - 1], 0..) |v, i| {
        mem.writeInt(u64, block[i * 8 ..][0..8], v, .little);
    }
    blake2bLong(out, &block);
}

fn indexAlpha(
    rand: u64,
    lanes: u32,
    segments: u32,
    threads: u24,
    n: u32,
    slice: u32,
    lane: u24,
    index: u32,
) u32 {
    var ref_lane = @as(u32, @intCast(rand >> 32)) % threads;
    if (n == 0 and slice == 0) {
        ref_lane = lane;
    }
    var m = 3 * segments;
    var s = ((slice + 1) % sync_points) * segments;
    if (lane == ref_lane) {
        m += index;
    }
    if (n == 0) {
        m = slice * segments;
        s = 0;
        if (slice == 0 or lane == ref_lane) {
            m += index;
        }
    }
    if (index == 0 or lane == ref_lane) {
        m -= 1;
    }
    var p = @as(u64, @as(u32, @truncate(rand)));
    p = (p * p) >> 32;
    p = (p * m) >> 32;
    return ref_lane * lanes + @as(u32, @intCast(((s + m - (p + 1)) % lanes)));
}

/// Derives a key from the password, salt, and argon2 parameters.
///
/// Derived key has to be at least 4 bytes length.
///
/// Salt has to be at least 8 bytes length.
pub fn kdf(
    allocator: mem.Allocator,
    derived_key: []u8,
    password: []const u8,
    salt: []const u8,
    params: Params,
    mode: Mode,
    io: Io,
) KdfError!void {
    if (derived_key.len < 4) return KdfError.WeakParameters;
    if (derived_key.len > max_int) return KdfError.OutputTooLong;

    if (password.len > max_int) return KdfError.WeakParameters;
    if (salt.len < 8 or salt.len > max_int) return KdfError.WeakParameters;
    if (params.t < 1 or params.p < 1) return KdfError.WeakParameters;
    if (params.m / 8 < params.p) return KdfError.WeakParameters;

    var h0 = initHash(password, salt, params, derived_key.len, mode);
    const memory = @max(
        params.m / (sync_points * params.p) * (sync_points * params.p),
        2 * sync_points * params.p,
    );

    var blocks = try Blocks.initCapacity(allocator, memory);
    defer blocks.deinit();

    blocks.appendNTimesAssumeCapacity(@splat(0), memory);

    initBlocks(&blocks, &h0, memory, params.p);
    try processBlocks(&blocks, params.t, memory, params.p, mode, io);
    finalize(&blocks, memory, params.p, derived_key);
}

const PhcFormatHasher = struct {
    const BinValue = phc_format.BinValue;

    const HashResult = struct {
        alg_id: []const u8,
        alg_version: ?u32,
        m: u32,
        t: u32,
        p: u24,
        salt: BinValue(max_salt_len),
        hash: BinValue(max_hash_len),
    };

    pub fn create(
        allocator: mem.Allocator,
        password: []const u8,
        params: Params,
        mode: Mode,
        buf: []u8,
        io: Io,
    ) HasherError![]const u8 {
        if (params.secret != null or params.ad != null) return HasherError.InvalidEncoding;

        var salt: [default_salt_len]u8 = undefined;
        io.random(&salt);

        var hash: [default_hash_len]u8 = undefined;
        try kdf(allocator, &hash, password, &salt, params, mode, io);

        return phc_format.serialize(HashResult{
            .alg_id = @tagName(mode),
            .alg_version = version,
            .m = params.m,
            .t = params.t,
            .p = params.p,
            .salt = try BinValue(max_salt_len).fromSlice(&salt),
            .hash = try BinValue(max_hash_len).fromSlice(&hash),
        }, buf);
    }

    pub fn verify(
        allocator: mem.Allocator,
        str: []const u8,
        password: []const u8,
        io: Io,
    ) HasherError!void {
        const hash_result = try phc_format.deserialize(HashResult, str);

        const mode = std.meta.stringToEnum(Mode, hash_result.alg_id) orelse
            return HasherError.PasswordVerificationFailed;
        if (hash_result.alg_version) |v| {
            if (v != version) return HasherError.InvalidEncoding;
        }
        const params = Params{ .t = hash_result.t, .m = hash_result.m, .p = hash_result.p };

        const expected_hash = hash_result.hash.constSlice();
        var hash_buf: [max_hash_len]u8 = undefined;
        if (expected_hash.len > hash_buf.len) return HasherError.InvalidEncoding;
        const hash = hash_buf[0..expected_hash.len];

        try kdf(allocator, hash, password, hash_result.salt.constSlice(), params, mode, io);
        if (!mem.eql(u8, hash, expected_hash)) return HasherError.PasswordVerificationFailed;
    }
};

/// Options for hashing a password.
///
/// Allocator is required for argon2.
///
/// Only phc encoding is supported.
pub const HashOptions = struct {
    allocator: ?mem.Allocator,
    params: Params,
    mode: Mode = .argon2id,
    encoding: pwhash.Encoding = .phc,
};

/// Compute a hash of a password using the argon2 key derivation function.
/// The function returns a string that includes all the parameters required for verification.
pub fn strHash(
    password: []const u8,
    options: HashOptions,
    out: []u8,
    io: Io,
) Error![]const u8 {
    const allocator = options.allocator orelse return Error.AllocatorRequired;
    switch (options.encoding) {
        .phc => return PhcFormatHasher.create(
            allocator,
            password,
            options.params,
            options.mode,
            out,
            io,
        ),
        .crypt => return Error.InvalidEncoding,
    }
}

/// Options for hash verification.
///
/// Allocator is required for argon2.
pub const VerifyOptions = struct {
    allocator: ?mem.Allocator,
};

/// Verify that a previously computed hash is valid for a given password.
pub fn strVerify(
    str: []const u8,
    password: []const u8,
    options: VerifyOptions,
    io: Io,
) Error!void {
    const allocator = options.allocator orelse return Error.AllocatorRequired;
    return PhcFormatHasher.verify(allocator, str, password, io);
}

test "argon2d" {
    if (true) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/30074

    const password = [_]u8{0x01} ** 32;
    const salt = [_]u8{0x02} ** 16;
    const secret = [_]u8{0x03} ** 8;
    const ad = [_]u8{0x04} ** 12;

    var dk: [32]u8 = undefined;
    try kdf(
        std.testing.allocator,
        &dk,
        &password,
        &salt,
        .{ .t = 3, .m = 32, .p = 4, .secret = &secret, .ad = &ad },
        .argon2d,
        std.testing.io,
    );

    const want = [_]u8{
        0x51, 0x2b, 0x39, 0x1b, 0x6f, 0x11, 0x62, 0x97,
        0x53, 0x71, 0xd3, 0x09, 0x19, 0x73, 0x42, 0x94,
        0xf8, 0x68, 0xe3, 0xbe, 0x39, 0x84, 0xf3, 0xc1,
        0xa1, 0x3a, 0x4d, 0xb9, 0xfa, 0xbe, 0x4a, 0xcb,
    };
    try std.testing.expectEqualSlices(u8, &dk, &want);
}

test "argon2i" {
    const password = [_]u8{0x01} ** 32;
    const salt = [_]u8{0x02} ** 16;
    const secret = [_]u8{0x03} ** 8;
    const ad = [_]u8{0x04} ** 12;

    var dk: [32]u8 = undefined;
    try kdf(
        std.testing.allocator,
        &dk,
        &password,
        &salt,
        .{ .t = 3, .m = 32, .p = 4, .secret = &secret, .ad = &ad },
        .argon2i,
        std.testing.io,
    );

    const want = [_]u8{
        0xc8, 0x14, 0xd9, 0xd1, 0xdc, 0x7f, 0x37, 0xaa,
        0x13, 0xf0, 0xd7, 0x7f, 0x24, 0x94, 0xbd, 0xa1,
        0xc8, 0xde, 0x6b, 0x01, 0x6d, 0xd3, 0x88, 0xd2,
        0x99, 0x52, 0xa4, 0xc4, 0x67, 0x2b, 0x6c, 0xe8,
    };
    try std.testing.expectEqualSlices(u8, &dk, &want);
}

test "argon2id" {
    const password = [_]u8{0x01} ** 32;
    const salt = [_]u8{0x02} ** 16;
    const secret = [_]u8{0x03} ** 8;
    const ad = [_]u8{0x04} ** 12;

    var dk: [32]u8 = undefined;
    try kdf(
        std.testing.allocator,
        &dk,
        &password,
        &salt,
        .{ .t = 3, .m = 32, .p = 4, .secret = &secret, .ad = &ad },
        .argon2id,
        std.testing.io,
    );

    const want = [_]u8{
        0x0d, 0x64, 0x0d, 0xf5, 0x8d, 0x78, 0x76, 0x6c,
        0x08, 0xc0, 0x37, 0xa3, 0x4a, 0x8b, 0x53, 0xc9,
        0xd0, 0x1e, 0xf0, 0x45, 0x2d, 0x75, 0xb6, 0x5e,
        0xb5, 0x25, 0x20, 0xe9, 0x6b, 0x01, 0xe6, 0x59,
    };
    try std.testing.expectEqualSlices(u8, &dk, &want);
}

test "kdf" {
    if (true) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/31402

    const password = "password";
    const salt = "somesalt";

    const TestVector = struct {
        mode: Mode,
        time: u32,
        memory: u32,
        threads: u8,
        hash: []const u8,
    };
    const test_vectors = [_]TestVector{
        .{
            .mode = .argon2i,
            .time = 1,
            .memory = 64,
            .threads = 1,
            .hash = "b9c401d1844a67d50eae3967dc28870b22e508092e861a37",
        },
        .{
            .mode = .argon2d,
            .time = 1,
            .memory = 64,
            .threads = 1,
            .hash = "8727405fd07c32c78d64f547f24150d3f2e703a89f981a19",
        },
        .{
            .mode = .argon2id,
            .time = 1,
            .memory = 64,
            .threads = 1,
            .hash = "655ad15eac652dc59f7170a7332bf49b8469be1fdb9c28bb",
        },
        .{
            .mode = .argon2i,
            .time = 2,
            .memory = 64,
            .threads = 1,
            .hash = "8cf3d8f76a6617afe35fac48eb0b7433a9a670ca4a07ed64",
        },
        .{
            .mode = .argon2d,
            .time = 2,
            .memory = 64,
            .threads = 1,
            .hash = "3be9ec79a69b75d3752acb59a1fbb8b295a46529c48fbb75",
        },
        .{
            .mode = .argon2id,
            .time = 2,
            .memory = 64,
            .threads = 1,
            .hash = "068d62b26455936aa6ebe60060b0a65870dbfa3ddf8d41f7",
        },
        .{
            .mode = .argon2i,
            .time = 2,
            .memory = 64,
            .threads = 2,
            .hash = "2089f3e78a799720f80af806553128f29b132cafe40d059f",
        },
        .{
            .mode = .argon2d,
            .time = 2,
            .memory = 64,
            .threads = 2,
            .hash = "68e2462c98b8bc6bb60ec68db418ae2c9ed24fc6748a40e9",
        },
        .{
            .mode = .argon2id,
            .time = 2,
            .memory = 64,
            .threads = 2,
            .hash = "350ac37222f436ccb5c0972f1ebd3bf6b958bf2071841362",
        },
        .{
            .mode = .argon2i,
            .time = 3,
            .memory = 256,
            .threads = 2,
            .hash = "f5bbf5d4c3836af13193053155b73ec7476a6a2eb93fd5e6",
        },
        .{
            .mode = .argon2d,
            .time = 3,
            .memory = 256,
            .threads = 2,
            .hash = "f4f0669218eaf3641f39cc97efb915721102f4b128211ef2",
        },
        .{
            .mode = .argon2id,
            .time = 3,
            .memory = 256,
            .threads = 2,
            .hash = "4668d30ac4187e6878eedeacf0fd83c5a0a30db2cc16ef0b",
        },
        .{
            .mode = .argon2i,
            .time = 4,
            .memory = 256,
            .threads = 4,
            .hash = "f7dbbacbf16999e3700817a7e06f65a8db2e9fa9504ede4c",
        },
        .{
            .mode = .argon2d,
            .time = 4,
            .memory = 256,
            .threads = 4,
            .hash = "ea2970501cf49faa5ba1d2e6370204e9b57ca90a8fea937b",
        },
        .{
            .mode = .argon2id,
            .time = 4,
            .memory = 256,
            .threads = 4,
            .hash = "fbd40d5a8cb92f88c20bda4b3cdb1f9d5af1efa937032410",
        },
        .{
            .mode = .argon2i,
            .time = 4,
            .memory = 256,
            .threads = 8,
            .hash = "15d3c398364e53f68fd12d19baf3f21432d964254fe27467",
        },
        .{
            .mode = .argon2d,
            .time = 4,
            .memory = 256,
            .threads = 8,
            .hash = "23c9adc06f06e21e4612c1466a1be02627690932b02c0df0",
        },
        .{
            .mode = .argon2id,
            .time = 4,
            .memory = 256,
            .threads = 8,
            .hash = "f22802f8ca47be93f9954e4ce20c1e944e938fbd4a125d9d",
        },
        .{
            .mode = .argon2i,
            .time = 2,
            .memory = 64,
            .threads = 3,
            .hash = "5cab452fe6b8479c8661def8cd703b611a3905a6d5477fe6",
        },
        .{
            .mode = .argon2d,
            .time = 2,
            .memory = 64,
            .threads = 3,
            .hash = "22474a423bda2ccd36ec9afd5119e5c8949798cadf659f51",
        },
        .{
            .mode = .argon2id,
            .time = 2,
            .memory = 64,
            .threads = 3,
            .hash = "4a15b31aec7c2590b87d1f520be7d96f56658172deaa3079",
        },
        .{
            .mode = .argon2i,
            .time = 3,
            .memory = 256,
            .threads = 6,
            .hash = "ebc8f91964abd8ceab49a12963b0a9e57d635bfa2aad2884",
        },
        .{
            .mode = .argon2d,
            .time = 3,
            .memory = 256,
            .threads = 6,
            .hash = "1dd7202fd68da6675f769f4034b7a1db30d8785331954117",
        },
        .{
            .mode = .argon2id,
            .time = 3,
            .memory = 256,
            .threads = 6,
            .hash = "424436b6ee22a66b04b9d0cf78f190305c5c166bae8baa09",
        },
    };
    for (test_vectors) |v| {
        var want: [24]u8 = undefined;
        _ = try std.fmt.hexToBytes(&want, v.hash);

        var dk: [24]u8 = undefined;
        try kdf(
            std.testing.allocator,
            &dk,
            password,
            salt,
            .{ .t = v.time, .m = v.memory, .p = v.threads },
            v.mode,
            std.testing.io,
        );

        try std.testing.expectEqualSlices(u8, &dk, &want);
    }
}

test "phc format hasher" {
    const allocator = std.testing.allocator;
    const password = "testpass";
    const io = std.testing.io;

    var buf: [128]u8 = undefined;
    const hash = try PhcFormatHasher.create(
        allocator,
        password,
        .{ .t = 3, .m = 32, .p = 4 },
        .argon2id,
        &buf,
        io,
    );
    try PhcFormatHasher.verify(allocator, hash, password, io);
}

test "password hash and password verify" {
    const allocator = std.testing.allocator;
    const password = "testpass";
    const io = std.testing.io;

    var buf: [128]u8 = undefined;
    const hash = try strHash(
        password,
        .{ .allocator = allocator, .params = .{ .t = 3, .m = 32, .p = 4 } },
        &buf,
        io,
    );
    try strVerify(hash, password, .{ .allocator = allocator }, io);
}

test "kdf derived key length" {
    if (true) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/31504

    const allocator = std.testing.allocator;
    const io = std.testing.io;

    const password = "testpass";
    const salt = "saltsalt";
    const params = Params{ .t = 3, .m = 32, .p = 4 };
    const mode = Mode.argon2id;

    var dk1: [11]u8 = undefined;
    try kdf(allocator, &dk1, password, salt, params, mode, io);

    var dk2: [77]u8 = undefined;
    try kdf(allocator, &dk2, password, salt, params, mode, io);

    var dk3: [111]u8 = undefined;
    try kdf(allocator, &dk3, password, salt, params, mode, io);
}
