@@ -7,6 +7,7 @@ const math = std.math;
77const mem = std .mem ;
88const pwhash = crypto .pwhash ;
99const testing = std .testing ;
10+ const 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,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+
446486const 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.
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,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.
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 ;
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.
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 (
0 commit comments