aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2023-06-04 18:20:04 -0700
committerGravatar GitHub <noreply@github.com> 2023-06-04 18:20:04 -0700
commit9b996e702ef32d03b01b745642292e7a747485fa (patch)
tree4b49c2010c694afe5cf6ef738402f80e0ab7c5b8
parent2cb1376a93a59acca548769155e0d3b6110a7bd2 (diff)
downloadbun-9b996e702ef32d03b01b745642292e7a747485fa.tar.gz
bun-9b996e702ef32d03b01b745642292e7a747485fa.tar.zst
bun-9b996e702ef32d03b01b745642292e7a747485fa.zip
Implement `Bun.password` and `Bun.passwordSync` (#3204)
* Implement `Bun.password.{verify, hash}` and `Bun.passwordSync.{verify, hash}` * flip the booleans * delete unused * Add `cost` for `"bcrypt"`, add `"memoryCost"` and `"timeCost'` for argon2, use SHA512 * Update bun.zig --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
-rw-r--r--packages/bun-types/bun.d.ts191
-rw-r--r--src/bun.js/api/bun.zig803
-rw-r--r--src/bun.js/bindings/ZigGlobalObject.cpp50
-rw-r--r--src/bun.js/bindings/ZigGlobalObject.h5
-rw-r--r--src/bun.js/node/types.zig12
-rw-r--r--src/bun.zig2
-rw-r--r--test/js/bun/util/password.test.ts279
7 files changed, 1339 insertions, 3 deletions
diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts
index 27b01433c..2396ffbd9 100644
--- a/packages/bun-types/bun.d.ts
+++ b/packages/bun-types/bun.d.ts
@@ -1012,6 +1012,197 @@ declare module "bun" {
// importSource?: string; // default: "react"
// };
}
+ namespace Password {
+ export type AlgorithmLabel = "bcrypt" | "argon2id" | "argon2d" | "argon2i";
+
+ export interface Argon2Algorithm {
+ algorithm: "argon2id" | "argon2d" | "argon2i";
+ /**
+ * Memory cost, which defines the memory usage, given in kibibytes.
+ */
+ memoryCost?: number;
+ /**
+ * Defines the amount of computation realized and therefore the execution
+ * time, given in number of iterations.
+ */
+ timeCost?: number;
+ }
+
+ export interface BCryptAlgorithm {
+ algorithm: "bcrypt";
+ /**
+ * A number between 4 and 31. The default is 10.
+ */
+ cost?: number;
+ }
+ }
+
+ /**
+ * Hash and verify passwords using argon2 or bcrypt. The default is argon2.
+ * Password hashing functions are necessarily slow, and this object will
+ * automatically run in a worker thread.
+ *
+ * The underlying implementation of these functions are provided by the Zig
+ * Standard Library. Thanks to @jedisct1 and other Zig constributors for their
+ * work on this.
+ *
+ * ### Example with argon2
+ *
+ * ```ts
+ * import {password} from "bun";
+ *
+ * const hash = await password.hash("hello world");
+ * const verify = await password.verify("hello world", hash);
+ * console.log(verify); // true
+ * ```
+ *
+ * ### Example with bcrypt
+ * ```ts
+ * import {password} from "bun";
+ *
+ * const hash = await password.hash("hello world", "bcrypt");
+ * // algorithm is optional, will be inferred from the hash if not specified
+ * const verify = await password.verify("hello world", hash, "bcrypt");
+ *
+ * console.log(verify); // true
+ * ```
+ */
+ export const password: {
+ /**
+ * Verify a password against a previously hashed password.
+ *
+ * @returns true if the password matches, false otherwise
+ *
+ * @example
+ * ```ts
+ * import {password} from "bun";
+ * await password.verify("hey", "$argon2id$v=19$m=65536,t=2,p=1$ddbcyBcbAcagei7wSkZFiouX6TqnUQHmTyS5mxGCzeM$+3OIaFatZ3n6LtMhUlfWbgJyNp7h8/oIsLK+LzZO+WI");
+ * // true
+ * ```
+ *
+ * @throws If the algorithm is specified and does not match the hash
+ * @throws If the algorithm is invalid
+ * @throws if the hash is invalid
+ *
+ */
+ verify(
+ /**
+ * The password to verify.
+ *
+ * If empty, always returns false
+ */
+ password: StringOrBuffer,
+ /**
+ * Previously hashed password.
+ * If empty, always returns false
+ */
+ hash: StringOrBuffer,
+ /**
+ * If not specified, the algorithm will be inferred from the hash.
+ *
+ * If specified and the algorithm does not match the hash, this function
+ * throws an error.
+ */
+ algorithm?: Password.AlgorithmLabel,
+ ): Promise<boolean>;
+ /**
+ * Asynchronously hash a password using argon2 or bcrypt. The default is argon2.
+ *
+ * @returns A promise that resolves to the hashed password
+ *
+ * ## Example with argon2
+ * ```ts
+ * import {password} from "bun";
+ * const hash = await password.hash("hello world");
+ * console.log(hash); // $argon2id$v=1...
+ * const verify = await password.verify("hello world", hash);
+ * ```
+ * ## Example with bcrypt
+ * ```ts
+ * import {password} from "bun";
+ * const hash = await password.hash("hello world", "bcrypt");
+ * console.log(hash); // $2b$10$...
+ * const verify = await password.verify("hello world", hash);
+ * ```
+ */
+ hash(
+ /**
+ * The password to hash
+ *
+ * If empty, this function throws an error. It is usually a programming
+ * mistake to hash an empty password.
+ */
+ password: StringOrBuffer,
+ /**
+ * @default "argon2id"
+ *
+ * When using bcrypt, passwords exceeding 72 characters will be SHA512'd before
+ */
+ algorithm?:
+ | Password.AlgorithmLabel
+ | Password.Argon2Algorithm
+ | Password.BCryptAlgorithm,
+ ): Promise<string>;
+ };
+
+ /**
+ * Synchronously hash and verify passwords using argon2 or bcrypt. The default is argon2.
+ * Warning: password hashing is slow, consider using {@link Bun.password}
+ * instead which runs in a worker thread.
+ *
+ * The underlying implementation of these functions are provided by the Zig
+ * Standard Library. Thanks to @jedisct1 and other Zig constributors for their
+ * work on this.
+ *
+ * ### Example with argon2
+ *
+ * ```ts
+ * import {password} from "bun";
+ *
+ * const hash = await password.hash("hello world");
+ * const verify = await password.verify("hello world", hash);
+ * console.log(verify); // true
+ * ```
+ *
+ * ### Example with bcrypt
+ * ```ts
+ * import {password} from "bun";
+ *
+ * const hash = await password.hash("hello world", "bcrypt");
+ * // algorithm is optional, will be inferred from the hash if not specified
+ * const verify = await password.verify("hello world", hash, "bcrypt");
+ *
+ * console.log(verify); // true
+ * ```
+ */
+ export const passwordSync: {
+ verify(
+ password: StringOrBuffer,
+ hash: StringOrBuffer,
+ /**
+ * If not specified, the algorithm will be inferred from the hash.
+ */
+ algorithm?: Password.AlgorithmLabel,
+ ): boolean;
+ hash(
+ /**
+ * The password to hash
+ *
+ * If empty, this function throws an error. It is usually a programming
+ * mistake to hash an empty password.
+ */
+ password: StringOrBuffer,
+ /**
+ * @default "argon2id"
+ *
+ * When using bcrypt, passwords exceeding 72 characters will be SHA256'd before
+ */
+ algorithm?:
+ | Password.AlgorithmLabel
+ | Password.Argon2Algorithm
+ | Password.BCryptAlgorithm,
+ ): string;
+ };
interface BuildArtifact extends Blob {
path: string;
diff --git a/src/bun.js/api/bun.zig b/src/bun.js/api/bun.zig
index 3e09075e7..78a7379c2 100644
--- a/src/bun.js/api/bun.zig
+++ b/src/bun.js/api/bun.zig
@@ -1632,6 +1632,803 @@ pub const Crypto = struct {
return ZigString.fromUTF8(error_message).toErrorInstance(globalThis);
}
+ const unknwon_password_algorithm_message = "unknown algorithm, expected one of: \"bcrypt\", \"argon2id\", \"argon2d\", \"argon2i\" (default is \"argon2id\")";
+
+ pub const PasswordObject = struct {
+ pub const pwhash = std.crypto.pwhash;
+ pub const Algorithm = enum {
+ argon2i,
+ argon2d,
+ argon2id,
+ bcrypt,
+
+ pub const Value = union(Algorithm) {
+ argon2i: Argon2Params,
+ argon2d: Argon2Params,
+ argon2id: Argon2Params,
+ // bcrypt only accepts "cost"
+ bcrypt: u6,
+
+ pub const bcrpyt_default = 10;
+
+ pub const default = Algorithm.Value{
+ .argon2id = .{},
+ };
+
+ pub fn fromJS(globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) ?Value {
+ if (value.isObject()) {
+ if (value.getTruthy(globalObject, "algorithm")) |algorithm_value| {
+ if (!algorithm_value.isString()) {
+ globalObject.throwInvalidArgumentType("hash", "algorithm", "string");
+ return null;
+ }
+
+ const algorithm_string = algorithm_value.getZigString(globalObject);
+
+ switch (PasswordObject.Algorithm.label.getWithEql(algorithm_string, JSC.ZigString.eqlComptime) orelse {
+ globalObject.throwInvalidArgumentType("hash", "algorithm", unknwon_password_algorithm_message);
+ return null;
+ }) {
+ .bcrypt => {
+ var algorithm = PasswordObject.Algorithm.Value{
+ .bcrypt = PasswordObject.Algorithm.Value.bcrpyt_default,
+ };
+
+ if (value.getTruthy(globalObject, "cost")) |rounds_value| {
+ if (!rounds_value.isNumber()) {
+ globalObject.throwInvalidArgumentType("hash", "cost", "number");
+ return null;
+ }
+
+ const rounds = rounds_value.coerce(i32, globalObject);
+
+ if (rounds < 4 or rounds > 31) {
+ globalObject.throwInvalidArguments("Rounds must be between 4 and 31", .{});
+ return null;
+ }
+
+ algorithm.bcrypt = @intCast(u6, rounds);
+ }
+
+ return algorithm;
+ },
+ inline .argon2id, .argon2d, .argon2i => |tag| {
+ var argon = Algorithm.Argon2Params{};
+
+ if (value.getTruthy(globalObject, "timeCost")) |time_value| {
+ if (!time_value.isNumber()) {
+ globalObject.throwInvalidArgumentType("hash", "timeCost", "number");
+ return null;
+ }
+
+ const time_cost = time_value.coerce(i32, globalObject);
+
+ if (time_cost < 1) {
+ globalObject.throwInvalidArguments("Time cost must be greater than 0", .{});
+ return null;
+ }
+
+ argon.time_cost = @intCast(u32, time_cost);
+ }
+
+ if (value.getTruthy(globalObject, "memoryCost")) |memory_value| {
+ if (!memory_value.isNumber()) {
+ globalObject.throwInvalidArgumentType("hash", "memoryCost", "number");
+ return null;
+ }
+
+ const memory_cost = memory_value.coerce(i32, globalObject);
+
+ if (memory_cost < 1) {
+ globalObject.throwInvalidArguments("Memory cost must be greater than 0", .{});
+ return null;
+ }
+
+ argon.memory_cost = @intCast(u32, memory_cost);
+ }
+
+ return @unionInit(Algorithm.Value, @tagName(tag), argon);
+ },
+ }
+
+ unreachable;
+ } else {
+ globalObject.throwInvalidArgumentType("hash", "options.algorithm", "string");
+ return null;
+ }
+ } else if (value.isString()) {
+ const algorithm_string = value.getZigString(globalObject);
+
+ switch (PasswordObject.Algorithm.label.getWithEql(algorithm_string, JSC.ZigString.eqlComptime) orelse {
+ globalObject.throwInvalidArgumentType("hash", "algorithm", unknwon_password_algorithm_message);
+ return null;
+ }) {
+ .bcrypt => {
+ return PasswordObject.Algorithm.Value{
+ .bcrypt = PasswordObject.Algorithm.Value.bcrpyt_default,
+ };
+ },
+ .argon2id => {
+ return PasswordObject.Algorithm.Value{
+ .argon2id = .{},
+ };
+ },
+ .argon2d => {
+ return PasswordObject.Algorithm.Value{
+ .argon2d = .{},
+ };
+ },
+ .argon2i => {
+ return PasswordObject.Algorithm.Value{
+ .argon2i = .{},
+ };
+ },
+ }
+ } else {
+ globalObject.throwInvalidArgumentType("hash", "algorithm", "string");
+ return null;
+ }
+
+ unreachable;
+ }
+ };
+
+ pub const Argon2Params = struct {
+ // we don't support the other options right now, but can add them later if someone asks
+ memory_cost: u32 = pwhash.argon2.Params.interactive_2id.m,
+ time_cost: u32 = pwhash.argon2.Params.interactive_2id.t,
+
+ pub fn toParams(this: Argon2Params) pwhash.argon2.Params {
+ return pwhash.argon2.Params{
+ .t = this.time_cost,
+ .m = this.memory_cost,
+ .p = 1,
+ };
+ }
+ };
+
+ pub const argon2 = Algorithm.argon2id;
+
+ pub const label = bun.ComptimeStringMap(
+ Algorithm,
+ .{
+ .{ "argon2i", .argon2i },
+ .{ "argon2d", .argon2d },
+ .{ "argon2id", .argon2id },
+ .{ "bcrypt", .bcrypt },
+ },
+ );
+
+ pub const default = Algorithm.argon2;
+
+ pub fn get(pw: []const u8) ?Algorithm {
+ if (pw[0] != '$') {
+ return null;
+ }
+
+ // PHC format looks like $<algorithm>$<params>$<salt>$<hash><optional stuff>
+ if (strings.hasPrefixComptime(pw[1..], "argon2d$")) {
+ return .argon2d;
+ }
+ if (strings.hasPrefixComptime(pw[1..], "argon2i$")) {
+ return .argon2i;
+ }
+ if (strings.hasPrefixComptime(pw[1..], "argon2id$")) {
+ return .argon2id;
+ }
+
+ if (strings.hasPrefixComptime(pw[1..], "bcrypt")) {
+ return .bcrypt;
+ }
+
+ // https://en.wikipedia.org/wiki/Crypt_(C)
+ if (strings.hasPrefixComptime(pw[1..], "2")) {
+ return .bcrypt;
+ }
+
+ return null;
+ }
+ };
+
+ pub const HashError = pwhash.Error || error{UnsupportedAlgorithm};
+
+ // This is purposely simple because nobody asked to make it more complicated
+ pub fn hash(
+ allocator: std.mem.Allocator,
+ password: []const u8,
+ algorithm: Algorithm.Value,
+ ) HashError![]const u8 {
+ switch (algorithm) {
+ inline .argon2i, .argon2d, .argon2id => |argon| {
+ var outbuf: [4096]u8 = undefined;
+ const hash_options = pwhash.argon2.HashOptions{
+ .params = argon.toParams(),
+ .allocator = allocator,
+ .mode = switch (algorithm) {
+ .argon2i => .argon2i,
+ .argon2d => .argon2d,
+ .argon2id => .argon2id,
+ else => unreachable,
+ },
+ .encoding = .phc,
+ };
+ // warning: argon2's code may spin up threads if paralellism is set to > 0
+ // we don't expose this option
+ // but since it parses from phc format, it's possible that it will be set
+ // eventually we should do something that about that.
+ const out_bytes = try pwhash.argon2.strHash(password, hash_options, &outbuf);
+ return try allocator.dupe(u8, out_bytes);
+ },
+ .bcrypt => |cost| {
+ var outbuf: [4096]u8 = undefined;
+ var outbuf_slice: []u8 = outbuf[0..];
+ var password_to_use = password;
+ // bcrypt silently truncates passwords longer than 72 bytes
+ // we use SHA512 to hash the password if it's longer than 72 bytes
+ if (password.len > 72) {
+ var sha_256 = bun.sha.SHA512.init();
+ sha_256.update(password);
+ sha_256.final(outbuf[0..bun.sha.SHA512.digest]);
+ password_to_use = outbuf[0..bun.sha.SHA512.digest];
+ outbuf_slice = outbuf[bun.sha.SHA512.digest..];
+ }
+
+ const hash_options = pwhash.bcrypt.HashOptions{
+ .params = pwhash.bcrypt.Params{ .rounds_log = cost },
+ .allocator = allocator,
+ .encoding = .crypt,
+ };
+ const out_bytes = try pwhash.bcrypt.strHash(password_to_use, hash_options, outbuf_slice);
+ return try allocator.dupe(u8, out_bytes);
+ },
+ }
+ }
+
+ pub fn verify(
+ allocator: std.mem.Allocator,
+ password: []const u8,
+ previous_hash: []const u8,
+ algorithm: ?Algorithm,
+ ) HashError!bool {
+ if (previous_hash.len == 0) {
+ return false;
+ }
+
+ return verifyWithAlgorithm(
+ allocator,
+ password,
+ previous_hash,
+ algorithm orelse Algorithm.get(previous_hash) orelse return error.UnsupportedAlgorithm,
+ );
+ }
+
+ pub fn verifyWithAlgorithm(
+ allocator: std.mem.Allocator,
+ password: []const u8,
+ previous_hash: []const u8,
+ algorithm: Algorithm,
+ ) HashError!bool {
+ switch (algorithm) {
+ .argon2id, .argon2d, .argon2i => {
+ pwhash.argon2.strVerify(previous_hash, password, .{ .allocator = allocator }) catch |err| {
+ if (err == error.PasswordVerificationFailed) {
+ return false;
+ }
+
+ return err;
+ };
+ return true;
+ },
+ .bcrypt => {
+ pwhash.bcrypt.strVerify(previous_hash, password, .{ .allocator = allocator }) catch |err| {
+ if (err == error.PasswordVerificationFailed) {
+ return false;
+ }
+
+ return err;
+ };
+ return true;
+ },
+ }
+ }
+ };
+
+ pub const JSPasswordObject = struct {
+ const PascalToUpperUnderscoreCaseFormatter = struct {
+ input: []const u8,
+ pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
+ for (self.input) |c| {
+ if (std.ascii.isUpper(c)) {
+ try writer.writeByte('_');
+ try writer.writeByte(c);
+ } else if (std.ascii.isLower(c)) {
+ try writer.writeByte(std.ascii.toUpper(c));
+ } else {
+ try writer.writeByte(c);
+ }
+ }
+ }
+ };
+
+ pub export fn JSPasswordObject__create(globalObject: *JSC.JSGlobalObject, sync: bool) JSC.JSValue {
+ var object = JSValue.createEmptyObject(globalObject, 2);
+ object.put(
+ globalObject,
+ ZigString.static("hash"),
+ if (!sync)
+ JSC.NewFunction(globalObject, ZigString.static("hash"), 2, JSPasswordObject__hash, false)
+ else
+ JSC.NewFunction(globalObject, ZigString.static("hash"), 2, JSPasswordObject__hashSync, false),
+ );
+ object.put(
+ globalObject,
+ ZigString.static("verify"),
+ if (!sync)
+ JSC.NewFunction(globalObject, ZigString.static("verify"), 2, JSPasswordObject__verify, false)
+ else
+ JSC.NewFunction(globalObject, ZigString.static("verify"), 2, JSPasswordObject__verifySync, false),
+ );
+ return object;
+ }
+
+ const HashJob = struct {
+ algorithm: PasswordObject.Algorithm.Value,
+ password: []const u8,
+ promise: JSC.JSPromise.Strong,
+ event_loop: *JSC.EventLoop,
+ global: *JSC.JSGlobalObject,
+ ref: JSC.PollRef = .{},
+ task: JSC.WorkPoolTask = .{ .callback = &run },
+
+ pub const Result = struct {
+ value: Value,
+ ref: JSC.PollRef = .{},
+
+ task: JSC.AnyTask = undefined,
+ promise: JSC.JSPromise.Strong,
+ global: *JSC.JSGlobalObject,
+
+ pub const Value = union(enum) {
+ err: PasswordObject.HashError,
+ hash: []const u8,
+
+ pub fn toErrorInstance(this: Value, globalObject: *JSC.JSGlobalObject) JSC.JSValue {
+ var error_code = std.fmt.allocPrint(bun.default_allocator, "PASSWORD_{}", .{PascalToUpperUnderscoreCaseFormatter{ .input = @errorName(this.err) }}) catch @panic("out of memory");
+ defer bun.default_allocator.free(error_code);
+ const instance = globalObject.createErrorInstance("Password hashing failed with error \"{s}\"", .{@errorName(this.err)});
+ instance.put(globalObject, ZigString.static("code"), JSC.ZigString.init(error_code).toValueGC(globalObject));
+ return instance;
+ }
+ };
+
+ pub fn runFromJS(this: *Result) void {
+ var promise = this.promise;
+ this.promise = .{};
+ this.ref.unref(this.global.bunVM());
+ var global = this.global;
+ switch (this.value) {
+ .err => {
+ const error_instance = this.value.toErrorInstance(global);
+ bun.default_allocator.destroy(this);
+ promise.reject(global, error_instance);
+ },
+ .hash => |value| {
+ const js_string = JSC.ZigString.init(value).toValueGC(global);
+ bun.default_allocator.destroy(this);
+ promise.resolve(global, js_string);
+ },
+ }
+ }
+ };
+
+ pub fn deinit(this: *HashJob) void {
+ this.ref = .{};
+ this.promise.strong.deinit();
+ bun.default_allocator.free(this.password);
+ bun.default_allocator.destroy(this);
+ }
+
+ pub fn getValue(password: []const u8, algorithm: PasswordObject.Algorithm.Value) Result.Value {
+ const value = PasswordObject.hash(bun.default_allocator, password, algorithm) catch |err| {
+ return Result.Value{ .err = err };
+ };
+ return Result.Value{ .hash = value };
+ }
+
+ pub fn run(task: *bun.ThreadPool.Task) void {
+ var this = @fieldParentPtr(HashJob, "task", task);
+
+ var result = bun.default_allocator.create(Result) catch @panic("out of memory");
+ result.* = Result{
+ .value = getValue(this.password, this.algorithm),
+ .task = JSC.AnyTask.New(Result, Result.runFromJS).init(result),
+ .promise = this.promise,
+ .global = this.global,
+ .ref = this.ref,
+ };
+ this.ref = .{};
+ this.promise.strong = .{};
+
+ var concurrent_task = bun.default_allocator.create(JSC.ConcurrentTask) catch @panic("out of memory");
+ concurrent_task.* = JSC.ConcurrentTask{
+ .task = JSC.Task.init(&result.task),
+ .auto_delete = true,
+ };
+ this.event_loop.enqueueTaskConcurrent(concurrent_task);
+ this.deinit();
+ }
+ };
+ pub fn hash(
+ globalObject: *JSC.JSGlobalObject,
+ password: []const u8,
+ algorithm: PasswordObject.Algorithm.Value,
+ comptime sync: bool,
+ ) JSC.JSValue {
+ std.debug.assert(password.len > 0); // caller must check
+
+ if (comptime sync) {
+ const value = HashJob.getValue(password, algorithm);
+ switch (value) {
+ .err => {
+ const error_instance = value.toErrorInstance(globalObject);
+ globalObject.throwValue(error_instance);
+ },
+ .hash => |h| {
+ return JSC.ZigString.init(h).toValueGC(globalObject);
+ },
+ }
+
+ unreachable;
+ }
+
+ var job = bun.default_allocator.create(HashJob) catch @panic("out of memory");
+ var promise = JSC.JSPromise.Strong.init(globalObject);
+
+ job.* = HashJob{
+ .algorithm = algorithm,
+ .password = password,
+ .promise = promise,
+ .event_loop = globalObject.bunVM().eventLoop(),
+ .global = globalObject,
+ };
+
+ job.ref.ref(globalObject.bunVM());
+ JSC.WorkPool.schedule(&job.task);
+
+ return promise.value();
+ }
+
+ pub fn verify(
+ globalObject: *JSC.JSGlobalObject,
+ password: []const u8,
+ prev_hash: []const u8,
+ algorithm: ?PasswordObject.Algorithm,
+ comptime sync: bool,
+ ) JSC.JSValue {
+ std.debug.assert(password.len > 0); // caller must check
+
+ if (comptime sync) {
+ const value = VerifyJob.getValue(password, prev_hash, algorithm);
+ switch (value) {
+ .err => {
+ const error_instance = value.toErrorInstance(globalObject);
+ globalObject.throwValue(error_instance);
+ return JSC.JSValue.undefined;
+ },
+ .pass => |pass| {
+ return JSC.JSValue.jsBoolean(pass);
+ },
+ }
+
+ unreachable;
+ }
+
+ var job = bun.default_allocator.create(VerifyJob) catch @panic("out of memory");
+ var promise = JSC.JSPromise.Strong.init(globalObject);
+
+ job.* = VerifyJob{
+ .algorithm = algorithm,
+ .password = password,
+ .prev_hash = prev_hash,
+ .promise = promise,
+ .event_loop = globalObject.bunVM().eventLoop(),
+ .global = globalObject,
+ };
+
+ job.ref.ref(globalObject.bunVM());
+ JSC.WorkPool.schedule(&job.task);
+
+ return promise.value();
+ }
+
+ // Once we have bindings generator, this should be replaced with a generated function
+ pub export fn JSPasswordObject__hash(
+ globalObject: *JSC.JSGlobalObject,
+ callframe: *JSC.CallFrame,
+ ) callconv(.C) JSC.JSValue {
+ const arguments_ = callframe.arguments(2);
+ const arguments = arguments_.ptr[0..arguments_.len];
+
+ if (arguments.len < 1) {
+ globalObject.throwNotEnoughArguments("hash", 1, 0);
+ return JSC.JSValue.undefined;
+ }
+
+ var algorithm = PasswordObject.Algorithm.Value.default;
+
+ if (arguments.len > 1 and !arguments[1].isEmptyOrUndefinedOrNull()) {
+ algorithm = PasswordObject.Algorithm.Value.fromJS(globalObject, arguments[1]) orelse
+ return JSC.JSValue.undefined;
+ }
+
+ var string_or_buffer = JSC.Node.SliceOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[0]) orelse {
+ globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray");
+ return JSC.JSValue.undefined;
+ };
+
+ if (string_or_buffer.slice().len == 0) {
+ globalObject.throwInvalidArguments("password must not be empty", .{});
+ string_or_buffer.deinit();
+ return JSC.JSValue.undefined;
+ }
+
+ string_or_buffer.ensureCloned(bun.default_allocator) catch {
+ globalObject.throwOutOfMemory();
+ return JSC.JSValue.undefined;
+ };
+
+ return hash(globalObject, string_or_buffer.slice(), algorithm, false);
+ }
+
+ // Once we have bindings generator, this should be replaced with a generated function
+ pub export fn JSPasswordObject__hashSync(
+ globalObject: *JSC.JSGlobalObject,
+ callframe: *JSC.CallFrame,
+ ) callconv(.C) JSC.JSValue {
+ const arguments_ = callframe.arguments(2);
+ const arguments = arguments_.ptr[0..arguments_.len];
+
+ if (arguments.len < 1) {
+ globalObject.throwNotEnoughArguments("hash", 1, 0);
+ return JSC.JSValue.undefined;
+ }
+
+ var algorithm = PasswordObject.Algorithm.Value.default;
+
+ if (arguments.len > 1 and !arguments[1].isEmptyOrUndefinedOrNull()) {
+ algorithm = PasswordObject.Algorithm.Value.fromJS(globalObject, arguments[1]) orelse
+ return JSC.JSValue.undefined;
+ }
+
+ var string_or_buffer = JSC.Node.SliceOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[0]) orelse {
+ globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray");
+ return JSC.JSValue.undefined;
+ };
+
+ if (string_or_buffer.slice().len == 0) {
+ globalObject.throwInvalidArguments("password must not be empty", .{});
+ string_or_buffer.deinit();
+ return JSC.JSValue.undefined;
+ }
+
+ string_or_buffer.ensureCloned(bun.default_allocator) catch {
+ globalObject.throwOutOfMemory();
+ return JSC.JSValue.undefined;
+ };
+ defer string_or_buffer.deinit();
+
+ return hash(globalObject, string_or_buffer.slice(), algorithm, true);
+ }
+
+ const VerifyJob = struct {
+ algorithm: ?PasswordObject.Algorithm = null,
+ password: []const u8,
+ prev_hash: []const u8,
+ promise: JSC.JSPromise.Strong,
+ event_loop: *JSC.EventLoop,
+ global: *JSC.JSGlobalObject,
+ ref: JSC.PollRef = .{},
+ task: JSC.WorkPoolTask = .{ .callback = &run },
+
+ pub const Result = struct {
+ value: Value,
+ ref: JSC.PollRef = .{},
+
+ task: JSC.AnyTask = undefined,
+ promise: JSC.JSPromise.Strong,
+ global: *JSC.JSGlobalObject,
+
+ pub const Value = union(enum) {
+ err: PasswordObject.HashError,
+ pass: bool,
+
+ pub fn toErrorInstance(this: Value, globalObject: *JSC.JSGlobalObject) JSC.JSValue {
+ var error_code = std.fmt.allocPrint(bun.default_allocator, "PASSWORD{}", .{PascalToUpperUnderscoreCaseFormatter{ .input = @errorName(this.err) }}) catch @panic("out of memory");
+ defer bun.default_allocator.free(error_code);
+ const instance = globalObject.createErrorInstance("Password verification failed with error \"{s}\"", .{@errorName(this.err)});
+ instance.put(globalObject, ZigString.static("code"), JSC.ZigString.init(error_code).toValueGC(globalObject));
+ return instance;
+ }
+ };
+
+ pub fn runFromJS(this: *Result) void {
+ var promise = this.promise;
+ this.promise = .{};
+ this.ref.unref(this.global.bunVM());
+ var global = this.global;
+ switch (this.value) {
+ .err => {
+ const error_instance = this.value.toErrorInstance(global);
+ bun.default_allocator.destroy(this);
+ promise.reject(global, error_instance);
+ },
+ .pass => |pass| {
+ bun.default_allocator.destroy(this);
+ promise.resolve(global, JSC.JSValue.jsBoolean(pass));
+ },
+ }
+ }
+ };
+
+ pub fn deinit(this: *VerifyJob) void {
+ this.ref = .{};
+ this.promise.strong.deinit();
+ bun.default_allocator.free(this.password);
+ bun.default_allocator.free(this.prev_hash);
+ bun.default_allocator.destroy(this);
+ }
+
+ pub fn getValue(password: []const u8, prev_hash: []const u8, algorithm: ?PasswordObject.Algorithm) Result.Value {
+ const pass = PasswordObject.verify(bun.default_allocator, password, prev_hash, algorithm) catch |err| {
+ return Result.Value{ .err = err };
+ };
+ return Result.Value{ .pass = pass };
+ }
+
+ pub fn run(task: *bun.ThreadPool.Task) void {
+ var this = @fieldParentPtr(VerifyJob, "task", task);
+
+ var result = bun.default_allocator.create(Result) catch @panic("out of memory");
+ result.* = Result{
+ .value = getValue(this.password, this.prev_hash, this.algorithm),
+ .task = JSC.AnyTask.New(Result, Result.runFromJS).init(result),
+ .promise = this.promise,
+ .global = this.global,
+ .ref = this.ref,
+ };
+ this.ref = .{};
+ this.promise.strong = .{};
+
+ var concurrent_task = bun.default_allocator.create(JSC.ConcurrentTask) catch @panic("out of memory");
+ concurrent_task.* = JSC.ConcurrentTask{
+ .task = JSC.Task.init(&result.task),
+ .auto_delete = true,
+ };
+ this.event_loop.enqueueTaskConcurrent(concurrent_task);
+ this.deinit();
+ }
+ };
+
+ // Once we have bindings generator, this should be replaced with a generated function
+ pub export fn JSPasswordObject__verify(
+ globalObject: *JSC.JSGlobalObject,
+ callframe: *JSC.CallFrame,
+ ) callconv(.C) JSC.JSValue {
+ const arguments_ = callframe.arguments(3);
+ const arguments = arguments_.ptr[0..arguments_.len];
+
+ if (arguments.len < 2) {
+ globalObject.throwNotEnoughArguments("verify", 2, 0);
+ return JSC.JSValue.undefined;
+ }
+
+ var algorithm: ?PasswordObject.Algorithm = null;
+
+ if (arguments.len > 2 and !arguments[2].isEmptyOrUndefinedOrNull()) {
+ if (!arguments[2].isString()) {
+ globalObject.throwInvalidArgumentType("verify", "algorithm", "string");
+ return JSC.JSValue.undefined;
+ }
+
+ const algorithm_string = arguments[2].getZigString(globalObject);
+
+ algorithm = PasswordObject.Algorithm.label.getWithEql(algorithm_string, JSC.ZigString.eqlComptime) orelse {
+ globalObject.throwInvalidArgumentType("verify", "algorithm", unknwon_password_algorithm_message);
+ return JSC.JSValue.undefined;
+ };
+ }
+
+ var password = JSC.Node.SliceOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[0]) orelse {
+ globalObject.throwInvalidArgumentType("verify", "password", "string or TypedArray");
+ return JSC.JSValue.undefined;
+ };
+
+ var hash_ = JSC.Node.SliceOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[1]) orelse {
+ password.deinit();
+ globalObject.throwInvalidArgumentType("verify", "hash", "string or TypedArray");
+ return JSC.JSValue.undefined;
+ };
+
+ if (hash_.slice().len == 0) {
+ password.deinit();
+ return JSC.JSPromise.resolvedPromiseValue(globalObject, JSC.JSValue.jsBoolean(false));
+ }
+
+ if (password.slice().len == 0) {
+ hash_.deinit();
+ return JSC.JSPromise.resolvedPromiseValue(globalObject, JSC.JSValue.jsBoolean(false));
+ }
+
+ password.ensureCloned(bun.default_allocator) catch {
+ hash_.deinit();
+ globalObject.throwOutOfMemory();
+ return JSC.JSValue.undefined;
+ };
+
+ hash_.ensureCloned(bun.default_allocator) catch {
+ password.deinit();
+ globalObject.throwOutOfMemory();
+ return JSC.JSValue.undefined;
+ };
+
+ return verify(globalObject, password.slice(), hash_.slice(), algorithm, false);
+ }
+
+ // Once we have bindings generator, this should be replaced with a generated function
+ pub export fn JSPasswordObject__verifySync(
+ globalObject: *JSC.JSGlobalObject,
+ callframe: *JSC.CallFrame,
+ ) callconv(.C) JSC.JSValue {
+ const arguments_ = callframe.arguments(3);
+ const arguments = arguments_.ptr[0..arguments_.len];
+
+ if (arguments.len < 2) {
+ globalObject.throwNotEnoughArguments("verify", 2, 0);
+ return JSC.JSValue.undefined;
+ }
+
+ var algorithm: ?PasswordObject.Algorithm = null;
+
+ if (arguments.len > 2 and !arguments[2].isEmptyOrUndefinedOrNull()) {
+ if (!arguments[2].isString()) {
+ globalObject.throwInvalidArgumentType("verify", "algorithm", "string");
+ return JSC.JSValue.undefined;
+ }
+
+ const algorithm_string = arguments[2].getZigString(globalObject);
+
+ algorithm = PasswordObject.Algorithm.label.getWithEql(algorithm_string, JSC.ZigString.eqlComptime) orelse {
+ globalObject.throwInvalidArgumentType("verify", "algorithm", unknwon_password_algorithm_message);
+ return JSC.JSValue.undefined;
+ };
+ }
+
+ var password = JSC.Node.SliceOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[0]) orelse {
+ globalObject.throwInvalidArgumentType("verify", "password", "string or TypedArray");
+ return JSC.JSValue.undefined;
+ };
+
+ var hash_ = JSC.Node.SliceOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[1]) orelse {
+ password.deinit();
+ globalObject.throwInvalidArgumentType("verify", "hash", "string or TypedArray");
+ return JSC.JSValue.undefined;
+ };
+
+ defer password.deinit();
+ defer hash_.deinit();
+
+ if (hash_.slice().len == 0) {
+ return JSC.JSValue.jsBoolean(false);
+ }
+
+ if (password.slice().len == 0) {
+ return JSC.JSValue.jsBoolean(false);
+ }
+
+ return verify(globalObject, password.slice(), hash_.slice(), algorithm, true);
+ }
+ };
+
pub const CryptoHasher = struct {
evp: EVP = undefined,
@@ -4306,3 +5103,9 @@ pub const JSZlib = struct {
};
pub usingnamespace @import("./bun/subprocess.zig");
+
+comptime {
+ if (!JSC.is_bindgen) {
+ _ = Crypto.JSPasswordObject.JSPasswordObject__create;
+ }
+}
diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp
index 0a453a9c8..bd9c19133 100644
--- a/src/bun.js/bindings/ZigGlobalObject.cpp
+++ b/src/bun.js/bindings/ZigGlobalObject.cpp
@@ -361,6 +361,17 @@ extern "C" bool Zig__GlobalObject__resetModuleRegistryMap(JSC__JSGlobalObject* g
return true;
}
+#define BUN_LAZY_GETTER_FN_NAME(GetterName) BunLazyGetter##GetterName##_getter
+
+#define DEFINE_BUN_LAZY_GETTER(GetterName, __propertyName) \
+ JSC_DEFINE_CUSTOM_GETTER(GetterName, \
+ (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, \
+ JSC::PropertyName)) \
+ { \
+ Zig::GlobalObject* thisObject = JSC::jsCast<Zig::GlobalObject*>(lexicalGlobalObject); \
+ return JSC::JSValue::encode(thisObject->__propertyName()); \
+ }
+
#define GENERATED_CONSTRUCTOR_GETTER(ConstructorName) \
JSC_DECLARE_CUSTOM_GETTER(ConstructorName##_getter); \
JSC_DEFINE_CUSTOM_GETTER(ConstructorName##_getter, \
@@ -2492,6 +2503,7 @@ JSC::JSValue GlobalObject::formatStackTrace(JSC::VM& vm, JSC::JSGlobalObject* le
}
extern "C" void Bun__remapStackFramePositions(JSC::JSGlobalObject*, ZigStackFrame*, size_t);
+extern "C" EncodedJSValue JSPasswordObject__create(JSC::JSGlobalObject*, bool);
JSC_DECLARE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace);
JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
@@ -2599,6 +2611,24 @@ void GlobalObject::finishCreation(VM& vm)
init.set(result.toObject(globalObject));
});
+ m_lazyPasswordObject.initLater(
+ [](const Initializer<JSObject>& init) {
+ JSC::VM& vm = init.vm;
+ JSC::JSGlobalObject* globalObject = init.owner;
+
+ JSValue result = JSValue::decode(JSPasswordObject__create(globalObject, false));
+ init.set(result.toObject(globalObject));
+ });
+
+ m_lazyPasswordSyncObject.initLater(
+ [](const Initializer<JSObject>& init) {
+ JSC::VM& vm = init.vm;
+ JSC::JSGlobalObject* globalObject = init.owner;
+
+ JSValue result = JSValue::decode(JSPasswordObject__create(globalObject, true));
+ init.set(result.toObject(globalObject));
+ });
+
m_lazyPreloadTestModuleObject.initLater(
[](const Initializer<JSObject>& init) {
JSC::VM& vm = init.vm;
@@ -3525,6 +3555,9 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm)
extern "C" void Crypto__randomUUID__put(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue value);
extern "C" void Crypto__getRandomValues__put(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue value);
+DEFINE_BUN_LAZY_GETTER(BUN_LAZY_GETTER_FN_NAME(password), passwordObject)
+DEFINE_BUN_LAZY_GETTER(BUN_LAZY_GETTER_FN_NAME(passwordSync), passwordSyncObject)
+
// This is not a publicly exposed API currently.
// This is used by the bundler to make Response, Request, FetchEvent,
// and any other objects available globally.
@@ -3583,6 +3616,19 @@ void GlobalObject::installAPIGlobals(JSClassRef* globals, int count, JSC::VM& vm
JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0);
}
+ // TODO: code generate these
+ {
+ JSC::Identifier identifier = JSC::Identifier::fromString(vm, "password"_s);
+ object->putDirectCustomAccessor(vm, identifier, JSC::CustomGetterSetter::create(vm, BUN_LAZY_GETTER_FN_NAME(password), nullptr),
+ JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | 0);
+ }
+
+ {
+ JSC::Identifier identifier = JSC::Identifier::fromString(vm, "passwordSync"_s);
+ object->putDirectCustomAccessor(vm, identifier, JSC::CustomGetterSetter::create(vm, BUN_LAZY_GETTER_FN_NAME(passwordSync), nullptr),
+ JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | 0);
+ }
+
{
JSC::Identifier identifier = JSC::Identifier::fromString(vm, "readableStreamToArrayBuffer"_s);
object->putDirectBuiltinFunction(vm, this, identifier, readableStreamReadableStreamToArrayBufferCodeGenerator(vm),
@@ -3853,6 +3899,8 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor)
thisObject->m_lazyTestModuleObject.visit(visitor);
thisObject->m_lazyPreloadTestModuleObject.visit(visitor);
thisObject->m_commonJSModuleObjectStructure.visit(visitor);
+ thisObject->m_lazyPasswordObject.visit(visitor);
+ thisObject->m_lazyPasswordSyncObject.visit(visitor);
thisObject->m_commonJSFunctionArgumentsStructure.visit(visitor);
thisObject->m_cachedGlobalObjectStructure.visit(visitor);
thisObject->m_cachedGlobalProxyStructure.visit(visitor);
@@ -4094,7 +4142,6 @@ JSC::JSObject* GlobalObject::moduleLoaderCreateImportMetaProperties(JSGlobalObje
JSModuleRecord* record,
JSValue val)
{
-
JSC::VM& vm = globalObject->vm();
JSC::JSString* keyString = key.toStringOrNull(globalObject);
if (UNLIKELY(!keyString))
@@ -4108,7 +4155,6 @@ JSC::JSValue GlobalObject::moduleLoaderEvaluate(JSGlobalObject* globalObject,
JSValue moduleRecordValue, JSValue scriptFetcher,
JSValue sentValue, JSValue resumeMode)
{
-
if (UNLIKELY(scriptFetcher && scriptFetcher.isObject())) {
return scriptFetcher;
}
diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h
index 66853c909..2363df74d 100644
--- a/src/bun.js/bindings/ZigGlobalObject.h
+++ b/src/bun.js/bindings/ZigGlobalObject.h
@@ -264,6 +264,9 @@ public:
Structure* commonJSFunctionArgumentsStructure() { return m_commonJSFunctionArgumentsStructure.getInitializedOnMainThread(this); }
+ JSObject* passwordSyncObject() { return m_lazyPasswordSyncObject.getInitializedOnMainThread(this); }
+ JSObject* passwordObject() { return m_lazyPasswordObject.getInitializedOnMainThread(this); }
+
JSWeakMap* vmModuleContextMap() { return m_vmModuleContextMap.getInitializedOnMainThread(this); }
JSC::JSObject* processObject()
@@ -476,6 +479,8 @@ private:
LazyProperty<JSGlobalObject, JSObject> m_lazyRequireCacheObject;
LazyProperty<JSGlobalObject, JSObject> m_lazyTestModuleObject;
LazyProperty<JSGlobalObject, JSObject> m_lazyPreloadTestModuleObject;
+ LazyProperty<JSGlobalObject, JSObject> m_lazyPasswordSyncObject;
+ LazyProperty<JSGlobalObject, JSObject> m_lazyPasswordObject;
LazyProperty<JSGlobalObject, JSFunction> m_bunSleepThenCallback;
LazyProperty<JSGlobalObject, Structure> m_cachedGlobalObjectStructure;
diff --git a/src/bun.js/node/types.zig b/src/bun.js/node/types.zig
index 987b30d3c..23af9cc7c 100644
--- a/src/bun.js/node/types.zig
+++ b/src/bun.js/node/types.zig
@@ -312,6 +312,18 @@ pub const SliceOrBuffer = union(Tag) {
string: JSC.ZigString.Slice,
buffer: Buffer,
+ pub fn ensureCloned(this: *SliceOrBuffer, allocator: std.mem.Allocator) !void {
+ if (this.* == .string) {
+ this.string = try this.string.cloneIfNeeded(allocator);
+ return;
+ }
+
+ const bytes = this.buffer.buffer.byteSlice();
+ this.* = .{
+ .string = JSC.ZigString.Slice.from(try allocator.dupe(u8, bytes), allocator),
+ };
+ }
+
pub fn deinit(this: SliceOrBuffer) void {
switch (this) {
.string => {
diff --git a/src/bun.zig b/src/bun.zig
index 04537feb4..72a2ab8b7 100644
--- a/src/bun.zig
+++ b/src/bun.zig
@@ -25,7 +25,7 @@ pub const huge_allocator_threshold: comptime_int = @import("./memory_allocator.z
pub const fs_allocator = default_allocator;
pub const C = @import("c.zig");
-
+pub const sha = @import("./sha.zig");
pub const FeatureFlags = @import("feature_flags.zig");
pub const meta = @import("./meta.zig");
pub const ComptimeStringMap = @import("./comptime_string_map.zig").ComptimeStringMap;
diff --git a/test/js/bun/util/password.test.ts b/test/js/bun/util/password.test.ts
new file mode 100644
index 000000000..67ea332c4
--- /dev/null
+++ b/test/js/bun/util/password.test.ts
@@ -0,0 +1,279 @@
+import { test, expect, describe } from "bun:test";
+
+import { Password, password, passwordSync } from "bun";
+
+const placeholder = "hey";
+
+describe("hash", () => {
+ describe("arguments parsing", () => {
+ for (let { hash } of [password, passwordSync]) {
+ test("no blank password allowed", () => {
+ expect(() => hash("")).toThrow("password must not be empty");
+ });
+
+ test("password is required", () => {
+ // @ts-expect-error
+ expect(() => hash()).toThrow();
+ });
+
+ test("invalid algorithm throws", () => {
+ // @ts-expect-error
+ expect(() => hash(placeholder, "scrpyt")).toThrow();
+ // @ts-expect-error
+ expect(() => hash(placeholder, 123)).toThrow();
+
+ expect(() =>
+ hash(placeholder, {
+ // @ts-expect-error
+ toString() {
+ return "scrypt";
+ },
+ }),
+ ).toThrow();
+
+ expect(() =>
+ hash(placeholder, {
+ // @ts-expect-error
+ algorithm: "poop",
+ }),
+ ).toThrow();
+
+ expect(() =>
+ hash(placeholder, {
+ algorithm: "bcrypt",
+ cost: Infinity,
+ }),
+ ).toThrow();
+
+ expect(() =>
+ hash(placeholder, {
+ algorithm: "argon2id",
+ memoryCost: -1,
+ }),
+ ).toThrow();
+
+ expect(() =>
+ hash(placeholder, {
+ algorithm: "argon2id",
+ timeCost: -1,
+ }),
+ ).toThrow();
+
+ expect(() =>
+ hash(placeholder, {
+ algorithm: "bcrypt",
+ cost: -999,
+ }),
+ ).toThrow();
+ });
+
+ test("coercion throwing doesn't crash", () => {
+ // @ts-expect-error
+ expect(() => hash(Symbol())).toThrow();
+ expect(() =>
+ // @ts-expect-error
+ hash({
+ toString() {
+ throw new Error("toString() failed");
+ },
+ }),
+ ).toThrow();
+ });
+
+ for (let ArrayBufferView of [
+ Uint8Array,
+ Uint16Array,
+ Uint32Array,
+ Int8Array,
+ Int16Array,
+ Int32Array,
+ Float32Array,
+ Float64Array,
+ ArrayBuffer,
+ ]) {
+ test(`empty ${ArrayBufferView.name} throws`, () => {
+ expect(() => hash(new ArrayBufferView(0))).toThrow("password must not be empty");
+ });
+ }
+ }
+ });
+});
+
+describe("verify", () => {
+ describe("arguments parsing", () => {
+ for (let { verify } of [password, passwordSync]) {
+ test("minimum args", () => {
+ // @ts-expect-error
+ expect(() => verify()).toThrow();
+ // @ts-expect-error
+ expect(() => verify("")).toThrow();
+ });
+
+ test("empty values return false", async () => {
+ expect(await verify("", "$")).toBeFalse();
+ expect(await verify("$", "")).toBeFalse();
+ });
+
+ test("invalid algorithm throws", () => {
+ // @ts-expect-error
+ expect(() => verify(placeholder, "$", "scrpyt")).toThrow();
+ // @ts-expect-error
+ expect(() => verify(placeholder, "$", 123)).toThrow();
+ expect(() =>
+ // @ts-expect-error
+ verify(placeholder, "$", {
+ toString() {
+ return "scrypt";
+ },
+ }),
+ ).toThrow();
+ });
+
+ test("coercion throwing doesn't crash", () => {
+ // @ts-expect-error
+ expect(() => verify(Symbol(), Symbol())).toThrow();
+ expect(() =>
+ verify(
+ // @ts-expect-error
+ {
+ toString() {
+ throw new Error("toString() failed");
+ },
+ },
+ "valid",
+ ),
+ ).toThrow();
+ expect(() =>
+ // @ts-expect-error
+ verify("valid", {
+ toString() {
+ throw new Error("toString() failed");
+ },
+ }),
+ ).toThrow();
+ });
+
+ for (let ArrayBufferView of [
+ Uint8Array,
+ Uint16Array,
+ Uint32Array,
+ Int8Array,
+ Int16Array,
+ Int32Array,
+ Float32Array,
+ Float64Array,
+ ArrayBuffer,
+ ]) {
+ test(`empty ${ArrayBufferView.name} returns false`, async () => {
+ expect(await verify(new ArrayBufferView(0), new ArrayBufferView(0))).toBeFalse();
+ expect(await verify("", new ArrayBufferView(0))).toBeFalse();
+ expect(await verify(new ArrayBufferView(0), "")).toBeFalse();
+ });
+ }
+ }
+ });
+});
+
+test("bcrypt longer than 72 characters is the SHA-512", async () => {
+ const boop = Buffer.from("hey".repeat(100));
+ const hashed = await password.hash(boop, "bcrypt");
+ expect(await password.verify(Bun.SHA512.hash(boop), hashed, "bcrypt")).toBeTrue();
+});
+
+test("bcrypt shorter than 72 characters is NOT the SHA-512", async () => {
+ const boop = Buffer.from("hey".repeat(3));
+ const hashed = await password.hash(boop, "bcrypt");
+ expect(await password.verify(Bun.SHA512.hash(boop), hashed, "bcrypt")).toBeFalse();
+});
+
+const defaultAlgorithm = "argon2id";
+const algorithms = [undefined, "argon2id", "bcrypt"];
+const argons = ["argon2i", "argon2id", "argon2d"];
+
+for (let algorithmValue of algorithms) {
+ const prefix = algorithmValue === "bcrypt" ? "$2" : "$" + (algorithmValue || defaultAlgorithm);
+
+ describe(algorithmValue ? algorithmValue : "default", () => {
+ const hash = (value: string | TypedArray) => {
+ return algorithmValue ? passwordSync.hash(value, algorithmValue as any) : passwordSync.hash(value);
+ };
+
+ const hashSync = (value: string | TypedArray) => {
+ return algorithmValue ? passwordSync.hash(value, algorithmValue as any) : passwordSync.hash(value);
+ };
+
+ const verify = (pw: string | TypedArray, value: string | TypedArray) => {
+ return algorithmValue ? password.verify(pw, value, algorithmValue as any) : password.verify(pw, value);
+ };
+
+ const verifySync = (pw: string | TypedArray, value: string | TypedArray) => {
+ return algorithmValue ? passwordSync.verify(pw, value, algorithmValue as any) : passwordSync.verify(pw, value);
+ };
+
+ for (let input of [placeholder, Buffer.from(placeholder)]) {
+ describe(typeof input === "string" ? "string" : "buffer", () => {
+ test("passwordSync", () => {
+ const hashed = hashSync(input);
+ expect(hashed).toStartWith(prefix);
+ expect(verifySync(input, hashed)).toBeTrue();
+ expect(() => verifySync(hashed, input)).toThrow();
+ expect(verifySync(input + "\0", hashed)).toBeFalse();
+ });
+
+ test("password", async () => {
+ async function runSlowTest(algorithm = algorithmValue as any) {
+ const hashed = await password.hash(input, algorithm);
+ const prefix = "$" + algorithm;
+ expect(hashed).toStartWith(prefix);
+ expect(await password.verify(input, hashed, algorithm)).toBeTrue();
+ expect(() => password.verify(hashed, input, algorithm)).toThrow();
+ expect(await password.verify(input + "\0", hashed, algorithm)).toBeFalse();
+ }
+
+ async function runSlowTestWithOptions(algorithmLabel: any) {
+ const algorithm = { algorithm: algorithmLabel, timeCost: 5, memoryCost: 4 };
+ const hashed = await password.hash(input, algorithm);
+ const prefix = "$" + algorithmLabel;
+ expect(hashed).toStartWith(prefix);
+ expect(hashed).toContain("t=5");
+ expect(hashed).toContain("m=4");
+ expect(await password.verify(input, hashed, algorithmLabel)).toBeTrue();
+ expect(() => password.verify(hashed, input, algorithmLabel)).toThrow();
+ expect(await password.verify(input + "\0", hashed, algorithmLabel)).toBeFalse();
+ }
+
+ async function runSlowBCryptTest() {
+ const algorithm = { algorithm: "bcrypt", cost: 4 } as const;
+ const hashed = await password.hash(input, algorithm);
+ const prefix = "$" + "2b";
+ expect(hashed).toStartWith(prefix);
+ expect(await password.verify(input, hashed, "bcrypt")).toBeTrue();
+ expect(() => password.verify(hashed, input, "bcrypt")).toThrow();
+ expect(await password.verify(input + "\0", hashed, "bcrypt")).toBeFalse();
+ }
+
+ if (algorithmValue === defaultAlgorithm) {
+ // these tests are very slow
+ // run the hashing tests in parallel
+ await Promise.all([...argons.map(runSlowTest), ...argons.map(runSlowTestWithOptions)]);
+ return;
+ }
+
+ async function defaultTest() {
+ const hashed = await hash(input);
+ expect(hashed).toStartWith(prefix);
+ expect(await verify(input, hashed)).toBeTrue();
+ expect(() => verify(hashed, input)).toThrow();
+ expect(await verify(input + "\0", hashed)).toBeFalse();
+ }
+
+ if (algorithmValue === "bcrypt") {
+ await Promise.all([defaultTest(), runSlowBCryptTest()]);
+ } else {
+ await defaultTest();
+ }
+ });
+ });
+ }
+ });
+}