diff --git a/lib/std/crypto/bcrypt.zig b/lib/std/crypto/bcrypt.zig index dda5f5e3773a..7bd140d584e7 100644 --- a/lib/std/crypto/bcrypt.zig +++ b/lib/std/crypto/bcrypt.zig @@ -7,6 +7,7 @@ const math = std.math; const mem = std.mem; const pwhash = crypto.pwhash; const testing = std.testing; +const HmacSha512 = crypto.auth.hmac.sha2.HmacSha512; const Sha512 = crypto.hash.sha2.Sha512; const utils = crypto.utils; @@ -405,10 +406,21 @@ pub const State = struct { } }; +/// bcrypt parameters pub const Params = struct { + /// log2 of the number of rounds rounds_log: u6, }; +/// Compute a hash of a password using 2^rounds_log rounds of the bcrypt key stretching function. +/// bcrypt is a computationally expensive and cache-hard function, explicitly designed to slow down exhaustive searches. +/// +/// The function returns the hash as a `dk_length` byte array, that doesn't include anything besides the hash output. +/// +/// For a generic key-derivation function, use `bcrypt.pbkdf()` instead. +/// +/// IMPORTANT: by design, bcrypt silently truncates passwords to 72 bytes. +/// If this is an issue for your application, use `bcryptWithoutTruncation` instead. pub fn bcrypt( password: []const u8, salt: [salt_length]u8, @@ -443,6 +455,34 @@ pub fn bcrypt( return ct[0..dk_length].*; } +/// Compute a hash of a password using 2^rounds_log rounds of the bcrypt key stretching function. +/// bcrypt is a computationally expensive and cache-hard function, explicitly designed to slow down exhaustive searches. +/// +/// The function returns the hash as a `dk_length` byte array, that doesn't include anything besides the hash output. +/// +/// For a generic key-derivation function, use `bcrypt.pbkdf()` instead. +/// +/// This function is identical to `bcrypt`, except that it doesn't silently truncate passwords. +/// Instead, passwords longer than 72 bytes are pre-hashed using HMAC-SHA512 before being passed to bcrypt. +pub fn bcryptWithoutTruncation( + password: []const u8, + salt: [salt_length]u8, + params: Params, +) [dk_length]u8 { + if (password.len <= 72) { + return bcrypt(password, salt, params); + } + + var pre_hash: [HmacSha512.mac_length]u8 = undefined; + HmacSha512.create(&pre_hash, password, &salt); + + const Encoder = crypt_format.Codec.Encoder; + var pre_hash_b64: [Encoder.calcSize(pre_hash.len)]u8 = undefined; + _ = Encoder.encode(&pre_hash_b64, &pre_hash); + + return bcrypt(&pre_hash_b64, salt, params); +} + const pbkdf_prf = struct { const Self = @This(); pub const mac_length = 32; @@ -450,24 +490,24 @@ const pbkdf_prf = struct { hasher: Sha512, sha2pass: [Sha512.digest_length]u8, - pub fn create(out: *[mac_length]u8, msg: []const u8, key: []const u8) void { + fn create(out: *[mac_length]u8, msg: []const u8, key: []const u8) void { var ctx = Self.init(key); ctx.update(msg); ctx.final(out); } - pub fn init(key: []const u8) Self { + fn init(key: []const u8) Self { var self: Self = undefined; self.hasher = Sha512.init(.{}); Sha512.hash(key, &self.sha2pass, .{}); return self; } - pub fn update(self: *Self, msg: []const u8) void { + fn update(self: *Self, msg: []const u8) void { self.hasher.update(msg); } - pub fn final(self: *Self, out: *[mac_length]u8) void { + fn final(self: *Self, out: *[mac_length]u8) void { var sha2salt: [Sha512.digest_length]u8 = undefined; self.hasher.final(&sha2salt); out.* = hash(self.sha2pass, sha2salt); @@ -517,10 +557,12 @@ const pbkdf_prf = struct { } }; -/// bcrypt PBKDF2 implementation with variations to match OpenBSD -/// https://github.com/openbsd/src/blob/6df1256b7792691e66c2ed9d86a8c103069f9e34/lib/libutil/bcrypt_pbkdf.c#L98 +/// bcrypt-pbkdf is a key derivation function based on bcrypt. +/// This is the function used in OpenSSH to derive encryption keys from passphrases. /// -/// This particular variant is used in e.g. SSH +/// This implementation is compatible with the OpenBSD implementation (https://github.com/openbsd/src/blob/master/lib/libutil/bcrypt_pbkdf.c). +/// +/// Unlike the password hashing function `bcrypt`, this function doesn't silently truncate passwords longer than 72 bytes. pub fn pbkdf(pass: []const u8, salt: []const u8, key: []u8, rounds: u32) !void { try crypto.pwhash.pbkdf2(key, pass, salt, rounds, pbkdf_prf); } @@ -540,8 +582,9 @@ const crypt_format = struct { password: []const u8, salt: [salt_length]u8, params: Params, + silently_truncate_password: bool, ) [hash_length]u8 { - var dk = bcrypt(password, salt, params); + var dk = if (silently_truncate_password) bcrypt(password, salt, params) else bcryptWithoutTruncation(password, salt, params); var salt_str: [salt_str_length]u8 = undefined; _ = Codec.Encoder.encode(salt_str[0..], salt[0..]); @@ -573,15 +616,16 @@ const PhcFormatHasher = struct { }; /// Return a non-deterministic hash of the password encoded as a PHC-format string - pub fn create( + fn create( password: []const u8, params: Params, + silently_truncate_password: bool, buf: []u8, ) HasherError![]const u8 { var salt: [salt_length]u8 = undefined; crypto.random.bytes(&salt); - const hash = bcrypt(password, salt, params); + const hash = if (silently_truncate_password) bcrypt(password, salt, params) else bcryptWithoutTruncation(password, salt, params); return phc_format.serialize(HashResult{ .alg_id = alg_id, @@ -592,9 +636,10 @@ const PhcFormatHasher = struct { } /// Verify a password against a PHC-format encoded string - pub fn verify( + fn verify( str: []const u8, password: []const u8, + silently_truncate_password: bool, ) HasherError!void { const hash_result = try phc_format.deserialize(HashResult, str); @@ -602,7 +647,8 @@ const PhcFormatHasher = struct { if (hash_result.salt.len != salt_length or hash_result.hash.len != dk_length) return HasherError.InvalidEncoding; - const hash = bcrypt(password, hash_result.salt.buf, .{ .rounds_log = hash_result.r }); + const params = Params{ .rounds_log = hash_result.r }; + const hash = if (silently_truncate_password) bcrypt(password, hash_result.salt.buf, params) else bcryptWithoutTruncation(password, hash_result.salt.buf, params); const expected_hash = hash_result.hash.constSlice(); if (!mem.eql(u8, &hash, expected_hash)) return HasherError.PasswordVerificationFailed; @@ -612,12 +658,13 @@ const PhcFormatHasher = struct { /// Hash and verify passwords using the modular crypt format. const CryptFormatHasher = struct { /// Length of a string returned by the create() function - pub const pwhash_str_length: usize = hash_length; + const pwhash_str_length: usize = hash_length; /// Return a non-deterministic hash of the password encoded into the modular crypt format - pub fn create( + fn create( password: []const u8, params: Params, + silently_truncate_password: bool, buf: []u8, ) HasherError![]const u8 { if (buf.len < pwhash_str_length) return HasherError.NoSpaceLeft; @@ -625,16 +672,17 @@ const CryptFormatHasher = struct { var salt: [salt_length]u8 = undefined; crypto.random.bytes(&salt); - const hash = crypt_format.strHashInternal(password, salt, params); + const hash = crypt_format.strHashInternal(password, salt, params, silently_truncate_password); @memcpy(buf[0..hash.len], &hash); return buf[0..pwhash_str_length]; } /// Verify a password against a string in modular crypt format - pub fn verify( + fn verify( str: []const u8, password: []const u8, + silently_truncate_password: bool, ) HasherError!void { if (str.len != pwhash_str_length or str[3] != '$' or str[6] != '$') return HasherError.InvalidEncoding; @@ -647,16 +695,22 @@ const CryptFormatHasher = struct { var salt: [salt_length]u8 = undefined; crypt_format.Codec.Decoder.decode(salt[0..], salt_str[0..]) catch return HasherError.InvalidEncoding; - const wanted_s = crypt_format.strHashInternal(password, salt, .{ .rounds_log = rounds_log }); + const wanted_s = crypt_format.strHashInternal(password, salt, .{ .rounds_log = rounds_log }, silently_truncate_password); if (!mem.eql(u8, wanted_s[0..], str[0..])) return HasherError.PasswordVerificationFailed; } }; /// Options for hashing a password. pub const HashOptions = struct { + /// For `bcrypt`, that can be left to `null`. allocator: ?mem.Allocator = null, + /// Internal bcrypt parameters. params: Params, + /// Encoding to use for the output of the hash function. encoding: pwhash.Encoding, + /// Whether to silently truncate the password to 72 bytes, or pre-hash the password when it is longer. + /// The default is `true`, for compatibility with the original bcrypt implementation. + silently_truncate_password: bool = true, }; /// 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 { /// The function returns a string that includes all the parameters required for verification. /// /// IMPORTANT: by design, bcrypt silently truncates passwords to 72 bytes. -/// If this is an issue for your application, hash the password first using a function such as SHA-512, -/// and then use the resulting hash as the password parameter for bcrypt. +/// If this is an issue for your application, set the `silently_truncate_password` option to `false`. pub fn strHash( password: []const u8, options: HashOptions, out: []u8, ) Error![]const u8 { switch (options.encoding) { - .phc => return PhcFormatHasher.create(password, options.params, out), - .crypt => return CryptFormatHasher.create(password, options.params, out), + .phc => return PhcFormatHasher.create(password, options.params, options.silently_truncate_password, out), + .crypt => return CryptFormatHasher.create(password, options.params, options.silently_truncate_password, out), } } /// Options for hash verification. pub const VerifyOptions = struct { + /// For `bcrypt`, that can be left to `null`. allocator: ?mem.Allocator = null, + /// Whether to silently truncate the password to 72 bytes, or pre-hash the password when it is longer. + silently_truncate_password: bool = false, }; /// Verify that a previously computed hash is valid for a given password. pub fn strVerify( str: []const u8, password: []const u8, - _: VerifyOptions, + options: VerifyOptions, ) Error!void { if (mem.startsWith(u8, str, crypt_format.prefix)) { - return CryptFormatHasher.verify(str, password); + return CryptFormatHasher.verify(str, password, options.silently_truncate_password); } else { - return PhcFormatHasher.verify(str, password); + return PhcFormatHasher.verify(str, password, options.silently_truncate_password); } } @@ -707,11 +763,12 @@ test "bcrypt codec" { } test "bcrypt crypt format" { - const hash_options = HashOptions{ + var hash_options = HashOptions{ .params = .{ .rounds_log = 5 }, .encoding = .crypt, + .silently_truncate_password = false, }; - const verify_options = VerifyOptions{}; + var verify_options = VerifyOptions{}; var buf: [hash_length]u8 = undefined; const s = try strHash("password", hash_options, &buf); @@ -724,10 +781,18 @@ test "bcrypt crypt format" { ); var long_buf: [hash_length]u8 = undefined; - const long_s = try strHash("password" ** 100, hash_options, &long_buf); + var long_s = try strHash("password" ** 100, hash_options, &long_buf); try testing.expect(mem.startsWith(u8, long_s, crypt_format.prefix)); try strVerify(long_s, "password" ** 100, verify_options); + try testing.expectError( + error.PasswordVerificationFailed, + strVerify(long_s, "password" ** 101, verify_options), + ); + + hash_options.silently_truncate_password = true; + verify_options.silently_truncate_password = true; + long_s = try strHash("password" ** 100, hash_options, &long_buf); try strVerify(long_s, "password" ** 101, verify_options); try strVerify( @@ -738,11 +803,12 @@ test "bcrypt crypt format" { } test "bcrypt phc format" { - const hash_options = HashOptions{ + var hash_options = HashOptions{ .params = .{ .rounds_log = 5 }, .encoding = .phc, + .silently_truncate_password = false, }; - const verify_options = VerifyOptions{}; + var verify_options = VerifyOptions{}; const prefix = "$bcrypt$"; var buf: [hash_length * 2]u8 = undefined; @@ -756,10 +822,18 @@ test "bcrypt phc format" { ); var long_buf: [hash_length * 2]u8 = undefined; - const long_s = try strHash("password" ** 100, hash_options, &long_buf); + var long_s = try strHash("password" ** 100, hash_options, &long_buf); try testing.expect(mem.startsWith(u8, long_s, prefix)); try strVerify(long_s, "password" ** 100, verify_options); + try testing.expectError( + error.PasswordVerificationFailed, + strVerify(long_s, "password" ** 101, verify_options), + ); + + hash_options.silently_truncate_password = true; + verify_options.silently_truncate_password = true; + long_s = try strHash("password" ** 100, hash_options, &long_buf); try strVerify(long_s, "password" ** 101, verify_options); try strVerify(