mirror of
https://codeberg.org/ziglang/zig.git
synced 2025-12-06 05:44:20 +00:00
add scrypt kdf (#9577)
add phc encoding parser add password hash functions to benchmark change bcrypt to be consistent with scrypt Co-authored-by: lucky <>
This commit is contained in:
parent
a98fa56ae9
commit
8c41a8e761
5 changed files with 1392 additions and 104 deletions
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
377
lib/std/crypto/phc_encoding.zig
Normal file
377
lib/std/crypto/phc_encoding.zig
Normal file
|
|
@ -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);
|
||||
}
|
||||
663
lib/std/crypto/scrypt.zig
Normal file
663
lib/std/crypto/scrypt.zig
Normal file
|
|
@ -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);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue