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;...@@ -7,6 +7,7 @@ const math = std.math;
7const mem = std.mem;7const mem = std.mem;
8const pwhash = crypto.pwhash;8const pwhash = crypto.pwhash;
9const testing = std.testing;9const testing = std.testing;
10const HmacSha512 = crypto.auth.hmac.sha2.HmacSha512;
10const Sha512 = crypto.hash.sha2.Sha512;11const Sha512 = crypto.hash.sha2.Sha512;
11const utils = crypto.utils;12const utils = crypto.utils;
1213
...@@ -405,10 +406,21 @@ pub const State = struct {...@@ -405,10 +406,21 @@ pub const State = struct {
405 }406 }
406};407};
407408
409/// bcrypt parameters
408pub const Params = struct {410pub const Params = struct {
411 /// log2 of the number of rounds
409 rounds_log: u6,412 rounds_log: u6,
410};413};
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.
412pub fn bcrypt(424pub fn bcrypt(
413 password: []const u8,425 password: []const u8,
414 salt: [salt_length]u8,426 salt: [salt_length]u8,
...@@ -443,6 +455,34 @@ pub fn bcrypt(...@@ -443,6 +455,34 @@ pub fn bcrypt(
443 return ct[0..dk_length].*;455 return ct[0..dk_length].*;
444}456}
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
446const pbkdf_prf = struct {486const pbkdf_prf = struct {
447 const Self = @This();487 const Self = @This();
448 pub const mac_length = 32;488 pub const mac_length = 32;
...@@ -450,24 +490,24 @@ const pbkdf_prf = struct {...@@ -450,24 +490,24 @@ const pbkdf_prf = struct {
450 hasher: Sha512,490 hasher: Sha512,
451 sha2pass: [Sha512.digest_length]u8,491 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 {
454 var ctx = Self.init(key);494 var ctx = Self.init(key);
455 ctx.update(msg);495 ctx.update(msg);
456 ctx.final(out);496 ctx.final(out);
457 }497 }
458498
459 pub fn init(key: []const u8) Self {499 fn init(key: []const u8) Self {
460 var self: Self = undefined;500 var self: Self = undefined;
461 self.hasher = Sha512.init(.{});501 self.hasher = Sha512.init(.{});
462 Sha512.hash(key, &self.sha2pass, .{});502 Sha512.hash(key, &self.sha2pass, .{});
463 return self;503 return self;
464 }504 }
465505
466 pub fn update(self: *Self, msg: []const u8) void {506 fn update(self: *Self, msg: []const u8) void {
467 self.hasher.update(msg);507 self.hasher.update(msg);
468 }508 }
469509
470 pub fn final(self: *Self, out: *[mac_length]u8) void {510 fn final(self: *Self, out: *[mac_length]u8) void {
471 var sha2salt: [Sha512.digest_length]u8 = undefined;511 var sha2salt: [Sha512.digest_length]u8 = undefined;
472 self.hasher.final(&sha2salt);512 self.hasher.final(&sha2salt);
473 out.* = hash(self.sha2pass, sha2salt);513 out.* = hash(self.sha2pass, sha2salt);
...@@ -517,10 +557,12 @@ const pbkdf_prf = struct {...@@ -517,10 +557,12 @@ const pbkdf_prf = struct {
517 }557 }
518};558};
519559
520/// bcrypt PBKDF2 implementation with variations to match OpenBSD560/// bcrypt-pbkdf is a key derivation function based on bcrypt.
521/// https://github.com/openbsd/src/blob/6df1256b7792691e66c2ed9d86a8c103069f9e34/lib/libutil/bcrypt_pbkdf.c#L98561/// This is the function used in OpenSSH to derive encryption keys from passphrases.
522///562///
523/// This particular variant is used in e.g. SSH563/// 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.
524pub fn pbkdf(pass: []const u8, salt: []const u8, key: []u8, rounds: u32) !void {566pub fn pbkdf(pass: []const u8, salt: []const u8, key: []u8, rounds: u32) !void {
525 try crypto.pwhash.pbkdf2(key, pass, salt, rounds, pbkdf_prf);567 try crypto.pwhash.pbkdf2(key, pass, salt, rounds, pbkdf_prf);
526}568}
...@@ -540,8 +582,9 @@ const crypt_format = struct {...@@ -540,8 +582,9 @@ const crypt_format = struct {
540 password: []const u8,582 password: []const u8,
541 salt: [salt_length]u8,583 salt: [salt_length]u8,
542 params: Params,584 params: Params,
585 silently_truncate_password: bool,
543 ) [hash_length]u8 {586 ) [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
546 var salt_str: [salt_str_length]u8 = undefined;589 var salt_str: [salt_str_length]u8 = undefined;
547 _ = Codec.Encoder.encode(salt_str[0..], salt[0..]);590 _ = Codec.Encoder.encode(salt_str[0..], salt[0..]);
...@@ -573,15 +616,16 @@ const PhcFormatHasher = struct {...@@ -573,15 +616,16 @@ const PhcFormatHasher = struct {
573 };616 };
574617
575 /// Return a non-deterministic hash of the password encoded as a PHC-format string618 /// Return a non-deterministic hash of the password encoded as a PHC-format string
576 pub fn create(619 fn create(
577 password: []const u8,620 password: []const u8,
578 params: Params,621 params: Params,
622 silently_truncate_password: bool,
579 buf: []u8,623 buf: []u8,
580 ) HasherError![]const u8 {624 ) HasherError![]const u8 {
581 var salt: [salt_length]u8 = undefined;625 var salt: [salt_length]u8 = undefined;
582 crypto.random.bytes(&salt);626 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
586 return phc_format.serialize(HashResult{630 return phc_format.serialize(HashResult{
587 .alg_id = alg_id,631 .alg_id = alg_id,
...@@ -592,9 +636,10 @@ const PhcFormatHasher = struct {...@@ -592,9 +636,10 @@ const PhcFormatHasher = struct {
592 }636 }
593637
594 /// Verify a password against a PHC-format encoded string638 /// Verify a password against a PHC-format encoded string
595 pub fn verify(639 fn verify(
596 str: []const u8,640 str: []const u8,
597 password: []const u8,641 password: []const u8,
642 silently_truncate_password: bool,
598 ) HasherError!void {643 ) HasherError!void {
599 const hash_result = try phc_format.deserialize(HashResult, str);644 const hash_result = try phc_format.deserialize(HashResult, str);
600645
...@@ -602,7 +647,8 @@ const PhcFormatHasher = struct {...@@ -602,7 +647,8 @@ const PhcFormatHasher = struct {
602 if (hash_result.salt.len != salt_length or hash_result.hash.len != dk_length)647 if (hash_result.salt.len != salt_length or hash_result.hash.len != dk_length)
603 return HasherError.InvalidEncoding;648 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);
606 const expected_hash = hash_result.hash.constSlice();652 const expected_hash = hash_result.hash.constSlice();
607653
608 if (!mem.eql(u8, &hash, expected_hash)) return HasherError.PasswordVerificationFailed;654 if (!mem.eql(u8, &hash, expected_hash)) return HasherError.PasswordVerificationFailed;
...@@ -612,12 +658,13 @@ const PhcFormatHasher = struct {...@@ -612,12 +658,13 @@ const PhcFormatHasher = struct {
612/// Hash and verify passwords using the modular crypt format.658/// Hash and verify passwords using the modular crypt format.
613const CryptFormatHasher = struct {659const CryptFormatHasher = struct {
614 /// Length of a string returned by the create() function660 /// 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
617 /// Return a non-deterministic hash of the password encoded into the modular crypt format663 /// Return a non-deterministic hash of the password encoded into the modular crypt format
618 pub fn create(664 fn create(
619 password: []const u8,665 password: []const u8,
620 params: Params,666 params: Params,
667 silently_truncate_password: bool,
621 buf: []u8,668 buf: []u8,
622 ) HasherError![]const u8 {669 ) HasherError![]const u8 {
623 if (buf.len < pwhash_str_length) return HasherError.NoSpaceLeft;670 if (buf.len < pwhash_str_length) return HasherError.NoSpaceLeft;
...@@ -625,16 +672,17 @@ const CryptFormatHasher = struct {...@@ -625,16 +672,17 @@ const CryptFormatHasher = struct {
625 var salt: [salt_length]u8 = undefined;672 var salt: [salt_length]u8 = undefined;
626 crypto.random.bytes(&salt);673 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);
629 @memcpy(buf[0..hash.len], &hash);676 @memcpy(buf[0..hash.len], &hash);
630677
631 return buf[0..pwhash_str_length];678 return buf[0..pwhash_str_length];
632 }679 }
633680
634 /// Verify a password against a string in modular crypt format681 /// Verify a password against a string in modular crypt format
635 pub fn verify(682 fn verify(
636 str: []const u8,683 str: []const u8,
637 password: []const u8,684 password: []const u8,
685 silently_truncate_password: bool,
638 ) HasherError!void {686 ) HasherError!void {
639 if (str.len != pwhash_str_length or str[3] != '$' or str[6] != '$')687 if (str.len != pwhash_str_length or str[3] != '$' or str[6] != '$')
640 return HasherError.InvalidEncoding;688 return HasherError.InvalidEncoding;
...@@ -647,16 +695,22 @@ const CryptFormatHasher = struct {...@@ -647,16 +695,22 @@ const CryptFormatHasher = struct {
647 var salt: [salt_length]u8 = undefined;695 var salt: [salt_length]u8 = undefined;
648 crypt_format.Codec.Decoder.decode(salt[0..], salt_str[0..]) catch return HasherError.InvalidEncoding;696 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);
651 if (!mem.eql(u8, wanted_s[0..], str[0..])) return HasherError.PasswordVerificationFailed;699 if (!mem.eql(u8, wanted_s[0..], str[0..])) return HasherError.PasswordVerificationFailed;
652 }700 }
653};701};
654702
655/// Options for hashing a password.703/// Options for hashing a password.
656pub const HashOptions = struct {704pub const HashOptions = struct {
705 /// For `bcrypt`, that can be left to `null`.
657 allocator: ?mem.Allocator = null,706 allocator: ?mem.Allocator = null,
707 /// Internal bcrypt parameters.
658 params: Params,708 params: Params,
709 /// Encoding to use for the output of the hash function.
659 encoding: pwhash.Encoding,710 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,
660};714};
661715
662/// Compute a hash of a password using 2^rounds_log rounds of the bcrypt key stretching function.716/// 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 {...@@ -665,34 +719,36 @@ pub const HashOptions = struct {
665/// The function returns a string that includes all the parameters required for verification.719/// The function returns a string that includes all the parameters required for verification.
666///720///
667/// IMPORTANT: by design, bcrypt silently truncates passwords to 72 bytes.721/// 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,722/// If this is an issue for your application, set the `silently_truncate_password` option to `false`.
669/// and then use the resulting hash as the password parameter for bcrypt.
670pub fn strHash(723pub fn strHash(
671 password: []const u8,724 password: []const u8,
672 options: HashOptions,725 options: HashOptions,
673 out: []u8,726 out: []u8,
674) Error![]const u8 {727) Error![]const u8 {
675 switch (options.encoding) {728 switch (options.encoding) {
676 .phc => return PhcFormatHasher.create(password, options.params, out),729 .phc => return PhcFormatHasher.create(password, options.params, options.silently_truncate_password, out),
677 .crypt => return CryptFormatHasher.create(password, options.params, out),730 .crypt => return CryptFormatHasher.create(password, options.params, options.silently_truncate_password, out),
678 }731 }
679}732}
680733
681/// Options for hash verification.734/// Options for hash verification.
682pub const VerifyOptions = struct {735pub const VerifyOptions = struct {
736 /// For `bcrypt`, that can be left to `null`.
683 allocator: ?mem.Allocator = null,737 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,
684};740};
685741
686/// Verify that a previously computed hash is valid for a given password.742/// Verify that a previously computed hash is valid for a given password.
687pub fn strVerify(743pub fn strVerify(
688 str: []const u8,744 str: []const u8,
689 password: []const u8,745 password: []const u8,
690 _: VerifyOptions,746 options: VerifyOptions,
691) Error!void {747) Error!void {
692 if (mem.startsWith(u8, str, crypt_format.prefix)) {748 if (mem.startsWith(u8, str, crypt_format.prefix)) {
693 return CryptFormatHasher.verify(str, password);749 return CryptFormatHasher.verify(str, password, options.silently_truncate_password);
694 } else {750 } else {
695 return PhcFormatHasher.verify(str, password);751 return PhcFormatHasher.verify(str, password, options.silently_truncate_password);
696 }752 }
697}753}
698754
...@@ -707,11 +763,12 @@ test "bcrypt codec" {...@@ -707,11 +763,12 @@ test "bcrypt codec" {
707}763}
708764
709test "bcrypt crypt format" {765test "bcrypt crypt format" {
710 const hash_options = HashOptions{766 var hash_options = HashOptions{
711 .params = .{ .rounds_log = 5 },767 .params = .{ .rounds_log = 5 },
712 .encoding = .crypt,768 .encoding = .crypt,
769 .silently_truncate_password = false,
713 };770 };
714 const verify_options = VerifyOptions{};771 var verify_options = VerifyOptions{};
715772
716 var buf: [hash_length]u8 = undefined;773 var buf: [hash_length]u8 = undefined;
717 const s = try strHash("password", hash_options, &buf);774 const s = try strHash("password", hash_options, &buf);
...@@ -724,10 +781,18 @@ test "bcrypt crypt format" {...@@ -724,10 +781,18 @@ test "bcrypt crypt format" {
724 );781 );
725782
726 var long_buf: [hash_length]u8 = undefined;783 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
729 try testing.expect(mem.startsWith(u8, long_s, crypt_format.prefix));786 try testing.expect(mem.startsWith(u8, long_s, crypt_format.prefix));
730 try strVerify(long_s, "password" ** 100, verify_options);787 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);
731 try strVerify(long_s, "password" ** 101, verify_options);796 try strVerify(long_s, "password" ** 101, verify_options);
732797
733 try strVerify(798 try strVerify(
...@@ -738,11 +803,12 @@ test "bcrypt crypt format" {...@@ -738,11 +803,12 @@ test "bcrypt crypt format" {
738}803}
739804
740test "bcrypt phc format" {805test "bcrypt phc format" {
741 const hash_options = HashOptions{806 var hash_options = HashOptions{
742 .params = .{ .rounds_log = 5 },807 .params = .{ .rounds_log = 5 },
743 .encoding = .phc,808 .encoding = .phc,
809 .silently_truncate_password = false,
744 };810 };
745 const verify_options = VerifyOptions{};811 var verify_options = VerifyOptions{};
746 const prefix = "$bcrypt$";812 const prefix = "$bcrypt$";
747813
748 var buf: [hash_length * 2]u8 = undefined;814 var buf: [hash_length * 2]u8 = undefined;
...@@ -756,10 +822,18 @@ test "bcrypt phc format" {...@@ -756,10 +822,18 @@ test "bcrypt phc format" {
756 );822 );
757823
758 var long_buf: [hash_length * 2]u8 = undefined;824 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
761 try testing.expect(mem.startsWith(u8, long_s, prefix));827 try testing.expect(mem.startsWith(u8, long_s, prefix));
762 try strVerify(long_s, "password" ** 100, verify_options);828 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);
763 try strVerify(long_s, "password" ** 101, verify_options);837 try strVerify(long_s, "password" ** 101, verify_options);
764838
765 try strVerify(839 try strVerify(