mirror of
https://codeberg.org/ziglang/zig.git
synced 2025-12-06 05:44:20 +00:00
Also reduce the memory required by tests. 4GB for every test is way too much and doesn't provide much benefits in testing the algorithms.
954 lines
28 KiB
Zig
954 lines
28 KiB
Zig
// https://datatracker.ietf.org/doc/rfc9106
|
|
// https://github.com/golang/crypto/tree/master/argon2
|
|
// https://github.com/P-H-C/phc-winner-argon2
|
|
|
|
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
|
|
const blake2 = crypto.hash.blake2;
|
|
const crypto = std.crypto;
|
|
const Io = std.Io;
|
|
const math = std.math;
|
|
const mem = std.mem;
|
|
const phc_format = pwhash.phc_format;
|
|
const pwhash = crypto.pwhash;
|
|
const Blake2b512 = blake2.Blake2b512;
|
|
const Blocks = std.array_list.AlignedManaged([block_length]u64, .@"16");
|
|
const H0 = [Blake2b512.digest_length + 8]u8;
|
|
|
|
const EncodingError = crypto.errors.EncodingError;
|
|
const KdfError = pwhash.KdfError;
|
|
const HasherError = pwhash.HasherError;
|
|
const Error = pwhash.Error;
|
|
|
|
const version = 0x13;
|
|
const block_length = 128;
|
|
const sync_points = 4;
|
|
const max_int = 0xffff_ffff;
|
|
|
|
const default_salt_len = 32;
|
|
const default_hash_len = 32;
|
|
const max_salt_len = 64;
|
|
const max_hash_len = 64;
|
|
|
|
/// Argon2 type
|
|
pub const Mode = enum {
|
|
/// Argon2d is faster and uses data-depending memory access, which makes it highly resistant
|
|
/// against GPU cracking attacks and suitable for applications with no threats from side-channel
|
|
/// timing attacks (eg. cryptocurrencies).
|
|
argon2d,
|
|
|
|
/// Argon2i instead uses data-independent memory access, which is preferred for password
|
|
/// hashing and password-based key derivation, but it is slower as it makes more passes over
|
|
/// the memory to protect from tradeoff attacks.
|
|
argon2i,
|
|
|
|
/// Argon2id is a hybrid of Argon2i and Argon2d, using a combination of data-depending and
|
|
/// data-independent memory accesses, which gives some of Argon2i's resistance to side-channel
|
|
/// cache timing attacks and much of Argon2d's resistance to GPU cracking attacks.
|
|
argon2id,
|
|
};
|
|
|
|
/// Argon2 parameters
|
|
pub const Params = struct {
|
|
const Self = @This();
|
|
|
|
/// A [t]ime cost, which defines the amount of computation realized and therefore the execution
|
|
/// time, given in number of iterations.
|
|
t: u32,
|
|
|
|
/// A [m]emory cost, which defines the memory usage, given in kibibytes.
|
|
m: u32,
|
|
|
|
/// A [p]arallelism degree, which defines the number of parallel threads.
|
|
p: u24,
|
|
|
|
/// The [secret] parameter, which is used for keyed hashing. This allows a secret key to be input
|
|
/// at hashing time (from some external location) and be folded into the value of the hash. This
|
|
/// means that even if your salts and hashes are compromised, an attacker cannot brute-force to
|
|
/// find the password without the key.
|
|
secret: ?[]const u8 = null,
|
|
|
|
/// The [ad] parameter, which is used to fold any additional data into the hash value. Functionally,
|
|
/// this behaves almost exactly like the secret or salt parameters; the ad parameter is folding
|
|
/// into the value of the hash. However, this parameter is used for different data. The salt
|
|
/// should be a random string stored alongside your password. The secret should be a random key
|
|
/// only usable at hashing time. The ad is for any other data.
|
|
ad: ?[]const u8 = null,
|
|
|
|
/// Baseline parameters for interactive logins using argon2i type
|
|
pub const interactive_2i = Self.fromLimits(4, 33554432);
|
|
/// Baseline parameters for normal usage using argon2i type
|
|
pub const moderate_2i = Self.fromLimits(6, 134217728);
|
|
/// Baseline parameters for offline usage using argon2i type
|
|
pub const sensitive_2i = Self.fromLimits(8, 536870912);
|
|
|
|
/// Baseline parameters for interactive logins using argon2id type
|
|
pub const interactive_2id = Self.fromLimits(2, 67108864);
|
|
/// Baseline parameters for normal usage using argon2id type
|
|
pub const moderate_2id = Self.fromLimits(3, 268435456);
|
|
/// Baseline parameters for offline usage using argon2id type
|
|
pub const sensitive_2id = Self.fromLimits(4, 1073741824);
|
|
|
|
/// Recommended parameters for argon2id type according to the
|
|
/// [OWASP cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html).
|
|
pub const owasp_2id = Self{ .t = 2, .m = 19 * 1024, .p = 1 };
|
|
|
|
/// Create parameters from ops and mem limits, where mem_limit given in bytes
|
|
pub fn fromLimits(ops_limit: u32, mem_limit: usize) Self {
|
|
const m = mem_limit / 1024;
|
|
std.debug.assert(m <= max_int);
|
|
return .{ .t = ops_limit, .m = @as(u32, @intCast(m)), .p = 1 };
|
|
}
|
|
};
|
|
|
|
fn initHash(
|
|
password: []const u8,
|
|
salt: []const u8,
|
|
params: Params,
|
|
dk_len: usize,
|
|
mode: Mode,
|
|
) H0 {
|
|
var h0: H0 = undefined;
|
|
var parameters: [24]u8 = undefined;
|
|
var tmp: [4]u8 = undefined;
|
|
var b2 = Blake2b512.init(.{});
|
|
mem.writeInt(u32, parameters[0..4], params.p, .little);
|
|
mem.writeInt(u32, parameters[4..8], @as(u32, @intCast(dk_len)), .little);
|
|
mem.writeInt(u32, parameters[8..12], params.m, .little);
|
|
mem.writeInt(u32, parameters[12..16], params.t, .little);
|
|
mem.writeInt(u32, parameters[16..20], version, .little);
|
|
mem.writeInt(u32, parameters[20..24], @intFromEnum(mode), .little);
|
|
b2.update(¶meters);
|
|
mem.writeInt(u32, &tmp, @as(u32, @intCast(password.len)), .little);
|
|
b2.update(&tmp);
|
|
b2.update(password);
|
|
mem.writeInt(u32, &tmp, @as(u32, @intCast(salt.len)), .little);
|
|
b2.update(&tmp);
|
|
b2.update(salt);
|
|
const secret = params.secret orelse "";
|
|
std.debug.assert(secret.len <= max_int);
|
|
mem.writeInt(u32, &tmp, @as(u32, @intCast(secret.len)), .little);
|
|
b2.update(&tmp);
|
|
b2.update(secret);
|
|
const ad = params.ad orelse "";
|
|
std.debug.assert(ad.len <= max_int);
|
|
mem.writeInt(u32, &tmp, @as(u32, @intCast(ad.len)), .little);
|
|
b2.update(&tmp);
|
|
b2.update(ad);
|
|
b2.final(h0[0..Blake2b512.digest_length]);
|
|
return h0;
|
|
}
|
|
|
|
fn blake2bLong(out: []u8, in: []const u8) void {
|
|
const H = Blake2b512;
|
|
var outlen_bytes: [4]u8 = undefined;
|
|
mem.writeInt(u32, &outlen_bytes, @as(u32, @intCast(out.len)), .little);
|
|
|
|
var out_buf: [H.digest_length]u8 = undefined;
|
|
|
|
if (out.len <= H.digest_length) {
|
|
var h = H.init(.{ .expected_out_bits = out.len * 8 });
|
|
h.update(&outlen_bytes);
|
|
h.update(in);
|
|
h.final(&out_buf);
|
|
@memcpy(out, out_buf[0..out.len]);
|
|
return;
|
|
}
|
|
|
|
var h = H.init(.{});
|
|
h.update(&outlen_bytes);
|
|
h.update(in);
|
|
h.final(&out_buf);
|
|
var out_slice = out;
|
|
out_slice[0 .. H.digest_length / 2].* = out_buf[0 .. H.digest_length / 2].*;
|
|
out_slice = out_slice[H.digest_length / 2 ..];
|
|
|
|
var in_buf: [H.digest_length]u8 = undefined;
|
|
while (out_slice.len > H.digest_length) {
|
|
in_buf = out_buf;
|
|
H.hash(&in_buf, &out_buf, .{});
|
|
out_slice[0 .. H.digest_length / 2].* = out_buf[0 .. H.digest_length / 2].*;
|
|
out_slice = out_slice[H.digest_length / 2 ..];
|
|
}
|
|
in_buf = out_buf;
|
|
H.hash(&in_buf, &out_buf, .{ .expected_out_bits = out_slice.len * 8 });
|
|
@memcpy(out_slice, out_buf[0..out_slice.len]);
|
|
}
|
|
|
|
fn initBlocks(
|
|
blocks: *Blocks,
|
|
h0: *H0,
|
|
memory: u32,
|
|
threads: u24,
|
|
) void {
|
|
var block0: [1024]u8 = undefined;
|
|
var lane: u24 = 0;
|
|
while (lane < threads) : (lane += 1) {
|
|
const j = lane * (memory / threads);
|
|
mem.writeInt(u32, h0[Blake2b512.digest_length + 4 ..][0..4], lane, .little);
|
|
|
|
mem.writeInt(u32, h0[Blake2b512.digest_length..][0..4], 0, .little);
|
|
blake2bLong(&block0, h0);
|
|
for (&blocks.items[j + 0], 0..) |*v, i| {
|
|
v.* = mem.readInt(u64, block0[i * 8 ..][0..8], .little);
|
|
}
|
|
|
|
mem.writeInt(u32, h0[Blake2b512.digest_length..][0..4], 1, .little);
|
|
blake2bLong(&block0, h0);
|
|
for (&blocks.items[j + 1], 0..) |*v, i| {
|
|
v.* = mem.readInt(u64, block0[i * 8 ..][0..8], .little);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn processBlocks(
|
|
blocks: *Blocks,
|
|
time: u32,
|
|
memory: u32,
|
|
threads: u24,
|
|
mode: Mode,
|
|
io: Io,
|
|
) void {
|
|
const lanes = memory / threads;
|
|
const segments = lanes / sync_points;
|
|
|
|
if (builtin.single_threaded or threads == 1) {
|
|
processBlocksSt(blocks, time, memory, threads, mode, lanes, segments);
|
|
} else {
|
|
processBlocksMt(blocks, time, memory, threads, mode, lanes, segments, io);
|
|
}
|
|
}
|
|
|
|
fn processBlocksSt(
|
|
blocks: *Blocks,
|
|
time: u32,
|
|
memory: u32,
|
|
threads: u24,
|
|
mode: Mode,
|
|
lanes: u32,
|
|
segments: u32,
|
|
) void {
|
|
var n: u32 = 0;
|
|
while (n < time) : (n += 1) {
|
|
var slice: u32 = 0;
|
|
while (slice < sync_points) : (slice += 1) {
|
|
var lane: u24 = 0;
|
|
while (lane < threads) : (lane += 1) {
|
|
processSegment(blocks, time, memory, threads, mode, lanes, segments, n, slice, lane);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn processBlocksMt(
|
|
blocks: *Blocks,
|
|
time: u32,
|
|
memory: u32,
|
|
threads: u24,
|
|
mode: Mode,
|
|
lanes: u32,
|
|
segments: u32,
|
|
io: Io,
|
|
) void {
|
|
var n: u32 = 0;
|
|
while (n < time) : (n += 1) {
|
|
var slice: u32 = 0;
|
|
while (slice < sync_points) : (slice += 1) {
|
|
var group: Io.Group = .init;
|
|
var lane: u24 = 0;
|
|
while (lane < threads) : (lane += 1) {
|
|
group.async(io, processSegment, .{
|
|
blocks, time, memory, threads, mode, lanes, segments, n, slice, lane,
|
|
});
|
|
}
|
|
group.wait(io);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn processSegment(
|
|
blocks: *Blocks,
|
|
passes: u32,
|
|
memory: u32,
|
|
threads: u24,
|
|
mode: Mode,
|
|
lanes: u32,
|
|
segments: u32,
|
|
n: u32,
|
|
slice: u32,
|
|
lane: u24,
|
|
) void {
|
|
var addresses align(16) = [_]u64{0} ** block_length;
|
|
var in align(16) = [_]u64{0} ** block_length;
|
|
const zero align(16) = [_]u64{0} ** block_length;
|
|
if (mode == .argon2i or (mode == .argon2id and n == 0 and slice < sync_points / 2)) {
|
|
in[0] = n;
|
|
in[1] = lane;
|
|
in[2] = slice;
|
|
in[3] = memory;
|
|
in[4] = passes;
|
|
in[5] = @intFromEnum(mode);
|
|
}
|
|
var index: u32 = 0;
|
|
if (n == 0 and slice == 0) {
|
|
index = 2;
|
|
if (mode == .argon2i or mode == .argon2id) {
|
|
in[6] += 1;
|
|
processBlock(&addresses, &in, &zero);
|
|
processBlock(&addresses, &addresses, &zero);
|
|
}
|
|
}
|
|
var offset = lane * lanes + slice * segments + index;
|
|
var random: u64 = 0;
|
|
while (index < segments) : ({
|
|
index += 1;
|
|
offset += 1;
|
|
}) {
|
|
var prev = offset -% 1;
|
|
if (index == 0 and slice == 0) {
|
|
prev +%= lanes;
|
|
}
|
|
if (mode == .argon2i or (mode == .argon2id and n == 0 and slice < sync_points / 2)) {
|
|
if (index % block_length == 0) {
|
|
in[6] += 1;
|
|
processBlock(&addresses, &in, &zero);
|
|
processBlock(&addresses, &addresses, &zero);
|
|
}
|
|
random = addresses[index % block_length];
|
|
} else {
|
|
random = blocks.items[prev][0];
|
|
}
|
|
const new_offset = indexAlpha(random, lanes, segments, threads, n, slice, lane, index);
|
|
processBlockXor(&blocks.items[offset], &blocks.items[prev], &blocks.items[new_offset]);
|
|
}
|
|
}
|
|
|
|
fn processBlock(
|
|
out: *align(16) [block_length]u64,
|
|
in1: *align(16) const [block_length]u64,
|
|
in2: *align(16) const [block_length]u64,
|
|
) void {
|
|
processBlockGeneric(out, in1, in2, false);
|
|
}
|
|
|
|
fn processBlockXor(
|
|
out: *[block_length]u64,
|
|
in1: *const [block_length]u64,
|
|
in2: *const [block_length]u64,
|
|
) void {
|
|
processBlockGeneric(out, in1, in2, true);
|
|
}
|
|
|
|
fn processBlockGeneric(
|
|
out: *[block_length]u64,
|
|
in1: *const [block_length]u64,
|
|
in2: *const [block_length]u64,
|
|
comptime xor: bool,
|
|
) void {
|
|
var t: [block_length]u64 = undefined;
|
|
for (&t, 0..) |*v, i| {
|
|
v.* = in1[i] ^ in2[i];
|
|
}
|
|
var i: usize = 0;
|
|
while (i < block_length) : (i += 16) {
|
|
blamkaGeneric(t[i..][0..16]);
|
|
}
|
|
i = 0;
|
|
var buffer: [16]u64 = undefined;
|
|
while (i < block_length / 8) : (i += 2) {
|
|
var j: usize = 0;
|
|
while (j < block_length / 8) : (j += 2) {
|
|
buffer[j] = t[j * 8 + i];
|
|
buffer[j + 1] = t[j * 8 + i + 1];
|
|
}
|
|
blamkaGeneric(&buffer);
|
|
j = 0;
|
|
while (j < block_length / 8) : (j += 2) {
|
|
t[j * 8 + i] = buffer[j];
|
|
t[j * 8 + i + 1] = buffer[j + 1];
|
|
}
|
|
}
|
|
if (xor) {
|
|
for (t, 0..) |v, j| {
|
|
out[j] ^= in1[j] ^ in2[j] ^ v;
|
|
}
|
|
} else {
|
|
for (t, 0..) |v, j| {
|
|
out[j] = in1[j] ^ in2[j] ^ v;
|
|
}
|
|
}
|
|
}
|
|
|
|
const QuarterRound = struct { a: usize, b: usize, c: usize, d: usize };
|
|
|
|
fn Rp(a: usize, b: usize, c: usize, d: usize) QuarterRound {
|
|
return .{ .a = a, .b = b, .c = c, .d = d };
|
|
}
|
|
|
|
fn fBlaMka(x: u64, y: u64) u64 {
|
|
const xy = @as(u64, @as(u32, @truncate(x))) * @as(u64, @as(u32, @truncate(y)));
|
|
return x +% y +% 2 *% xy;
|
|
}
|
|
|
|
fn blamkaGeneric(x: *[16]u64) void {
|
|
const rounds = comptime [_]QuarterRound{
|
|
Rp(0, 4, 8, 12),
|
|
Rp(1, 5, 9, 13),
|
|
Rp(2, 6, 10, 14),
|
|
Rp(3, 7, 11, 15),
|
|
Rp(0, 5, 10, 15),
|
|
Rp(1, 6, 11, 12),
|
|
Rp(2, 7, 8, 13),
|
|
Rp(3, 4, 9, 14),
|
|
};
|
|
inline for (rounds) |r| {
|
|
x[r.a] = fBlaMka(x[r.a], x[r.b]);
|
|
x[r.d] = math.rotr(u64, x[r.d] ^ x[r.a], 32);
|
|
x[r.c] = fBlaMka(x[r.c], x[r.d]);
|
|
x[r.b] = math.rotr(u64, x[r.b] ^ x[r.c], 24);
|
|
x[r.a] = fBlaMka(x[r.a], x[r.b]);
|
|
x[r.d] = math.rotr(u64, x[r.d] ^ x[r.a], 16);
|
|
x[r.c] = fBlaMka(x[r.c], x[r.d]);
|
|
x[r.b] = math.rotr(u64, x[r.b] ^ x[r.c], 63);
|
|
}
|
|
}
|
|
|
|
fn finalize(
|
|
blocks: *Blocks,
|
|
memory: u32,
|
|
threads: u24,
|
|
out: []u8,
|
|
) void {
|
|
const lanes = memory / threads;
|
|
var lane: u24 = 0;
|
|
while (lane < threads - 1) : (lane += 1) {
|
|
for (blocks.items[(lane * lanes) + lanes - 1], 0..) |v, i| {
|
|
blocks.items[memory - 1][i] ^= v;
|
|
}
|
|
}
|
|
var block: [1024]u8 = undefined;
|
|
for (blocks.items[memory - 1], 0..) |v, i| {
|
|
mem.writeInt(u64, block[i * 8 ..][0..8], v, .little);
|
|
}
|
|
blake2bLong(out, &block);
|
|
}
|
|
|
|
fn indexAlpha(
|
|
rand: u64,
|
|
lanes: u32,
|
|
segments: u32,
|
|
threads: u24,
|
|
n: u32,
|
|
slice: u32,
|
|
lane: u24,
|
|
index: u32,
|
|
) u32 {
|
|
var ref_lane = @as(u32, @intCast(rand >> 32)) % threads;
|
|
if (n == 0 and slice == 0) {
|
|
ref_lane = lane;
|
|
}
|
|
var m = 3 * segments;
|
|
var s = ((slice + 1) % sync_points) * segments;
|
|
if (lane == ref_lane) {
|
|
m += index;
|
|
}
|
|
if (n == 0) {
|
|
m = slice * segments;
|
|
s = 0;
|
|
if (slice == 0 or lane == ref_lane) {
|
|
m += index;
|
|
}
|
|
}
|
|
if (index == 0 or lane == ref_lane) {
|
|
m -= 1;
|
|
}
|
|
var p = @as(u64, @as(u32, @truncate(rand)));
|
|
p = (p * p) >> 32;
|
|
p = (p * m) >> 32;
|
|
return ref_lane * lanes + @as(u32, @intCast(((s + m - (p + 1)) % lanes)));
|
|
}
|
|
|
|
/// Derives a key from the password, salt, and argon2 parameters.
|
|
///
|
|
/// Derived key has to be at least 4 bytes length.
|
|
///
|
|
/// Salt has to be at least 8 bytes length.
|
|
pub fn kdf(
|
|
allocator: mem.Allocator,
|
|
derived_key: []u8,
|
|
password: []const u8,
|
|
salt: []const u8,
|
|
params: Params,
|
|
mode: Mode,
|
|
io: Io,
|
|
) KdfError!void {
|
|
if (derived_key.len < 4) return KdfError.WeakParameters;
|
|
if (derived_key.len > max_int) return KdfError.OutputTooLong;
|
|
|
|
if (password.len > max_int) return KdfError.WeakParameters;
|
|
if (salt.len < 8 or salt.len > max_int) return KdfError.WeakParameters;
|
|
if (params.t < 1 or params.p < 1) return KdfError.WeakParameters;
|
|
if (params.m / 8 < params.p) return KdfError.WeakParameters;
|
|
|
|
var h0 = initHash(password, salt, params, derived_key.len, mode);
|
|
const memory = @max(
|
|
params.m / (sync_points * params.p) * (sync_points * params.p),
|
|
2 * sync_points * params.p,
|
|
);
|
|
|
|
var blocks = try Blocks.initCapacity(allocator, memory);
|
|
defer blocks.deinit();
|
|
|
|
blocks.appendNTimesAssumeCapacity(@splat(0), memory);
|
|
|
|
initBlocks(&blocks, &h0, memory, params.p);
|
|
processBlocks(&blocks, params.t, memory, params.p, mode, io);
|
|
finalize(&blocks, memory, params.p, derived_key);
|
|
}
|
|
|
|
const PhcFormatHasher = struct {
|
|
const BinValue = phc_format.BinValue;
|
|
|
|
const HashResult = struct {
|
|
alg_id: []const u8,
|
|
alg_version: ?u32,
|
|
m: u32,
|
|
t: u32,
|
|
p: u24,
|
|
salt: BinValue(max_salt_len),
|
|
hash: BinValue(max_hash_len),
|
|
};
|
|
|
|
pub fn create(
|
|
allocator: mem.Allocator,
|
|
password: []const u8,
|
|
params: Params,
|
|
mode: Mode,
|
|
buf: []u8,
|
|
io: Io,
|
|
) HasherError![]const u8 {
|
|
if (params.secret != null or params.ad != null) return HasherError.InvalidEncoding;
|
|
|
|
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, mode, io);
|
|
|
|
return phc_format.serialize(HashResult{
|
|
.alg_id = @tagName(mode),
|
|
.alg_version = version,
|
|
.m = params.m,
|
|
.t = params.t,
|
|
.p = params.p,
|
|
.salt = try BinValue(max_salt_len).fromSlice(&salt),
|
|
.hash = try BinValue(max_hash_len).fromSlice(&hash),
|
|
}, buf);
|
|
}
|
|
|
|
pub fn verify(
|
|
allocator: mem.Allocator,
|
|
str: []const u8,
|
|
password: []const u8,
|
|
io: Io,
|
|
) HasherError!void {
|
|
const hash_result = try phc_format.deserialize(HashResult, str);
|
|
|
|
const mode = std.meta.stringToEnum(Mode, hash_result.alg_id) orelse
|
|
return HasherError.PasswordVerificationFailed;
|
|
if (hash_result.alg_version) |v| {
|
|
if (v != version) return HasherError.InvalidEncoding;
|
|
}
|
|
const params = Params{ .t = hash_result.t, .m = hash_result.m, .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;
|
|
const hash = hash_buf[0..expected_hash.len];
|
|
|
|
try kdf(allocator, hash, password, hash_result.salt.constSlice(), params, mode, io);
|
|
if (!mem.eql(u8, hash, expected_hash)) return HasherError.PasswordVerificationFailed;
|
|
}
|
|
};
|
|
|
|
/// Options for hashing a password.
|
|
///
|
|
/// Allocator is required for argon2.
|
|
///
|
|
/// Only phc encoding is supported.
|
|
pub const HashOptions = struct {
|
|
allocator: ?mem.Allocator,
|
|
params: Params,
|
|
mode: Mode = .argon2id,
|
|
encoding: pwhash.Encoding = .phc,
|
|
};
|
|
|
|
/// Compute a hash of a password using the argon2 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,
|
|
io: Io,
|
|
) Error![]const u8 {
|
|
const allocator = options.allocator orelse return Error.AllocatorRequired;
|
|
switch (options.encoding) {
|
|
.phc => return PhcFormatHasher.create(
|
|
allocator,
|
|
password,
|
|
options.params,
|
|
options.mode,
|
|
out,
|
|
io,
|
|
),
|
|
.crypt => return Error.InvalidEncoding,
|
|
}
|
|
}
|
|
|
|
/// Options for hash verification.
|
|
///
|
|
/// Allocator is required for argon2.
|
|
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,
|
|
io: Io,
|
|
) Error!void {
|
|
const allocator = options.allocator orelse return Error.AllocatorRequired;
|
|
return PhcFormatHasher.verify(allocator, str, password, io);
|
|
}
|
|
|
|
test "argon2d" {
|
|
const password = [_]u8{0x01} ** 32;
|
|
const salt = [_]u8{0x02} ** 16;
|
|
const secret = [_]u8{0x03} ** 8;
|
|
const ad = [_]u8{0x04} ** 12;
|
|
|
|
var dk: [32]u8 = undefined;
|
|
try kdf(
|
|
std.testing.allocator,
|
|
&dk,
|
|
&password,
|
|
&salt,
|
|
.{ .t = 3, .m = 32, .p = 4, .secret = &secret, .ad = &ad },
|
|
.argon2d,
|
|
std.testing.io,
|
|
);
|
|
|
|
const want = [_]u8{
|
|
0x51, 0x2b, 0x39, 0x1b, 0x6f, 0x11, 0x62, 0x97,
|
|
0x53, 0x71, 0xd3, 0x09, 0x19, 0x73, 0x42, 0x94,
|
|
0xf8, 0x68, 0xe3, 0xbe, 0x39, 0x84, 0xf3, 0xc1,
|
|
0xa1, 0x3a, 0x4d, 0xb9, 0xfa, 0xbe, 0x4a, 0xcb,
|
|
};
|
|
try std.testing.expectEqualSlices(u8, &dk, &want);
|
|
}
|
|
|
|
test "argon2i" {
|
|
const password = [_]u8{0x01} ** 32;
|
|
const salt = [_]u8{0x02} ** 16;
|
|
const secret = [_]u8{0x03} ** 8;
|
|
const ad = [_]u8{0x04} ** 12;
|
|
|
|
var dk: [32]u8 = undefined;
|
|
try kdf(
|
|
std.testing.allocator,
|
|
&dk,
|
|
&password,
|
|
&salt,
|
|
.{ .t = 3, .m = 32, .p = 4, .secret = &secret, .ad = &ad },
|
|
.argon2i,
|
|
std.testing.io,
|
|
);
|
|
|
|
const want = [_]u8{
|
|
0xc8, 0x14, 0xd9, 0xd1, 0xdc, 0x7f, 0x37, 0xaa,
|
|
0x13, 0xf0, 0xd7, 0x7f, 0x24, 0x94, 0xbd, 0xa1,
|
|
0xc8, 0xde, 0x6b, 0x01, 0x6d, 0xd3, 0x88, 0xd2,
|
|
0x99, 0x52, 0xa4, 0xc4, 0x67, 0x2b, 0x6c, 0xe8,
|
|
};
|
|
try std.testing.expectEqualSlices(u8, &dk, &want);
|
|
}
|
|
|
|
test "argon2id" {
|
|
const password = [_]u8{0x01} ** 32;
|
|
const salt = [_]u8{0x02} ** 16;
|
|
const secret = [_]u8{0x03} ** 8;
|
|
const ad = [_]u8{0x04} ** 12;
|
|
|
|
var dk: [32]u8 = undefined;
|
|
try kdf(
|
|
std.testing.allocator,
|
|
&dk,
|
|
&password,
|
|
&salt,
|
|
.{ .t = 3, .m = 32, .p = 4, .secret = &secret, .ad = &ad },
|
|
.argon2id,
|
|
std.testing.io,
|
|
);
|
|
|
|
const want = [_]u8{
|
|
0x0d, 0x64, 0x0d, 0xf5, 0x8d, 0x78, 0x76, 0x6c,
|
|
0x08, 0xc0, 0x37, 0xa3, 0x4a, 0x8b, 0x53, 0xc9,
|
|
0xd0, 0x1e, 0xf0, 0x45, 0x2d, 0x75, 0xb6, 0x5e,
|
|
0xb5, 0x25, 0x20, 0xe9, 0x6b, 0x01, 0xe6, 0x59,
|
|
};
|
|
try std.testing.expectEqualSlices(u8, &dk, &want);
|
|
}
|
|
|
|
test "kdf" {
|
|
const password = "password";
|
|
const salt = "somesalt";
|
|
|
|
const TestVector = struct {
|
|
mode: Mode,
|
|
time: u32,
|
|
memory: u32,
|
|
threads: u8,
|
|
hash: []const u8,
|
|
};
|
|
const test_vectors = [_]TestVector{
|
|
.{
|
|
.mode = .argon2i,
|
|
.time = 1,
|
|
.memory = 64,
|
|
.threads = 1,
|
|
.hash = "b9c401d1844a67d50eae3967dc28870b22e508092e861a37",
|
|
},
|
|
.{
|
|
.mode = .argon2d,
|
|
.time = 1,
|
|
.memory = 64,
|
|
.threads = 1,
|
|
.hash = "8727405fd07c32c78d64f547f24150d3f2e703a89f981a19",
|
|
},
|
|
.{
|
|
.mode = .argon2id,
|
|
.time = 1,
|
|
.memory = 64,
|
|
.threads = 1,
|
|
.hash = "655ad15eac652dc59f7170a7332bf49b8469be1fdb9c28bb",
|
|
},
|
|
.{
|
|
.mode = .argon2i,
|
|
.time = 2,
|
|
.memory = 64,
|
|
.threads = 1,
|
|
.hash = "8cf3d8f76a6617afe35fac48eb0b7433a9a670ca4a07ed64",
|
|
},
|
|
.{
|
|
.mode = .argon2d,
|
|
.time = 2,
|
|
.memory = 64,
|
|
.threads = 1,
|
|
.hash = "3be9ec79a69b75d3752acb59a1fbb8b295a46529c48fbb75",
|
|
},
|
|
.{
|
|
.mode = .argon2id,
|
|
.time = 2,
|
|
.memory = 64,
|
|
.threads = 1,
|
|
.hash = "068d62b26455936aa6ebe60060b0a65870dbfa3ddf8d41f7",
|
|
},
|
|
.{
|
|
.mode = .argon2i,
|
|
.time = 2,
|
|
.memory = 64,
|
|
.threads = 2,
|
|
.hash = "2089f3e78a799720f80af806553128f29b132cafe40d059f",
|
|
},
|
|
.{
|
|
.mode = .argon2d,
|
|
.time = 2,
|
|
.memory = 64,
|
|
.threads = 2,
|
|
.hash = "68e2462c98b8bc6bb60ec68db418ae2c9ed24fc6748a40e9",
|
|
},
|
|
.{
|
|
.mode = .argon2id,
|
|
.time = 2,
|
|
.memory = 64,
|
|
.threads = 2,
|
|
.hash = "350ac37222f436ccb5c0972f1ebd3bf6b958bf2071841362",
|
|
},
|
|
.{
|
|
.mode = .argon2i,
|
|
.time = 3,
|
|
.memory = 256,
|
|
.threads = 2,
|
|
.hash = "f5bbf5d4c3836af13193053155b73ec7476a6a2eb93fd5e6",
|
|
},
|
|
.{
|
|
.mode = .argon2d,
|
|
.time = 3,
|
|
.memory = 256,
|
|
.threads = 2,
|
|
.hash = "f4f0669218eaf3641f39cc97efb915721102f4b128211ef2",
|
|
},
|
|
.{
|
|
.mode = .argon2id,
|
|
.time = 3,
|
|
.memory = 256,
|
|
.threads = 2,
|
|
.hash = "4668d30ac4187e6878eedeacf0fd83c5a0a30db2cc16ef0b",
|
|
},
|
|
.{
|
|
.mode = .argon2i,
|
|
.time = 4,
|
|
.memory = 256,
|
|
.threads = 4,
|
|
.hash = "f7dbbacbf16999e3700817a7e06f65a8db2e9fa9504ede4c",
|
|
},
|
|
.{
|
|
.mode = .argon2d,
|
|
.time = 4,
|
|
.memory = 256,
|
|
.threads = 4,
|
|
.hash = "ea2970501cf49faa5ba1d2e6370204e9b57ca90a8fea937b",
|
|
},
|
|
.{
|
|
.mode = .argon2id,
|
|
.time = 4,
|
|
.memory = 256,
|
|
.threads = 4,
|
|
.hash = "fbd40d5a8cb92f88c20bda4b3cdb1f9d5af1efa937032410",
|
|
},
|
|
.{
|
|
.mode = .argon2i,
|
|
.time = 4,
|
|
.memory = 256,
|
|
.threads = 8,
|
|
.hash = "15d3c398364e53f68fd12d19baf3f21432d964254fe27467",
|
|
},
|
|
.{
|
|
.mode = .argon2d,
|
|
.time = 4,
|
|
.memory = 256,
|
|
.threads = 8,
|
|
.hash = "23c9adc06f06e21e4612c1466a1be02627690932b02c0df0",
|
|
},
|
|
.{
|
|
.mode = .argon2id,
|
|
.time = 4,
|
|
.memory = 256,
|
|
.threads = 8,
|
|
.hash = "f22802f8ca47be93f9954e4ce20c1e944e938fbd4a125d9d",
|
|
},
|
|
.{
|
|
.mode = .argon2i,
|
|
.time = 2,
|
|
.memory = 64,
|
|
.threads = 3,
|
|
.hash = "5cab452fe6b8479c8661def8cd703b611a3905a6d5477fe6",
|
|
},
|
|
.{
|
|
.mode = .argon2d,
|
|
.time = 2,
|
|
.memory = 64,
|
|
.threads = 3,
|
|
.hash = "22474a423bda2ccd36ec9afd5119e5c8949798cadf659f51",
|
|
},
|
|
.{
|
|
.mode = .argon2id,
|
|
.time = 2,
|
|
.memory = 64,
|
|
.threads = 3,
|
|
.hash = "4a15b31aec7c2590b87d1f520be7d96f56658172deaa3079",
|
|
},
|
|
.{
|
|
.mode = .argon2i,
|
|
.time = 3,
|
|
.memory = 256,
|
|
.threads = 6,
|
|
.hash = "ebc8f91964abd8ceab49a12963b0a9e57d635bfa2aad2884",
|
|
},
|
|
.{
|
|
.mode = .argon2d,
|
|
.time = 3,
|
|
.memory = 256,
|
|
.threads = 6,
|
|
.hash = "1dd7202fd68da6675f769f4034b7a1db30d8785331954117",
|
|
},
|
|
.{
|
|
.mode = .argon2id,
|
|
.time = 3,
|
|
.memory = 256,
|
|
.threads = 6,
|
|
.hash = "424436b6ee22a66b04b9d0cf78f190305c5c166bae8baa09",
|
|
},
|
|
};
|
|
for (test_vectors) |v| {
|
|
var want: [24]u8 = undefined;
|
|
_ = try std.fmt.hexToBytes(&want, v.hash);
|
|
|
|
var dk: [24]u8 = undefined;
|
|
try kdf(
|
|
std.testing.allocator,
|
|
&dk,
|
|
password,
|
|
salt,
|
|
.{ .t = v.time, .m = v.memory, .p = v.threads },
|
|
v.mode,
|
|
std.testing.io,
|
|
);
|
|
|
|
try std.testing.expectEqualSlices(u8, &dk, &want);
|
|
}
|
|
}
|
|
|
|
test "phc format hasher" {
|
|
const allocator = std.testing.allocator;
|
|
const password = "testpass";
|
|
const io = std.testing.io;
|
|
|
|
var buf: [128]u8 = undefined;
|
|
const hash = try PhcFormatHasher.create(
|
|
allocator,
|
|
password,
|
|
.{ .t = 3, .m = 32, .p = 4 },
|
|
.argon2id,
|
|
&buf,
|
|
io,
|
|
);
|
|
try PhcFormatHasher.verify(allocator, hash, password, io);
|
|
}
|
|
|
|
test "password hash and password verify" {
|
|
const allocator = std.testing.allocator;
|
|
const password = "testpass";
|
|
const io = std.testing.io;
|
|
|
|
var buf: [128]u8 = undefined;
|
|
const hash = try strHash(
|
|
password,
|
|
.{ .allocator = allocator, .params = .{ .t = 3, .m = 32, .p = 4 } },
|
|
&buf,
|
|
io,
|
|
);
|
|
try strVerify(hash, password, .{ .allocator = allocator }, io);
|
|
}
|
|
|
|
test "kdf derived key length" {
|
|
const allocator = std.testing.allocator;
|
|
const io = std.testing.io;
|
|
|
|
const password = "testpass";
|
|
const salt = "saltsalt";
|
|
const params = Params{ .t = 3, .m = 32, .p = 4 };
|
|
const mode = Mode.argon2id;
|
|
|
|
var dk1: [11]u8 = undefined;
|
|
try kdf(allocator, &dk1, password, salt, params, mode, io);
|
|
|
|
var dk2: [77]u8 = undefined;
|
|
try kdf(allocator, &dk2, password, salt, params, mode, io);
|
|
|
|
var dk3: [111]u8 = undefined;
|
|
try kdf(allocator, &dk3, password, salt, params, mode, io);
|
|
}
|