diff --git a/lib/std/crypto.zig b/lib/std/crypto.zig index 42ded1f2ea..84b291d57e 100644 --- a/lib/std/crypto.zig +++ b/lib/std/crypto.zig @@ -110,7 +110,16 @@ pub const onetimeauth = struct { /// /// Password hashing functions must be used whenever sensitive data has to be directly derived from a password. pub const pwhash = struct { + pub const Encoding = enum { + phc, + crypt, + }; + pub const KdfError = errors.Error || std.mem.Allocator.Error; + pub const HasherError = KdfError || @import("crypto/phc_encoding.zig").Error; + pub const Error = HasherError || error{AllocatorRequired}; + pub const bcrypt = @import("crypto/bcrypt.zig"); + pub const scrypt = @import("crypto/scrypt.zig"); pub const pbkdf2 = @import("crypto/pbkdf2.zig").pbkdf2; }; diff --git a/lib/std/crypto/bcrypt.zig b/lib/std/crypto/bcrypt.zig index 07c2142a97..7d56350664 100644 --- a/lib/std/crypto/bcrypt.zig +++ b/lib/std/crypto/bcrypt.zig @@ -6,21 +6,28 @@ const std = @import("std"); const crypto = std.crypto; +const debug = std.debug; const fmt = std.fmt; const math = std.math; const mem = std.mem; -const debug = std.debug; +const pwhash = crypto.pwhash; const testing = std.testing; const utils = crypto.utils; -const EncodingError = crypto.errors.EncodingError; -const PasswordVerificationError = crypto.errors.PasswordVerificationError; + +const phc_format = @import("phc_encoding.zig"); + +const KdfError = pwhash.KdfError; +const HasherError = pwhash.HasherError; +const EncodingError = phc_format.Error; +const Error = pwhash.Error; const salt_length: usize = 16; const salt_str_length: usize = 22; const ct_str_length: usize = 31; const ct_length: usize = 24; +const dk_length: usize = ct_length - 1; -/// Length (in bytes) of a password hash +/// Length (in bytes) of a password hash in crypt encoding pub const hash_length: usize = 60; const State = struct { @@ -139,71 +146,15 @@ const State = struct { } }; -// bcrypt has its own variant of base64, with its own alphabet and no padding -const Codec = struct { - const alphabet = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - fn encode(b64: []u8, bin: []const u8) void { - var i: usize = 0; - var j: usize = 0; - while (i < bin.len) { - var c1 = bin[i]; - i += 1; - b64[j] = alphabet[c1 >> 2]; - j += 1; - c1 = (c1 & 3) << 4; - if (i >= bin.len) { - b64[j] = alphabet[c1]; - j += 1; - break; - } - var c2 = bin[i]; - i += 1; - c1 |= (c2 >> 4) & 0x0f; - b64[j] = alphabet[c1]; - j += 1; - c1 = (c2 & 0x0f) << 2; - if (i >= bin.len) { - b64[j] = alphabet[c1]; - j += 1; - break; - } - c2 = bin[i]; - i += 1; - c1 |= (c2 >> 6) & 3; - b64[j] = alphabet[c1]; - b64[j + 1] = alphabet[c2 & 0x3f]; - j += 2; - } - debug.assert(j == b64.len); - } - - fn decode(bin: []u8, b64: []const u8) EncodingError!void { - var i: usize = 0; - var j: usize = 0; - while (j < bin.len) { - const c1 = @intCast(u8, mem.indexOfScalar(u8, alphabet, b64[i]) orelse return error.InvalidEncoding); - const c2 = @intCast(u8, mem.indexOfScalar(u8, alphabet, b64[i + 1]) orelse return error.InvalidEncoding); - bin[j] = (c1 << 2) | ((c2 & 0x30) >> 4); - j += 1; - if (j >= bin.len) { - break; - } - const c3 = @intCast(u8, mem.indexOfScalar(u8, alphabet, b64[i + 2]) orelse return error.InvalidEncoding); - bin[j] = ((c2 & 0x0f) << 4) | ((c3 & 0x3c) >> 2); - j += 1; - if (j >= bin.len) { - break; - } - const c4 = @intCast(u8, mem.indexOfScalar(u8, alphabet, b64[i + 3]) orelse return error.InvalidEncoding); - bin[j] = ((c3 & 0x03) << 6) | c4; - j += 1; - i += 4; - } - } +pub const Params = struct { + rounds_log: u6, }; -fn strHashInternal(password: []const u8, rounds_log: u6, salt: [salt_length]u8) ![hash_length]u8 { +pub fn bcrypt( + password: []const u8, + salt: [salt_length]u8, + params: Params, +) [dk_length]u8 { var state = State{}; var password_buf: [73]u8 = undefined; const trimmed_len = math.min(password.len, password_buf.len - 1); @@ -212,7 +163,7 @@ fn strHashInternal(password: []const u8, rounds_log: u6, salt: [salt_length]u8) var passwordZ = password_buf[0 .. trimmed_len + 1]; state.expand(salt[0..], passwordZ); - const rounds: u64 = @as(u64, 1) << rounds_log; + const rounds: u64 = @as(u64, 1) << params.rounds_log; var k: u64 = 0; while (k < rounds) : (k += 1) { state.expand0(passwordZ); @@ -230,19 +181,204 @@ fn strHashInternal(password: []const u8, rounds_log: u6, salt: [salt_length]u8) for (cdata) |c, i| { mem.writeIntBig(u32, ct[i * 4 ..][0..4], c); } - - var salt_str: [salt_str_length]u8 = undefined; - Codec.encode(salt_str[0..], salt[0..]); - - var ct_str: [ct_str_length]u8 = undefined; - Codec.encode(ct_str[0..], ct[0 .. ct.len - 1]); - - var s_buf: [hash_length]u8 = undefined; - const s = fmt.bufPrint(s_buf[0..], "$2b${d}{d}${s}{s}", .{ rounds_log / 10, rounds_log % 10, salt_str, ct_str }) catch unreachable; - debug.assert(s.len == s_buf.len); - return s_buf; + return ct[0..dk_length].*; } +const crypt_format = struct { + /// String prefix for bcrypt + pub const prefix = "$2"; + + // bcrypt has its own variant of base64, with its own alphabet and no padding + const Codec = struct { + const alphabet = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + fn encode(b64: []u8, bin: []const u8) void { + var i: usize = 0; + var j: usize = 0; + while (i < bin.len) { + var c1 = bin[i]; + i += 1; + b64[j] = alphabet[c1 >> 2]; + j += 1; + c1 = (c1 & 3) << 4; + if (i >= bin.len) { + b64[j] = alphabet[c1]; + j += 1; + break; + } + var c2 = bin[i]; + i += 1; + c1 |= (c2 >> 4) & 0x0f; + b64[j] = alphabet[c1]; + j += 1; + c1 = (c2 & 0x0f) << 2; + if (i >= bin.len) { + b64[j] = alphabet[c1]; + j += 1; + break; + } + c2 = bin[i]; + i += 1; + c1 |= (c2 >> 6) & 3; + b64[j] = alphabet[c1]; + b64[j + 1] = alphabet[c2 & 0x3f]; + j += 2; + } + debug.assert(j == b64.len); + } + + fn decode(bin: []u8, b64: []const u8) EncodingError!void { + var i: usize = 0; + var j: usize = 0; + while (j < bin.len) { + const c1 = @intCast(u8, mem.indexOfScalar(u8, alphabet, b64[i]) orelse + return EncodingError.InvalidEncoding); + const c2 = @intCast(u8, mem.indexOfScalar(u8, alphabet, b64[i + 1]) orelse + return EncodingError.InvalidEncoding); + bin[j] = (c1 << 2) | ((c2 & 0x30) >> 4); + j += 1; + if (j >= bin.len) { + break; + } + const c3 = @intCast(u8, mem.indexOfScalar(u8, alphabet, b64[i + 2]) orelse + return EncodingError.InvalidEncoding); + bin[j] = ((c2 & 0x0f) << 4) | ((c3 & 0x3c) >> 2); + j += 1; + if (j >= bin.len) { + break; + } + const c4 = @intCast(u8, mem.indexOfScalar(u8, alphabet, b64[i + 3]) orelse + return EncodingError.InvalidEncoding); + bin[j] = ((c3 & 0x03) << 6) | c4; + j += 1; + i += 4; + } + } + }; + + fn strHashInternal( + password: []const u8, + salt: [salt_length]u8, + params: Params, + ) [hash_length]u8 { + var dk = bcrypt(password, salt, params); + + var salt_str: [salt_str_length]u8 = undefined; + Codec.encode(salt_str[0..], salt[0..]); + + var ct_str: [ct_str_length]u8 = undefined; + Codec.encode(ct_str[0..], dk[0..]); + + var s_buf: [hash_length]u8 = undefined; + const s = fmt.bufPrint( + s_buf[0..], + "{s}b${d}{d}${s}{s}", + .{ prefix, params.rounds_log / 10, params.rounds_log % 10, salt_str, ct_str }, + ) catch unreachable; + debug.assert(s.len == s_buf.len); + return s_buf; + } +}; + +/// Hash and verify passwords using the PHC format. +const PhcFormatHasher = struct { + const alg_id = "bcrypt"; + const BinValue = phc_format.BinValue; + + const HashResult = struct { + alg_id: []const u8, + r: u6, + salt: BinValue(salt_length), + hash: BinValue(dk_length), + }; + + /// Return a non-deterministic hash of the password encoded as a PHC-format string + pub fn create( + password: []const u8, + params: Params, + buf: []u8, + ) HasherError![]const u8 { + var salt: [salt_length]u8 = undefined; + crypto.random.bytes(&salt); + + const hash = bcrypt(password, salt, params); + + return phc_format.serialize(HashResult{ + .alg_id = alg_id, + .r = params.rounds_log, + .salt = try BinValue(salt_length).fromSlice(&salt), + .hash = try BinValue(dk_length).fromSlice(&hash), + }, buf); + } + + /// Verify a password against a PHC-format encoded string + pub fn verify( + str: []const u8, + password: []const u8, + ) HasherError!void { + const hash_result = try phc_format.deserialize(HashResult, str); + + if (!mem.eql(u8, hash_result.alg_id, alg_id)) return HasherError.PasswordVerificationFailed; + 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 expected_hash = hash_result.hash.constSlice(); + + if (!mem.eql(u8, &hash, expected_hash)) return HasherError.PasswordVerificationFailed; + } +}; + +/// 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; + + /// Return a non-deterministic hash of the password encoded into the modular crypt format + pub fn create( + password: []const u8, + params: Params, + buf: []u8, + ) HasherError![]const u8 { + if (buf.len < pwhash_str_length) return HasherError.NoSpaceLeft; + + var salt: [salt_length]u8 = undefined; + crypto.random.bytes(&salt); + + const hash = crypt_format.strHashInternal(password, salt, params); + mem.copy(u8, buf, &hash); + + return buf[0..pwhash_str_length]; + } + + /// Verify a password against a string in modular crypt format + pub fn verify( + str: []const u8, + password: []const u8, + ) HasherError!void { + if (str.len != pwhash_str_length or str[3] != '$' or str[6] != '$') + return HasherError.InvalidEncoding; + + const rounds_log_str = str[4..][0..2]; + const rounds_log = fmt.parseInt(u6, rounds_log_str[0..], 10) catch + return HasherError.InvalidEncoding; + + const salt_str = str[7..][0..salt_str_length]; + var salt: [salt_length]u8 = undefined; + try crypt_format.Codec.decode(salt[0..], salt_str[0..]); + + const wanted_s = crypt_format.strHashInternal(password, salt, .{ .rounds_log = rounds_log }); + if (!mem.eql(u8, wanted_s[0..], str[0..])) return HasherError.PasswordVerificationFailed; + } +}; + +/// Options for hashing a password. +pub const HashOptions = struct { + allocator: ?*mem.Allocator = null, + params: Params, + encoding: pwhash.Encoding, +}; + /// 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. /// @@ -251,24 +387,32 @@ fn strHashInternal(password: []const u8, rounds_log: u6, salt: [salt_length]u8) /// 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. -pub fn strHash(password: []const u8, rounds_log: u6) ![hash_length]u8 { - var salt: [salt_length]u8 = undefined; - crypto.random.bytes(&salt); - return strHashInternal(password, rounds_log, salt); +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), + } } +/// Options for hash verification. +pub const VerifyOptions = struct { + allocator: ?*mem.Allocator = null, +}; + /// Verify that a previously computed hash is valid for a given password. -pub fn strVerify(h: [hash_length]u8, password: []const u8) (EncodingError || PasswordVerificationError)!void { - if (!mem.eql(u8, "$2", h[0..2])) return error.InvalidEncoding; - if (h[3] != '$' or h[6] != '$') return error.InvalidEncoding; - const rounds_log_str = h[4..][0..2]; - const salt_str = h[7..][0..salt_str_length]; - var salt: [salt_length]u8 = undefined; - try Codec.decode(salt[0..], salt_str[0..]); - const rounds_log = fmt.parseInt(u6, rounds_log_str[0..], 10) catch return error.InvalidEncoding; - const wanted_s = try strHashInternal(password, rounds_log, salt); - if (!mem.eql(u8, wanted_s[0..], h[0..])) { - return error.PasswordVerificationFailed; +pub fn strVerify( + str: []const u8, + password: []const u8, + _: VerifyOptions, +) Error!void { + if (mem.startsWith(u8, str, crypt_format.prefix)) { + return CryptFormatHasher.verify(str, password); + } else { + return PhcFormatHasher.verify(str, password); } } @@ -276,20 +420,71 @@ test "bcrypt codec" { var salt: [salt_length]u8 = undefined; crypto.random.bytes(&salt); var salt_str: [salt_str_length]u8 = undefined; - Codec.encode(salt_str[0..], salt[0..]); + crypt_format.Codec.encode(salt_str[0..], salt[0..]); var salt2: [salt_length]u8 = undefined; - try Codec.decode(salt2[0..], salt_str[0..]); + try crypt_format.Codec.decode(salt2[0..], salt_str[0..]); try testing.expectEqualSlices(u8, salt[0..], salt2[0..]); } -test "bcrypt" { - const s = try strHash("password", 5); - try strVerify(s, "password"); - try testing.expectError(error.PasswordVerificationFailed, strVerify(s, "invalid password")); +test "bcrypt crypt format" { + const hash_options = HashOptions{ + .params = .{ .rounds_log = 5 }, + .encoding = .crypt, + }; + const verify_options = VerifyOptions{}; - const long_s = try strHash("password" ** 100, 5); - try strVerify(long_s, "password" ** 100); - try strVerify(long_s, "password" ** 101); + var buf: [hash_length]u8 = undefined; + const s = try strHash("password", hash_options, &buf); - try strVerify("$2b$08$WUQKyBCaKpziCwUXHiMVvu40dYVjkTxtWJlftl0PpjY2BxWSvFIEe".*, "The devil himself"); + try testing.expect(mem.startsWith(u8, s, crypt_format.prefix)); + try strVerify(s, "password", verify_options); + try testing.expectError( + error.PasswordVerificationFailed, + strVerify(s, "invalid password", verify_options), + ); + + var long_buf: [hash_length]u8 = undefined; + const 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 strVerify(long_s, "password" ** 101, verify_options); + + try strVerify( + "$2b$08$WUQKyBCaKpziCwUXHiMVvu40dYVjkTxtWJlftl0PpjY2BxWSvFIEe", + "The devil himself", + verify_options, + ); +} + +test "bcrypt phc format" { + const hash_options = HashOptions{ + .params = .{ .rounds_log = 5 }, + .encoding = .phc, + }; + const verify_options = VerifyOptions{}; + const prefix = "$bcrypt$"; + + var buf: [hash_length * 2]u8 = undefined; + const s = try strHash("password", hash_options, &buf); + + try testing.expect(mem.startsWith(u8, s, prefix)); + try strVerify(s, "password", verify_options); + try testing.expectError( + error.PasswordVerificationFailed, + strVerify(s, "invalid password", verify_options), + ); + + var long_buf: [hash_length * 2]u8 = undefined; + const 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 strVerify(long_s, "password" ** 101, verify_options); + + try strVerify( + "$bcrypt$r=5$2NopntlgE2lX3cTwr4qz8A$r3T7iKYQNnY4hAhGjk9RmuyvgrYJZwc", + "The devil himself", + verify_options, + ); } diff --git a/lib/std/crypto/benchmark.zig b/lib/std/crypto/benchmark.zig index e8e50b95ea..efaca227c1 100644 --- a/lib/std/crypto/benchmark.zig +++ b/lib/std/crypto/benchmark.zig @@ -300,6 +300,43 @@ pub fn benchmarkAes8(comptime Aes: anytype, comptime count: comptime_int) !u64 { return throughput; } +const CryptoPwhash = struct { + hashFn: anytype, + params: anytype, + name: []const u8, +}; +const bcrypt_params = bcrypt.Params{ .rounds_log = 5 }; +const pwhashes = [_]CryptoPwhash{ + CryptoPwhash{ .hashFn = bcrypt.strHash, .params = bcrypt_params, .name = "bcrypt" }, + CryptoPwhash{ .hashFn = scrypt.strHash, .params = scrypt.Params.interactive, .name = "scrypt" }, +}; + +fn benchmarkPwhash( + comptime hashFn: anytype, + comptime params: anytype, + comptime count: comptime_int, +) !u64 { + const password = "testpass" ** 2; + const opts = .{ .allocator = std.testing.allocator, .params = params, .encoding = .phc }; + var buf: [256]u8 = undefined; + + var timer = try Timer.start(); + const start = timer.lap(); + { + var i: usize = 0; + while (i < count) : (i += 1) { + _ = try hashFn(password, opts, &buf); + mem.doNotOptimizeAway(&buf); + } + } + const end = timer.read(); + + const elapsed_s = @intToFloat(f64, end - start) / time.ns_per_s; + const throughput = @floatToInt(u64, count / elapsed_s); + + return throughput; +} + fn usage() void { std.debug.warn( \\throughput_test [options] @@ -418,4 +455,11 @@ pub fn main() !void { try stdout.print("{s:>17}: {:10} ops/s\n", .{ E.name, throughput }); } } + + inline for (pwhashes) |H| { + if (filter == null or std.mem.indexOf(u8, H.name, filter.?) != null) { + const throughput = try benchmarkPwhash(H.hashFn, H.params, mode(64)); + try stdout.print("{s:>17}: {:10} ops/s\n", .{ H.name, throughput }); + } + } } diff --git a/lib/std/crypto/phc_encoding.zig b/lib/std/crypto/phc_encoding.zig new file mode 100644 index 0000000000..812c1d54ff --- /dev/null +++ b/lib/std/crypto/phc_encoding.zig @@ -0,0 +1,377 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2015-2021 Zig Contributors +// This file is part of [zig](https://ziglang.org/), which is MIT licensed. +// The MIT license requires this copyright notice to be included in all copies +// and substantial portions of the software. + +// https://github.com/P-H-C/phc-string-format + +const std = @import("std"); +const fmt = std.fmt; +const io = std.io; +const mem = std.mem; +const meta = std.meta; + +const fields_delimiter = "$"; +const version_param_name = "v"; +const params_delimiter = ","; +const kv_delimiter = "="; + +pub const Error = std.crypto.errors.EncodingError || error{NoSpaceLeft}; + +const B64Decoder = std.base64.standard_no_pad.Decoder; +const B64Encoder = std.base64.standard_no_pad.Encoder; + +/// A wrapped binary value whose maximum size is `max_len`. +/// +/// This type must be used whenever a binary value is encoded in a PHC-formatted string. +/// This includes `salt`, `hash`, and any other binary parameters such as keys. +/// +/// Once initialized, the actual value can be read with the `constSlice()` function. +pub fn BinValue(comptime max_len: usize) type { + return struct { + const Self = @This(); + const capacity = max_len; + const max_encoded_length = B64Encoder.calcSize(max_len); + + buf: [max_len]u8 = undefined, + len: usize = 0, + + /// Wrap an existing byte slice + pub fn fromSlice(slice: []const u8) Error!Self { + if (slice.len > capacity) return Error.NoSpaceLeft; + var bin_value: Self = undefined; + mem.copy(u8, &bin_value.buf, slice); + bin_value.len = slice.len; + return bin_value; + } + + /// Return the slice containing the actual value. + pub fn constSlice(self: Self) []const u8 { + return self.buf[0..self.len]; + } + + fn fromB64(self: *Self, str: []const u8) !void { + const len = B64Decoder.calcSizeForSlice(str) catch return Error.InvalidEncoding; + if (len > self.buf.len) return Error.NoSpaceLeft; + B64Decoder.decode(&self.buf, str) catch return Error.InvalidEncoding; + self.len = len; + } + + fn toB64(self: Self, buf: []u8) ![]const u8 { + const value = self.constSlice(); + const len = B64Encoder.calcSize(value.len); + if (len > buf.len) return Error.NoSpaceLeft; + return B64Encoder.encode(buf, value); + } + }; +} + +/// Deserialize a PHC-formatted string into a structure `HashResult`. +/// +/// Required field in the `HashResult` structure: +/// - `alg_id`: algorithm identifier +/// Optional, special fields: +/// - `alg_version`: algorithm version (unsigned integer) +/// - `salt`: salt +/// - `hash`: output of the hash function +/// +/// Other fields will also be deserialized from the function parameters section. +pub fn deserialize(comptime HashResult: type, str: []const u8) Error!HashResult { + var out = mem.zeroes(HashResult); + var it = mem.split(u8, str, fields_delimiter); + var set_fields: usize = 0; + + while (true) { + // Read the algorithm identifier + if ((it.next() orelse return Error.InvalidEncoding).len != 0) return Error.InvalidEncoding; + out.alg_id = it.next() orelse return Error.InvalidEncoding; + set_fields += 1; + + // Read the optional version number + var field = it.next() orelse break; + if (kvSplit(field)) |opt_version| { + if (mem.eql(u8, opt_version.key, version_param_name)) { + if (@hasField(HashResult, "alg_version")) { + const value_type_info = switch (@typeInfo(@TypeOf(out.alg_version))) { + .Optional => |opt| comptime @typeInfo(opt.child), + else => |t| t, + }; + out.alg_version = fmt.parseUnsigned( + @Type(value_type_info), + opt_version.value, + 10, + ) catch return Error.InvalidEncoding; + set_fields += 1; + } + field = it.next() orelse break; + } + } else |_| {} + + // Read optional parameters + var has_params = false; + var it_params = mem.split(u8, field, params_delimiter); + while (it_params.next()) |params| { + const param = kvSplit(params) catch break; + var found = false; + inline for (comptime meta.fields(HashResult)) |p| { + if (mem.eql(u8, p.name, param.key)) { + switch (@typeInfo(p.field_type)) { + .Int => @field(out, p.name) = fmt.parseUnsigned( + p.field_type, + param.value, + 10, + ) catch return Error.InvalidEncoding, + .Pointer => |ptr| { + if (!ptr.is_const) @compileError("Value slice must be constant"); + @field(out, p.name) = param.value; + }, + .Struct => try @field(out, p.name).fromB64(param.value), + else => std.debug.panic( + "Value for [{s}] must be an integer, a constant slice or a BinValue", + .{p.name}, + ), + } + set_fields += 1; + found = true; + break; + } + } + if (!found) return Error.InvalidEncoding; // An unexpected parameter was found in the string + has_params = true; + } + + // No separator between an empty parameters set and the salt + if (has_params) field = it.next() orelse break; + + // Read an optional salt + if (@hasField(HashResult, "salt")) { + try out.salt.fromB64(field); + set_fields += 1; + } else { + return Error.InvalidEncoding; + } + + // Read an optional hash + field = it.next() orelse break; + if (@hasField(HashResult, "hash")) { + try out.hash.fromB64(field); + set_fields += 1; + } else { + return Error.InvalidEncoding; + } + break; + } + + // Check that all the required fields have been set, excluding optional values and parameters + // with default values + var expected_fields: usize = 0; + inline for (comptime meta.fields(HashResult)) |p| { + if (@typeInfo(p.field_type) != .Optional and p.default_value == null) { + expected_fields += 1; + } + } + if (set_fields < expected_fields) return Error.InvalidEncoding; + + return out; +} + +/// Serialize parameters into a PHC string. +/// +/// Required field for `params`: +/// - `alg_id`: algorithm identifier +/// Optional, special fields: +/// - `alg_version`: algorithm version (unsigned integer) +/// - `salt`: salt +/// - `hash`: output of the hash function +/// +/// `params` can also include any additional parameters. +pub fn serialize(params: anytype, str: []u8) Error![]const u8 { + var buf = io.fixedBufferStream(str); + try serializeTo(params, buf.writer()); + return buf.getWritten(); +} + +/// Compute the number of bytes required to serialize `params` +pub fn calcSize(params: anytype) usize { + var buf = io.countingWriter(io.null_writer); + serializeTo(params, buf.writer()) catch unreachable; + return @intCast(usize, buf.bytes_written); +} + +fn serializeTo(params: anytype, out: anytype) !void { + const HashResult = @TypeOf(params); + try out.writeAll(fields_delimiter); + try out.writeAll(params.alg_id); + + if (@hasField(HashResult, "alg_version")) { + if (@typeInfo(@TypeOf(params.alg_version)) == .Optional) { + if (params.alg_version) |alg_version| { + try out.print( + "{s}{s}{s}{}", + .{ fields_delimiter, version_param_name, kv_delimiter, alg_version }, + ); + } + } else { + try out.print( + "{s}{s}{s}{}", + .{ fields_delimiter, version_param_name, kv_delimiter, params.alg_version }, + ); + } + } + + var has_params = false; + inline for (comptime meta.fields(HashResult)) |p| { + if (!(mem.eql(u8, p.name, "alg_id") or + mem.eql(u8, p.name, "alg_version") or + mem.eql(u8, p.name, "hash") or + mem.eql(u8, p.name, "salt"))) + { + const value = @field(params, p.name); + try out.writeAll(if (has_params) params_delimiter else fields_delimiter); + if (@typeInfo(p.field_type) == .Struct) { + var buf: [@TypeOf(value).max_encoded_length]u8 = undefined; + try out.print("{s}{s}{s}", .{ p.name, kv_delimiter, try value.toB64(&buf) }); + } else { + try out.print( + if (@typeInfo(@TypeOf(value)) == .Pointer) "{s}{s}{s}" else "{s}{s}{}", + .{ p.name, kv_delimiter, value }, + ); + } + has_params = true; + } + } + + var has_salt = false; + if (@hasField(HashResult, "salt")) { + var buf: [@TypeOf(params.salt).max_encoded_length]u8 = undefined; + try out.print("{s}{s}", .{ fields_delimiter, try params.salt.toB64(&buf) }); + has_salt = true; + } + + if (@hasField(HashResult, "hash")) { + var buf: [@TypeOf(params.hash).max_encoded_length]u8 = undefined; + if (!has_salt) try out.writeAll(fields_delimiter); + try out.print("{s}{s}", .{ fields_delimiter, try params.hash.toB64(&buf) }); + } +} + +// Split a `key=value` string into `key` and `value` +fn kvSplit(str: []const u8) !struct { key: []const u8, value: []const u8 } { + var it = mem.split(u8, str, kv_delimiter); + const key = it.next() orelse return Error.InvalidEncoding; + const value = it.next() orelse return Error.InvalidEncoding; + const ret = .{ .key = key, .value = value }; + return ret; +} + +test "phc format - encoding/decoding" { + const Input = struct { + str: []const u8, + HashResult: type, + }; + const inputs = [_]Input{ + .{ + .str = "$argon2id$v=19$key=a2V5,m=4096,t=0,p=1$X1NhbHQAAAAAAAAAAAAAAA$bWh++MKN1OiFHKgIWTLvIi1iHicmHH7+Fv3K88ifFfI", + .HashResult = struct { + alg_id: []const u8, + alg_version: u16, + key: BinValue(16), + m: usize, + t: u64, + p: u32, + salt: BinValue(16), + hash: BinValue(32), + }, + }, + .{ + .str = "$scrypt$v=1$ln=15,r=8,p=1$c2FsdHNhbHQ$dGVzdHBhc3M", + .HashResult = struct { + alg_id: []const u8, + alg_version: ?u30, + ln: u6, + r: u30, + p: u30, + salt: BinValue(16), + hash: BinValue(16), + }, + }, + .{ + .str = "$scrypt", + .HashResult = struct { alg_id: []const u8 }, + }, + .{ .str = "$scrypt$v=1", .HashResult = struct { alg_id: []const u8, alg_version: u16 } }, + .{ + .str = "$scrypt$ln=15,r=8,p=1", + .HashResult = struct { alg_id: []const u8, alg_version: ?u30, ln: u6, r: u30, p: u30 }, + }, + .{ + .str = "$scrypt$c2FsdHNhbHQ", + .HashResult = struct { alg_id: []const u8, salt: BinValue(16) }, + }, + .{ + .str = "$scrypt$v=1$ln=15,r=8,p=1$c2FsdHNhbHQ", + .HashResult = struct { + alg_id: []const u8, + alg_version: u16, + ln: u6, + r: u30, + p: u30, + salt: BinValue(16), + }, + }, + .{ + .str = "$scrypt$v=1$ln=15,r=8,p=1", + .HashResult = struct { alg_id: []const u8, alg_version: ?u30, ln: u6, r: u30, p: u30 }, + }, + .{ + .str = "$scrypt$v=1$c2FsdHNhbHQ$dGVzdHBhc3M", + .HashResult = struct { + alg_id: []const u8, + alg_version: u16, + salt: BinValue(16), + hash: BinValue(16), + }, + }, + .{ + .str = "$scrypt$v=1$c2FsdHNhbHQ", + .HashResult = struct { alg_id: []const u8, alg_version: u16, salt: BinValue(16) }, + }, + .{ + .str = "$scrypt$c2FsdHNhbHQ$dGVzdHBhc3M", + .HashResult = struct { alg_id: []const u8, salt: BinValue(16), hash: BinValue(16) }, + }, + }; + inline for (inputs) |input| { + const v = try deserialize(input.HashResult, input.str); + var buf: [input.str.len]u8 = undefined; + const s1 = try serialize(v, &buf); + try std.testing.expectEqualSlices(u8, input.str, s1); + } +} + +test "phc format - empty input string" { + const s = ""; + const v = deserialize(struct { alg_id: []const u8 }, s); + try std.testing.expectError(Error.InvalidEncoding, v); +} + +test "phc format - hash without salt" { + const s = "$scrypt"; + const v = deserialize(struct { alg_id: []const u8, hash: BinValue(16) }, s); + try std.testing.expectError(Error.InvalidEncoding, v); +} + +test "phc format - calcSize" { + const s = "$scrypt$v=1$ln=15,r=8,p=1$c2FsdHNhbHQ$dGVzdHBhc3M"; + const v = try deserialize(struct { + alg_id: []const u8, + alg_version: u16, + ln: u6, + r: u30, + p: u30, + salt: BinValue(8), + hash: BinValue(8), + }, s); + try std.testing.expectEqual(calcSize(v), s.len); +} diff --git a/lib/std/crypto/scrypt.zig b/lib/std/crypto/scrypt.zig new file mode 100644 index 0000000000..8d8e227408 --- /dev/null +++ b/lib/std/crypto/scrypt.zig @@ -0,0 +1,663 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2015-2021 Zig Contributors +// This file is part of [zig](https://ziglang.org/), which is MIT licensed. +// The MIT license requires this copyright notice to be included in all copies +// and substantial portions of the software. + +// https://tools.ietf.org/html/rfc7914 +// https://github.com/golang/crypto/blob/master/scrypt/scrypt.go + +const std = @import("std"); +const crypto = std.crypto; +const fmt = std.fmt; +const io = std.io; +const math = std.math; +const mem = std.mem; +const meta = std.meta; +const pwhash = crypto.pwhash; + +const phc_format = @import("phc_encoding.zig"); + +const HmacSha256 = crypto.auth.hmac.sha2.HmacSha256; +const KdfError = pwhash.KdfError; +const HasherError = pwhash.HasherError; +const EncodingError = phc_format.Error; +const Error = pwhash.Error; + +const max_size = math.maxInt(usize); +const max_int = max_size >> 1; +const default_salt_len = 32; +const default_hash_len = 32; +const max_salt_len = 64; +const max_hash_len = 64; + +fn blockCopy(dst: []align(16) u32, src: []align(16) const u32, n: usize) void { + mem.copy(u32, dst, src[0 .. n * 16]); +} + +fn blockXor(dst: []align(16) u32, src: []align(16) const u32, n: usize) void { + for (src[0 .. n * 16]) |v, i| { + dst[i] ^= v; + } +} + +const QuarterRound = struct { a: usize, b: usize, c: usize, d: u6 }; + +fn Rp(a: usize, b: usize, c: usize, d: u6) QuarterRound { + return QuarterRound{ .a = a, .b = b, .c = c, .d = d }; +} + +fn salsa8core(b: *align(16) [16]u32) void { + const arx_steps = comptime [_]QuarterRound{ + Rp(4, 0, 12, 7), Rp(8, 4, 0, 9), Rp(12, 8, 4, 13), Rp(0, 12, 8, 18), + Rp(9, 5, 1, 7), Rp(13, 9, 5, 9), Rp(1, 13, 9, 13), Rp(5, 1, 13, 18), + Rp(14, 10, 6, 7), Rp(2, 14, 10, 9), Rp(6, 2, 14, 13), Rp(10, 6, 2, 18), + Rp(3, 15, 11, 7), Rp(7, 3, 15, 9), Rp(11, 7, 3, 13), Rp(15, 11, 7, 18), + Rp(1, 0, 3, 7), Rp(2, 1, 0, 9), Rp(3, 2, 1, 13), Rp(0, 3, 2, 18), + Rp(6, 5, 4, 7), Rp(7, 6, 5, 9), Rp(4, 7, 6, 13), Rp(5, 4, 7, 18), + Rp(11, 10, 9, 7), Rp(8, 11, 10, 9), Rp(9, 8, 11, 13), Rp(10, 9, 8, 18), + Rp(12, 15, 14, 7), Rp(13, 12, 15, 9), Rp(14, 13, 12, 13), Rp(15, 14, 13, 18), + }; + var x = b.*; + var j: usize = 0; + while (j < 8) : (j += 2) { + inline for (arx_steps) |r| { + x[r.a] ^= math.rotl(u32, x[r.b] +% x[r.c], r.d); + } + } + j = 0; + while (j < 16) : (j += 1) { + b[j] +%= x[j]; + } +} + +fn salsaXor(tmp: *align(16) [16]u32, in: []align(16) const u32, out: []align(16) u32) void { + blockXor(tmp, in, 1); + salsa8core(tmp); + blockCopy(out, tmp, 1); +} + +fn blockMix(tmp: *align(16) [16]u32, in: []align(16) const u32, out: []align(16) u32, r: u30) void { + blockCopy(tmp, in[(2 * r - 1) * 16 ..], 1); + var i: usize = 0; + while (i < 2 * r) : (i += 2) { + salsaXor(tmp, in[i * 16 ..], out[i * 8 ..]); + salsaXor(tmp, in[i * 16 + 16 ..], out[i * 8 + r * 16 ..]); + } +} + +fn integerify(b: []align(16) const u32, r: u30) u64 { + const j = (2 * r - 1) * 16; + return @as(u64, b[j]) | @as(u64, b[j + 1]) << 32; +} + +fn smix(b: []align(16) u8, r: u30, n: usize, v: []align(16) u32, xy: []align(16) u32) void { + var x = xy[0 .. 32 * r]; + var y = xy[32 * r ..]; + + for (x) |*v1, j| { + v1.* = mem.readIntSliceLittle(u32, b[4 * j ..]); + } + + var tmp: [16]u32 align(16) = undefined; + var i: usize = 0; + while (i < n) : (i += 2) { + blockCopy(v[i * (32 * r) ..], x, 2 * r); + blockMix(&tmp, x, y, r); + + blockCopy(v[(i + 1) * (32 * r) ..], y, 2 * r); + blockMix(&tmp, y, x, r); + } + + i = 0; + while (i < n) : (i += 2) { + var j = @intCast(usize, integerify(x, r) & (n - 1)); + blockXor(x, v[j * (32 * r) ..], 2 * r); + blockMix(&tmp, x, y, r); + + j = @intCast(usize, integerify(y, r) & (n - 1)); + blockXor(y, v[j * (32 * r) ..], 2 * r); + blockMix(&tmp, y, x, r); + } + + for (x) |v1, j| { + mem.writeIntLittle(u32, b[4 * j ..][0..4], v1); + } +} + +pub const Params = struct { + const Self = @This(); + + ln: u6, + r: u30, + p: u30, + + /// Baseline parameters for interactive logins + pub const interactive = Self.fromLimits(524288, 16777216); + + /// Baseline parameters for offline usage + pub const sensitive = Self.fromLimits(33554432, 1073741824); + + /// Create parameters from ops and mem limits + pub fn fromLimits(ops_limit: u64, mem_limit: usize) Self { + const ops = math.max(32768, ops_limit); + const r: u30 = 8; + if (ops < mem_limit / 32) { + const max_n = ops / (r * 4); + return Self{ .r = r, .p = 1, .ln = @intCast(u6, math.log2(max_n)) }; + } else { + const max_n = mem_limit / (@intCast(usize, r) * 128); + const ln = @intCast(u6, math.log2(max_n)); + const max_rp = math.min(0x3fffffff, (ops / 4) / (@as(u64, 1) << ln)); + return Self{ .r = r, .p = @intCast(u30, max_rp / @as(u64, r)), .ln = ln }; + } + } +}; + +/// Apply scrypt to generate a key from a password. +/// +/// scrypt is defined in RFC 7914. +/// +/// allocator: *mem.Allocator. +/// +/// derived_key: Slice of appropriate size for generated key. Generally 16 or 32 bytes in length. +/// May be uninitialized. All bytes will be overwritten. +/// Maximum size is `derived_key.len / 32 == 0xffff_ffff`. +/// +/// password: Arbitrary sequence of bytes of any length. +/// +/// salt: Arbitrary sequence of bytes of any length. +/// +/// params: Params. +pub fn kdf( + allocator: *mem.Allocator, + derived_key: []u8, + password: []const u8, + salt: []const u8, + params: Params, +) KdfError!void { + if (derived_key.len == 0 or derived_key.len / 32 > 0xffff_ffff) return KdfError.OutputTooLong; + if (params.ln == 0 or params.r == 0 or params.p == 0) return KdfError.WeakParameters; + + const n64 = @as(u64, 1) << params.ln; + if (n64 > max_size) return KdfError.WeakParameters; + const n = @intCast(usize, n64); + if (@as(u64, params.r) * @as(u64, params.p) >= 1 << 30 or + params.r > max_int / 128 / @as(u64, params.p) or + params.r > max_int / 256 or + n > max_int / 128 / @as(u64, params.r)) return KdfError.WeakParameters; + + var xy = try allocator.alignedAlloc(u32, 16, 64 * params.r); + defer allocator.free(xy); + var v = try allocator.alignedAlloc(u32, 16, 32 * n * params.r); + defer allocator.free(v); + var dk = try allocator.alignedAlloc(u8, 16, params.p * 128 * params.r); + defer allocator.free(dk); + + try pwhash.pbkdf2(dk, password, salt, 1, HmacSha256); + var i: u32 = 0; + while (i < params.p) : (i += 1) { + smix(dk[i * 128 * params.r ..], params.r, n, v, xy); + } + try pwhash.pbkdf2(derived_key, password, dk, 1, HmacSha256); +} + +const crypt_format = struct { + /// String prefix for scrypt + pub const prefix = "$7$"; + + /// Standard type for a set of scrypt parameters, with the salt and hash. + pub fn HashResult(comptime crypt_max_hash_len: usize) type { + return struct { + ln: u6, + r: u30, + p: u30, + salt: []const u8, + hash: BinValue(crypt_max_hash_len), + }; + } + + const Codec = CustomB64Codec("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".*); + + /// A wrapped binary value whose maximum size is `max_len`. + /// + /// This type must be used whenever a binary value is encoded in a PHC-formatted string. + /// This includes `salt`, `hash`, and any other binary parameters such as keys. + /// + /// Once initialized, the actual value can be read with the `constSlice()` function. + pub fn BinValue(comptime max_len: usize) type { + return struct { + const Self = @This(); + const capacity = max_len; + const max_encoded_length = Codec.encodedLen(max_len); + + buf: [max_len]u8 = undefined, + len: usize = 0, + + /// Wrap an existing byte slice + pub fn fromSlice(slice: []const u8) EncodingError!Self { + if (slice.len > capacity) return EncodingError.NoSpaceLeft; + var bin_value: Self = undefined; + mem.copy(u8, &bin_value.buf, slice); + bin_value.len = slice.len; + return bin_value; + } + + /// Return the slice containing the actual value. + pub fn constSlice(self: Self) []const u8 { + return self.buf[0..self.len]; + } + + fn fromB64(self: *Self, str: []const u8) !void { + const len = Codec.decodedLen(str.len); + if (len > self.buf.len) return EncodingError.NoSpaceLeft; + try Codec.decode(self.buf[0..len], str); + self.len = len; + } + + fn toB64(self: Self, buf: []u8) ![]const u8 { + const value = self.constSlice(); + const len = Codec.encodedLen(value.len); + if (len > buf.len) return EncodingError.NoSpaceLeft; + var encoded = buf[0..len]; + Codec.encode(encoded, value); + return encoded; + } + }; + } + + /// Expand binary data into a salt for the modular crypt format. + pub fn saltFromBin(comptime len: usize, salt: [len]u8) [Codec.encodedLen(len)]u8 { + var buf: [Codec.encodedLen(len)]u8 = undefined; + Codec.encode(&buf, &salt); + return buf; + } + + /// Deserialize a string into a structure `T` (matching `HashResult`). + pub fn deserialize(comptime T: type, str: []const u8) EncodingError!T { + var out: T = undefined; + + if (str.len < 16) return EncodingError.InvalidEncoding; + if (!mem.eql(u8, prefix, str[0..3])) return EncodingError.InvalidEncoding; + out.ln = try Codec.intDecode(u6, str[3..4]); + out.r = try Codec.intDecode(u30, str[4..9]); + out.p = try Codec.intDecode(u30, str[9..14]); + + var it = mem.split(u8, str[14..], "$"); + + const salt = it.next() orelse return EncodingError.InvalidEncoding; + if (@hasField(T, "salt")) out.salt = salt; + + const hash_str = it.next() orelse return EncodingError.InvalidEncoding; + if (@hasField(T, "hash")) try out.hash.fromB64(hash_str); + + return out; + } + + /// Serialize parameters into a string in modular crypt format. + pub fn serialize(params: anytype, str: []u8) EncodingError![]const u8 { + var buf = io.fixedBufferStream(str); + try serializeTo(params, buf.writer()); + return buf.getWritten(); + } + + /// Compute the number of bytes required to serialize `params` + pub fn calcSize(params: anytype) usize { + var buf = io.countingWriter(io.null_writer); + serializeTo(params, buf.writer()) catch unreachable; + return @intCast(usize, buf.bytes_written); + } + + fn serializeTo(params: anytype, out: anytype) !void { + var header: [14]u8 = undefined; + mem.copy(u8, header[0..3], prefix); + Codec.intEncode(header[3..4], params.ln); + Codec.intEncode(header[4..9], params.r); + Codec.intEncode(header[9..14], params.p); + try out.writeAll(&header); + try out.writeAll(params.salt); + try out.writeAll("$"); + var buf: [@TypeOf(params.hash).max_encoded_length]u8 = undefined; + const hash_str = try params.hash.toB64(&buf); + try out.writeAll(hash_str); + } + + /// Custom codec that maps 6 bits into 8 like regular Base64, but uses its own alphabet, + /// encodes bits in little-endian, and can also encode integers. + fn CustomB64Codec(comptime map: [64]u8) type { + return struct { + const map64 = map; + + fn encodedLen(len: usize) usize { + return (len * 4 + 2) / 3; + } + + fn decodedLen(len: usize) usize { + return len / 4 * 3 + (len % 4) * 3 / 4; + } + + fn intEncode(dst: []u8, src: anytype) void { + var n = src; + for (dst) |*x| { + x.* = map64[@truncate(u6, n)]; + n = math.shr(@TypeOf(src), n, 6); + } + } + + fn intDecode(comptime T: type, src: *const [(meta.bitCount(T) + 5) / 6]u8) !T { + var v: T = 0; + for (src) |x, i| { + const vi = mem.indexOfScalar(u8, &map64, x) orelse return EncodingError.InvalidEncoding; + v |= @intCast(T, vi) << @intCast(math.Log2Int(T), i * 6); + } + return v; + } + + fn decode(dst: []u8, src: []const u8) !void { + std.debug.assert(dst.len == decodedLen(src.len)); + var i: usize = 0; + while (i < src.len / 4) : (i += 1) { + mem.writeIntSliceLittle(u24, dst[i * 3 ..], try intDecode(u24, src[i * 4 ..][0..4])); + } + const leftover = src[i * 4 ..]; + var v: u24 = 0; + for (leftover) |_, j| { + v |= @as(u24, try intDecode(u6, leftover[j..][0..1])) << @intCast(u5, j * 6); + } + for (dst[i * 3 ..]) |*x, j| { + x.* = @truncate(u8, v >> @intCast(u5, j * 8)); + } + } + + fn encode(dst: []u8, src: []const u8) void { + std.debug.assert(dst.len == encodedLen(src.len)); + var i: usize = 0; + while (i < src.len / 3) : (i += 1) { + intEncode(dst[i * 4 ..][0..4], mem.readIntSliceLittle(u24, src[i * 3 ..])); + } + const leftover = src[i * 3 ..]; + var v: u24 = 0; + for (leftover) |x, j| { + v |= @as(u24, x) << @intCast(u5, j * 8); + } + intEncode(dst[i * 4 ..], v); + } + }; + } +}; + +/// Hash and verify passwords using the PHC format. +const PhcFormatHasher = struct { + const alg_id = "scrypt"; + const BinValue = phc_format.BinValue; + + const HashResult = struct { + alg_id: []const u8, + ln: u6, + r: u30, + p: u30, + salt: BinValue(max_salt_len), + hash: BinValue(max_hash_len), + }; + + /// Return a non-deterministic hash of the password encoded as a PHC-format string + pub fn create( + allocator: *mem.Allocator, + password: []const u8, + params: Params, + buf: []u8, + ) HasherError![]const u8 { + var salt: [default_salt_len]u8 = undefined; + crypto.random.bytes(&salt); + + var hash: [default_hash_len]u8 = undefined; + try kdf(allocator, &hash, password, &salt, params); + + return phc_format.serialize(HashResult{ + .alg_id = alg_id, + .ln = params.ln, + .r = params.r, + .p = params.p, + .salt = try BinValue(max_salt_len).fromSlice(&salt), + .hash = try BinValue(max_hash_len).fromSlice(&hash), + }, buf); + } + + /// Verify a password against a PHC-format encoded string + pub fn verify( + allocator: *mem.Allocator, + str: []const u8, + password: []const u8, + ) HasherError!void { + const hash_result = try phc_format.deserialize(HashResult, str); + if (!mem.eql(u8, hash_result.alg_id, alg_id)) return HasherError.PasswordVerificationFailed; + const params = Params{ .ln = hash_result.ln, .r = hash_result.r, .p = hash_result.p }; + const expected_hash = hash_result.hash.constSlice(); + var hash_buf: [max_hash_len]u8 = undefined; + if (expected_hash.len > hash_buf.len) return HasherError.InvalidEncoding; + var hash = hash_buf[0..expected_hash.len]; + try kdf(allocator, hash, password, hash_result.salt.constSlice(), params); + if (!mem.eql(u8, hash, expected_hash)) return HasherError.PasswordVerificationFailed; + } +}; + +/// Hash and verify passwords using the modular crypt format. +const CryptFormatHasher = struct { + const BinValue = crypt_format.BinValue; + const HashResult = crypt_format.HashResult(max_hash_len); + + /// Length of a string returned by the create() function + pub const pwhash_str_length: usize = 101; + + /// Return a non-deterministic hash of the password encoded into the modular crypt format + pub fn create( + allocator: *mem.Allocator, + password: []const u8, + params: Params, + buf: []u8, + ) HasherError![]const u8 { + var salt_bin: [default_salt_len]u8 = undefined; + crypto.random.bytes(&salt_bin); + const salt = crypt_format.saltFromBin(salt_bin.len, salt_bin); + + var hash: [default_hash_len]u8 = undefined; + try kdf(allocator, &hash, password, &salt, params); + + return crypt_format.serialize(HashResult{ + .ln = params.ln, + .r = params.r, + .p = params.p, + .salt = &salt, + .hash = try BinValue(max_hash_len).fromSlice(&hash), + }, buf); + } + + /// Verify a password against a string in modular crypt format + pub fn verify( + allocator: *mem.Allocator, + str: []const u8, + password: []const u8, + ) HasherError!void { + const hash_result = try crypt_format.deserialize(HashResult, str); + const params = Params{ .ln = hash_result.ln, .r = hash_result.r, .p = hash_result.p }; + const expected_hash = hash_result.hash.constSlice(); + var hash_buf: [max_hash_len]u8 = undefined; + if (expected_hash.len > hash_buf.len) return HasherError.InvalidEncoding; + var hash = hash_buf[0..expected_hash.len]; + try kdf(allocator, hash, password, hash_result.salt, params); + if (!mem.eql(u8, hash, expected_hash)) return HasherError.PasswordVerificationFailed; + } +}; + +/// Options for hashing a password. +pub const HashOptions = struct { + allocator: ?*mem.Allocator, + params: Params, + encoding: pwhash.Encoding, +}; + +/// Compute a hash of a password using the scrypt key derivation function. +/// The function returns a string that includes all the parameters required for verification. +pub fn strHash( + password: []const u8, + options: HashOptions, + out: []u8, +) Error![]const u8 { + const allocator = options.allocator orelse return Error.AllocatorRequired; + switch (options.encoding) { + .phc => return PhcFormatHasher.create(allocator, password, options.params, out), + .crypt => return CryptFormatHasher.create(allocator, password, options.params, out), + } +} + +/// Options for hash verification. +pub const VerifyOptions = struct { + allocator: ?*mem.Allocator, +}; + +/// Verify that a previously computed hash is valid for a given password. +pub fn strVerify( + str: []const u8, + password: []const u8, + options: VerifyOptions, +) Error!void { + const allocator = options.allocator orelse return Error.AllocatorRequired; + if (mem.startsWith(u8, str, crypt_format.prefix)) { + return CryptFormatHasher.verify(allocator, str, password); + } else { + return PhcFormatHasher.verify(allocator, str, password); + } +} + +test "scrypt kdf" { + const password = "testpass"; + const salt = "saltsalt"; + + var dk: [32]u8 = undefined; + try kdf(std.testing.allocator, &dk, password, salt, .{ .ln = 15, .r = 8, .p = 1 }); + + const hex = "1e0f97c3f6609024022fbe698da29c2fe53ef1087a8e396dc6d5d2a041e886de"; + var bytes: [hex.len / 2]u8 = undefined; + _ = try fmt.hexToBytes(&bytes, hex); + + try std.testing.expectEqualSlices(u8, &bytes, &dk); +} + +test "scrypt kdf rfc 1" { + const password = ""; + const salt = ""; + + var dk: [64]u8 = undefined; + try kdf(std.testing.allocator, &dk, password, salt, .{ .ln = 4, .r = 1, .p = 1 }); + + const hex = "77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906"; + var bytes: [hex.len / 2]u8 = undefined; + _ = try fmt.hexToBytes(&bytes, hex); + + try std.testing.expectEqualSlices(u8, &bytes, &dk); +} + +test "scrypt kdf rfc 2" { + const password = "password"; + const salt = "NaCl"; + + var dk: [64]u8 = undefined; + try kdf(std.testing.allocator, &dk, password, salt, .{ .ln = 10, .r = 8, .p = 16 }); + + const hex = "fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640"; + var bytes: [hex.len / 2]u8 = undefined; + _ = try fmt.hexToBytes(&bytes, hex); + + try std.testing.expectEqualSlices(u8, &bytes, &dk); +} + +test "scrypt kdf rfc 3" { + const password = "pleaseletmein"; + const salt = "SodiumChloride"; + + var dk: [64]u8 = undefined; + try kdf(std.testing.allocator, &dk, password, salt, .{ .ln = 14, .r = 8, .p = 1 }); + + const hex = "7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887"; + var bytes: [hex.len / 2]u8 = undefined; + _ = try fmt.hexToBytes(&bytes, hex); + + try std.testing.expectEqualSlices(u8, &bytes, &dk); +} + +test "scrypt kdf rfc 4" { + // skip slow test + if (true) { + return error.SkipZigTest; + } + + const password = "pleaseletmein"; + const salt = "SodiumChloride"; + + var dk: [64]u8 = undefined; + try kdf(std.testing.allocator, &dk, password, salt, .{ .ln = 20, .r = 8, .p = 1 }); + + const hex = "2101cb9b6a511aaeaddbbe09cf70f881ec568d574a2ffd4dabe5ee9820adaa478e56fd8f4ba5d09ffa1c6d927c40f4c337304049e8a952fbcbf45c6fa77a41a4"; + var bytes: [hex.len / 2]u8 = undefined; + _ = try fmt.hexToBytes(&bytes, hex); + + try std.testing.expectEqualSlices(u8, &bytes, &dk); +} + +test "scrypt password hashing (crypt format)" { + const str = "$7$A6....1....TrXs5Zk6s8sWHpQgWDIXTR8kUU3s6Jc3s.DtdS8M2i4$a4ik5hGDN7foMuHOW.cp.CtX01UyCeO0.JAG.AHPpx5"; + const password = "Y0!?iQa9M%5ekffW(`"; + try CryptFormatHasher.verify(std.testing.allocator, str, password); + + const params = Params.interactive; + var buf: [CryptFormatHasher.pwhash_str_length]u8 = undefined; + const str2 = try CryptFormatHasher.create(std.testing.allocator, password, params, &buf); + try CryptFormatHasher.verify(std.testing.allocator, str2, password); +} + +test "scrypt strHash and strVerify" { + const alloc = std.testing.allocator; + + const password = "testpass"; + const verify_options = VerifyOptions{ .allocator = alloc }; + var buf: [128]u8 = undefined; + + const s = try strHash( + password, + HashOptions{ .allocator = alloc, .params = Params.interactive, .encoding = .crypt }, + &buf, + ); + try strVerify(s, password, verify_options); + + const s1 = try strHash( + password, + HashOptions{ .allocator = alloc, .params = Params.interactive, .encoding = .phc }, + &buf, + ); + try strVerify(s1, password, verify_options); +} + +test "scrypt unix-scrypt" { + const alloc = std.testing.allocator; + + // https://gitlab.com/jas/scrypt-unix-crypt/blob/master/unix-scrypt.txt + { + const str = "$7$C6..../....SodiumChloride$kBGj9fHznVYFQMEn/qDCfrDevf9YDtcDdKvEqHJLV8D"; + const password = "pleaseletmein"; + try strVerify(str, password, .{ .allocator = alloc }); + } + // one of the libsodium test vectors + { + const str = "$7$B6....1....75gBMAGwfFWZqBdyF3WdTQnWdUsuTiWjG1fF9c1jiSD$tc8RoB3.Em3/zNgMLWo2u00oGIoTyJv4fl3Fl8Tix72"; + const password = "^T5H$JYt39n%K*j:W]!1s?vg!:jGi]Ax?..l7[p0v:1jHTpla9;]bUN;?bWyCbtqg nrDFal+Jxl3,2`#^tFSu%v_+7iYse8-cCkNf!tD=KrW)"; + try strVerify(str, password, .{ .allocator = alloc }); + } +} + +test "scrypt crypt format" { + const str = "$7$C6..../....SodiumChloride$kBGj9fHznVYFQMEn/qDCfrDevf9YDtcDdKvEqHJLV8D"; + const params = try crypt_format.deserialize(crypt_format.HashResult(32), str); + var buf: [str.len]u8 = undefined; + const s1 = try crypt_format.serialize(params, &buf); + try std.testing.expectEqualStrings(s1, str); +}