authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2023-06-06 09:08:57+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2023-06-06 09:08:57+02:00
log9ee0a706da66e271ce0f79d00566dbe40b57b83e
tree78d131370e5af989a391194c7f379eee0abe4ae0
parent7e0a02ee2593d99236bc9a1ff98a8f957add6ec4
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

crypto.bcrypt: allow very large passwords to be pre-hashed (#15955)

crypto.bcrypt: allow very large passwords to be pre-hashed bcrypt has a slightly annoying limitation: passwords are limited to 72 characters. In the original implementation, additional characters are silently ignored. When they care, applications adopt different strategies to work around this, in incompatible ways. Ideally, large passwords should be pre-hashed using a hash function that hinders GPU attackers, and the hashed function should not be deterministic in order to defeat shucking attacks. This change improves the developer experience by adding a very explicit `silently_truncate_password` option, that can be set to `false` in order to do that automatically, and consistently across Zig applications. By default, passwords are still truncated, so this is not a breaking change. Add some inline documentation for our beloved autodoc by the way.

1 files changed, 104 insertions(+), 30 deletions(-)

lib/std/crypto/bcrypt.zig+104-30
......@@ -7,6 +7,7 @@ const math = std.math;
77const mem = std.mem;
88const pwhash = crypto.pwhash;
99const testing = std.testing;
10const HmacSha512 = crypto.auth.hmac.sha2.HmacSha512;
1011const Sha512 = crypto.hash.sha2.Sha512;
1112const utils = crypto.utils;
1213
......@@ -405,10 +406,21 @@ pub const State = struct {
405406 }
406407};
407408
409/// bcrypt parameters
408410pub const Params = struct {
411 /// log2 of the number of rounds
409412 rounds_log: u6,
410413};
411414
415/// Compute a hash of a password using 2^rounds_log rounds of the bcrypt key stretching function.
416/// bcrypt is a computationally expensive and cache-hard function, explicitly designed to slow down exhaustive searches.
417///
418/// The function returns the hash as a `dk_length` byte array, that doesn't include anything besides the hash output.
419///
420/// For a generic key-derivation function, use `bcrypt.pbkdf()` instead.
421///
422/// IMPORTANT: by design, bcrypt silently truncates passwords to 72 bytes.
423/// If this is an issue for your application, use `bcryptWithoutTruncation` instead.
412424pub fn bcrypt(
413425 password: []const u8,
414426 salt: [salt_length]u8,
......@@ -443,6 +455,34 @@ pub fn bcrypt(
443455 return ct[0..dk_length].*;
444456}
445457
458/// Compute a hash of a password using 2^rounds_log rounds of the bcrypt key stretching function.
459/// bcrypt is a computationally expensive and cache-hard function, explicitly designed to slow down exhaustive searches.
460///
461/// The function returns the hash as a `dk_length` byte array, that doesn't include anything besides the hash output.
462///
463/// For a generic key-derivation function, use `bcrypt.pbkdf()` instead.
464///
465/// This function is identical to `bcrypt`, except that it doesn't silently truncate passwords.
466/// Instead, passwords longer than 72 bytes are pre-hashed using HMAC-SHA512 before being passed to bcrypt.
467pub fn bcryptWithoutTruncation(
468 password: []const u8,
469 salt: [salt_length]u8,
470 params: Params,
471) [dk_length]u8 {
472 if (password.len <= 72) {
473 return bcrypt(password, salt, params);
474 }
475
476 var pre_hash: [HmacSha512.mac_length]u8 = undefined;
477 HmacSha512.create(&pre_hash, password, &salt);
478
479 const Encoder = crypt_format.Codec.Encoder;
480 var pre_hash_b64: [Encoder.calcSize(pre_hash.len)]u8 = undefined;
481 _ = Encoder.encode(&pre_hash_b64, &pre_hash);
482
483 return bcrypt(&pre_hash_b64, salt, params);
484}
485
446486const pbkdf_prf = struct {
447487 const Self = @This();
448488 pub const mac_length = 32;
......@@ -450,24 +490,24 @@ const pbkdf_prf = struct {
450490 hasher: Sha512,
451491 sha2pass: [Sha512.digest_length]u8,
452492
453 pub fn create(out: *[mac_length]u8, msg: []const u8, key: []const u8) void {
493 fn create(out: *[mac_length]u8, msg: []const u8, key: []const u8) void {
454494 var ctx = Self.init(key);
455495 ctx.update(msg);
456496 ctx.final(out);
457497 }
458498
459 pub fn init(key: []const u8) Self {
499 fn init(key: []const u8) Self {
460500 var self: Self = undefined;
461501 self.hasher = Sha512.init(.{});
462502 Sha512.hash(key, &self.sha2pass, .{});
463503 return self;
464504 }
465505
466 pub fn update(self: *Self, msg: []const u8) void {
506 fn update(self: *Self, msg: []const u8) void {
467507 self.hasher.update(msg);
468508 }
469509
470 pub fn final(self: *Self, out: *[mac_length]u8) void {
510 fn final(self: *Self, out: *[mac_length]u8) void {
471511 var sha2salt: [Sha512.digest_length]u8 = undefined;
472512 self.hasher.final(&sha2salt);
473513 out.* = hash(self.sha2pass, sha2salt);
......@@ -517,10 +557,12 @@ const pbkdf_prf = struct {
517557 }
518558};
519559
520/// bcrypt PBKDF2 implementation with variations to match OpenBSD
521/// https://github.com/openbsd/src/blob/6df1256b7792691e66c2ed9d86a8c103069f9e34/lib/libutil/bcrypt_pbkdf.c#L98
560/// bcrypt-pbkdf is a key derivation function based on bcrypt.
561/// This is the function used in OpenSSH to derive encryption keys from passphrases.
522562///
523/// This particular variant is used in e.g. SSH
563/// This implementation is compatible with the OpenBSD implementation (https://github.com/openbsd/src/blob/master/lib/libutil/bcrypt_pbkdf.c).
564///
565/// Unlike the password hashing function `bcrypt`, this function doesn't silently truncate passwords longer than 72 bytes.
524566pub fn pbkdf(pass: []const u8, salt: []const u8, key: []u8, rounds: u32) !void {
525567 try crypto.pwhash.pbkdf2(key, pass, salt, rounds, pbkdf_prf);
526568}
......@@ -540,8 +582,9 @@ const crypt_format = struct {
540582 password: []const u8,
541583 salt: [salt_length]u8,
542584 params: Params,
585 silently_truncate_password: bool,
543586 ) [hash_length]u8 {
544 var dk = bcrypt(password, salt, params);
587 var dk = if (silently_truncate_password) bcrypt(password, salt, params) else bcryptWithoutTruncation(password, salt, params);
545588
546589 var salt_str: [salt_str_length]u8 = undefined;
547590 _ = Codec.Encoder.encode(salt_str[0..], salt[0..]);
......@@ -573,15 +616,16 @@ const PhcFormatHasher = struct {
573616 };
574617
575618 /// Return a non-deterministic hash of the password encoded as a PHC-format string
576 pub fn create(
619 fn create(
577620 password: []const u8,
578621 params: Params,
622 silently_truncate_password: bool,
579623 buf: []u8,
580624 ) HasherError![]const u8 {
581625 var salt: [salt_length]u8 = undefined;
582626 crypto.random.bytes(&salt);
583627
584 const hash = bcrypt(password, salt, params);
628 const hash = if (silently_truncate_password) bcrypt(password, salt, params) else bcryptWithoutTruncation(password, salt, params);
585629
586630 return phc_format.serialize(HashResult{
587631 .alg_id = alg_id,
......@@ -592,9 +636,10 @@ const PhcFormatHasher = struct {
592636 }
593637
594638 /// Verify a password against a PHC-format encoded string
595 pub fn verify(
639 fn verify(
596640 str: []const u8,
597641 password: []const u8,
642 silently_truncate_password: bool,
598643 ) HasherError!void {
599644 const hash_result = try phc_format.deserialize(HashResult, str);
600645
......@@ -602,7 +647,8 @@ const PhcFormatHasher = struct {
602647 if (hash_result.salt.len != salt_length or hash_result.hash.len != dk_length)
603648 return HasherError.InvalidEncoding;
604649
605 const hash = bcrypt(password, hash_result.salt.buf, .{ .rounds_log = hash_result.r });
650 const params = Params{ .rounds_log = hash_result.r };
651 const hash = if (silently_truncate_password) bcrypt(password, hash_result.salt.buf, params) else bcryptWithoutTruncation(password, hash_result.salt.buf, params);
606652 const expected_hash = hash_result.hash.constSlice();
607653
608654 if (!mem.eql(u8, &hash, expected_hash)) return HasherError.PasswordVerificationFailed;
......@@ -612,12 +658,13 @@ const PhcFormatHasher = struct {
612658/// Hash and verify passwords using the modular crypt format.
613659const CryptFormatHasher = struct {
614660 /// Length of a string returned by the create() function
615 pub const pwhash_str_length: usize = hash_length;
661 const pwhash_str_length: usize = hash_length;
616662
617663 /// Return a non-deterministic hash of the password encoded into the modular crypt format
618 pub fn create(
664 fn create(
619665 password: []const u8,
620666 params: Params,
667 silently_truncate_password: bool,
621668 buf: []u8,
622669 ) HasherError![]const u8 {
623670 if (buf.len < pwhash_str_length) return HasherError.NoSpaceLeft;
......@@ -625,16 +672,17 @@ const CryptFormatHasher = struct {
625672 var salt: [salt_length]u8 = undefined;
626673 crypto.random.bytes(&salt);
627674
628 const hash = crypt_format.strHashInternal(password, salt, params);
675 const hash = crypt_format.strHashInternal(password, salt, params, silently_truncate_password);
629676 @memcpy(buf[0..hash.len], &hash);
630677
631678 return buf[0..pwhash_str_length];
632679 }
633680
634681 /// Verify a password against a string in modular crypt format
635 pub fn verify(
682 fn verify(
636683 str: []const u8,
637684 password: []const u8,
685 silently_truncate_password: bool,
638686 ) HasherError!void {
639687 if (str.len != pwhash_str_length or str[3] != '$' or str[6] != '$')
640688 return HasherError.InvalidEncoding;
......@@ -647,16 +695,22 @@ const CryptFormatHasher = struct {
647695 var salt: [salt_length]u8 = undefined;
648696 crypt_format.Codec.Decoder.decode(salt[0..], salt_str[0..]) catch return HasherError.InvalidEncoding;
649697
650 const wanted_s = crypt_format.strHashInternal(password, salt, .{ .rounds_log = rounds_log });
698 const wanted_s = crypt_format.strHashInternal(password, salt, .{ .rounds_log = rounds_log }, silently_truncate_password);
651699 if (!mem.eql(u8, wanted_s[0..], str[0..])) return HasherError.PasswordVerificationFailed;
652700 }
653701};
654702
655703/// Options for hashing a password.
656704pub const HashOptions = struct {
705 /// For `bcrypt`, that can be left to `null`.
657706 allocator: ?mem.Allocator = null,
707 /// Internal bcrypt parameters.
658708 params: Params,
709 /// Encoding to use for the output of the hash function.
659710 encoding: pwhash.Encoding,
711 /// Whether to silently truncate the password to 72 bytes, or pre-hash the password when it is longer.
712 /// The default is `true`, for compatibility with the original bcrypt implementation.
713 silently_truncate_password: bool = true,
660714};
661715
662716/// Compute a hash of a password using 2^rounds_log rounds of the bcrypt key stretching function.
......@@ -665,34 +719,36 @@ pub const HashOptions = struct {
665719/// The function returns a string that includes all the parameters required for verification.
666720///
667721/// IMPORTANT: by design, bcrypt silently truncates passwords to 72 bytes.
668/// If this is an issue for your application, hash the password first using a function such as SHA-512,
669/// and then use the resulting hash as the password parameter for bcrypt.
722/// If this is an issue for your application, set the `silently_truncate_password` option to `false`.
670723pub fn strHash(
671724 password: []const u8,
672725 options: HashOptions,
673726 out: []u8,
674727) Error![]const u8 {
675728 switch (options.encoding) {
676 .phc => return PhcFormatHasher.create(password, options.params, out),
677 .crypt => return CryptFormatHasher.create(password, options.params, out),
729 .phc => return PhcFormatHasher.create(password, options.params, options.silently_truncate_password, out),
730 .crypt => return CryptFormatHasher.create(password, options.params, options.silently_truncate_password, out),
678731 }
679732}
680733
681734/// Options for hash verification.
682735pub const VerifyOptions = struct {
736 /// For `bcrypt`, that can be left to `null`.
683737 allocator: ?mem.Allocator = null,
738 /// Whether to silently truncate the password to 72 bytes, or pre-hash the password when it is longer.
739 silently_truncate_password: bool = false,
684740};
685741
686742/// Verify that a previously computed hash is valid for a given password.
687743pub fn strVerify(
688744 str: []const u8,
689745 password: []const u8,
690 _: VerifyOptions,
746 options: VerifyOptions,
691747) Error!void {
692748 if (mem.startsWith(u8, str, crypt_format.prefix)) {
693 return CryptFormatHasher.verify(str, password);
749 return CryptFormatHasher.verify(str, password, options.silently_truncate_password);
694750 } else {
695 return PhcFormatHasher.verify(str, password);
751 return PhcFormatHasher.verify(str, password, options.silently_truncate_password);
696752 }
697753}
698754
......@@ -707,11 +763,12 @@ test "bcrypt codec" {
707763}
708764
709765test "bcrypt crypt format" {
710 const hash_options = HashOptions{
766 var hash_options = HashOptions{
711767 .params = .{ .rounds_log = 5 },
712768 .encoding = .crypt,
769 .silently_truncate_password = false,
713770 };
714 const verify_options = VerifyOptions{};
771 var verify_options = VerifyOptions{};
715772
716773 var buf: [hash_length]u8 = undefined;
717774 const s = try strHash("password", hash_options, &buf);
......@@ -724,10 +781,18 @@ test "bcrypt crypt format" {
724781 );
725782
726783 var long_buf: [hash_length]u8 = undefined;
727 const long_s = try strHash("password" ** 100, hash_options, &long_buf);
784 var long_s = try strHash("password" ** 100, hash_options, &long_buf);
728785
729786 try testing.expect(mem.startsWith(u8, long_s, crypt_format.prefix));
730787 try strVerify(long_s, "password" ** 100, verify_options);
788 try testing.expectError(
789 error.PasswordVerificationFailed,
790 strVerify(long_s, "password" ** 101, verify_options),
791 );
792
793 hash_options.silently_truncate_password = true;
794 verify_options.silently_truncate_password = true;
795 long_s = try strHash("password" ** 100, hash_options, &long_buf);
731796 try strVerify(long_s, "password" ** 101, verify_options);
732797
733798 try strVerify(
......@@ -738,11 +803,12 @@ test "bcrypt crypt format" {
738803}
739804
740805test "bcrypt phc format" {
741 const hash_options = HashOptions{
806 var hash_options = HashOptions{
742807 .params = .{ .rounds_log = 5 },
743808 .encoding = .phc,
809 .silently_truncate_password = false,
744810 };
745 const verify_options = VerifyOptions{};
811 var verify_options = VerifyOptions{};
746812 const prefix = "$bcrypt$";
747813
748814 var buf: [hash_length * 2]u8 = undefined;
......@@ -756,10 +822,18 @@ test "bcrypt phc format" {
756822 );
757823
758824 var long_buf: [hash_length * 2]u8 = undefined;
759 const long_s = try strHash("password" ** 100, hash_options, &long_buf);
825 var long_s = try strHash("password" ** 100, hash_options, &long_buf);
760826
761827 try testing.expect(mem.startsWith(u8, long_s, prefix));
762828 try strVerify(long_s, "password" ** 100, verify_options);
829 try testing.expectError(
830 error.PasswordVerificationFailed,
831 strVerify(long_s, "password" ** 101, verify_options),
832 );
833
834 hash_options.silently_truncate_password = true;
835 verify_options.silently_truncate_password = true;
836 long_s = try strHash("password" ** 100, hash_options, &long_buf);
763837 try strVerify(long_s, "password" ** 101, verify_options);
764838
765839 try strVerify(