diff options
| author | 2023-06-04 18:20:04 -0700 | |
|---|---|---|
| committer | 2023-06-04 18:20:04 -0700 | |
| commit | 9b996e702ef32d03b01b745642292e7a747485fa (patch) | |
| tree | 4b49c2010c694afe5cf6ef738402f80e0ab7c5b8 /src | |
| parent | 2cb1376a93a59acca548769155e0d3b6110a7bd2 (diff) | |
| download | bun-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>
Diffstat (limited to 'src')
| -rw-r--r-- | src/bun.js/api/bun.zig | 803 | ||||
| -rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.cpp | 50 | ||||
| -rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.h | 5 | ||||
| -rw-r--r-- | src/bun.js/node/types.zig | 12 | ||||
| -rw-r--r-- | src/bun.zig | 2 | 
5 files changed, 869 insertions, 3 deletions
| 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; | 
