authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2025-02-19 22:37:51+01:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2025-02-19 22:37:51+01:00
log8d824dfdd06c6b0f307c6ea41dfdf316acf4a662
tree7c44262644f7d81ba3ace09351d88cee3b03b82a
parentbd237bced4c45228dadba07154660e91746d165b
signaturebadge-check Signed by PGP key B5690EEEBB952194

crypto.pwhash.bcrypt: make silently_truncate_password a member of Params (#22792)

* bcrypt: make silently_truncate_password a member of Params This removes the need for having both `bcrypt()` and `bcryptWithTruncation()` in the public API. And whether truncation happens or not becomes even more explicit. * Update crypto benchmark

2 files changed, 37 insertions(+), 43 deletions(-)

lib/std/crypto/bcrypt.zig+36-42
...@@ -412,21 +412,19 @@ pub const Params = struct {...@@ -412,21 +412,19 @@ pub const Params = struct {
412 /// log2 of the number of rounds412 /// log2 of the number of rounds
413 rounds_log: u6,413 rounds_log: u6,
414414
415 /// As originally defined, bcrypt silently truncates passwords to 72 bytes.
416 /// In order to overcome this limitation, if `silently_truncate_password` is set to `false`,
417 /// long passwords will be automatically pre-hashed using HMAC-SHA512 before being passed to bcrypt.
418 /// Only set `silently_truncate_password` to `true` for compatibility with traditional bcrypt implementations,
419 /// or if you want to handle the truncation yourself.
420 silently_truncate_password: bool,
421
415 /// Minimum recommended parameters according to the422 /// Minimum recommended parameters according to the
416 /// [OWASP cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html).423 /// [OWASP cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html).
417 pub const owasp = Self{ .rounds_log = 10 };424 pub const owasp = Self{ .rounds_log = 10, .silently_truncate_password = false };
418};425};
419426
420/// Compute a hash of a password using 2^rounds_log rounds of the bcrypt key stretching function.427fn bcryptWithTruncation(
421/// bcrypt is a computationally expensive and cache-hard function, explicitly designed to slow down exhaustive searches.
422///
423/// The function returns the hash as a `dk_length` byte array, that doesn't include anything besides the hash output.
424///
425/// For a generic key-derivation function, use `bcrypt.pbkdf()` instead.
426///
427/// IMPORTANT: by design, bcrypt silently truncates passwords to 72 bytes.
428/// If this is an issue for your application, use `bcryptWithoutTruncation` instead.
429pub fn bcrypt(
430 password: []const u8,428 password: []const u8,
431 salt: [salt_length]u8,429 salt: [salt_length]u8,
432 params: Params,430 params: Params,
...@@ -465,17 +463,15 @@ pub fn bcrypt(...@@ -465,17 +463,15 @@ pub fn bcrypt(
465///463///
466/// The function returns the hash as a `dk_length` byte array, that doesn't include anything besides the hash output.464/// The function returns the hash as a `dk_length` byte array, that doesn't include anything besides the hash output.
467///465///
468/// For a generic key-derivation function, use `bcrypt.pbkdf()` instead.466/// This function was designed for password storage, not for key derivation.
469///467/// For key derivation, use `bcrypt.pbkdf()` or `bcrypt.opensshKdf()` instead.
470/// This function is identical to `bcrypt`, except that it doesn't silently truncate passwords.468pub fn bcrypt(
471/// Instead, passwords longer than 72 bytes are pre-hashed using HMAC-SHA512 before being passed to bcrypt.
472pub fn bcryptWithoutTruncation(
473 password: []const u8,469 password: []const u8,
474 salt: [salt_length]u8,470 salt: [salt_length]u8,
475 params: Params,471 params: Params,
476) [dk_length]u8 {472) [dk_length]u8 {
477 if (password.len <= 72) {473 if (password.len <= 72 or params.silently_truncate_password) {
478 return bcrypt(password, salt, params);474 return bcryptWithTruncation(password, salt, params);
479 }475 }
480476
481 var pre_hash: [HmacSha512.mac_length]u8 = undefined;477 var pre_hash: [HmacSha512.mac_length]u8 = undefined;
...@@ -485,7 +481,7 @@ pub fn bcryptWithoutTruncation(...@@ -485,7 +481,7 @@ pub fn bcryptWithoutTruncation(
485 var pre_hash_b64: [Encoder.calcSize(pre_hash.len)]u8 = undefined;481 var pre_hash_b64: [Encoder.calcSize(pre_hash.len)]u8 = undefined;
486 _ = Encoder.encode(&pre_hash_b64, &pre_hash);482 _ = Encoder.encode(&pre_hash_b64, &pre_hash);
487483
488 return bcrypt(&pre_hash_b64, salt, params);484 return bcryptWithTruncation(&pre_hash_b64, salt, params);
489}485}
490486
491const pbkdf_prf = struct {487const pbkdf_prf = struct {
...@@ -629,9 +625,8 @@ const crypt_format = struct {...@@ -629,9 +625,8 @@ const crypt_format = struct {
629 password: []const u8,625 password: []const u8,
630 salt: [salt_length]u8,626 salt: [salt_length]u8,
631 params: Params,627 params: Params,
632 silently_truncate_password: bool,
633 ) [hash_length]u8 {628 ) [hash_length]u8 {
634 var dk = if (silently_truncate_password) bcrypt(password, salt, params) else bcryptWithoutTruncation(password, salt, params);629 var dk = bcrypt(password, salt, params);
635630
636 var salt_str: [salt_str_length]u8 = undefined;631 var salt_str: [salt_str_length]u8 = undefined;
637 _ = Codec.Encoder.encode(salt_str[0..], salt[0..]);632 _ = Codec.Encoder.encode(salt_str[0..], salt[0..]);
...@@ -666,13 +661,12 @@ const PhcFormatHasher = struct {...@@ -666,13 +661,12 @@ const PhcFormatHasher = struct {
666 fn create(661 fn create(
667 password: []const u8,662 password: []const u8,
668 params: Params,663 params: Params,
669 silently_truncate_password: bool,
670 buf: []u8,664 buf: []u8,
671 ) HasherError![]const u8 {665 ) HasherError![]const u8 {
672 var salt: [salt_length]u8 = undefined;666 var salt: [salt_length]u8 = undefined;
673 crypto.random.bytes(&salt);667 crypto.random.bytes(&salt);
674668
675 const hash = if (silently_truncate_password) bcrypt(password, salt, params) else bcryptWithoutTruncation(password, salt, params);669 const hash = bcrypt(password, salt, params);
676670
677 return phc_format.serialize(HashResult{671 return phc_format.serialize(HashResult{
678 .alg_id = alg_id,672 .alg_id = alg_id,
...@@ -694,8 +688,11 @@ const PhcFormatHasher = struct {...@@ -694,8 +688,11 @@ const PhcFormatHasher = struct {
694 if (hash_result.salt.len != salt_length or hash_result.hash.len != dk_length)688 if (hash_result.salt.len != salt_length or hash_result.hash.len != dk_length)
695 return HasherError.InvalidEncoding;689 return HasherError.InvalidEncoding;
696690
697 const params = Params{ .rounds_log = hash_result.r };691 const params = Params{
698 const hash = if (silently_truncate_password) bcrypt(password, hash_result.salt.buf, params) else bcryptWithoutTruncation(password, hash_result.salt.buf, params);692 .rounds_log = hash_result.r,
693 .silently_truncate_password = silently_truncate_password,
694 };
695 const hash = bcrypt(password, hash_result.salt.buf, params);
699 const expected_hash = hash_result.hash.constSlice();696 const expected_hash = hash_result.hash.constSlice();
700697
701 if (!mem.eql(u8, &hash, expected_hash)) return HasherError.PasswordVerificationFailed;698 if (!mem.eql(u8, &hash, expected_hash)) return HasherError.PasswordVerificationFailed;
...@@ -711,7 +708,6 @@ const CryptFormatHasher = struct {...@@ -711,7 +708,6 @@ const CryptFormatHasher = struct {
711 fn create(708 fn create(
712 password: []const u8,709 password: []const u8,
713 params: Params,710 params: Params,
714 silently_truncate_password: bool,
715 buf: []u8,711 buf: []u8,
716 ) HasherError![]const u8 {712 ) HasherError![]const u8 {
717 if (buf.len < pwhash_str_length) return HasherError.NoSpaceLeft;713 if (buf.len < pwhash_str_length) return HasherError.NoSpaceLeft;
...@@ -719,7 +715,7 @@ const CryptFormatHasher = struct {...@@ -719,7 +715,7 @@ const CryptFormatHasher = struct {
719 var salt: [salt_length]u8 = undefined;715 var salt: [salt_length]u8 = undefined;
720 crypto.random.bytes(&salt);716 crypto.random.bytes(&salt);
721717
722 const hash = crypt_format.strHashInternal(password, salt, params, silently_truncate_password);718 const hash = crypt_format.strHashInternal(password, salt, params);
723 @memcpy(buf[0..hash.len], &hash);719 @memcpy(buf[0..hash.len], &hash);
724720
725 return buf[0..pwhash_str_length];721 return buf[0..pwhash_str_length];
...@@ -742,7 +738,10 @@ const CryptFormatHasher = struct {...@@ -742,7 +738,10 @@ const CryptFormatHasher = struct {
742 var salt: [salt_length]u8 = undefined;738 var salt: [salt_length]u8 = undefined;
743 crypt_format.Codec.Decoder.decode(salt[0..], salt_str[0..]) catch return HasherError.InvalidEncoding;739 crypt_format.Codec.Decoder.decode(salt[0..], salt_str[0..]) catch return HasherError.InvalidEncoding;
744740
745 const wanted_s = crypt_format.strHashInternal(password, salt, .{ .rounds_log = rounds_log }, silently_truncate_password);741 const wanted_s = crypt_format.strHashInternal(password, salt, .{
742 .rounds_log = rounds_log,
743 .silently_truncate_password = silently_truncate_password,
744 });
746 if (!mem.eql(u8, wanted_s[0..], str[0..])) return HasherError.PasswordVerificationFailed;745 if (!mem.eql(u8, wanted_s[0..], str[0..])) return HasherError.PasswordVerificationFailed;
747 }746 }
748};747};
...@@ -755,9 +754,6 @@ pub const HashOptions = struct {...@@ -755,9 +754,6 @@ pub const HashOptions = struct {
755 params: Params,754 params: Params,
756 /// Encoding to use for the output of the hash function.755 /// Encoding to use for the output of the hash function.
757 encoding: pwhash.Encoding,756 encoding: pwhash.Encoding,
758 /// Whether to silently truncate the password to 72 bytes, or pre-hash the password when it is longer.
759 /// The default is `true`, for compatibility with the original bcrypt implementation.
760 silently_truncate_password: bool = true,
761};757};
762758
763/// Compute a hash of a password using 2^rounds_log rounds of the bcrypt key stretching function.759/// Compute a hash of a password using 2^rounds_log rounds of the bcrypt key stretching function.
...@@ -773,8 +769,8 @@ pub fn strHash(...@@ -773,8 +769,8 @@ pub fn strHash(
773 out: []u8,769 out: []u8,
774) Error![]const u8 {770) Error![]const u8 {
775 switch (options.encoding) {771 switch (options.encoding) {
776 .phc => return PhcFormatHasher.create(password, options.params, options.silently_truncate_password, out),772 .phc => return PhcFormatHasher.create(password, options.params, out),
777 .crypt => return CryptFormatHasher.create(password, options.params, options.silently_truncate_password, out),773 .crypt => return CryptFormatHasher.create(password, options.params, out),
778 }774 }
779}775}
780776
...@@ -783,7 +779,7 @@ pub const VerifyOptions = struct {...@@ -783,7 +779,7 @@ pub const VerifyOptions = struct {
783 /// For `bcrypt`, that can be left to `null`.779 /// For `bcrypt`, that can be left to `null`.
784 allocator: ?mem.Allocator = null,780 allocator: ?mem.Allocator = null,
785 /// Whether to silently truncate the password to 72 bytes, or pre-hash the password when it is longer.781 /// Whether to silently truncate the password to 72 bytes, or pre-hash the password when it is longer.
786 silently_truncate_password: bool = false,782 silently_truncate_password: bool,
787};783};
788784
789/// Verify that a previously computed hash is valid for a given password.785/// Verify that a previously computed hash is valid for a given password.
...@@ -811,11 +807,10 @@ test "bcrypt codec" {...@@ -811,11 +807,10 @@ test "bcrypt codec" {
811807
812test "bcrypt crypt format" {808test "bcrypt crypt format" {
813 var hash_options = HashOptions{809 var hash_options = HashOptions{
814 .params = .{ .rounds_log = 5 },810 .params = .{ .rounds_log = 5, .silently_truncate_password = false },
815 .encoding = .crypt,811 .encoding = .crypt,
816 .silently_truncate_password = false,
817 };812 };
818 var verify_options = VerifyOptions{};813 var verify_options = VerifyOptions{ .silently_truncate_password = false };
819814
820 var buf: [hash_length]u8 = undefined;815 var buf: [hash_length]u8 = undefined;
821 const s = try strHash("password", hash_options, &buf);816 const s = try strHash("password", hash_options, &buf);
...@@ -837,7 +832,7 @@ test "bcrypt crypt format" {...@@ -837,7 +832,7 @@ test "bcrypt crypt format" {
837 strVerify(long_s, "password" ** 101, verify_options),832 strVerify(long_s, "password" ** 101, verify_options),
838 );833 );
839834
840 hash_options.silently_truncate_password = true;835 hash_options.params.silently_truncate_password = true;
841 verify_options.silently_truncate_password = true;836 verify_options.silently_truncate_password = true;
842 long_s = try strHash("password" ** 100, hash_options, &long_buf);837 long_s = try strHash("password" ** 100, hash_options, &long_buf);
843 try strVerify(long_s, "password" ** 101, verify_options);838 try strVerify(long_s, "password" ** 101, verify_options);
...@@ -851,11 +846,10 @@ test "bcrypt crypt format" {...@@ -851,11 +846,10 @@ test "bcrypt crypt format" {
851846
852test "bcrypt phc format" {847test "bcrypt phc format" {
853 var hash_options = HashOptions{848 var hash_options = HashOptions{
854 .params = .{ .rounds_log = 5 },849 .params = .{ .rounds_log = 5, .silently_truncate_password = false },
855 .encoding = .phc,850 .encoding = .phc,
856 .silently_truncate_password = false,
857 };851 };
858 var verify_options = VerifyOptions{};852 var verify_options = VerifyOptions{ .silently_truncate_password = false };
859 const prefix = "$bcrypt$";853 const prefix = "$bcrypt$";
860854
861 var buf: [hash_length * 2]u8 = undefined;855 var buf: [hash_length * 2]u8 = undefined;
...@@ -878,7 +872,7 @@ test "bcrypt phc format" {...@@ -878,7 +872,7 @@ test "bcrypt phc format" {
878 strVerify(long_s, "password" ** 101, verify_options),872 strVerify(long_s, "password" ** 101, verify_options),
879 );873 );
880874
881 hash_options.silently_truncate_password = true;875 hash_options.params.silently_truncate_password = true;
882 verify_options.silently_truncate_password = true;876 verify_options.silently_truncate_password = true;
883 long_s = try strHash("password" ** 100, hash_options, &long_buf);877 long_s = try strHash("password" ** 100, hash_options, &long_buf);
884 try strVerify(long_s, "password" ** 101, verify_options);878 try strVerify(long_s, "password" ** 101, verify_options);
lib/std/crypto/benchmark.zig+1-1
...@@ -391,7 +391,7 @@ const CryptoPwhash = struct {...@@ -391,7 +391,7 @@ const CryptoPwhash = struct {
391 params: *const anyopaque,391 params: *const anyopaque,
392 name: []const u8,392 name: []const u8,
393};393};
394const bcrypt_params = crypto.pwhash.bcrypt.Params{ .rounds_log = 8 };394const bcrypt_params = crypto.pwhash.bcrypt.Params{ .rounds_log = 8, .silently_truncate_password = true };
395const pwhashes = [_]CryptoPwhash{395const pwhashes = [_]CryptoPwhash{
396 .{396 .{
397 .ty = crypto.pwhash.bcrypt,397 .ty = crypto.pwhash.bcrypt,