Argon2: use the std.Io interface

Also reduce the memory required by tests.

4GB for every test is way too much and doesn't provide much benefits
in testing the algorithms.
This commit is contained in:
Frank Denis 2025-12-02 23:03:52 +01:00
parent 95f93a0b28
commit 6fe95c28cf
2 changed files with 62 additions and 46 deletions

View file

@ -7,12 +7,11 @@ const builtin = @import("builtin");
const blake2 = crypto.hash.blake2; const blake2 = crypto.hash.blake2;
const crypto = std.crypto; const crypto = std.crypto;
const Io = std.Io;
const math = std.math; const math = std.math;
const mem = std.mem; const mem = std.mem;
const phc_format = pwhash.phc_format; const phc_format = pwhash.phc_format;
const pwhash = crypto.pwhash; const pwhash = crypto.pwhash;
const Thread = std.Thread;
const Blake2b512 = blake2.Blake2b512; const Blake2b512 = blake2.Blake2b512;
const Blocks = std.array_list.AlignedManaged([block_length]u64, .@"16"); const Blocks = std.array_list.AlignedManaged([block_length]u64, .@"16");
const H0 = [Blake2b512.digest_length + 8]u8; const H0 = [Blake2b512.digest_length + 8]u8;
@ -204,20 +203,20 @@ fn initBlocks(
} }
fn processBlocks( fn processBlocks(
allocator: mem.Allocator,
blocks: *Blocks, blocks: *Blocks,
time: u32, time: u32,
memory: u32, memory: u32,
threads: u24, threads: u24,
mode: Mode, mode: Mode,
) KdfError!void { io: Io,
) void {
const lanes = memory / threads; const lanes = memory / threads;
const segments = lanes / sync_points; const segments = lanes / sync_points;
if (builtin.single_threaded or threads == 1) { if (builtin.single_threaded or threads == 1) {
processBlocksSt(blocks, time, memory, threads, mode, lanes, segments); processBlocksSt(blocks, time, memory, threads, mode, lanes, segments);
} else { } else {
try processBlocksMt(allocator, blocks, time, memory, threads, mode, lanes, segments); processBlocksMt(blocks, time, memory, threads, mode, lanes, segments, io);
} }
} }
@ -243,7 +242,6 @@ fn processBlocksSt(
} }
fn processBlocksMt( fn processBlocksMt(
allocator: mem.Allocator,
blocks: *Blocks, blocks: *Blocks,
time: u32, time: u32,
memory: u32, memory: u32,
@ -251,26 +249,20 @@ fn processBlocksMt(
mode: Mode, mode: Mode,
lanes: u32, lanes: u32,
segments: u32, segments: u32,
) KdfError!void { io: Io,
var threads_list = try std.array_list.Managed(Thread).initCapacity(allocator, threads); ) void {
defer threads_list.deinit();
var n: u32 = 0; var n: u32 = 0;
while (n < time) : (n += 1) { while (n < time) : (n += 1) {
var slice: u32 = 0; var slice: u32 = 0;
while (slice < sync_points) : (slice += 1) { while (slice < sync_points) : (slice += 1) {
var group: Io.Group = .init;
var lane: u24 = 0; var lane: u24 = 0;
while (lane < threads) : (lane += 1) { while (lane < threads) : (lane += 1) {
const thread = try Thread.spawn(.{}, processSegment, .{ group.async(io, processSegment, .{
blocks, time, memory, threads, mode, lanes, segments, n, slice, lane, blocks, time, memory, threads, mode, lanes, segments, n, slice, lane,
}); });
threads_list.appendAssumeCapacity(thread);
} }
lane = 0; group.wait(io);
while (lane < threads) : (lane += 1) {
threads_list.items[lane].join();
}
threads_list.clearRetainingCapacity();
} }
} }
} }
@ -489,6 +481,7 @@ pub fn kdf(
salt: []const u8, salt: []const u8,
params: Params, params: Params,
mode: Mode, mode: Mode,
io: Io,
) KdfError!void { ) KdfError!void {
if (derived_key.len < 4) return KdfError.WeakParameters; if (derived_key.len < 4) return KdfError.WeakParameters;
if (derived_key.len > max_int) return KdfError.OutputTooLong; if (derived_key.len > max_int) return KdfError.OutputTooLong;
@ -510,7 +503,7 @@ pub fn kdf(
blocks.appendNTimesAssumeCapacity(@splat(0), memory); blocks.appendNTimesAssumeCapacity(@splat(0), memory);
initBlocks(&blocks, &h0, memory, params.p); initBlocks(&blocks, &h0, memory, params.p);
try processBlocks(allocator, &blocks, params.t, memory, params.p, mode); processBlocks(&blocks, params.t, memory, params.p, mode, io);
finalize(&blocks, memory, params.p, derived_key); finalize(&blocks, memory, params.p, derived_key);
} }
@ -533,6 +526,7 @@ const PhcFormatHasher = struct {
params: Params, params: Params,
mode: Mode, mode: Mode,
buf: []u8, buf: []u8,
io: Io,
) HasherError![]const u8 { ) HasherError![]const u8 {
if (params.secret != null or params.ad != null) return HasherError.InvalidEncoding; if (params.secret != null or params.ad != null) return HasherError.InvalidEncoding;
@ -540,7 +534,7 @@ const PhcFormatHasher = struct {
crypto.random.bytes(&salt); crypto.random.bytes(&salt);
var hash: [default_hash_len]u8 = undefined; var hash: [default_hash_len]u8 = undefined;
try kdf(allocator, &hash, password, &salt, params, mode); try kdf(allocator, &hash, password, &salt, params, mode, io);
return phc_format.serialize(HashResult{ return phc_format.serialize(HashResult{
.alg_id = @tagName(mode), .alg_id = @tagName(mode),
@ -557,6 +551,7 @@ const PhcFormatHasher = struct {
allocator: mem.Allocator, allocator: mem.Allocator,
str: []const u8, str: []const u8,
password: []const u8, password: []const u8,
io: Io,
) HasherError!void { ) HasherError!void {
const hash_result = try phc_format.deserialize(HashResult, str); const hash_result = try phc_format.deserialize(HashResult, str);
@ -572,7 +567,7 @@ const PhcFormatHasher = struct {
if (expected_hash.len > hash_buf.len) return HasherError.InvalidEncoding; if (expected_hash.len > hash_buf.len) return HasherError.InvalidEncoding;
const hash = hash_buf[0..expected_hash.len]; const hash = hash_buf[0..expected_hash.len];
try kdf(allocator, hash, password, hash_result.salt.constSlice(), params, mode); try kdf(allocator, hash, password, hash_result.salt.constSlice(), params, mode, io);
if (!mem.eql(u8, hash, expected_hash)) return HasherError.PasswordVerificationFailed; if (!mem.eql(u8, hash, expected_hash)) return HasherError.PasswordVerificationFailed;
} }
}; };
@ -595,6 +590,7 @@ pub fn strHash(
password: []const u8, password: []const u8,
options: HashOptions, options: HashOptions,
out: []u8, out: []u8,
io: Io,
) Error![]const u8 { ) Error![]const u8 {
const allocator = options.allocator orelse return Error.AllocatorRequired; const allocator = options.allocator orelse return Error.AllocatorRequired;
switch (options.encoding) { switch (options.encoding) {
@ -604,6 +600,7 @@ pub fn strHash(
options.params, options.params,
options.mode, options.mode,
out, out,
io,
), ),
.crypt => return Error.InvalidEncoding, .crypt => return Error.InvalidEncoding,
} }
@ -621,9 +618,10 @@ pub fn strVerify(
str: []const u8, str: []const u8,
password: []const u8, password: []const u8,
options: VerifyOptions, options: VerifyOptions,
io: Io,
) Error!void { ) Error!void {
const allocator = options.allocator orelse return Error.AllocatorRequired; const allocator = options.allocator orelse return Error.AllocatorRequired;
return PhcFormatHasher.verify(allocator, str, password); return PhcFormatHasher.verify(allocator, str, password, io);
} }
test "argon2d" { test "argon2d" {
@ -640,6 +638,7 @@ test "argon2d" {
&salt, &salt,
.{ .t = 3, .m = 32, .p = 4, .secret = &secret, .ad = &ad }, .{ .t = 3, .m = 32, .p = 4, .secret = &secret, .ad = &ad },
.argon2d, .argon2d,
std.testing.io,
); );
const want = [_]u8{ const want = [_]u8{
@ -665,6 +664,7 @@ test "argon2i" {
&salt, &salt,
.{ .t = 3, .m = 32, .p = 4, .secret = &secret, .ad = &ad }, .{ .t = 3, .m = 32, .p = 4, .secret = &secret, .ad = &ad },
.argon2i, .argon2i,
std.testing.io,
); );
const want = [_]u8{ const want = [_]u8{
@ -690,6 +690,7 @@ test "argon2id" {
&salt, &salt,
.{ .t = 3, .m = 32, .p = 4, .secret = &secret, .ad = &ad }, .{ .t = 3, .m = 32, .p = 4, .secret = &secret, .ad = &ad },
.argon2id, .argon2id,
std.testing.io,
); );
const want = [_]u8{ const want = [_]u8{
@ -800,44 +801,44 @@ test "kdf" {
.{ .{
.mode = .argon2i, .mode = .argon2i,
.time = 4, .time = 4,
.memory = 4096, .memory = 256,
.threads = 4, .threads = 4,
.hash = "a11f7b7f3f93f02ad4bddb59ab62d121e278369288a0d0e7", .hash = "f7dbbacbf16999e3700817a7e06f65a8db2e9fa9504ede4c",
}, },
.{ .{
.mode = .argon2d, .mode = .argon2d,
.time = 4, .time = 4,
.memory = 4096, .memory = 256,
.threads = 4, .threads = 4,
.hash = "935598181aa8dc2b720914aa6435ac8d3e3a4210c5b0fb2d", .hash = "ea2970501cf49faa5ba1d2e6370204e9b57ca90a8fea937b",
}, },
.{ .{
.mode = .argon2id, .mode = .argon2id,
.time = 4, .time = 4,
.memory = 4096, .memory = 256,
.threads = 4, .threads = 4,
.hash = "145db9733a9f4ee43edf33c509be96b934d505a4efb33c5a", .hash = "fbd40d5a8cb92f88c20bda4b3cdb1f9d5af1efa937032410",
}, },
.{ .{
.mode = .argon2i, .mode = .argon2i,
.time = 4, .time = 4,
.memory = 1024, .memory = 256,
.threads = 8, .threads = 8,
.hash = "0cdd3956aa35e6b475a7b0c63488822f774f15b43f6e6e17", .hash = "15d3c398364e53f68fd12d19baf3f21432d964254fe27467",
}, },
.{ .{
.mode = .argon2d, .mode = .argon2d,
.time = 4, .time = 4,
.memory = 1024, .memory = 256,
.threads = 8, .threads = 8,
.hash = "83604fc2ad0589b9d055578f4d3cc55bc616df3578a896e9", .hash = "23c9adc06f06e21e4612c1466a1be02627690932b02c0df0",
}, },
.{ .{
.mode = .argon2id, .mode = .argon2id,
.time = 4, .time = 4,
.memory = 1024, .memory = 256,
.threads = 8, .threads = 8,
.hash = "8dafa8e004f8ea96bf7c0f93eecf67a6047476143d15577f", .hash = "f22802f8ca47be93f9954e4ce20c1e944e938fbd4a125d9d",
}, },
.{ .{
.mode = .argon2i, .mode = .argon2i,
@ -863,23 +864,23 @@ test "kdf" {
.{ .{
.mode = .argon2i, .mode = .argon2i,
.time = 3, .time = 3,
.memory = 1024, .memory = 256,
.threads = 6, .threads = 6,
.hash = "d236b29c2b2a09babee842b0dec6aa1e83ccbdea8023dced", .hash = "ebc8f91964abd8ceab49a12963b0a9e57d635bfa2aad2884",
}, },
.{ .{
.mode = .argon2d, .mode = .argon2d,
.time = 3, .time = 3,
.memory = 1024, .memory = 256,
.threads = 6, .threads = 6,
.hash = "a3351b0319a53229152023d9206902f4ef59661cdca89481", .hash = "1dd7202fd68da6675f769f4034b7a1db30d8785331954117",
}, },
.{ .{
.mode = .argon2id, .mode = .argon2id,
.time = 3, .time = 3,
.memory = 1024, .memory = 256,
.threads = 6, .threads = 6,
.hash = "1640b932f4b60e272f5d2207b9a9c626ffa1bd88d2349016", .hash = "424436b6ee22a66b04b9d0cf78f190305c5c166bae8baa09",
}, },
}; };
for (test_vectors) |v| { for (test_vectors) |v| {
@ -894,6 +895,7 @@ test "kdf" {
salt, salt,
.{ .t = v.time, .m = v.memory, .p = v.threads }, .{ .t = v.time, .m = v.memory, .p = v.threads },
v.mode, v.mode,
std.testing.io,
); );
try std.testing.expectEqualSlices(u8, &dk, &want); try std.testing.expectEqualSlices(u8, &dk, &want);
@ -903,6 +905,7 @@ test "kdf" {
test "phc format hasher" { test "phc format hasher" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
const password = "testpass"; const password = "testpass";
const io = std.testing.io;
var buf: [128]u8 = undefined; var buf: [128]u8 = undefined;
const hash = try PhcFormatHasher.create( const hash = try PhcFormatHasher.create(
@ -911,25 +914,29 @@ test "phc format hasher" {
.{ .t = 3, .m = 32, .p = 4 }, .{ .t = 3, .m = 32, .p = 4 },
.argon2id, .argon2id,
&buf, &buf,
io,
); );
try PhcFormatHasher.verify(allocator, hash, password); try PhcFormatHasher.verify(allocator, hash, password, io);
} }
test "password hash and password verify" { test "password hash and password verify" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
const password = "testpass"; const password = "testpass";
const io = std.testing.io;
var buf: [128]u8 = undefined; var buf: [128]u8 = undefined;
const hash = try strHash( const hash = try strHash(
password, password,
.{ .allocator = allocator, .params = .{ .t = 3, .m = 32, .p = 4 } }, .{ .allocator = allocator, .params = .{ .t = 3, .m = 32, .p = 4 } },
&buf, &buf,
io,
); );
try strVerify(hash, password, .{ .allocator = allocator }); try strVerify(hash, password, .{ .allocator = allocator }, io);
} }
test "kdf derived key length" { test "kdf derived key length" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
const io = std.testing.io;
const password = "testpass"; const password = "testpass";
const salt = "saltsalt"; const salt = "saltsalt";
@ -937,11 +944,11 @@ test "kdf derived key length" {
const mode = Mode.argon2id; const mode = Mode.argon2id;
var dk1: [11]u8 = undefined; var dk1: [11]u8 = undefined;
try kdf(allocator, &dk1, password, salt, params, mode); try kdf(allocator, &dk1, password, salt, params, mode, io);
var dk2: [77]u8 = undefined; var dk2: [77]u8 = undefined;
try kdf(allocator, &dk2, password, salt, params, mode); try kdf(allocator, &dk2, password, salt, params, mode, io);
var dk3: [111]u8 = undefined; var dk3: [111]u8 = undefined;
try kdf(allocator, &dk3, password, salt, params, mode); try kdf(allocator, &dk3, password, salt, params, mode, io);
} }

View file

@ -450,6 +450,7 @@ fn benchmarkPwhash(
comptime ty: anytype, comptime ty: anytype,
comptime params: *const anyopaque, comptime params: *const anyopaque,
comptime count: comptime_int, comptime count: comptime_int,
io: std.Io,
) !f64 { ) !f64 {
const password = "testpass" ** 2; const password = "testpass" ** 2;
const opts = ty.HashOptions{ const opts = ty.HashOptions{
@ -459,12 +460,20 @@ fn benchmarkPwhash(
}; };
var buf: [256]u8 = undefined; var buf: [256]u8 = undefined;
const strHash = ty.strHash;
const strHashFnInfo = @typeInfo(@TypeOf(strHash)).@"fn";
const needs_io = strHashFnInfo.params.len == 4;
var timer = try Timer.start(); var timer = try Timer.start();
const start = timer.lap(); const start = timer.lap();
{ {
var i: usize = 0; var i: usize = 0;
while (i < count) : (i += 1) { while (i < count) : (i += 1) {
_ = try ty.strHash(password, opts, &buf); if (needs_io) {
_ = try strHash(password, opts, &buf, io);
} else {
_ = try strHash(password, opts, &buf);
}
mem.doNotOptimizeAway(&buf); mem.doNotOptimizeAway(&buf);
} }
} }
@ -623,7 +632,7 @@ pub fn main() !void {
inline for (pwhashes) |H| { inline for (pwhashes) |H| {
if (filter == null or std.mem.indexOf(u8, H.name, filter.?) != null) { if (filter == null or std.mem.indexOf(u8, H.name, filter.?) != null) {
const throughput = try benchmarkPwhash(arena_allocator, H.ty, H.params, mode(64)); const throughput = try benchmarkPwhash(arena_allocator, H.ty, H.params, mode(64), io);
try stdout.print("{s:>17}: {d:10.3} s/ops\n", .{ H.name, throughput }); try stdout.print("{s:>17}: {d:10.3} s/ops\n", .{ H.name, throughput });
try stdout.flush(); try stdout.flush();
} }