Skip to content

pwhash: Simplify strHash/strVerify APIs #24112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 13 additions & 33 deletions lib/std/crypto/argon2.zig
Original file line number Diff line number Diff line change
Expand Up @@ -578,51 +578,30 @@ const PhcFormatHasher = struct {
};

/// 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,
strhash_max_bytes: usize = 128,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

};

/// 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(
allocator: mem.Allocator,
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,
options.mode,
out,
),
.crypt => return Error.InvalidEncoding,
}
comptime options: HashOptions,
) Error![]u8 {
var buf: [options.strhash_max_bytes]u8 = undefined;
const out = try PhcFormatHasher.create(allocator, password, options.params, options.mode, &buf);
return allocator.dupe(u8, out);
}

/// 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(
allocator: mem.Allocator,
str: []const u8,
password: []const u8,
options: VerifyOptions,
) Error!void {
const allocator = options.allocator orelse return Error.AllocatorRequired;
return PhcFormatHasher.verify(allocator, str, password);
}

Expand Down Expand Up @@ -919,13 +898,14 @@ test "password hash and password verify" {
const allocator = std.testing.allocator;
const password = "testpass";

var buf: [128]u8 = undefined;
const hash = try strHash(
allocator,
password,
.{ .allocator = allocator, .params = .{ .t = 3, .m = 32, .p = 4 } },
&buf,
.{ .params = .{ .t = 3, .m = 32, .p = 4 } },
);
try strVerify(hash, password, .{ .allocator = allocator });
defer allocator.free(hash);

try strVerify(allocator, hash, password);
}

