authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-09 09:39:35+01:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-09 09:39:35+01:00
log721bdb6256302540bf019370ff3085596fe866e3
treebb148afba8fb8c62ecc2bf3d2efc7682521c341f
parent7c0b42ba0c8a52018ad6c5c498786bc560fcc50c
parent3ee092536ec9739794e40f879febd5edadc7b388

Merge pull request 'crypto.scrypt: accept an std.Io parameter instead of direct entropy' (#30738) from jedisct1/zig:scryptfixes into master

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/30738 Reviewed-by: Andrew Kelley <andrew@ziglang.org>

2 files changed, 149 insertions(+), 49 deletions(-)

lib/std/crypto/bcrypt.zig+72-39
......@@ -662,15 +662,27 @@ const PhcFormatHasher = struct {
662662 password: []const u8,
663663 params: Params,
664664 buf: []u8,
665 /// Filled with cryptographically secure entropy.
666 salt: *const [salt_length]u8,
665 io: std.Io,
667666 ) HasherError![]const u8 {
668 const hash = bcrypt(password, salt, params);
667 var salt: [salt_length]u8 = undefined;
668 io.random(&salt);
669 return createWithSalt(password, params, buf, salt);
670 }
671
672 /// Return a deterministic hash of the password encoded as a PHC-format string.
673 /// Uses the provided salt instead of generating one randomly.
674 fn createWithSalt(
675 password: []const u8,
676 params: Params,
677 buf: []u8,
678 salt: [salt_length]u8,
679 ) HasherError![]const u8 {
680 const hash = bcrypt(password, &salt, params);
669681
670682 return phc_format.serialize(HashResult{
671683 .alg_id = alg_id,
672684 .r = params.rounds_log,
673 .salt = try BinValue(salt_length).fromSlice(salt),
685 .salt = try BinValue(salt_length).fromSlice(&salt),
674686 .hash = try BinValue(dk_length).fromSlice(&hash),
675687 }, buf);
676688 }
......@@ -708,7 +720,19 @@ const CryptFormatHasher = struct {
708720 password: []const u8,
709721 params: Params,
710722 buf: []u8,
711 /// Filled with cryptographically secure entropy.
723 io: std.Io,
724 ) HasherError![]const u8 {
725 var salt: [salt_length]u8 = undefined;
726 io.random(&salt);
727 return createWithSalt(password, params, buf, &salt);
728 }
729
730 /// Return a deterministic hash of the password encoded into the modular crypt format.
731 /// Uses the provided salt instead of generating one randomly.
732 fn createWithSalt(
733 password: []const u8,
734 params: Params,
735 buf: []u8,
712736 salt: *const [salt_length]u8,
713737 ) HasherError![]const u8 {
714738 if (buf.len < pwhash_str_length) return HasherError.NoSpaceLeft;
......@@ -770,12 +794,26 @@ pub fn strHash(
770794 password: []const u8,
771795 options: HashOptions,
772796 out: []u8,
773 /// Filled with cryptographically secure entropy.
774 salt: *const [salt_length]u8,
797 io: std.Io,
775798) Error![]const u8 {
776799 switch (options.encoding) {
777 .phc => return PhcFormatHasher.create(password, options.params, out, salt),
778 .crypt => return CryptFormatHasher.create(password, options.params, out, salt),
800 .phc => return PhcFormatHasher.create(password, options.params, out, io),
801 .crypt => return CryptFormatHasher.create(password, options.params, out, io),
802 }
803}
804
805/// Compute a deterministic hash of a password using the bcrypt key derivation function.
806/// The function returns a string that includes all the parameters required for verification.
807/// Uses the provided salt instead of generating one randomly.
808pub fn strHashWithSalt(
809 password: []const u8,
810 options: HashOptions,
811 out: []u8,
812 salt: [salt_length]u8,
813) Error![]const u8 {
814 switch (options.encoding) {
815 .phc => return PhcFormatHasher.createWithSalt(password, options.params, out, salt),
816 .crypt => return CryptFormatHasher.createWithSalt(password, options.params, out, &salt),
779817 }
780818}
781819
......@@ -821,11 +859,7 @@ test "bcrypt crypt format" {
821859 var verify_options: VerifyOptions = .{ .silently_truncate_password = false };
822860
823861 var buf: [hash_length]u8 = undefined;
824 const s = s: {
825 var salt: [salt_length]u8 = undefined;
826 io.random(&salt);
827 break :s try strHash("password", hash_options, &buf, &salt);
828 };
862 const s = try strHash("password", hash_options, &buf, io);
829863
830864 try testing.expect(mem.startsWith(u8, s, crypt_format.prefix));
831865 try strVerify(s, "password", verify_options);
......@@ -835,11 +869,7 @@ test "bcrypt crypt format" {
835869 );
836870
837871 var long_buf: [hash_length]u8 = undefined;
838 var long_s = s: {
839 var salt: [salt_length]u8 = undefined;
840 io.random(&salt);
841 break :s try strHash("password" ** 100, hash_options, &long_buf, &salt);
842 };
872 var long_s = try strHash("password" ** 100, hash_options, &long_buf, io);
843873
844874 try testing.expect(mem.startsWith(u8, long_s, crypt_format.prefix));
845875 try strVerify(long_s, "password" ** 100, verify_options);
......@@ -850,11 +880,7 @@ test "bcrypt crypt format" {
850880
851881 hash_options.params.silently_truncate_password = true;
852882 verify_options.silently_truncate_password = true;
853 long_s = s: {
854 var salt: [salt_length]u8 = undefined;
855 io.random(&salt);
856 break :s try strHash("password" ** 100, hash_options, &long_buf, &salt);
857 };
883 long_s = try strHash("password" ** 100, hash_options, &long_buf, io);
858884 try strVerify(long_s, "password" ** 101, verify_options);
859885
860886 try strVerify(
......@@ -874,11 +900,7 @@ test "bcrypt phc format" {
874900 const prefix = "$bcrypt$";
875901
876902 var buf: [hash_length * 2]u8 = undefined;
877 const s = s: {
878 var salt: [salt_length]u8 = undefined;
879 io.random(&salt);
880 break :s try strHash("password", hash_options, &buf, &salt);
881 };
903 const s = try strHash("password", hash_options, &buf, io);
882904
883905 try testing.expect(mem.startsWith(u8, s, prefix));
884906 try strVerify(s, "password", verify_options);
......@@ -888,11 +910,7 @@ test "bcrypt phc format" {
888910 );
889911
890912 var long_buf: [hash_length * 2]u8 = undefined;
891 var long_s = s: {
892 var salt: [salt_length]u8 = undefined;
893 io.random(&salt);
894 break :s try strHash("password" ** 100, hash_options, &long_buf, &salt);
895 };
913 var long_s = try strHash("password" ** 100, hash_options, &long_buf, io);
896914
897915 try testing.expect(mem.startsWith(u8, long_s, prefix));
898916 try strVerify(long_s, "password" ** 100, verify_options);
......@@ -903,11 +921,7 @@ test "bcrypt phc format" {
903921
904922 hash_options.params.silently_truncate_password = true;
905923 verify_options.silently_truncate_password = true;
906 long_s = s: {
907 var salt: [salt_length]u8 = undefined;
908 io.random(&salt);
909 break :s try strHash("password" ** 100, hash_options, &long_buf, &salt);
910 };
924 long_s = try strHash("password" ** 100, hash_options, &long_buf, io);
911925 try strVerify(long_s, "password" ** 101, verify_options);
912926
913927 try strVerify(
......@@ -917,6 +931,25 @@ test "bcrypt phc format" {
917931 );
918932}
919933
934test "strHashWithSalt deterministic" {
935 const password = "testpass";
936 const salt: [salt_length]u8 = "0123456789abcdef".*;
937 const params: Params = .{ .rounds_log = 5, .silently_truncate_password = false };
938
939 var buf1: [hash_length * 2]u8 = undefined;
940 var buf2: [hash_length * 2]u8 = undefined;
941
942 const str1 = try strHashWithSalt(password, .{ .params = params, .encoding = .phc }, &buf1, salt);
943 const str2 = try strHashWithSalt(password, .{ .params = params, .encoding = .phc }, &buf2, salt);
944 try testing.expectEqualStrings(str1, str2);
945 try strVerify(str1, password, .{ .silently_truncate_password = false });
946
947 const str3 = try strHashWithSalt(password, .{ .params = params, .encoding = .crypt }, &buf1, salt);
948 const str4 = try strHashWithSalt(password, .{ .params = params, .encoding = .crypt }, &buf2, salt);
949 try testing.expectEqualStrings(str3, str4);
950 try strVerify(str3, password, .{ .silently_truncate_password = false });
951}
952
920953test "openssh kdf" {
921954 var key: [100]u8 = undefined;
922955 const pass = "password";
lib/std/crypto/scrypt.zig+77-10
......@@ -417,18 +417,31 @@ const PhcFormatHasher = struct {
417417 password: []const u8,
418418 params: Params,
419419 buf: []u8,
420 /// Filled with cryptographically secure entropy.
421 salt: []const u8,
420 io: std.Io,
421 ) HasherError![]const u8 {
422 var salt: [default_salt_len]u8 = undefined;
423 io.random(&salt);
424 return createWithSalt(allocator, password, params, buf, &salt);
425 }
426
427 /// Return a deterministic hash of the password encoded as a PHC-format string.
428 /// Uses the provided salt instead of generating one randomly.
429 pub fn createWithSalt(
430 allocator: mem.Allocator,
431 password: []const u8,
432 params: Params,
433 buf: []u8,
434 salt: *const [default_salt_len]u8,
422435 ) HasherError![]const u8 {
423436 var hash: [default_hash_len]u8 = undefined;
424 try kdf(allocator, &hash, password, &salt, params);
437 try kdf(allocator, &hash, password, salt, params);
425438
426439 return phc_format.serialize(HashResult{
427440 .alg_id = alg_id,
428441 .ln = params.ln,
429442 .r = params.r,
430443 .p = params.p,
431 .salt = try BinValue(max_salt_len).fromSlice(&salt),
444 .salt = try BinValue(max_salt_len).fromSlice(salt),
432445 .hash = try BinValue(max_hash_len).fromSlice(&hash),
433446 }, buf);
434447 }
......@@ -465,10 +478,23 @@ const CryptFormatHasher = struct {
465478 password: []const u8,
466479 params: Params,
467480 buf: []u8,
468 /// Filled with cryptographically secure entropy.
469 salt_bin: []const u8,
481 io: std.Io,
470482 ) HasherError![]const u8 {
471 const salt = crypt_format.saltFromBin(salt_bin.len, salt_bin);
483 var salt_bin: [default_salt_len]u8 = undefined;
484 io.random(&salt_bin);
485 return createWithSalt(allocator, password, params, buf, &salt_bin);
486 }
487
488 /// Return a deterministic hash of the password encoded into the modular crypt format.
489 /// Uses the provided salt instead of generating one randomly.
490 pub fn createWithSalt(
491 allocator: mem.Allocator,
492 password: []const u8,
493 params: Params,
494 buf: []u8,
495 salt_bin: *const [default_salt_len]u8,
496 ) HasherError![]const u8 {
497 const salt = crypt_format.saltFromBin(salt_bin.len, salt_bin.*);
472498
473499 var hash: [default_hash_len]u8 = undefined;
474500 try kdf(allocator, &hash, password, &salt, params);
......@@ -514,11 +540,28 @@ pub fn strHash(
514540 password: []const u8,
515541 options: HashOptions,
516542 out: []u8,
543 io: std.Io,
517544) Error![]const u8 {
518545 const allocator = options.allocator orelse return Error.AllocatorRequired;
519546 switch (options.encoding) {
520 .phc => return PhcFormatHasher.create(allocator, password, options.params, out),
521 .crypt => return CryptFormatHasher.create(allocator, password, options.params, out),
547 .phc => return PhcFormatHasher.create(allocator, password, options.params, out, io),
548 .crypt => return CryptFormatHasher.create(allocator, password, options.params, out, io),
549 }
550}
551
552/// Compute a deterministic hash of a password using the scrypt key derivation function.
553/// The function returns a string that includes all the parameters required for verification.
554/// Uses the provided salt instead of generating one randomly.
555pub fn strHashWithSalt(
556 password: []const u8,
557 options: HashOptions,
558 out: []u8,
559 salt: *const [default_salt_len]u8,
560) Error![]const u8 {
561 const allocator = options.allocator orelse return Error.AllocatorRequired;
562 switch (options.encoding) {
563 .phc => return PhcFormatHasher.createWithSalt(allocator, password, options.params, out, salt),
564 .crypt => return CryptFormatHasher.createWithSalt(allocator, password, options.params, out, salt),
522565 }
523566}
524567
......@@ -630,6 +673,7 @@ test "password hashing (crypt format)" {
630673 if (!run_long_tests) return error.SkipZigTest;
631674
632675 const alloc = std.testing.allocator;
676 const io = std.testing.io;
633677
634678 const str = "$7$A6....1....TrXs5Zk6s8sWHpQgWDIXTR8kUU3s6Jc3s.DtdS8M2i4$a4ik5hGDN7foMuHOW.cp.CtX01UyCeO0.JAG.AHPpx5";
635679 const password = "Y0!?iQa9M%5ekffW(`";
......@@ -637,7 +681,7 @@ test "password hashing (crypt format)" {
637681
638682 const params = Params.interactive;
639683 var buf: [CryptFormatHasher.pwhash_str_length]u8 = undefined;
640 const str2 = try CryptFormatHasher.create(alloc, password, params, &buf);
684 const str2 = try CryptFormatHasher.create(alloc, password, params, &buf, io);
641685 try CryptFormatHasher.verify(alloc, str2, password);
642686}
643687
......@@ -645,6 +689,7 @@ test "strHash and strVerify" {
645689 if (!run_long_tests) return error.SkipZigTest;
646690
647691 const alloc = std.testing.allocator;
692 const io = std.testing.io;
648693
649694 const password = "testpass";
650695 const params = Params.interactive;
......@@ -656,6 +701,7 @@ test "strHash and strVerify" {
656701 password,
657702 .{ .allocator = alloc, .params = params, .encoding = .crypt },
658703 &buf,
704 io,
659705 );
660706 try strVerify(str, password, verify_options);
661707 }
......@@ -664,6 +710,7 @@ test "strHash and strVerify" {
664710 password,
665711 .{ .allocator = alloc, .params = params, .encoding = .phc },
666712 &buf,
713 io,
667714 );
668715 try strVerify(str, password, verify_options);
669716 }
......@@ -720,3 +767,23 @@ test "kdf fast" {
720767 try std.testing.expectEqualSlices(u8, &dk, v.want);
721768 }
722769}
770
771test "strHashWithSalt deterministic" {
772 const alloc = std.testing.allocator;
773 const password = "testpass";
774 const salt: [default_salt_len]u8 = "0123456789abcdef0123456789abcdef".*;
775 const params: Params = .{ .ln = 1, .r = 1, .p = 1 };
776
777 var buf1: [128]u8 = undefined;
778 var buf2: [128]u8 = undefined;
779
780 const str1 = try strHashWithSalt(password, .{ .allocator = alloc, .params = params, .encoding = .phc }, &buf1, &salt);
781 const str2 = try strHashWithSalt(password, .{ .allocator = alloc, .params = params, .encoding = .phc }, &buf2, &salt);
782 try std.testing.expectEqualStrings(str1, str2);
783 try strVerify(str1, password, .{ .allocator = alloc });
784
785 const str3 = try strHashWithSalt(password, .{ .allocator = alloc, .params = params, .encoding = .crypt }, &buf1, &salt);
786 const str4 = try strHashWithSalt(password, .{ .allocator = alloc, .params = params, .encoding = .crypt }, &buf2, &salt);
787 try std.testing.expectEqualStrings(str3, str4);
788 try strVerify(str3, password, .{ .allocator = alloc });
789}