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 {...@@ -662,15 +662,27 @@ const PhcFormatHasher = struct {
662 password: []const u8,662 password: []const u8,
663 params: Params,663 params: Params,
664 buf: []u8,664 buf: []u8,
665 /// Filled with cryptographically secure entropy.665 io: std.Io,
666 salt: *const [salt_length]u8,
667 ) HasherError![]const u8 {666 ) 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
670 return phc_format.serialize(HashResult{682 return phc_format.serialize(HashResult{
671 .alg_id = alg_id,683 .alg_id = alg_id,
672 .r = params.rounds_log,684 .r = params.rounds_log,
673 .salt = try BinValue(salt_length).fromSlice(salt),685 .salt = try BinValue(salt_length).fromSlice(&salt),
674 .hash = try BinValue(dk_length).fromSlice(&hash),686 .hash = try BinValue(dk_length).fromSlice(&hash),
675 }, buf);687 }, buf);
676 }688 }
...@@ -708,7 +720,19 @@ const CryptFormatHasher = struct {...@@ -708,7 +720,19 @@ const CryptFormatHasher = struct {
708 password: []const u8,720 password: []const u8,
709 params: Params,721 params: Params,
710 buf: []u8,722 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,
712 salt: *const [salt_length]u8,736 salt: *const [salt_length]u8,
713 ) HasherError![]const u8 {737 ) HasherError![]const u8 {
714 if (buf.len < pwhash_str_length) return HasherError.NoSpaceLeft;738 if (buf.len < pwhash_str_length) return HasherError.NoSpaceLeft;
...@@ -770,12 +794,26 @@ pub fn strHash(...@@ -770,12 +794,26 @@ pub fn strHash(
770 password: []const u8,794 password: []const u8,
771 options: HashOptions,795 options: HashOptions,
772 out: []u8,796 out: []u8,
773 /// Filled with cryptographically secure entropy.797 io: std.Io,
774 salt: *const [salt_length]u8,
775) Error![]const u8 {798) Error![]const u8 {
776 switch (options.encoding) {799 switch (options.encoding) {
777 .phc => return PhcFormatHasher.create(password, options.params, out, salt),800 .phc => return PhcFormatHasher.create(password, options.params, out, io),
778 .crypt => return CryptFormatHasher.create(password, options.params, out, salt),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),
779 }817 }
780}818}
781819
...@@ -821,11 +859,7 @@ test "bcrypt crypt format" {...@@ -821,11 +859,7 @@ test "bcrypt crypt format" {
821 var verify_options: VerifyOptions = .{ .silently_truncate_password = false };859 var verify_options: VerifyOptions = .{ .silently_truncate_password = false };
822860
823 var buf: [hash_length]u8 = undefined;861 var buf: [hash_length]u8 = undefined;
824 const s = s: {862 const s = try strHash("password", hash_options, &buf, io);
825 var salt: [salt_length]u8 = undefined;
826 io.random(&salt);
827 break :s try strHash("password", hash_options, &buf, &salt);
828 };
829863
830 try testing.expect(mem.startsWith(u8, s, crypt_format.prefix));864 try testing.expect(mem.startsWith(u8, s, crypt_format.prefix));
831 try strVerify(s, "password", verify_options);865 try strVerify(s, "password", verify_options);
...@@ -835,11 +869,7 @@ test "bcrypt crypt format" {...@@ -835,11 +869,7 @@ test "bcrypt crypt format" {
835 );869 );
836870
837 var long_buf: [hash_length]u8 = undefined;871 var long_buf: [hash_length]u8 = undefined;
838 var long_s = s: {872 var long_s = try strHash("password" ** 100, hash_options, &long_buf, io);
839 var salt: [salt_length]u8 = undefined;
840 io.random(&salt);
841 break :s try strHash("password" ** 100, hash_options, &long_buf, &salt);
842 };
843873
844 try testing.expect(mem.startsWith(u8, long_s, crypt_format.prefix));874 try testing.expect(mem.startsWith(u8, long_s, crypt_format.prefix));
845 try strVerify(long_s, "password" ** 100, verify_options);875 try strVerify(long_s, "password" ** 100, verify_options);
...@@ -850,11 +880,7 @@ test "bcrypt crypt format" {...@@ -850,11 +880,7 @@ test "bcrypt crypt format" {
850880
851 hash_options.params.silently_truncate_password = true;881 hash_options.params.silently_truncate_password = true;
852 verify_options.silently_truncate_password = true;882 verify_options.silently_truncate_password = true;
853 long_s = s: {883 long_s = try strHash("password" ** 100, hash_options, &long_buf, io);
854 var salt: [salt_length]u8 = undefined;
855 io.random(&salt);
856 break :s try strHash("password" ** 100, hash_options, &long_buf, &salt);
857 };
858 try strVerify(long_s, "password" ** 101, verify_options);884 try strVerify(long_s, "password" ** 101, verify_options);
859885
860 try strVerify(886 try strVerify(
...@@ -874,11 +900,7 @@ test "bcrypt phc format" {...@@ -874,11 +900,7 @@ test "bcrypt phc format" {
874 const prefix = "$bcrypt$";900 const prefix = "$bcrypt$";
875901
876 var buf: [hash_length * 2]u8 = undefined;902 var buf: [hash_length * 2]u8 = undefined;
877 const s = s: {903 const s = try strHash("password", hash_options, &buf, io);
878 var salt: [salt_length]u8 = undefined;
879 io.random(&salt);
880 break :s try strHash("password", hash_options, &buf, &salt);
881 };
882904
883 try testing.expect(mem.startsWith(u8, s, prefix));905 try testing.expect(mem.startsWith(u8, s, prefix));
884 try strVerify(s, "password", verify_options);906 try strVerify(s, "password", verify_options);
...@@ -888,11 +910,7 @@ test "bcrypt phc format" {...@@ -888,11 +910,7 @@ test "bcrypt phc format" {
888 );910 );
889911
890 var long_buf: [hash_length * 2]u8 = undefined;912 var long_buf: [hash_length * 2]u8 = undefined;
891 var long_s = s: {913 var long_s = try strHash("password" ** 100, hash_options, &long_buf, io);
892 var salt: [salt_length]u8 = undefined;
893 io.random(&salt);
894 break :s try strHash("password" ** 100, hash_options, &long_buf, &salt);
895 };
896914
897 try testing.expect(mem.startsWith(u8, long_s, prefix));915 try testing.expect(mem.startsWith(u8, long_s, prefix));
898 try strVerify(long_s, "password" ** 100, verify_options);916 try strVerify(long_s, "password" ** 100, verify_options);
...@@ -903,11 +921,7 @@ test "bcrypt phc format" {...@@ -903,11 +921,7 @@ test "bcrypt phc format" {
903921
904 hash_options.params.silently_truncate_password = true;922 hash_options.params.silently_truncate_password = true;
905 verify_options.silently_truncate_password = true;923 verify_options.silently_truncate_password = true;
906 long_s = s: {924 long_s = try strHash("password" ** 100, hash_options, &long_buf, io);
907 var salt: [salt_length]u8 = undefined;
908 io.random(&salt);
909 break :s try strHash("password" ** 100, hash_options, &long_buf, &salt);
910 };
911 try strVerify(long_s, "password" ** 101, verify_options);925 try strVerify(long_s, "password" ** 101, verify_options);
912926
913 try strVerify(927 try strVerify(
...@@ -917,6 +931,25 @@ test "bcrypt phc format" {...@@ -917,6 +931,25 @@ test "bcrypt phc format" {
917 );931 );
918}932}
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
920test "openssh kdf" {953test "openssh kdf" {
921 var key: [100]u8 = undefined;954 var key: [100]u8 = undefined;
922 const pass = "password";955 const pass = "password";
lib/std/crypto/scrypt.zig+77-10
...@@ -417,18 +417,31 @@ const PhcFormatHasher = struct {...@@ -417,18 +417,31 @@ const PhcFormatHasher = struct {
417 password: []const u8,417 password: []const u8,
418 params: Params,418 params: Params,
419 buf: []u8,419 buf: []u8,
420 /// Filled with cryptographically secure entropy.420 io: std.Io,
421 salt: []const u8,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,
422 ) HasherError![]const u8 {435 ) HasherError![]const u8 {
423 var hash: [default_hash_len]u8 = undefined;436 var hash: [default_hash_len]u8 = undefined;
424 try kdf(allocator, &hash, password, &salt, params);437 try kdf(allocator, &hash, password, salt, params);
425438
426 return phc_format.serialize(HashResult{439 return phc_format.serialize(HashResult{
427 .alg_id = alg_id,440 .alg_id = alg_id,
428 .ln = params.ln,441 .ln = params.ln,
429 .r = params.r,442 .r = params.r,
430 .p = params.p,443 .p = params.p,
431 .salt = try BinValue(max_salt_len).fromSlice(&salt),444 .salt = try BinValue(max_salt_len).fromSlice(salt),
432 .hash = try BinValue(max_hash_len).fromSlice(&hash),445 .hash = try BinValue(max_hash_len).fromSlice(&hash),
433 }, buf);446 }, buf);
434 }447 }
...@@ -465,10 +478,23 @@ const CryptFormatHasher = struct {...@@ -465,10 +478,23 @@ const CryptFormatHasher = struct {
465 password: []const u8,478 password: []const u8,
466 params: Params,479 params: Params,
467 buf: []u8,480 buf: []u8,
468 /// Filled with cryptographically secure entropy.481 io: std.Io,
469 salt_bin: []const u8,
470 ) HasherError![]const u8 {482 ) 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
473 var hash: [default_hash_len]u8 = undefined;499 var hash: [default_hash_len]u8 = undefined;
474 try kdf(allocator, &hash, password, &salt, params);500 try kdf(allocator, &hash, password, &salt, params);
...@@ -514,11 +540,28 @@ pub fn strHash(...@@ -514,11 +540,28 @@ pub fn strHash(
514 password: []const u8,540 password: []const u8,
515 options: HashOptions,541 options: HashOptions,
516 out: []u8,542 out: []u8,
543 io: std.Io,
517) Error![]const u8 {544) Error![]const u8 {
518 const allocator = options.allocator orelse return Error.AllocatorRequired;545 const allocator = options.allocator orelse return Error.AllocatorRequired;
519 switch (options.encoding) {546 switch (options.encoding) {
520 .phc => return PhcFormatHasher.create(allocator, password, options.params, out),547 .phc => return PhcFormatHasher.create(allocator, password, options.params, out, io),
521 .crypt => return CryptFormatHasher.create(allocator, password, options.params, out),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),
522 }565 }
523}566}
524567
...@@ -630,6 +673,7 @@ test "password hashing (crypt format)" {...@@ -630,6 +673,7 @@ test "password hashing (crypt format)" {
630 if (!run_long_tests) return error.SkipZigTest;673 if (!run_long_tests) return error.SkipZigTest;
631674
632 const alloc = std.testing.allocator;675 const alloc = std.testing.allocator;
676 const io = std.testing.io;
633677
634 const str = "$7$A6....1....TrXs5Zk6s8sWHpQgWDIXTR8kUU3s6Jc3s.DtdS8M2i4$a4ik5hGDN7foMuHOW.cp.CtX01UyCeO0.JAG.AHPpx5";678 const str = "$7$A6....1....TrXs5Zk6s8sWHpQgWDIXTR8kUU3s6Jc3s.DtdS8M2i4$a4ik5hGDN7foMuHOW.cp.CtX01UyCeO0.JAG.AHPpx5";
635 const password = "Y0!?iQa9M%5ekffW(`";679 const password = "Y0!?iQa9M%5ekffW(`";
...@@ -637,7 +681,7 @@ test "password hashing (crypt format)" {...@@ -637,7 +681,7 @@ test "password hashing (crypt format)" {
637681
638 const params = Params.interactive;682 const params = Params.interactive;
639 var buf: [CryptFormatHasher.pwhash_str_length]u8 = undefined;683 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);
641 try CryptFormatHasher.verify(alloc, str2, password);685 try CryptFormatHasher.verify(alloc, str2, password);
642}686}
643687
...@@ -645,6 +689,7 @@ test "strHash and strVerify" {...@@ -645,6 +689,7 @@ test "strHash and strVerify" {
645 if (!run_long_tests) return error.SkipZigTest;689 if (!run_long_tests) return error.SkipZigTest;
646690
647 const alloc = std.testing.allocator;691 const alloc = std.testing.allocator;
692 const io = std.testing.io;
648693
649 const password = "testpass";694 const password = "testpass";
650 const params = Params.interactive;695 const params = Params.interactive;
...@@ -656,6 +701,7 @@ test "strHash and strVerify" {...@@ -656,6 +701,7 @@ test "strHash and strVerify" {
656 password,701 password,
657 .{ .allocator = alloc, .params = params, .encoding = .crypt },702 .{ .allocator = alloc, .params = params, .encoding = .crypt },
658 &buf,703 &buf,
704 io,
659 );705 );
660 try strVerify(str, password, verify_options);706 try strVerify(str, password, verify_options);
661 }707 }
...@@ -664,6 +710,7 @@ test "strHash and strVerify" {...@@ -664,6 +710,7 @@ test "strHash and strVerify" {
664 password,710 password,
665 .{ .allocator = alloc, .params = params, .encoding = .phc },711 .{ .allocator = alloc, .params = params, .encoding = .phc },
666 &buf,712 &buf,
713 io,
667 );714 );
668 try strVerify(str, password, verify_options);715 try strVerify(str, password, verify_options);
669 }716 }
...@@ -720,3 +767,23 @@ test "kdf fast" {...@@ -720,3 +767,23 @@ test "kdf fast" {
720 try std.testing.expectEqualSlices(u8, &dk, v.want);767 try std.testing.expectEqualSlices(u8, &dk, v.want);
721 }768 }
722}769}
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}