test "kdf derived key length" {
Expand Down
82 changes: 50 additions & 32 deletions lib/std/crypto/bcrypt.zig
Original file line number Diff line number Diff line change
Expand Up @@ -748,12 +748,17 @@ const CryptFormatHasher = struct {

/// Options for hashing a password.
pub const HashOptions = struct {
/// For `bcrypt`, that can be left to `null`.
allocator: ?mem.Allocator = null,
/// Internal bcrypt parameters.
params: Params,
/// Encoding to use for the output of the hash function.
encoding: pwhash.Encoding,
/// The maximum length in bytes for a hashed password. If not
/// set by the developer, this varies based on encoding.
/// `crypt` encoding uses 60 bytes, and `phc` encoding uses
/// 120 bytes.
///
/// The returned hash may be smaller than the maximum length.
strhash_max_bytes: ?usize = null,
};

/// Compute a hash of a password using 2^rounds_log rounds of the bcrypt key stretching function.
Expand All @@ -764,20 +769,25 @@ pub const HashOptions = struct {
/// IMPORTANT: by design, bcrypt silently truncates passwords to 72 bytes.
/// If this is an issue for your application, set the `silently_truncate_password` option to `false`.
pub fn strHash(
allocator: mem.Allocator,
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),
}
comptime options: HashOptions,
) Error![]u8 {
const buf_len = comptime if (options.strhash_max_bytes) |len| len else switch (options.encoding) {
.crypt => hash_length,
.phc => hash_length * 2,
};
Comment on lines +776 to +779
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These default values were based on the comments for the hash_length constant and the tests. Please let me know if something else should be chosen.

var buf: [buf_len]u8 = undefined;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe provide strHash and strHashAlloc for bcrypt? We still use the stack based approach in std, but don't give the developer any choice if they know what they're doing


const out = switch (options.encoding) {
.phc => try PhcFormatHasher.create(password, options.params, &buf),
.crypt => try CryptFormatHasher.create(password, options.params, &buf),
};
return allocator.dupe(u8, out);
}

/// Options for hash verification.
pub const VerifyOptions = struct {
/// For `bcrypt`, that can be left to `null`.
allocator: ?mem.Allocator = null,
/// Whether to silently truncate the password to 72 bytes, or pre-hash the password when it is longer.
silently_truncate_password: bool,
};
Expand Down Expand Up @@ -806,14 +816,15 @@ test "bcrypt codec" {
}

test "bcrypt crypt format" {
var hash_options = HashOptions{
const allocator = std.testing.allocator;
const hash_options = HashOptions{
.params = .{ .rounds_log = 5, .silently_truncate_password = false },
.encoding = .crypt,
};
var verify_options = VerifyOptions{ .silently_truncate_password = false };
const verify_options = VerifyOptions{ .silently_truncate_password = false };

var buf: [hash_length]u8 = undefined;
const s = try strHash("password", hash_options, &buf);
const s = try strHash(allocator, "password", hash_options);
defer allocator.free(s);

try testing.expect(mem.startsWith(u8, s, crypt_format.prefix));
try strVerify(s, "password", verify_options);
Expand All @@ -822,8 +833,7 @@ test "bcrypt crypt format" {
strVerify(s, "invalid password", verify_options),
);

var long_buf: [hash_length]u8 = undefined;
var long_s = try strHash("password" ** 100, hash_options, &long_buf);
var long_s = try strHash(allocator, "password" ** 100, hash_options);

try testing.expect(mem.startsWith(u8, long_s, crypt_format.prefix));
try strVerify(long_s, "password" ** 100, verify_options);
Expand All @@ -832,28 +842,33 @@ test "bcrypt crypt format" {
strVerify(long_s, "password" ** 101, verify_options),
);

hash_options.params.silently_truncate_password = true;
verify_options.silently_truncate_password = true;
long_s = try strHash("password" ** 100, hash_options, &long_buf);
try strVerify(long_s, "password" ** 101, verify_options);
allocator.free(long_s);

long_s = try strHash(allocator, "password" ** 100, .{
.params = .{ .rounds_log = 5, .silently_truncate_password = true },
.encoding = .crypt,
});
defer allocator.free(long_s);
try strVerify(long_s, "password" ** 101, .{ .silently_truncate_password = true });

try strVerify(
"$2b$08$WUQKyBCaKpziCwUXHiMVvu40dYVjkTxtWJlftl0PpjY2BxWSvFIEe",
"The devil himself",
verify_options,
.{ .silently_truncate_password = true },
);
}

test "bcrypt phc format" {
var hash_options = HashOptions{
const allocator = std.testing.allocator;
const hash_options = HashOptions{
.params = .{ .rounds_log = 5, .silently_truncate_password = false },
.encoding = .phc,
};
var verify_options = VerifyOptions{ .silently_truncate_password = false };
const verify_options = VerifyOptions{ .silently_truncate_password = false };
const prefix = "$bcrypt$";

var buf: [hash_length * 2]u8 = undefined;
const s = try strHash("password", hash_options, &buf);
const s = try strHash(allocator, "password", hash_options);
defer allocator.free(s);

try testing.expect(mem.startsWith(u8, s, prefix));
try strVerify(s, "password", verify_options);
Expand All @@ -862,8 +877,7 @@ test "bcrypt phc format" {
strVerify(s, "invalid password", verify_options),
);

var long_buf: [hash_length * 2]u8 = undefined;
var long_s = try strHash("password" ** 100, hash_options, &long_buf);
var long_s = try strHash(allocator, "password" ** 100, hash_options);

try testing.expect(mem.startsWith(u8, long_s, prefix));
try strVerify(long_s, "password" ** 100, verify_options);
Expand All @@ -872,10 +886,14 @@ test "bcrypt phc format" {
strVerify(long_s, "password" ** 101, verify_options),
);

hash_options.params.silently_truncate_password = true;
verify_options.silently_truncate_password = true;
long_s = try strHash("password" ** 100, hash_options, &long_buf);
try strVerify(long_s, "password" ** 101, verify_options);
allocator.free(long_s);

long_s = try strHash(allocator, "password" ** 100, .{
.params = .{ .rounds_log = 5, .silently_truncate_password = true },
.encoding = .crypt,
});
defer allocator.free(long_s);
try strVerify(long_s, "password" ** 101, .{ .silently_truncate_password = true });

try strVerify(
"$bcrypt$r=5$2NopntlgE2lX3cTwr4qz8A$r3T7iKYQNnY4hAhGjk9RmuyvgrYJZwc",
Expand Down
81 changes: 63 additions & 18 deletions lib/std/crypto/benchmark.zig
Original file line number Diff line number Diff line change
Expand Up @@ -387,50 +387,95 @@ pub fn benchmarkAes8(comptime Aes: anytype, comptime count: comptime_int) !u64 {
}

const CryptoPwhash = struct {
ty: type,
params: *const anyopaque,
name: []const u8,
benchmark: fn (mem.Allocator, comptime comptime_int) anyerror!f64,
};
const bcrypt_params = crypto.pwhash.bcrypt.Params{ .rounds_log = 8, .silently_truncate_password = true };
const pwhashes = [_]CryptoPwhash{
.{
.ty = crypto.pwhash.bcrypt,
.params = &bcrypt_params,
.name = "bcrypt",
.benchmark = benchmarkBcryptPwhash,
},
.{
.ty = crypto.pwhash.scrypt,
.params = &crypto.pwhash.scrypt.Params.interactive,
.name = "scrypt",
.benchmark = benchmarkScryptPwhash,
},
.{
.ty = crypto.pwhash.argon2,
.params = &crypto.pwhash.argon2.Params.interactive_2id,
.name = "argon2",
.benchmark = benchmarkArgon2Pwhash,
},
};

fn benchmarkPwhash(
fn benchmarkBcryptPwhash(
allocator: mem.Allocator,
comptime count: comptime_int,
) !f64 {
const password = "testpass" ** 2;
const opts = crypto.pwhash.bcrypt.HashOptions{
.params = bcrypt_params,
.encoding = .phc,
.strhash_max_bytes = 256,
};

var timer = try Timer.start();
const start = timer.lap();
{
var i: usize = 0;
while (i < count) : (i += 1) {
_ = try crypto.pwhash.bcrypt.strHash(allocator, password, opts);
}
}
const end = timer.read();

const elapsed_s = @as(f64, @floatFromInt(end - start)) / time.ns_per_s;
const throughput = elapsed_s / count;

return throughput;
}

fn benchmarkScryptPwhash(
allocator: mem.Allocator,
comptime ty: anytype,
comptime params: *const anyopaque,
comptime count: comptime_int,
) !f64 {
const password = "testpass" ** 2;
const opts = ty.HashOptions{
.allocator = allocator,
.params = @as(*const ty.Params, @ptrCast(@alignCast(params))).*,
const opts = crypto.pwhash.scrypt.HashOptions{
.params = crypto.pwhash.scrypt.Params.interactive,
.encoding = .phc,
.strhash_max_bytes = 256,
};

var timer = try Timer.start();
const start = timer.lap();
{
var i: usize = 0;
while (i < count) : (i += 1) {
_ = try crypto.pwhash.scrypt.strHash(allocator, password, opts);
}
}
const end = timer.read();

const elapsed_s = @as(f64, @floatFromInt(end - start)) / time.ns_per_s;
const throughput = elapsed_s / count;

return throughput;
}

fn benchmarkArgon2Pwhash(
allocator: mem.Allocator,
comptime count: comptime_int,
) !f64 {
const password = "testpass" ** 2;
const opts = crypto.pwhash.argon2.HashOptions{
.params = crypto.pwhash.argon2.Params.interactive_2id,
.strhash_max_bytes = 256,
};
var buf: [256]u8 = undefined;

var timer = try Timer.start();
const start = timer.lap();
{
var i: usize = 0;
while (i < count) : (i += 1) {
_ = try ty.strHash(password, opts, &buf);
mem.doNotOptimizeAway(&buf);
_ = try crypto.pwhash.argon2.strHash(allocator, password, opts);
}
}
const end = timer.read();
Expand Down Expand Up @@ -563,7 +608,7 @@ pub fn main() !void {

inline for (pwhashes) |H| {
if (filter == null or std.mem.indexOf(u8, H.name, filter.?) != null) {
const throughput = try benchmarkPwhash(arena_allocator, H.ty, H.params, mode(64));
const throughput = try H.benchmark(arena_allocator, mode(64));
try stdout.print("{s:>17}: {d:10.3} s/ops\n", .{ H.name, throughput });
}
}
Expand Down
Loading