From 2153759a22f584c5ad43e15fdc13b1afcb5449a7 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Wed, 7 Jan 2026 23:01:38 +0100 Subject: [PATCH 1/3] crypto.scrypt: accept an std.Io parameter instead of direct entropy Safer, and consistent with what is being done in other similar functions. --- lib/std/crypto/scrypt.zig | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/std/crypto/scrypt.zig b/lib/std/crypto/scrypt.zig index 43bf5ff054bfb8ca516fe110e166f87830eeffc7..63a29c4e27d5514fdc334c6ac600e3f81b90a661 100644 --- a/lib/std/crypto/scrypt.zig +++ b/lib/std/crypto/scrypt.zig @@ -417,9 +417,11 @@ const PhcFormatHasher = struct { password: []const u8, params: Params, buf: []u8, - /// Filled with cryptographically secure entropy. - salt: []const u8, + io: std.Io, ) HasherError![]const u8 { + var salt: [default_salt_len]u8 = undefined; + io.random(&salt); + var hash: [default_hash_len]u8 = undefined; try kdf(allocator, &hash, password, &salt, params); @@ -465,9 +467,10 @@ const CryptFormatHasher = struct { password: []const u8, params: Params, buf: []u8, - /// Filled with cryptographically secure entropy. - salt_bin: []const u8, + io: std.Io, ) HasherError![]const u8 { + var salt_bin: [default_salt_len]u8 = undefined; + io.random(&salt_bin); const salt = crypt_format.saltFromBin(salt_bin.len, salt_bin); var hash: [default_hash_len]u8 = undefined; @@ -514,11 +517,12 @@ pub fn strHash( password: []const u8, options: HashOptions, out: []u8, + io: std.Io, ) Error![]const u8 { const allocator = options.allocator orelse return Error.AllocatorRequired; switch (options.encoding) { - .phc => return PhcFormatHasher.create(allocator, password, options.params, out), - .crypt => return CryptFormatHasher.create(allocator, password, options.params, out), + .phc => return PhcFormatHasher.create(allocator, password, options.params, out, io), + .crypt => return CryptFormatHasher.create(allocator, password, options.params, out, io), } } @@ -630,6 +634,7 @@ test "password hashing (crypt format)" { if (!run_long_tests) return error.SkipZigTest; const alloc = std.testing.allocator; + const io = std.testing.io; const str = "$7$A6....1....TrXs5Zk6s8sWHpQgWDIXTR8kUU3s6Jc3s.DtdS8M2i4$a4ik5hGDN7foMuHOW.cp.CtX01UyCeO0.JAG.AHPpx5"; const password = "Y0!?iQa9M%5ekffW(`"; @@ -637,7 +642,7 @@ test "password hashing (crypt format)" { const params = Params.interactive; var buf: [CryptFormatHasher.pwhash_str_length]u8 = undefined; - const str2 = try CryptFormatHasher.create(alloc, password, params, &buf); + const str2 = try CryptFormatHasher.create(alloc, password, params, &buf, io); try CryptFormatHasher.verify(alloc, str2, password); } @@ -645,6 +650,7 @@ test "strHash and strVerify" { if (!run_long_tests) return error.SkipZigTest; const alloc = std.testing.allocator; + const io = std.testing.io; const password = "testpass"; const params = Params.interactive; @@ -656,6 +662,7 @@ test "strHash and strVerify" { password, .{ .allocator = alloc, .params = params, .encoding = .crypt }, &buf, + io, ); try strVerify(str, password, verify_options); } @@ -664,6 +671,7 @@ test "strHash and strVerify" { password, .{ .allocator = alloc, .params = params, .encoding = .phc }, &buf, + io, ); try strVerify(str, password, verify_options); } -- 2.54.0 From 4a29a6e43265f37ac504e4f0a9c78f4917fcab65 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Wed, 7 Jan 2026 23:14:37 +0100 Subject: [PATCH 2/3] Add scrypt.createWithSalt --- lib/std/crypto/scrypt.zig | 65 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/lib/std/crypto/scrypt.zig b/lib/std/crypto/scrypt.zig index 63a29c4e27d5514fdc334c6ac600e3f81b90a661..913feb4b380e909b816924a95131df14ef97cf69 100644 --- a/lib/std/crypto/scrypt.zig +++ b/lib/std/crypto/scrypt.zig @@ -421,16 +421,27 @@ const PhcFormatHasher = struct { ) HasherError![]const u8 { var salt: [default_salt_len]u8 = undefined; io.random(&salt); + return createWithSalt(allocator, password, params, buf, &salt); + } + /// Return a deterministic hash of the password encoded as a PHC-format string. + /// Uses the provided salt instead of generating one randomly. + pub fn createWithSalt( + allocator: mem.Allocator, + password: []const u8, + params: Params, + buf: []u8, + salt: *const [default_salt_len]u8, + ) HasherError![]const u8 { var hash: [default_hash_len]u8 = undefined; - try kdf(allocator, &hash, password, &salt, params); + try kdf(allocator, &hash, password, salt, params); return phc_format.serialize(HashResult{ .alg_id = alg_id, .ln = params.ln, .r = params.r, .p = params.p, - .salt = try BinValue(max_salt_len).fromSlice(&salt), + .salt = try BinValue(max_salt_len).fromSlice(salt), .hash = try BinValue(max_hash_len).fromSlice(&hash), }, buf); } @@ -471,7 +482,19 @@ const CryptFormatHasher = struct { ) HasherError![]const u8 { var salt_bin: [default_salt_len]u8 = undefined; io.random(&salt_bin); - const salt = crypt_format.saltFromBin(salt_bin.len, salt_bin); + return createWithSalt(allocator, password, params, buf, &salt_bin); + } + + /// Return a deterministic hash of the password encoded into the modular crypt format. + /// Uses the provided salt instead of generating one randomly. + pub fn createWithSalt( + allocator: mem.Allocator, + password: []const u8, + params: Params, + buf: []u8, + salt_bin: *const [default_salt_len]u8, + ) HasherError![]const u8 { + const salt = crypt_format.saltFromBin(salt_bin.len, salt_bin.*); var hash: [default_hash_len]u8 = undefined; try kdf(allocator, &hash, password, &salt, params); @@ -526,6 +549,22 @@ pub fn strHash( } } +/// Compute a deterministic hash of a password using the scrypt key derivation function. +/// The function returns a string that includes all the parameters required for verification. +/// Uses the provided salt instead of generating one randomly. +pub fn strHashWithSalt( + password: []const u8, + options: HashOptions, + out: []u8, + salt: *const [default_salt_len]u8, +) Error![]const u8 { + const allocator = options.allocator orelse return Error.AllocatorRequired; + switch (options.encoding) { + .phc => return PhcFormatHasher.createWithSalt(allocator, password, options.params, out, salt), + .crypt => return CryptFormatHasher.createWithSalt(allocator, password, options.params, out, salt), + } +} + /// Options for hash verification. /// /// Allocator is required for scrypt. @@ -728,3 +767,23 @@ test "kdf fast" { try std.testing.expectEqualSlices(u8, &dk, v.want); } } + +test "strHashWithSalt deterministic" { + const alloc = std.testing.allocator; + const password = "testpass"; + const salt: [default_salt_len]u8 = "0123456789abcdef0123456789abcdef".*; + const params: Params = .{ .ln = 1, .r = 1, .p = 1 }; + + var buf1: [128]u8 = undefined; + var buf2: [128]u8 = undefined; + + const str1 = try strHashWithSalt(password, .{ .allocator = alloc, .params = params, .encoding = .phc }, &buf1, &salt); + const str2 = try strHashWithSalt(password, .{ .allocator = alloc, .params = params, .encoding = .phc }, &buf2, &salt); + try std.testing.expectEqualStrings(str1, str2); + try strVerify(str1, password, .{ .allocator = alloc }); + + const str3 = try strHashWithSalt(password, .{ .allocator = alloc, .params = params, .encoding = .crypt }, &buf1, &salt); + const str4 = try strHashWithSalt(password, .{ .allocator = alloc, .params = params, .encoding = .crypt }, &buf2, &salt); + try std.testing.expectEqualStrings(str3, str4); + try strVerify(str3, password, .{ .allocator = alloc }); +} -- 2.54.0 From 3ee092536ec9739794e40f879febd5edadc7b388 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Wed, 7 Jan 2026 23:14:21 +0100 Subject: [PATCH 3/3] Allow the salt to be passed as a parameter in bcrypt --- lib/std/crypto/bcrypt.zig | 111 ++++++++++++++++++++++++-------------- 1 file changed, 72 insertions(+), 39 deletions(-) diff --git a/lib/std/crypto/bcrypt.zig b/lib/std/crypto/bcrypt.zig index 738d4060dfac205dddc253184a102d41eb87accc..267827c75c9375077b76948a05d5a70772fd5aac 100644 --- a/lib/std/crypto/bcrypt.zig +++ b/lib/std/crypto/bcrypt.zig @@ -662,15 +662,27 @@ const PhcFormatHasher = struct { password: []const u8, params: Params, buf: []u8, - /// Filled with cryptographically secure entropy. - salt: *const [salt_length]u8, + io: std.Io, ) HasherError![]const u8 { - const hash = bcrypt(password, salt, params); + var salt: [salt_length]u8 = undefined; + io.random(&salt); + return createWithSalt(password, params, buf, salt); + } + + /// Return a deterministic hash of the password encoded as a PHC-format string. + /// Uses the provided salt instead of generating one randomly. + fn createWithSalt( + password: []const u8, + params: Params, + buf: []u8, + salt: [salt_length]u8, + ) HasherError![]const u8 { + const hash = bcrypt(password, &salt, params); return phc_format.serialize(HashResult{ .alg_id = alg_id, .r = params.rounds_log, - .salt = try BinValue(salt_length).fromSlice(salt), + .salt = try BinValue(salt_length).fromSlice(&salt), .hash = try BinValue(dk_length).fromSlice(&hash), }, buf); } @@ -708,7 +720,19 @@ const CryptFormatHasher = struct { password: []const u8, params: Params, buf: []u8, - /// Filled with cryptographically secure entropy. + io: std.Io, + ) HasherError![]const u8 { + var salt: [salt_length]u8 = undefined; + io.random(&salt); + return createWithSalt(password, params, buf, &salt); + } + + /// Return a deterministic hash of the password encoded into the modular crypt format. + /// Uses the provided salt instead of generating one randomly. + fn createWithSalt( + password: []const u8, + params: Params, + buf: []u8, salt: *const [salt_length]u8, ) HasherError![]const u8 { if (buf.len < pwhash_str_length) return HasherError.NoSpaceLeft; @@ -770,12 +794,26 @@ pub fn strHash( password: []const u8, options: HashOptions, out: []u8, - /// Filled with cryptographically secure entropy. - salt: *const [salt_length]u8, + io: std.Io, ) Error![]const u8 { switch (options.encoding) { - .phc => return PhcFormatHasher.create(password, options.params, out, salt), - .crypt => return CryptFormatHasher.create(password, options.params, out, salt), + .phc => return PhcFormatHasher.create(password, options.params, out, io), + .crypt => return CryptFormatHasher.create(password, options.params, out, io), + } +} + +/// Compute a deterministic hash of a password using the bcrypt key derivation function. +/// The function returns a string that includes all the parameters required for verification. +/// Uses the provided salt instead of generating one randomly. +pub fn strHashWithSalt( + password: []const u8, + options: HashOptions, + out: []u8, + salt: [salt_length]u8, +) Error![]const u8 { + switch (options.encoding) { + .phc => return PhcFormatHasher.createWithSalt(password, options.params, out, salt), + .crypt => return CryptFormatHasher.createWithSalt(password, options.params, out, &salt), } } @@ -821,11 +859,7 @@ test "bcrypt crypt format" { var verify_options: VerifyOptions = .{ .silently_truncate_password = false }; var buf: [hash_length]u8 = undefined; - const s = s: { - var salt: [salt_length]u8 = undefined; - io.random(&salt); - break :s try strHash("password", hash_options, &buf, &salt); - }; + const s = try strHash("password", hash_options, &buf, io); try testing.expect(mem.startsWith(u8, s, crypt_format.prefix)); try strVerify(s, "password", verify_options); @@ -835,11 +869,7 @@ test "bcrypt crypt format" { ); var long_buf: [hash_length]u8 = undefined; - var long_s = s: { - var salt: [salt_length]u8 = undefined; - io.random(&salt); - break :s try strHash("password" ** 100, hash_options, &long_buf, &salt); - }; + var long_s = try strHash("password" ** 100, hash_options, &long_buf, io); try testing.expect(mem.startsWith(u8, long_s, crypt_format.prefix)); try strVerify(long_s, "password" ** 100, verify_options); @@ -850,11 +880,7 @@ test "bcrypt crypt format" { hash_options.params.silently_truncate_password = true; verify_options.silently_truncate_password = true; - long_s = s: { - var salt: [salt_length]u8 = undefined; - io.random(&salt); - break :s try strHash("password" ** 100, hash_options, &long_buf, &salt); - }; + long_s = try strHash("password" ** 100, hash_options, &long_buf, io); try strVerify(long_s, "password" ** 101, verify_options); try strVerify( @@ -874,11 +900,7 @@ test "bcrypt phc format" { const prefix = "$bcrypt$"; var buf: [hash_length * 2]u8 = undefined; - const s = s: { - var salt: [salt_length]u8 = undefined; - io.random(&salt); - break :s try strHash("password", hash_options, &buf, &salt); - }; + const s = try strHash("password", hash_options, &buf, io); try testing.expect(mem.startsWith(u8, s, prefix)); try strVerify(s, "password", verify_options); @@ -888,11 +910,7 @@ test "bcrypt phc format" { ); var long_buf: [hash_length * 2]u8 = undefined; - var long_s = s: { - var salt: [salt_length]u8 = undefined; - io.random(&salt); - break :s try strHash("password" ** 100, hash_options, &long_buf, &salt); - }; + var long_s = try strHash("password" ** 100, hash_options, &long_buf, io); try testing.expect(mem.startsWith(u8, long_s, prefix)); try strVerify(long_s, "password" ** 100, verify_options); @@ -903,11 +921,7 @@ test "bcrypt phc format" { hash_options.params.silently_truncate_password = true; verify_options.silently_truncate_password = true; - long_s = s: { - var salt: [salt_length]u8 = undefined; - io.random(&salt); - break :s try strHash("password" ** 100, hash_options, &long_buf, &salt); - }; + long_s = try strHash("password" ** 100, hash_options, &long_buf, io); try strVerify(long_s, "password" ** 101, verify_options); try strVerify( @@ -917,6 +931,25 @@ test "bcrypt phc format" { ); } +test "strHashWithSalt deterministic" { + const password = "testpass"; + const salt: [salt_length]u8 = "0123456789abcdef".*; + const params: Params = .{ .rounds_log = 5, .silently_truncate_password = false }; + + var buf1: [hash_length * 2]u8 = undefined; + var buf2: [hash_length * 2]u8 = undefined; + + const str1 = try strHashWithSalt(password, .{ .params = params, .encoding = .phc }, &buf1, salt); + const str2 = try strHashWithSalt(password, .{ .params = params, .encoding = .phc }, &buf2, salt); + try testing.expectEqualStrings(str1, str2); + try strVerify(str1, password, .{ .silently_truncate_password = false }); + + const str3 = try strHashWithSalt(password, .{ .params = params, .encoding = .crypt }, &buf1, salt); + const str4 = try strHashWithSalt(password, .{ .params = params, .encoding = .crypt }, &buf2, salt); + try testing.expectEqualStrings(str3, str4); + try strVerify(str3, password, .{ .silently_truncate_password = false }); +} + test "openssh kdf" { var key: [100]u8 = undefined; const pass = "password"; -- 2.54.0