Skip to content

Commit 9ee0a70

Browse files
authored
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 parent 7e0a02e commit 9ee0a70

File tree

1 file changed

+104
-30
lines changed

1 file changed

+104
-30
lines changed

lib/std/crypto/bcrypt.zig

Lines changed: 104 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const math = std.math;
77
const mem = std.mem;
88
const pwhash = crypto.pwhash;
99
const testing = std.testing;
10+
const HmacSha512 = crypto.auth.hmac.sha2.HmacSha512;
1011
const Sha512 = crypto.hash.sha2.Sha512;
1112
const utils = crypto.utils;
1213

@@ -405,10 +406,21 @@ pub const State = struct {
405406
}
406407
};
407408

409+
/// bcrypt parameters
408410
pub 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.
412424
pub fn bcrypt(
413425
password: []const u8,
414426
salt: [salt_length]u8,
@@ -443,31 +455,59 @@ 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.
467+
pub 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+
446486
const pbkdf_prf = struct {
447487
const Self = @This();
448488
pub const mac_length = 32;
449489

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.
524566
pub 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,17 +636,19 @@ 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

601646
if (!mem.eql(u8, hash_result.alg_id, alg_id)) return HasherError.PasswordVerificationFailed;
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,29 +658,31 @@ const PhcFormatHasher = struct {
612658
/// Hash and verify passwords using the modular crypt format.
613659
const 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;
624671

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.
656704
pub 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`.
670723
pub 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.
682735
pub 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.
687743
pub 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

709765
test "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

740805
test "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(

0 commit comments

Comments
 (0)