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"; diff --git a/lib/std/crypto/scrypt.zig b/lib/std/crypto/scrypt.zig index 43bf5ff054bfb8ca516fe110e166f87830eeffc7..913feb4b380e909b816924a95131df14ef97cf69 100644 --- a/lib/std/crypto/scrypt.zig +++ b/lib/std/crypto/scrypt.zig @@ -417,18 +417,31 @@ 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); + 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); } @@ -465,10 +478,23 @@ 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 { - const salt = crypt_format.saltFromBin(salt_bin.len, salt_bin); + var salt_bin: [default_salt_len]u8 = undefined; + io.random(&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); @@ -514,11 +540,28 @@ 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), + } +} + +/// 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), } } @@ -630,6 +673,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 +681,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 +689,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 +701,7 @@ test "strHash and strVerify" { password, .{ .allocator = alloc, .params = params, .encoding = .crypt }, &buf, + io, ); try strVerify(str, password, verify_options); } @@ -664,6 +710,7 @@ test "strHash and strVerify" { password, .{ .allocator = alloc, .params = params, .encoding = .phc }, &buf, + io, ); try strVerify(str, password, verify_options); } @@ -720,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 }); +}