Split bun crypto APIs into more files (#18431)

This commit is contained in:
Jarred Sumner
2025-03-24 17:22:05 -07:00
committed by GitHub
parent fefdaefb97
commit 84a21234d4
12 changed files with 3275 additions and 3131 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,638 @@
pub fn newCString(globalThis: *JSGlobalObject, value: JSValue, byteOffset: ?JSValue, lengthValue: ?JSValue) JSC.JSValue {
switch (FFIObject.getPtrSlice(globalThis, value, byteOffset, lengthValue)) {
.err => |err| {
return err;
},
.slice => |slice| {
return bun.String.createUTF8ForJS(globalThis, slice);
},
}
}
pub const dom_call = JSC.DOMCall("FFI", @This(), "ptr", JSC.DOMEffect.forRead(.TypedArrayProperties));
pub fn toJS(globalObject: *JSC.JSGlobalObject) JSC.JSValue {
const object = JSC.JSValue.createEmptyObject(globalObject, comptime std.meta.fieldNames(@TypeOf(fields)).len + 2);
inline for (comptime std.meta.fieldNames(@TypeOf(fields))) |field| {
object.put(
globalObject,
comptime ZigString.static(field),
JSC.createCallback(globalObject, comptime ZigString.static(field), 1, comptime @field(fields, field)),
);
}
dom_call.put(globalObject, object);
object.put(globalObject, ZigString.static("read"), Reader.toJS(globalObject));
return object;
}
pub const Reader = struct {
pub const DOMCalls = .{
.u8 = JSC.DOMCall("Reader", @This(), "u8", JSC.DOMEffect.forRead(.World)),
.u16 = JSC.DOMCall("Reader", @This(), "u16", JSC.DOMEffect.forRead(.World)),
.u32 = JSC.DOMCall("Reader", @This(), "u32", JSC.DOMEffect.forRead(.World)),
.ptr = JSC.DOMCall("Reader", @This(), "ptr", JSC.DOMEffect.forRead(.World)),
.i8 = JSC.DOMCall("Reader", @This(), "i8", JSC.DOMEffect.forRead(.World)),
.i16 = JSC.DOMCall("Reader", @This(), "i16", JSC.DOMEffect.forRead(.World)),
.i32 = JSC.DOMCall("Reader", @This(), "i32", JSC.DOMEffect.forRead(.World)),
.i64 = JSC.DOMCall("Reader", @This(), "i64", JSC.DOMEffect.forRead(.World)),
.u64 = JSC.DOMCall("Reader", @This(), "u64", JSC.DOMEffect.forRead(.World)),
.intptr = JSC.DOMCall("Reader", @This(), "intptr", JSC.DOMEffect.forRead(.World)),
.f32 = JSC.DOMCall("Reader", @This(), "f32", JSC.DOMEffect.forRead(.World)),
.f64 = JSC.DOMCall("Reader", @This(), "f64", JSC.DOMEffect.forRead(.World)),
};
pub fn toJS(globalThis: *JSC.JSGlobalObject) JSC.JSValue {
const obj = JSC.JSValue.createEmptyObject(globalThis, std.meta.fieldNames(@TypeOf(Reader.DOMCalls)).len);
inline for (comptime std.meta.fieldNames(@TypeOf(Reader.DOMCalls))) |field| {
@field(Reader.DOMCalls, field).put(globalThis, obj);
}
return obj;
}
pub fn @"u8"(
globalObject: *JSGlobalObject,
_: JSValue,
arguments: []const JSValue,
) bun.JSError!JSValue {
if (arguments.len == 0 or !arguments[0].isNumber()) {
return globalObject.throwInvalidArguments("Expected a pointer", .{});
}
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
const value = @as(*align(1) u8, @ptrFromInt(addr)).*;
return JSValue.jsNumber(value);
}
pub fn @"u16"(
globalObject: *JSGlobalObject,
_: JSValue,
arguments: []const JSValue,
) bun.JSError!JSValue {
if (arguments.len == 0 or !arguments[0].isNumber()) {
return globalObject.throwInvalidArguments("Expected a pointer", .{});
}
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
const value = @as(*align(1) u16, @ptrFromInt(addr)).*;
return JSValue.jsNumber(value);
}
pub fn @"u32"(
globalObject: *JSGlobalObject,
_: JSValue,
arguments: []const JSValue,
) bun.JSError!JSValue {
if (arguments.len == 0 or !arguments[0].isNumber()) {
return globalObject.throwInvalidArguments("Expected a pointer", .{});
}
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
const value = @as(*align(1) u32, @ptrFromInt(addr)).*;
return JSValue.jsNumber(value);
}
pub fn ptr(
globalObject: *JSGlobalObject,
_: JSValue,
arguments: []const JSValue,
) bun.JSError!JSValue {
if (arguments.len == 0 or !arguments[0].isNumber()) {
return globalObject.throwInvalidArguments("Expected a pointer", .{});
}
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
const value = @as(*align(1) u64, @ptrFromInt(addr)).*;
return JSValue.jsNumber(value);
}
pub fn @"i8"(
globalObject: *JSGlobalObject,
_: JSValue,
arguments: []const JSValue,
) bun.JSError!JSValue {
if (arguments.len == 0 or !arguments[0].isNumber()) {
return globalObject.throwInvalidArguments("Expected a pointer", .{});
}
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
const value = @as(*align(1) i8, @ptrFromInt(addr)).*;
return JSValue.jsNumber(value);
}
pub fn @"i16"(
globalObject: *JSGlobalObject,
_: JSValue,
arguments: []const JSValue,
) bun.JSError!JSValue {
if (arguments.len == 0 or !arguments[0].isNumber()) {
return globalObject.throwInvalidArguments("Expected a pointer", .{});
}
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
const value = @as(*align(1) i16, @ptrFromInt(addr)).*;
return JSValue.jsNumber(value);
}
pub fn @"i32"(
globalObject: *JSGlobalObject,
_: JSValue,
arguments: []const JSValue,
) bun.JSError!JSValue {
if (arguments.len == 0 or !arguments[0].isNumber()) {
return globalObject.throwInvalidArguments("Expected a pointer", .{});
}
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
const value = @as(*align(1) i32, @ptrFromInt(addr)).*;
return JSValue.jsNumber(value);
}
pub fn intptr(
globalObject: *JSGlobalObject,
_: JSValue,
arguments: []const JSValue,
) bun.JSError!JSValue {
if (arguments.len == 0 or !arguments[0].isNumber()) {
return globalObject.throwInvalidArguments("Expected a pointer", .{});
}
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
const value = @as(*align(1) i64, @ptrFromInt(addr)).*;
return JSValue.jsNumber(value);
}
pub fn @"f32"(
globalObject: *JSGlobalObject,
_: JSValue,
arguments: []const JSValue,
) bun.JSError!JSValue {
if (arguments.len == 0 or !arguments[0].isNumber()) {
return globalObject.throwInvalidArguments("Expected a pointer", .{});
}
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
const value = @as(*align(1) f32, @ptrFromInt(addr)).*;
return JSValue.jsNumber(value);
}
pub fn @"f64"(
globalObject: *JSGlobalObject,
_: JSValue,
arguments: []const JSValue,
) bun.JSError!JSValue {
if (arguments.len == 0 or !arguments[0].isNumber()) {
return globalObject.throwInvalidArguments("Expected a pointer", .{});
}
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
const value = @as(*align(1) f64, @ptrFromInt(addr)).*;
return JSValue.jsNumber(value);
}
pub fn @"i64"(
globalObject: *JSGlobalObject,
_: JSValue,
arguments: []const JSValue,
) bun.JSError!JSValue {
if (arguments.len == 0 or !arguments[0].isNumber()) {
return globalObject.throwInvalidArguments("Expected a pointer", .{});
}
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
const value = @as(*align(1) i64, @ptrFromInt(addr)).*;
return JSValue.fromInt64NoTruncate(globalObject, value);
}
pub fn @"u64"(
globalObject: *JSGlobalObject,
_: JSValue,
arguments: []const JSValue,
) bun.JSError!JSValue {
if (arguments.len == 0 or !arguments[0].isNumber()) {
return globalObject.throwInvalidArguments("Expected a pointer", .{});
}
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
const value = @as(*align(1) u64, @ptrFromInt(addr)).*;
return JSValue.fromUInt64NoTruncate(globalObject, value);
}
pub fn u8WithoutTypeChecks(
_: *JSGlobalObject,
_: *anyopaque,
raw_addr: i64,
offset: i32,
) callconv(JSC.conv) JSValue {
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
const value = @as(*align(1) u8, @ptrFromInt(addr)).*;
return JSValue.jsNumber(value);
}
pub fn u16WithoutTypeChecks(
_: *JSGlobalObject,
_: *anyopaque,
raw_addr: i64,
offset: i32,
) callconv(JSC.conv) JSValue {
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
const value = @as(*align(1) u16, @ptrFromInt(addr)).*;
return JSValue.jsNumber(value);
}
pub fn u32WithoutTypeChecks(
_: *JSGlobalObject,
_: *anyopaque,
raw_addr: i64,
offset: i32,
) callconv(JSC.conv) JSValue {
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
const value = @as(*align(1) u32, @ptrFromInt(addr)).*;
return JSValue.jsNumber(value);
}
pub fn ptrWithoutTypeChecks(
_: *JSGlobalObject,
_: *anyopaque,
raw_addr: i64,
offset: i32,
) callconv(JSC.conv) JSValue {
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
const value = @as(*align(1) u64, @ptrFromInt(addr)).*;
return JSValue.jsNumber(value);
}
pub fn i8WithoutTypeChecks(
_: *JSGlobalObject,
_: *anyopaque,
raw_addr: i64,
offset: i32,
) callconv(JSC.conv) JSValue {
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
const value = @as(*align(1) i8, @ptrFromInt(addr)).*;
return JSValue.jsNumber(value);
}
pub fn i16WithoutTypeChecks(
_: *JSGlobalObject,
_: *anyopaque,
raw_addr: i64,
offset: i32,
) callconv(JSC.conv) JSValue {
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
const value = @as(*align(1) i16, @ptrFromInt(addr)).*;
return JSValue.jsNumber(value);
}
pub fn i32WithoutTypeChecks(
_: *JSGlobalObject,
_: *anyopaque,
raw_addr: i64,
offset: i32,
) callconv(JSC.conv) JSValue {
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
const value = @as(*align(1) i32, @ptrFromInt(addr)).*;
return JSValue.jsNumber(value);
}
pub fn intptrWithoutTypeChecks(
_: *JSGlobalObject,
_: *anyopaque,
raw_addr: i64,
offset: i32,
) callconv(JSC.conv) JSValue {
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
const value = @as(*align(1) i64, @ptrFromInt(addr)).*;
return JSValue.jsNumber(value);
}
pub fn f32WithoutTypeChecks(
_: *JSGlobalObject,
_: *anyopaque,
raw_addr: i64,
offset: i32,
) callconv(JSC.conv) JSValue {
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
const value = @as(*align(1) f32, @ptrFromInt(addr)).*;
return JSValue.jsNumber(value);
}
pub fn f64WithoutTypeChecks(
_: *JSGlobalObject,
_: *anyopaque,
raw_addr: i64,
offset: i32,
) callconv(JSC.conv) JSValue {
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
const value = @as(*align(1) f64, @ptrFromInt(addr)).*;
return JSValue.jsNumber(value);
}
pub fn u64WithoutTypeChecks(
global: *JSGlobalObject,
_: *anyopaque,
raw_addr: i64,
offset: i32,
) callconv(JSC.conv) JSValue {
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
const value = @as(*align(1) u64, @ptrFromInt(addr)).*;
return JSValue.fromUInt64NoTruncate(global, value);
}
pub fn i64WithoutTypeChecks(
global: *JSGlobalObject,
_: *anyopaque,
raw_addr: i64,
offset: i32,
) callconv(JSC.conv) JSValue {
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
const value = @as(*align(1) i64, @ptrFromInt(addr)).*;
return JSValue.fromInt64NoTruncate(global, value);
}
};
pub fn ptr(
globalThis: *JSGlobalObject,
_: JSValue,
arguments: []const JSValue,
) JSValue {
return switch (arguments.len) {
0 => ptr_(globalThis, JSValue.zero, null),
1 => ptr_(globalThis, arguments[0], null),
else => ptr_(globalThis, arguments[0], arguments[1]),
};
}
pub fn ptrWithoutTypeChecks(
_: *JSGlobalObject,
_: *anyopaque,
array: *JSC.JSUint8Array,
) callconv(JSC.conv) JSValue {
return JSValue.fromPtrAddress(@intFromPtr(array.ptr()));
}
fn ptr_(
globalThis: *JSGlobalObject,
value: JSValue,
byteOffset: ?JSValue,
) JSValue {
if (value == .zero) {
return JSC.JSValue.jsNull();
}
const array_buffer = value.asArrayBuffer(globalThis) orelse {
return JSC.toInvalidArguments("Expected ArrayBufferView but received {s}", .{@tagName(value.jsType())}, globalThis);
};
if (array_buffer.len == 0) {
return JSC.toInvalidArguments("ArrayBufferView must have a length > 0. A pointer to empty memory doesn't work", .{}, globalThis);
}
var addr: usize = @intFromPtr(array_buffer.ptr);
// const Sizes = @import("../bindings/sizes.zig");
// assert(addr == @intFromPtr(value.asEncoded().ptr) + Sizes.Bun_FFI_PointerOffsetToTypedArrayVector);
if (byteOffset) |off| {
if (!off.isEmptyOrUndefinedOrNull()) {
if (!off.isNumber()) {
return JSC.toInvalidArguments("Expected number for byteOffset", .{}, globalThis);
}
}
const bytei64 = off.toInt64();
if (bytei64 < 0) {
addr -|= @as(usize, @intCast(bytei64 * -1));
} else {
addr += @as(usize, @intCast(bytei64));
}
if (addr > @intFromPtr(array_buffer.ptr) + @as(usize, array_buffer.byte_len)) {
return JSC.toInvalidArguments("byteOffset out of bounds", .{}, globalThis);
}
}
if (addr > max_addressable_memory) {
return JSC.toInvalidArguments("Pointer is outside max addressible memory, which usually means a bug in your program.", .{}, globalThis);
}
if (addr == 0) {
return JSC.toInvalidArguments("Pointer must not be 0", .{}, globalThis);
}
if (addr == 0xDEADBEEF or addr == 0xaaaaaaaa or addr == 0xAAAAAAAA) {
return JSC.toInvalidArguments("ptr to invalid memory, that would segfault Bun :(", .{}, globalThis);
}
if (comptime Environment.allow_assert) {
assert(JSC.JSValue.fromPtrAddress(addr).asPtrAddress() == addr);
}
return JSC.JSValue.fromPtrAddress(addr);
}
const ValueOrError = union(enum) {
err: JSValue,
slice: []u8,
};
pub fn getPtrSlice(globalThis: *JSGlobalObject, value: JSValue, byteOffset: ?JSValue, byteLength: ?JSValue) ValueOrError {
if (!value.isNumber()) {
return .{ .err = JSC.toInvalidArguments("ptr must be a number.", .{}, globalThis) };
}
const num = value.asPtrAddress();
if (num == 0) {
return .{ .err = JSC.toInvalidArguments("ptr cannot be zero, that would segfault Bun :(", .{}, globalThis) };
}
// if (!std.math.isFinite(num)) {
// return .{ .err = JSC.toInvalidArguments("ptr must be a finite number.", .{}, globalThis) };
// }
var addr = @as(usize, @bitCast(num));
if (byteOffset) |byte_off| {
if (byte_off.isNumber()) {
const off = byte_off.toInt64();
if (off < 0) {
addr -|= @as(usize, @intCast(off * -1));
} else {
addr +|= @as(usize, @intCast(off));
}
if (addr == 0) {
return .{ .err = JSC.toInvalidArguments("ptr cannot be zero, that would segfault Bun :(", .{}, globalThis) };
}
if (!std.math.isFinite(byte_off.asNumber())) {
return .{ .err = JSC.toInvalidArguments("ptr must be a finite number.", .{}, globalThis) };
}
} else if (!byte_off.isEmptyOrUndefinedOrNull()) {
// do nothing
} else {
return .{ .err = JSC.toInvalidArguments("Expected number for byteOffset", .{}, globalThis) };
}
}
if (addr == 0xDEADBEEF or addr == 0xaaaaaaaa or addr == 0xAAAAAAAA) {
return .{ .err = JSC.toInvalidArguments("ptr to invalid memory, that would segfault Bun :(", .{}, globalThis) };
}
if (byteLength) |valueLength| {
if (!valueLength.isEmptyOrUndefinedOrNull()) {
if (!valueLength.isNumber()) {
return .{ .err = JSC.toInvalidArguments("length must be a number.", .{}, globalThis) };
}
if (valueLength.asNumber() == 0.0) {
return .{ .err = JSC.toInvalidArguments("length must be > 0. This usually means a bug in your code.", .{}, globalThis) };
}
const length_i = valueLength.toInt64();
if (length_i < 0) {
return .{ .err = JSC.toInvalidArguments("length must be > 0. This usually means a bug in your code.", .{}, globalThis) };
}
if (length_i > max_addressable_memory) {
return .{ .err = JSC.toInvalidArguments("length exceeds max addressable memory. This usually means a bug in your code.", .{}, globalThis) };
}
const length = @as(usize, @intCast(length_i));
return .{ .slice = @as([*]u8, @ptrFromInt(addr))[0..length] };
}
}
return .{ .slice = bun.span(@as([*:0]u8, @ptrFromInt(addr))) };
}
fn getCPtr(value: JSValue) ?usize {
// pointer to C function
if (value.isNumber()) {
const addr = value.asPtrAddress();
if (addr > 0) return addr;
} else if (value.isBigInt()) {
const addr = @as(u64, @bitCast(value.toUInt64NoTruncate()));
if (addr > 0) {
return addr;
}
}
return null;
}
pub fn toArrayBuffer(
globalThis: *JSGlobalObject,
value: JSValue,
byteOffset: ?JSValue,
valueLength: ?JSValue,
finalizationCtxOrPtr: ?JSValue,
finalizationCallback: ?JSValue,
) JSC.JSValue {
switch (getPtrSlice(globalThis, value, byteOffset, valueLength)) {
.err => |erro| {
return erro;
},
.slice => |slice| {
var callback: JSC.C.JSTypedArrayBytesDeallocator = null;
var ctx: ?*anyopaque = null;
if (finalizationCallback) |callback_value| {
if (getCPtr(callback_value)) |callback_ptr| {
callback = @as(JSC.C.JSTypedArrayBytesDeallocator, @ptrFromInt(callback_ptr));
if (finalizationCtxOrPtr) |ctx_value| {
if (getCPtr(ctx_value)) |ctx_ptr| {
ctx = @as(*anyopaque, @ptrFromInt(ctx_ptr));
} else if (!ctx_value.isUndefinedOrNull()) {
return JSC.toInvalidArguments("Expected user data to be a C pointer (number or BigInt)", .{}, globalThis);
}
}
} else if (!callback_value.isEmptyOrUndefinedOrNull()) {
return JSC.toInvalidArguments("Expected callback to be a C pointer (number or BigInt)", .{}, globalThis);
}
} else if (finalizationCtxOrPtr) |callback_value| {
if (getCPtr(callback_value)) |callback_ptr| {
callback = @as(JSC.C.JSTypedArrayBytesDeallocator, @ptrFromInt(callback_ptr));
} else if (!callback_value.isEmptyOrUndefinedOrNull()) {
return JSC.toInvalidArguments("Expected callback to be a C pointer (number or BigInt)", .{}, globalThis);
}
}
return JSC.ArrayBuffer.fromBytes(slice, JSC.JSValue.JSType.ArrayBuffer).toJSWithContext(globalThis, ctx, callback, null);
},
}
}
pub fn toBuffer(
globalThis: *JSGlobalObject,
value: JSValue,
byteOffset: ?JSValue,
valueLength: ?JSValue,
finalizationCtxOrPtr: ?JSValue,
finalizationCallback: ?JSValue,
) JSC.JSValue {
switch (getPtrSlice(globalThis, value, byteOffset, valueLength)) {
.err => |err| {
return err;
},
.slice => |slice| {
var callback: JSC.C.JSTypedArrayBytesDeallocator = null;
var ctx: ?*anyopaque = null;
if (finalizationCallback) |callback_value| {
if (getCPtr(callback_value)) |callback_ptr| {
callback = @as(JSC.C.JSTypedArrayBytesDeallocator, @ptrFromInt(callback_ptr));
if (finalizationCtxOrPtr) |ctx_value| {
if (getCPtr(ctx_value)) |ctx_ptr| {
ctx = @as(*anyopaque, @ptrFromInt(ctx_ptr));
} else if (!ctx_value.isEmptyOrUndefinedOrNull()) {
return JSC.toInvalidArguments("Expected user data to be a C pointer (number or BigInt)", .{}, globalThis);
}
}
} else if (!callback_value.isEmptyOrUndefinedOrNull()) {
return JSC.toInvalidArguments("Expected callback to be a C pointer (number or BigInt)", .{}, globalThis);
}
} else if (finalizationCtxOrPtr) |callback_value| {
if (getCPtr(callback_value)) |callback_ptr| {
callback = @as(JSC.C.JSTypedArrayBytesDeallocator, @ptrFromInt(callback_ptr));
} else if (!callback_value.isEmptyOrUndefinedOrNull()) {
return JSC.toInvalidArguments("Expected callback to be a C pointer (number or BigInt)", .{}, globalThis);
}
}
if (callback != null or ctx != null) {
return JSC.JSValue.createBufferWithCtx(globalThis, slice, ctx, callback);
}
return JSC.JSValue.createBuffer(globalThis, slice, null);
},
}
}
pub fn toCStringBuffer(
globalThis: *JSGlobalObject,
value: JSValue,
byteOffset: ?JSValue,
valueLength: ?JSValue,
) JSC.JSValue {
switch (getPtrSlice(globalThis, value, byteOffset, valueLength)) {
.err => |err| {
return err;
},
.slice => |slice| {
return JSC.JSValue.createBuffer(globalThis, slice, null);
},
}
}
pub fn getter(
globalObject: *JSC.JSGlobalObject,
_: *JSC.JSObject,
) JSC.JSValue {
return FFIObject.toJS(globalObject);
}
const fields = .{
.viewSource = JSC.wrapStaticMethod(
JSC.FFI,
"print",
false,
),
.dlopen = JSC.wrapStaticMethod(JSC.FFI, "open", false),
.callback = JSC.wrapStaticMethod(JSC.FFI, "callback", false),
.linkSymbols = JSC.wrapStaticMethod(JSC.FFI, "linkSymbols", false),
.toBuffer = JSC.wrapStaticMethod(@This(), "toBuffer", false),
.toArrayBuffer = JSC.wrapStaticMethod(@This(), "toArrayBuffer", false),
.closeCallback = JSC.wrapStaticMethod(JSC.FFI, "closeCallback", false),
.CString = JSC.wrapStaticMethod(Bun.FFIObject, "newCString", false),
};
const max_addressable_memory = std.math.maxInt(u56);
const JSGlobalObject = JSC.JSGlobalObject;
const JSObject = JSC.JSObject;
const JSValue = JSC.JSValue;
const JSC = bun.JSC;
const bun = @import("root").bun;
const FFIObject = @This();
const Bun = JSC.API.Bun;
const Environment = bun.Environment;
const std = @import("std");
const assert = bun.assert;
const ZigString = JSC.ZigString;

View File

@@ -0,0 +1,144 @@
pub const wyhash = hashWrap(std.hash.Wyhash);
pub const adler32 = hashWrap(std.hash.Adler32);
pub const crc32 = hashWrap(std.hash.Crc32);
pub const cityHash32 = hashWrap(std.hash.CityHash32);
pub const cityHash64 = hashWrap(std.hash.CityHash64);
pub const xxHash32 = hashWrap(struct {
pub fn hash(seed: u32, bytes: []const u8) u32 {
// sidestep .hash taking in anytype breaking ArgTuple
// downstream by forcing a type signature on the input
return std.hash.XxHash32.hash(seed, bytes);
}
});
pub const xxHash64 = hashWrap(struct {
pub fn hash(seed: u32, bytes: []const u8) u64 {
// sidestep .hash taking in anytype breaking ArgTuple
// downstream by forcing a type signature on the input
return std.hash.XxHash64.hash(seed, bytes);
}
});
pub const xxHash3 = hashWrap(struct {
pub fn hash(seed: u32, bytes: []const u8) u64 {
// sidestep .hash taking in anytype breaking ArgTuple
// downstream by forcing a type signature on the input
return std.hash.XxHash3.hash(seed, bytes);
}
});
pub const murmur32v2 = hashWrap(std.hash.murmur.Murmur2_32);
pub const murmur32v3 = hashWrap(std.hash.murmur.Murmur3_32);
pub const murmur64v2 = hashWrap(std.hash.murmur.Murmur2_64);
pub fn create(globalThis: *JSC.JSGlobalObject) JSC.JSValue {
const function = JSC.createCallback(globalThis, ZigString.static("hash"), 1, wyhash);
const fns = comptime .{
"wyhash",
"adler32",
"crc32",
"cityHash32",
"cityHash64",
"xxHash32",
"xxHash64",
"xxHash3",
"murmur32v2",
"murmur32v3",
"murmur64v2",
};
inline for (fns) |name| {
const value = JSC.createCallback(
globalThis,
ZigString.static(name),
1,
@field(HashObject, name),
);
function.put(globalThis, comptime ZigString.static(name), value);
}
return function;
}
fn hashWrap(comptime Hasher_: anytype) JSC.JSHostZigFunction {
return struct {
const Hasher = Hasher_;
pub fn hash(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
const arguments = callframe.arguments_old(2).slice();
var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments);
defer args.deinit();
var input: []const u8 = "";
var input_slice = ZigString.Slice.empty;
defer input_slice.deinit();
if (args.nextEat()) |arg| {
if (arg.as(JSC.WebCore.Blob)) |blob| {
// TODO: files
input = blob.sharedView();
} else {
switch (arg.jsTypeLoose()) {
.ArrayBuffer,
.Int8Array,
.Uint8Array,
.Uint8ClampedArray,
.Int16Array,
.Uint16Array,
.Int32Array,
.Uint32Array,
.Float16Array,
.Float32Array,
.Float64Array,
.BigInt64Array,
.BigUint64Array,
.DataView,
=> {
var array_buffer = arg.asArrayBuffer(globalThis) orelse {
return globalThis.throwInvalidArguments("ArrayBuffer conversion error", .{});
};
input = array_buffer.byteSlice();
},
else => {
input_slice = try arg.toSlice(globalThis, bun.default_allocator);
input = input_slice.slice();
},
}
}
}
// std.hash has inconsistent interfaces
//
const Function = if (@hasDecl(Hasher, "hashWithSeed")) Hasher.hashWithSeed else Hasher.hash;
var function_args: std.meta.ArgsTuple(@TypeOf(Function)) = undefined;
if (comptime std.meta.fields(std.meta.ArgsTuple(@TypeOf(Function))).len == 1) {
return JSC.JSValue.jsNumber(Function(input));
} else {
var seed: u64 = 0;
if (args.nextEat()) |arg| {
if (arg.isNumber() or arg.isBigInt()) {
seed = arg.toUInt64NoTruncate();
}
}
if (comptime bun.trait.isNumber(@TypeOf(function_args[0]))) {
function_args[0] = @as(@TypeOf(function_args[0]), @truncate(seed));
function_args[1] = input;
} else {
function_args[0] = input;
function_args[1] = @as(@TypeOf(function_args[1]), @truncate(seed));
}
const value = @call(.auto, Function, function_args);
if (@TypeOf(value) == u32) {
return JSC.JSValue.jsNumber(@as(u32, @bitCast(value)));
}
return JSC.JSValue.fromUInt64NoTruncate(globalThis, value);
}
}
}.hash;
}
const HashObject = @This();
const JSC = bun.JSC;
const JSValue = JSC.JSValue;
const JSGlobalObject = JSC.JSGlobalObject;
const JSObject = JSC.JSObject;
const std = @import("std");
const bun = @import("root").bun;
const ZigString = JSC.ZigString;

View File

@@ -0,0 +1,72 @@
pub fn create(globalThis: *JSC.JSGlobalObject) JSC.JSValue {
const object = JSValue.createEmptyObject(globalThis, 1);
object.put(
globalThis,
ZigString.static("parse"),
JSC.createCallback(
globalThis,
ZigString.static("parse"),
1,
parse,
),
);
return object;
}
pub fn parse(
globalThis: *JSC.JSGlobalObject,
callframe: *JSC.CallFrame,
) bun.JSError!JSC.JSValue {
var arena = bun.ArenaAllocator.init(globalThis.allocator());
const allocator = arena.allocator();
defer arena.deinit();
var log = logger.Log.init(default_allocator);
const arguments = callframe.arguments_old(1).slice();
if (arguments.len == 0 or arguments[0].isEmptyOrUndefinedOrNull()) {
return globalThis.throwInvalidArguments("Expected a string to parse", .{});
}
var input_slice = try arguments[0].toSlice(globalThis, bun.default_allocator);
defer input_slice.deinit();
var source = logger.Source.initPathString("input.toml", input_slice.slice());
const parse_result = TOMLParser.parse(&source, &log, allocator, false) catch {
return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to parse toml"));
};
// for now...
const buffer_writer = js_printer.BufferWriter.init(allocator) catch {
return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to print toml"));
};
var writer = js_printer.BufferPrinter.init(buffer_writer);
_ = js_printer.printJSON(
*js_printer.BufferPrinter,
&writer,
parse_result,
&source,
.{
.mangled_props = null,
},
) catch {
return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to print toml"));
};
const slice = writer.ctx.buffer.slice();
var out = bun.String.fromUTF8(slice);
defer out.deref();
return out.toJSByParseJSON(globalThis);
}
const TOMLObject = @This();
const JSC = bun.JSC;
const JSValue = JSC.JSValue;
const JSGlobalObject = JSC.JSGlobalObject;
const JSObject = JSC.JSObject;
const std = @import("std");
const ZigString = JSC.ZigString;
const logger = bun.logger;
const bun = @import("root").bun;
const js_printer = bun.js_printer;
const default_allocator = bun.default_allocator;
const TOMLParser = @import("../../toml/toml_parser.zig").TOML;

View File

@@ -0,0 +1,76 @@
pub fn create(globalThis: *JSC.JSGlobalObject) JSC.JSValue {
const object = JSValue.createEmptyObject(globalThis, 3);
const fields = comptime .{
.gcAggressionLevel = gcAggressionLevel,
.arrayBufferToString = arrayBufferToString,
.mimallocDump = dump_mimalloc,
};
inline for (comptime std.meta.fieldNames(@TypeOf(fields))) |name| {
object.put(
globalThis,
comptime ZigString.static(name),
JSC.createCallback(globalThis, comptime ZigString.static(name), 1, comptime @field(fields, name)),
);
}
return object;
}
pub fn gcAggressionLevel(
globalThis: *JSC.JSGlobalObject,
callframe: *JSC.CallFrame,
) bun.JSError!JSC.JSValue {
const ret = JSValue.jsNumber(@as(i32, @intFromEnum(globalThis.bunVM().aggressive_garbage_collection)));
const value = callframe.arguments_old(1).ptr[0];
if (!value.isEmptyOrUndefinedOrNull()) {
switch (value.coerce(i32, globalThis)) {
1 => globalThis.bunVM().aggressive_garbage_collection = .mild,
2 => globalThis.bunVM().aggressive_garbage_collection = .aggressive,
0 => globalThis.bunVM().aggressive_garbage_collection = .none,
else => {},
}
}
return ret;
}
pub fn arrayBufferToString(
globalThis: *JSC.JSGlobalObject,
callframe: *JSC.CallFrame,
) bun.JSError!JSC.JSValue {
const args = callframe.arguments_old(2).slice();
if (args.len < 1 or !args[0].isCell() or !args[0].jsType().isTypedArrayOrArrayBuffer()) {
return globalThis.throwInvalidArguments("Expected an ArrayBuffer", .{});
}
const array_buffer = JSC.ArrayBuffer.fromTypedArray(globalThis, args[0]);
switch (array_buffer.typed_array_type) {
.Uint16Array, .Int16Array => {
var zig_str = ZigString.init("");
zig_str._unsafe_ptr_do_not_use = @as([*]const u8, @ptrCast(@alignCast(array_buffer.ptr)));
zig_str.len = array_buffer.len;
zig_str.markUTF16();
return zig_str.toJS(globalThis);
},
else => {
return ZigString.init(array_buffer.slice()).toJS(globalThis);
},
}
}
extern fn dump_zone_malloc_stats() void;
fn dump_mimalloc(globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue {
globalObject.bunVM().arena.dumpStats();
if (bun.heap_breakdown.enabled) {
dump_zone_malloc_stats();
}
return .undefined;
}
const JSC = bun.JSC;
const JSValue = JSC.JSValue;
const JSGlobalObject = JSC.JSGlobalObject;
const JSObject = JSC.JSObject;
const std = @import("std");
const bun = @import("root").bun;
const ZigString = JSC.ZigString;

32
src/bun.js/api/crypto.zig Normal file
View File

@@ -0,0 +1,32 @@
pub fn createCryptoError(globalThis: *JSC.JSGlobalObject, err_code: u32) JSValue {
return bun.BoringSSL.ERR_toJS(globalThis, err_code);
}
pub const PasswordObject = @import("./crypto/PasswordObject.zig").PasswordObject;
pub const JSPasswordObject = @import("./crypto/PasswordObject.zig").JSPasswordObject;
pub const CryptoHasher = @import("./crypto/CryptoHasher.zig").CryptoHasher;
pub const MD4 = @import("./crypto/CryptoHasher.zig").MD4;
pub const MD5 = @import("./crypto/CryptoHasher.zig").MD5;
pub const SHA1 = @import("./crypto/CryptoHasher.zig").SHA1;
pub const SHA224 = @import("./crypto/CryptoHasher.zig").SHA224;
pub const SHA256 = @import("./crypto/CryptoHasher.zig").SHA256;
pub const SHA384 = @import("./crypto/CryptoHasher.zig").SHA384;
pub const SHA512 = @import("./crypto/CryptoHasher.zig").SHA512;
pub const SHA512_256 = @import("./crypto/CryptoHasher.zig").SHA512_256;
pub const HMAC = @import("./crypto/HMAC.zig");
pub const EVP = @import("./crypto/EVP.zig");
comptime {
CryptoHasher.Extern.@"export"();
}
const std = @import("std");
const bun = @import("root").bun;
const string = bun.string;
const strings = bun.strings;
const MutableString = bun.MutableString;
const stringZ = bun.stringZ;
const JSC = bun.JSC;
const JSValue = JSC.JSValue;
const JSGlobalObject = JSC.JSGlobalObject;

View File

@@ -0,0 +1,898 @@
pub const CryptoHasher = union(enum) {
// HMAC_CTX contains 3 EVP_CTX, so let's store it as a pointer.
hmac: ?*HMAC,
evp: EVP,
zig: CryptoHasherZig,
const Digest = EVP.Digest;
pub usingnamespace JSC.Codegen.JSCryptoHasher;
usingnamespace bun.New(@This());
// For using only CryptoHasherZig in c++
pub const Extern = struct {
fn getByName(global: *JSGlobalObject, name_bytes: [*:0]const u8, name_len: usize) callconv(.C) ?*CryptoHasher {
const name = name_bytes[0..name_len];
if (CryptoHasherZig.init(name)) |inner| {
return CryptoHasher.new(.{
.zig = inner,
});
}
const algorithm = EVP.Algorithm.map.get(name) orelse {
return null;
};
switch (algorithm) {
.ripemd160,
.blake2b256,
.blake2b512,
.@"sha512-224",
=> {
if (algorithm.md()) |md| {
return CryptoHasher.new(.{
.evp = EVP.init(algorithm, md, global.bunVM().rareData().boringEngine()),
});
}
},
else => {
return null;
},
}
return null;
}
fn getFromOther(global: *JSGlobalObject, other_handle: *CryptoHasher) callconv(.C) ?*CryptoHasher {
switch (other_handle.*) {
.zig => |other| {
const hasher = CryptoHasher.new(.{
.zig = other.copy(),
});
return hasher;
},
.evp => |other| {
return CryptoHasher.new(.{
.evp = other.copy(global.bunVM().rareData().boringEngine()) catch {
return null;
},
});
},
else => {
return null;
},
}
}
fn destroy(handle: *CryptoHasher) callconv(.C) void {
handle.finalize();
}
fn update(handle: *CryptoHasher, input_bytes: [*]const u8, input_len: usize) callconv(.C) bool {
const input = input_bytes[0..input_len];
switch (handle.*) {
.zig => {
handle.zig.update(input);
return true;
},
.evp => {
handle.evp.update(input);
return true;
},
else => {
return false;
},
}
}
fn digest(handle: *CryptoHasher, global: *JSGlobalObject, buf: [*]u8, buf_len: usize) callconv(.C) u32 {
const digest_buf = buf[0..buf_len];
switch (handle.*) {
.zig => {
const res = handle.zig.finalWithLen(digest_buf, buf_len);
return @intCast(res.len);
},
.evp => {
const res = handle.evp.final(global.bunVM().rareData().boringEngine(), digest_buf);
return @intCast(res.len);
},
else => {
return 0;
},
}
}
fn getDigestSize(handle: *CryptoHasher) callconv(.C) u32 {
return switch (handle.*) {
.zig => |inner| inner.digest_length,
.evp => |inner| inner.size(),
else => 0,
};
}
pub fn @"export"() void {
@export(&CryptoHasher.Extern.getByName, .{ .name = "Bun__CryptoHasherExtern__getByName" });
@export(&CryptoHasher.Extern.getFromOther, .{ .name = "Bun__CryptoHasherExtern__getFromOther" });
@export(&CryptoHasher.Extern.destroy, .{ .name = "Bun__CryptoHasherExtern__destroy" });
@export(&CryptoHasher.Extern.update, .{ .name = "Bun__CryptoHasherExtern__update" });
@export(&CryptoHasher.Extern.digest, .{ .name = "Bun__CryptoHasherExtern__digest" });
@export(&CryptoHasher.Extern.getDigestSize, .{ .name = "Bun__CryptoHasherExtern__getDigestSize" });
}
};
pub const digest = JSC.wrapInstanceMethod(CryptoHasher, "digest_", false);
pub const hash = JSC.wrapStaticMethod(CryptoHasher, "hash_", false);
fn throwHmacConsumed(globalThis: *JSC.JSGlobalObject) bun.JSError {
return globalThis.throw("HMAC has been consumed and is no longer usable", .{});
}
pub fn getByteLength(this: *CryptoHasher, globalThis: *JSC.JSGlobalObject) JSC.JSValue {
return JSC.JSValue.jsNumber(switch (this.*) {
.evp => |*inner| inner.size(),
.hmac => |inner| if (inner) |hmac| hmac.size() else {
throwHmacConsumed(globalThis) catch return .zero;
},
.zig => |*inner| inner.digest_length,
});
}
pub fn getAlgorithm(this: *CryptoHasher, globalObject: *JSC.JSGlobalObject) JSC.JSValue {
return switch (this.*) {
inline .evp, .zig => |*inner| ZigString.fromUTF8(bun.asByteSlice(@tagName(inner.algorithm))).toJS(globalObject),
.hmac => |inner| if (inner) |hmac| ZigString.fromUTF8(bun.asByteSlice(@tagName(hmac.algorithm))).toJS(globalObject) else {
throwHmacConsumed(globalObject) catch return .zero;
},
};
}
pub fn getAlgorithms(
globalThis_: *JSC.JSGlobalObject,
_: JSValue,
_: JSValue,
) JSC.JSValue {
return bun.String.toJSArray(globalThis_, &EVP.Algorithm.names.values);
}
fn hashToEncoding(globalThis: *JSGlobalObject, evp: *EVP, input: JSC.Node.BlobOrStringOrBuffer, encoding: JSC.Node.Encoding) bun.JSError!JSC.JSValue {
var output_digest_buf: Digest = undefined;
defer input.deinit();
if (input == .blob and input.blob.isBunFile()) {
return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{});
}
const len = evp.hash(globalThis.bunVM().rareData().boringEngine(), input.slice(), &output_digest_buf) orelse {
const err = BoringSSL.ERR_get_error();
const instance = createCryptoError(globalThis, err);
BoringSSL.ERR_clear_error();
return globalThis.throwValue(instance);
};
return encoding.encodeWithMaxSize(globalThis, BoringSSL.EVP_MAX_MD_SIZE, output_digest_buf[0..len]);
}
fn hashToBytes(globalThis: *JSGlobalObject, evp: *EVP, input: JSC.Node.BlobOrStringOrBuffer, output: ?JSC.ArrayBuffer) bun.JSError!JSC.JSValue {
var output_digest_buf: Digest = undefined;
var output_digest_slice: []u8 = &output_digest_buf;
defer input.deinit();
if (input == .blob and input.blob.isBunFile()) {
return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{});
}
if (output) |output_buf| {
const size = evp.size();
var bytes = output_buf.byteSlice();
if (bytes.len < size) {
return globalThis.throwInvalidArguments("TypedArray must be at least {d} bytes", .{size});
}
output_digest_slice = bytes[0..size];
}
const len = evp.hash(globalThis.bunVM().rareData().boringEngine(), input.slice(), output_digest_slice) orelse {
const err = BoringSSL.ERR_get_error();
const instance = createCryptoError(globalThis, err);
BoringSSL.ERR_clear_error();
return globalThis.throwValue(instance);
};
if (output) |output_buf| {
return output_buf.value;
} else {
// Clone to GC-managed memory
return JSC.ArrayBuffer.createBuffer(globalThis, output_digest_slice[0..len]);
}
}
pub fn hash_(
globalThis: *JSGlobalObject,
algorithm: ZigString,
input: JSC.Node.BlobOrStringOrBuffer,
output: ?JSC.Node.StringOrBuffer,
) bun.JSError!JSC.JSValue {
var evp = EVP.byName(algorithm, globalThis) orelse return try CryptoHasherZig.hashByName(globalThis, algorithm, input, output) orelse {
return globalThis.throwInvalidArguments("Unsupported algorithm \"{any}\"", .{algorithm});
};
defer evp.deinit();
if (output) |string_or_buffer| {
switch (string_or_buffer) {
inline else => |*str| {
defer str.deinit();
const encoding = JSC.Node.Encoding.from(str.slice()) orelse {
return globalThis.ERR_INVALID_ARG_VALUE("Unknown encoding: {s}", .{str.slice()}).throw();
};
return hashToEncoding(globalThis, &evp, input, encoding);
},
.buffer => |buffer| {
return hashToBytes(globalThis, &evp, input, buffer.buffer);
},
}
} else {
return hashToBytes(globalThis, &evp, input, null);
}
}
// Bun.CryptoHasher(algorithm, hmacKey?: string | Buffer)
pub fn constructor(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!*CryptoHasher {
const arguments = callframe.arguments_old(2);
if (arguments.len == 0) {
return globalThis.throwInvalidArguments("Expected an algorithm name as an argument", .{});
}
const algorithm_name = arguments.ptr[0];
if (algorithm_name.isEmptyOrUndefinedOrNull() or !algorithm_name.isString()) {
return globalThis.throwInvalidArguments("algorithm must be a string", .{});
}
const algorithm = try algorithm_name.getZigString(globalThis);
if (algorithm.len == 0) {
return globalThis.throwInvalidArguments("Invalid algorithm name", .{});
}
const hmac_value = arguments.ptr[1];
var hmac_key: ?JSC.Node.StringOrBuffer = null;
defer {
if (hmac_key) |*key| {
key.deinit();
}
}
if (!hmac_value.isEmptyOrUndefinedOrNull()) {
hmac_key = try JSC.Node.StringOrBuffer.fromJS(globalThis, bun.default_allocator, hmac_value) orelse {
return globalThis.throwInvalidArguments("key must be a string or buffer", .{});
};
}
return CryptoHasher.new(brk: {
if (hmac_key) |*key| {
const chosen_algorithm = try algorithm_name.toEnumFromMap(globalThis, "algorithm", EVP.Algorithm, EVP.Algorithm.map);
if (chosen_algorithm == .ripemd160) {
// crashes at runtime.
return globalThis.throw("ripemd160 is not supported", .{});
}
break :brk .{
.hmac = HMAC.init(chosen_algorithm, key.slice()) orelse {
if (!globalThis.hasException()) {
const err = BoringSSL.ERR_get_error();
if (err != 0) {
const instance = createCryptoError(globalThis, err);
BoringSSL.ERR_clear_error();
return globalThis.throwValue(instance);
} else {
return globalThis.throwTODO("HMAC is not supported for this algorithm yet");
}
}
return error.JSError;
},
};
}
break :brk .{
.evp = EVP.byName(algorithm, globalThis) orelse return CryptoHasherZig.constructor(algorithm) orelse {
return globalThis.throwInvalidArguments("Unsupported algorithm {any}", .{algorithm});
},
};
});
}
pub fn getter(
globalObject: *JSC.JSGlobalObject,
_: *JSC.JSObject,
) JSC.JSValue {
return CryptoHasher.getConstructor(globalObject);
}
pub fn update(this: *CryptoHasher, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
const thisValue = callframe.this();
const arguments = callframe.arguments_old(2);
const input = arguments.ptr[0];
if (input.isEmptyOrUndefinedOrNull()) {
return globalThis.throwInvalidArguments("expected blob, string or buffer", .{});
}
const encoding = arguments.ptr[1];
const buffer = try JSC.Node.BlobOrStringOrBuffer.fromJSWithEncodingValue(globalThis, globalThis.bunVM().allocator, input, encoding) orelse {
if (!globalThis.hasException()) return globalThis.throwInvalidArguments("expected blob, string or buffer", .{});
return error.JSError;
};
defer buffer.deinit();
if (buffer == .blob and buffer.blob.isBunFile()) {
return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{});
}
switch (this.*) {
.evp => |*inner| {
inner.update(buffer.slice());
const err = BoringSSL.ERR_get_error();
if (err != 0) {
const instance = createCryptoError(globalThis, err);
BoringSSL.ERR_clear_error();
return globalThis.throwValue(instance);
}
},
.hmac => |inner| {
const hmac = inner orelse {
return throwHmacConsumed(globalThis);
};
hmac.update(buffer.slice());
const err = BoringSSL.ERR_get_error();
if (err != 0) {
const instance = createCryptoError(globalThis, err);
BoringSSL.ERR_clear_error();
return globalThis.throwValue(instance);
}
},
.zig => |*inner| {
inner.update(buffer.slice());
return thisValue;
},
}
return thisValue;
}
pub fn copy(
this: *CryptoHasher,
globalObject: *JSC.JSGlobalObject,
_: *JSC.CallFrame,
) bun.JSError!JSC.JSValue {
var new: CryptoHasher = undefined;
switch (this.*) {
.evp => |*inner| {
new = .{ .evp = inner.copy(globalObject.bunVM().rareData().boringEngine()) catch bun.outOfMemory() };
},
.hmac => |inner| {
const hmac = inner orelse {
return throwHmacConsumed(globalObject);
};
new = .{
.hmac = hmac.copy() catch {
const err = createCryptoError(globalObject, BoringSSL.ERR_get_error());
BoringSSL.ERR_clear_error();
return globalObject.throwValue(err);
},
};
},
.zig => |*inner| {
new = .{ .zig = inner.copy() };
},
}
return CryptoHasher.new(new).toJS(globalObject);
}
pub fn digest_(this: *CryptoHasher, globalThis: *JSGlobalObject, output: ?JSC.Node.StringOrBuffer) bun.JSError!JSC.JSValue {
if (output) |string_or_buffer| {
switch (string_or_buffer) {
inline else => |*str| {
defer str.deinit();
const encoding = JSC.Node.Encoding.from(str.slice()) orelse {
return globalThis.ERR_INVALID_ARG_VALUE("Unknown encoding: {s}", .{str.slice()}).throw();
};
return this.digestToEncoding(globalThis, encoding);
},
.buffer => |buffer| {
return this.digestToBytes(
globalThis,
buffer.buffer,
);
},
}
} else {
return this.digestToBytes(globalThis, null);
}
}
fn digestToBytes(this: *CryptoHasher, globalThis: *JSGlobalObject, output: ?JSC.ArrayBuffer) bun.JSError!JSC.JSValue {
var output_digest_buf: EVP.Digest = undefined;
var output_digest_slice: []u8 = &output_digest_buf;
if (output) |output_buf| {
var bytes = output_buf.byteSlice();
if (bytes.len < output_digest_buf.len) {
return globalThis.throwInvalidArguments(comptime std.fmt.comptimePrint("TypedArray must be at least {d} bytes", .{output_digest_buf.len}), .{});
}
output_digest_slice = bytes[0..bytes.len];
} else {
output_digest_buf = std.mem.zeroes(EVP.Digest);
}
const result = this.final(globalThis, output_digest_slice) catch return .zero;
if (globalThis.hasException()) {
return error.JSError;
}
if (output) |output_buf| {
return output_buf.value;
} else {
// Clone to GC-managed memory
return JSC.ArrayBuffer.createBuffer(globalThis, result);
}
}
fn digestToEncoding(this: *CryptoHasher, globalThis: *JSGlobalObject, encoding: JSC.Node.Encoding) bun.JSError!JSC.JSValue {
var output_digest_buf: EVP.Digest = std.mem.zeroes(EVP.Digest);
const output_digest_slice: []u8 = &output_digest_buf;
const out = this.final(globalThis, output_digest_slice) catch return .zero;
if (globalThis.hasException()) {
return error.JSError;
}
return encoding.encodeWithMaxSize(globalThis, BoringSSL.EVP_MAX_MD_SIZE, out);
}
fn final(this: *CryptoHasher, globalThis: *JSGlobalObject, output_digest_slice: []u8) bun.JSError![]u8 {
return switch (this.*) {
.hmac => |inner| brk: {
const hmac: *HMAC = inner orelse {
return throwHmacConsumed(globalThis);
};
this.hmac = null;
defer hmac.deinit();
break :brk hmac.final(output_digest_slice);
},
.evp => |*inner| inner.final(globalThis.bunVM().rareData().boringEngine(), output_digest_slice),
.zig => |*inner| inner.final(output_digest_slice),
};
}
pub fn finalize(this: *CryptoHasher) void {
switch (this.*) {
.evp => |*inner| {
// https://github.com/oven-sh/bun/issues/3250
inner.deinit();
},
.zig => |*inner| {
inner.deinit();
},
.hmac => |inner| {
if (inner) |hmac| {
hmac.deinit();
}
},
}
this.destroy();
}
};
const CryptoHasherZig = struct {
algorithm: EVP.Algorithm,
state: *anyopaque,
digest_length: u8,
const algo_map = [_]struct { string, type }{
.{ "sha3-224", std.crypto.hash.sha3.Sha3_224 },
.{ "sha3-256", std.crypto.hash.sha3.Sha3_256 },
.{ "sha3-384", std.crypto.hash.sha3.Sha3_384 },
.{ "sha3-512", std.crypto.hash.sha3.Sha3_512 },
.{ "shake128", std.crypto.hash.sha3.Shake128 },
.{ "shake256", std.crypto.hash.sha3.Shake256 },
};
inline fn digestLength(Algorithm: type) comptime_int {
return switch (Algorithm) {
std.crypto.hash.sha3.Shake128 => 16,
std.crypto.hash.sha3.Shake256 => 32,
else => Algorithm.digest_length,
};
}
pub fn hashByName(globalThis: *JSGlobalObject, algorithm: ZigString, input: JSC.Node.BlobOrStringOrBuffer, output: ?JSC.Node.StringOrBuffer) bun.JSError!?JSC.JSValue {
inline for (algo_map) |item| {
if (bun.strings.eqlComptime(algorithm.slice(), item[0])) {
return try hashByNameInner(globalThis, item[1], input, output);
}
}
return null;
}
fn hashByNameInner(globalThis: *JSGlobalObject, comptime Algorithm: type, input: JSC.Node.BlobOrStringOrBuffer, output: ?JSC.Node.StringOrBuffer) bun.JSError!JSC.JSValue {
if (output) |string_or_buffer| {
switch (string_or_buffer) {
inline else => |*str| {
defer str.deinit();
const encoding = JSC.Node.Encoding.from(str.slice()) orelse {
return globalThis.ERR_INVALID_ARG_VALUE("Unknown encoding: {s}", .{str.slice()}).throw();
};
if (encoding == .buffer) {
return hashByNameInnerToBytes(globalThis, Algorithm, input, null);
}
return hashByNameInnerToString(globalThis, Algorithm, input, encoding);
},
.buffer => |buffer| {
return hashByNameInnerToBytes(globalThis, Algorithm, input, buffer.buffer);
},
}
}
return hashByNameInnerToBytes(globalThis, Algorithm, input, null);
}
fn hashByNameInnerToString(globalThis: *JSGlobalObject, comptime Algorithm: type, input: JSC.Node.BlobOrStringOrBuffer, encoding: JSC.Node.Encoding) bun.JSError!JSC.JSValue {
defer input.deinit();
if (input == .blob and input.blob.isBunFile()) {
return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{});
}
var h = Algorithm.init(.{});
h.update(input.slice());
var out: [digestLength(Algorithm)]u8 = undefined;
h.final(&out);
return encoding.encodeWithSize(globalThis, digestLength(Algorithm), &out);
}
fn hashByNameInnerToBytes(globalThis: *JSGlobalObject, comptime Algorithm: type, input: JSC.Node.BlobOrStringOrBuffer, output: ?JSC.ArrayBuffer) bun.JSError!JSC.JSValue {
defer input.deinit();
if (input == .blob and input.blob.isBunFile()) {
return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{});
}
var h = Algorithm.init(.{});
const digest_length_comptime = digestLength(Algorithm);
if (output) |output_buf| {
if (output_buf.byteSlice().len < digest_length_comptime) {
return globalThis.throwInvalidArguments("TypedArray must be at least {d} bytes", .{digest_length_comptime});
}
}
h.update(input.slice());
if (output) |output_buf| {
h.final(output_buf.slice()[0..digest_length_comptime]);
return output_buf.value;
} else {
var out: [digestLength(Algorithm)]u8 = undefined;
h.final(&out);
// Clone to GC-managed memory
return JSC.ArrayBuffer.createBuffer(globalThis, &out);
}
}
fn constructor(algorithm: ZigString) ?*CryptoHasher {
inline for (algo_map) |item| {
if (bun.strings.eqlComptime(algorithm.slice(), item[0])) {
return CryptoHasher.new(.{ .zig = .{
.algorithm = @field(EVP.Algorithm, item[0]),
.state = bun.new(item[1], item[1].init(.{})),
.digest_length = digestLength(item[1]),
} });
}
}
return null;
}
pub fn init(algorithm: []const u8) ?CryptoHasherZig {
inline for (algo_map) |item| {
const name, const T = item;
if (bun.strings.eqlComptime(algorithm, name)) {
const handle: CryptoHasherZig = .{
.algorithm = @field(EVP.Algorithm, name),
.state = bun.new(T, T.init(.{})),
.digest_length = digestLength(T),
};
return handle;
}
}
return null;
}
fn update(self: *CryptoHasherZig, bytes: []const u8) void {
inline for (algo_map) |item| {
if (self.algorithm == @field(EVP.Algorithm, item[0])) {
return item[1].update(@ptrCast(@alignCast(self.state)), bytes);
}
}
@panic("unreachable");
}
fn copy(self: *const CryptoHasherZig) CryptoHasherZig {
inline for (algo_map) |item| {
if (self.algorithm == @field(EVP.Algorithm, item[0])) {
return .{
.algorithm = self.algorithm,
.state = bun.dupe(item[1], @ptrCast(@alignCast(self.state))),
.digest_length = self.digest_length,
};
}
}
@panic("unreachable");
}
fn finalWithLen(self: *CryptoHasherZig, output_digest_slice: []u8, res_len: usize) []u8 {
inline for (algo_map) |pair| {
const name, const T = pair;
if (self.algorithm == @field(EVP.Algorithm, name)) {
T.final(@ptrCast(@alignCast(self.state)), @ptrCast(output_digest_slice));
const reset: *T = @ptrCast(@alignCast(self.state));
reset.* = T.init(.{});
return output_digest_slice[0..res_len];
}
}
@panic("unreachable");
}
fn final(self: *CryptoHasherZig, output_digest_slice: []u8) []u8 {
return self.finalWithLen(output_digest_slice, self.digest_length);
}
fn deinit(self: *CryptoHasherZig) void {
inline for (algo_map) |item| {
if (self.algorithm == @field(EVP.Algorithm, item[0])) {
return bun.destroy(@as(*item[1], @ptrCast(@alignCast(self.state))));
}
}
@panic("unreachable");
}
};
fn StaticCryptoHasher(comptime Hasher: type, comptime name: [:0]const u8) type {
return struct {
hashing: Hasher = Hasher{},
digested: bool = false,
const ThisHasher = @This();
pub usingnamespace @field(JSC.Codegen, "JS" ++ name);
pub const digest = JSC.wrapInstanceMethod(ThisHasher, "digest_", false);
pub const hash = JSC.wrapStaticMethod(ThisHasher, "hash_", false);
pub fn getByteLength(
_: *@This(),
_: *JSC.JSGlobalObject,
) JSC.JSValue {
return JSC.JSValue.jsNumber(@as(u16, Hasher.digest));
}
pub fn getByteLengthStatic(
_: *JSC.JSGlobalObject,
_: JSValue,
_: JSValue,
) JSC.JSValue {
return JSC.JSValue.jsNumber(@as(u16, Hasher.digest));
}
fn hashToEncoding(globalThis: *JSGlobalObject, input: JSC.Node.BlobOrStringOrBuffer, encoding: JSC.Node.Encoding) bun.JSError!JSC.JSValue {
var output_digest_buf: Hasher.Digest = undefined;
if (input == .blob and input.blob.isBunFile()) {
return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{});
}
if (comptime @typeInfo(@TypeOf(Hasher.hash)).@"fn".params.len == 3) {
Hasher.hash(input.slice(), &output_digest_buf, JSC.VirtualMachine.get().rareData().boringEngine());
} else {
Hasher.hash(input.slice(), &output_digest_buf);
}
return encoding.encodeWithSize(globalThis, Hasher.digest, &output_digest_buf);
}
fn hashToBytes(globalThis: *JSGlobalObject, input: JSC.Node.BlobOrStringOrBuffer, output: ?JSC.ArrayBuffer) bun.JSError!JSC.JSValue {
var output_digest_buf: Hasher.Digest = undefined;
var output_digest_slice: *Hasher.Digest = &output_digest_buf;
if (output) |output_buf| {
var bytes = output_buf.byteSlice();
if (bytes.len < Hasher.digest) {
return globalThis.throwInvalidArguments(comptime std.fmt.comptimePrint("TypedArray must be at least {d} bytes", .{Hasher.digest}), .{});
}
output_digest_slice = bytes[0..Hasher.digest];
}
if (comptime @typeInfo(@TypeOf(Hasher.hash)).@"fn".params.len == 3) {
Hasher.hash(input.slice(), output_digest_slice, JSC.VirtualMachine.get().rareData().boringEngine());
} else {
Hasher.hash(input.slice(), output_digest_slice);
}
if (output) |output_buf| {
return output_buf.value;
} else {
var array_buffer_out = JSC.ArrayBuffer.fromBytes(bun.default_allocator.dupe(u8, output_digest_slice) catch unreachable, .Uint8Array);
return array_buffer_out.toJSUnchecked(globalThis, null);
}
}
pub fn hash_(
globalThis: *JSGlobalObject,
input: JSC.Node.BlobOrStringOrBuffer,
output: ?JSC.Node.StringOrBuffer,
) bun.JSError!JSC.JSValue {
defer input.deinit();
if (input == .blob and input.blob.isBunFile()) {
return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{});
}
if (output) |string_or_buffer| {
switch (string_or_buffer) {
inline else => |*str| {
defer str.deinit();
const encoding = JSC.Node.Encoding.from(str.slice()) orelse {
return globalThis.ERR_INVALID_ARG_VALUE("Unknown encoding: {s}", .{str.slice()}).throw();
};
return hashToEncoding(globalThis, input, encoding);
},
.buffer => |buffer| {
return hashToBytes(globalThis, input, buffer.buffer);
},
}
} else {
return hashToBytes(globalThis, input, null);
}
}
pub fn constructor(_: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!*@This() {
const this = try bun.default_allocator.create(@This());
this.* = .{ .hashing = Hasher.init() };
return this;
}
pub fn getter(
globalObject: *JSC.JSGlobalObject,
_: *JSC.JSObject,
) JSC.JSValue {
return ThisHasher.getConstructor(globalObject);
}
pub fn update(this: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
if (this.digested) {
return globalThis.ERR_INVALID_STATE(name ++ " hasher already digested, create a new instance to update", .{}).throw();
}
const thisValue = callframe.this();
const input = callframe.argument(0);
const buffer = try JSC.Node.BlobOrStringOrBuffer.fromJS(globalThis, globalThis.bunVM().allocator, input) orelse {
return globalThis.throwInvalidArguments("expected blob or string or buffer", .{});
};
defer buffer.deinit();
if (buffer == .blob and buffer.blob.isBunFile()) {
return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{});
}
this.hashing.update(buffer.slice());
return thisValue;
}
pub fn digest_(
this: *@This(),
globalThis: *JSGlobalObject,
output: ?JSC.Node.StringOrBuffer,
) bun.JSError!JSC.JSValue {
if (this.digested) {
return globalThis.ERR_INVALID_STATE(name ++ " hasher already digested, create a new instance to digest again", .{}).throw();
}
if (output) |*string_or_buffer| {
switch (string_or_buffer.*) {
inline else => |*str| {
defer str.deinit();
const encoding = JSC.Node.Encoding.from(str.slice()) orelse {
return globalThis.ERR_INVALID_ARG_VALUE("Unknown encoding: {s}", .{str.slice()}).throw();
};
return this.digestToEncoding(globalThis, encoding);
},
.buffer => |*buffer| {
return this.digestToBytes(
globalThis,
buffer.buffer,
);
},
}
} else {
return this.digestToBytes(globalThis, null);
}
}
fn digestToBytes(this: *@This(), globalThis: *JSGlobalObject, output: ?JSC.ArrayBuffer) bun.JSError!JSC.JSValue {
var output_digest_buf: Hasher.Digest = undefined;
var output_digest_slice: *Hasher.Digest = &output_digest_buf;
if (output) |output_buf| {
var bytes = output_buf.byteSlice();
if (bytes.len < Hasher.digest) {
return globalThis.throwInvalidArguments(comptime std.fmt.comptimePrint("TypedArray must be at least {d} bytes", .{Hasher.digest}), .{});
}
output_digest_slice = bytes[0..Hasher.digest];
} else {
output_digest_buf = std.mem.zeroes(Hasher.Digest);
}
this.hashing.final(output_digest_slice);
this.digested = true;
if (output) |output_buf| {
return output_buf.value;
} else {
var array_buffer_out = JSC.ArrayBuffer.fromBytes(bun.default_allocator.dupe(u8, &output_digest_buf) catch unreachable, .Uint8Array);
return array_buffer_out.toJSUnchecked(globalThis, null);
}
}
fn digestToEncoding(this: *@This(), globalThis: *JSGlobalObject, encoding: JSC.Node.Encoding) JSC.JSValue {
var output_digest_buf: Hasher.Digest = comptime brk: {
var bytes: Hasher.Digest = undefined;
var i: usize = 0;
while (i < Hasher.digest) {
bytes[i] = 0;
i += 1;
}
break :brk bytes;
};
const output_digest_slice: *Hasher.Digest = &output_digest_buf;
this.hashing.final(output_digest_slice);
this.digested = true;
return encoding.encodeWithSize(globalThis, Hasher.digest, output_digest_slice);
}
pub fn finalize(this: *@This()) void {
VirtualMachine.get().allocator.destroy(this);
}
};
}
pub const MD4 = StaticCryptoHasher(Hashers.MD4, "MD4");
pub const MD5 = StaticCryptoHasher(Hashers.MD5, "MD5");
pub const SHA1 = StaticCryptoHasher(Hashers.SHA1, "SHA1");
pub const SHA224 = StaticCryptoHasher(Hashers.SHA224, "SHA224");
pub const SHA256 = StaticCryptoHasher(Hashers.SHA256, "SHA256");
pub const SHA384 = StaticCryptoHasher(Hashers.SHA384, "SHA384");
pub const SHA512 = StaticCryptoHasher(Hashers.SHA512, "SHA512");
pub const SHA512_256 = StaticCryptoHasher(Hashers.SHA512_256, "SHA512_256");
const Crypto = JSC.API.Bun.Crypto;
const Hashers = @import("../../../sha.zig");
const std = @import("std");
const bun = @import("root").bun;
const string = bun.string;
const strings = bun.strings;
const MutableString = bun.MutableString;
const stringZ = bun.stringZ;
const default_allocator = bun.default_allocator;
const JSC = bun.JSC;
const Async = bun.Async;
const ZigString = JSC.ZigString;
const JSValue = JSC.JSValue;
const JSGlobalObject = JSC.JSGlobalObject;
const CallFrame = JSC.CallFrame;
const assert = bun.assert;
const HMAC = Crypto.HMAC;
const EVP = Crypto.EVP;
const BoringSSL = bun.BoringSSL.c;
const createCryptoError = Crypto.createCryptoError;
const VirtualMachine = JSC.VirtualMachine;

View File

@@ -0,0 +1,221 @@
ctx: BoringSSL.EVP_MD_CTX = undefined,
md: *const BoringSSL.EVP_MD = undefined,
algorithm: Algorithm,
// we do this to avoid asking BoringSSL what the digest name is
// because that API is confusing
pub const Algorithm = enum {
// @"DSA-SHA",
// @"DSA-SHA1",
// @"MD5-SHA1",
// @"RSA-MD5",
// @"RSA-RIPEMD160",
// @"RSA-SHA1",
// @"RSA-SHA1-2",
// @"RSA-SHA224",
// @"RSA-SHA256",
// @"RSA-SHA384",
// @"RSA-SHA512",
// @"ecdsa-with-SHA1",
blake2b256,
blake2b512,
md4,
md5,
ripemd160,
sha1,
sha224,
sha256,
sha384,
sha512,
@"sha512-224",
@"sha512-256",
@"sha3-224",
@"sha3-256",
@"sha3-384",
@"sha3-512",
shake128,
shake256,
pub fn md(this: Algorithm) ?*const BoringSSL.EVP_MD {
return switch (this) {
.blake2b256 => BoringSSL.EVP_blake2b256(),
.blake2b512 => BoringSSL.EVP_blake2b512(),
.md4 => BoringSSL.EVP_md4(),
.md5 => BoringSSL.EVP_md5(),
.ripemd160 => BoringSSL.EVP_ripemd160(),
.sha1 => BoringSSL.EVP_sha1(),
.sha224 => BoringSSL.EVP_sha224(),
.sha256 => BoringSSL.EVP_sha256(),
.sha384 => BoringSSL.EVP_sha384(),
.sha512 => BoringSSL.EVP_sha512(),
.@"sha512-224" => BoringSSL.EVP_sha512_224(),
.@"sha512-256" => BoringSSL.EVP_sha512_256(),
else => null,
};
}
pub const names: std.EnumArray(Algorithm, bun.String) = brk: {
var all = std.EnumArray(Algorithm, bun.String).initUndefined();
var iter = all.iterator();
while (iter.next()) |entry| {
entry.value.* = bun.String.init(@tagName(entry.key));
}
break :brk all;
};
pub const map = bun.ComptimeStringMap(Algorithm, .{
.{ "blake2b256", .blake2b256 },
.{ "blake2b512", .blake2b512 },
.{ "ripemd160", .ripemd160 },
.{ "rmd160", .ripemd160 },
.{ "md4", .md4 },
.{ "md5", .md5 },
.{ "sha1", .sha1 },
.{ "sha128", .sha1 },
.{ "sha224", .sha224 },
.{ "sha256", .sha256 },
.{ "sha384", .sha384 },
.{ "sha512", .sha512 },
.{ "sha-1", .sha1 },
.{ "sha-224", .sha224 },
.{ "sha-256", .sha256 },
.{ "sha-384", .sha384 },
.{ "sha-512", .sha512 },
.{ "sha-512/224", .@"sha512-224" },
.{ "sha-512_224", .@"sha512-224" },
.{ "sha-512224", .@"sha512-224" },
.{ "sha512-224", .@"sha512-224" },
.{ "sha-512/256", .@"sha512-256" },
.{ "sha-512_256", .@"sha512-256" },
.{ "sha-512256", .@"sha512-256" },
.{ "sha512-256", .@"sha512-256" },
.{ "sha384", .sha384 },
.{ "sha3-224", .@"sha3-224" },
.{ "sha3-256", .@"sha3-256" },
.{ "sha3-384", .@"sha3-384" },
.{ "sha3-512", .@"sha3-512" },
.{ "shake128", .shake128 },
.{ "shake256", .shake256 },
// .{ "md5-sha1", .@"MD5-SHA1" },
// .{ "dsa-sha", .@"DSA-SHA" },
// .{ "dsa-sha1", .@"DSA-SHA1" },
// .{ "ecdsa-with-sha1", .@"ecdsa-with-SHA1" },
// .{ "rsa-md5", .@"RSA-MD5" },
// .{ "rsa-sha1", .@"RSA-SHA1" },
// .{ "rsa-sha1-2", .@"RSA-SHA1-2" },
// .{ "rsa-sha224", .@"RSA-SHA224" },
// .{ "rsa-sha256", .@"RSA-SHA256" },
// .{ "rsa-sha384", .@"RSA-SHA384" },
// .{ "rsa-sha512", .@"RSA-SHA512" },
// .{ "rsa-ripemd160", .@"RSA-RIPEMD160" },
});
};
pub fn init(algorithm: Algorithm, md: *const BoringSSL.EVP_MD, engine: *BoringSSL.ENGINE) EVP {
bun.BoringSSL.load();
var ctx: BoringSSL.EVP_MD_CTX = undefined;
BoringSSL.EVP_MD_CTX_init(&ctx);
_ = BoringSSL.EVP_DigestInit_ex(&ctx, md, engine);
return .{
.ctx = ctx,
.md = md,
.algorithm = algorithm,
};
}
pub fn reset(this: *EVP, engine: *BoringSSL.ENGINE) void {
BoringSSL.ERR_clear_error();
_ = BoringSSL.EVP_DigestInit_ex(&this.ctx, this.md, engine);
}
pub fn hash(this: *EVP, engine: *BoringSSL.ENGINE, input: []const u8, output: []u8) ?u32 {
BoringSSL.ERR_clear_error();
var outsize: c_uint = @min(@as(u16, @truncate(output.len)), this.size());
if (BoringSSL.EVP_Digest(input.ptr, input.len, output.ptr, &outsize, this.md, engine) != 1) {
return null;
}
return outsize;
}
pub fn final(this: *EVP, engine: *BoringSSL.ENGINE, output: []u8) []u8 {
BoringSSL.ERR_clear_error();
var outsize: u32 = @min(@as(u16, @truncate(output.len)), this.size());
if (BoringSSL.EVP_DigestFinal_ex(
&this.ctx,
output.ptr,
&outsize,
) != 1) {
return "";
}
this.reset(engine);
return output[0..outsize];
}
pub fn update(this: *EVP, input: []const u8) void {
BoringSSL.ERR_clear_error();
_ = BoringSSL.EVP_DigestUpdate(&this.ctx, input.ptr, input.len);
}
pub fn size(this: *const EVP) u16 {
return @as(u16, @truncate(BoringSSL.EVP_MD_CTX_size(&this.ctx)));
}
pub fn copy(this: *const EVP, engine: *BoringSSL.ENGINE) error{OutOfMemory}!EVP {
BoringSSL.ERR_clear_error();
var new = init(this.algorithm, this.md, engine);
if (BoringSSL.EVP_MD_CTX_copy_ex(&new.ctx, &this.ctx) == 0) {
return error.OutOfMemory;
}
return new;
}
pub fn byNameAndEngine(engine: *BoringSSL.ENGINE, name: []const u8) ?EVP {
if (Algorithm.map.getWithEql(name, strings.eqlCaseInsensitiveASCIIIgnoreLength)) |algorithm| {
if (algorithm.md()) |md| {
return EVP.init(algorithm, md, engine);
}
if (BoringSSL.EVP_get_digestbyname(@tagName(algorithm))) |md| {
return EVP.init(algorithm, md, engine);
}
}
return null;
}
pub fn byName(name: ZigString, global: *JSC.JSGlobalObject) ?EVP {
var name_str = name.toSlice(global.allocator());
defer name_str.deinit();
return byNameAndEngine(global.bunVM().rareData().boringEngine(), name_str.slice());
}
pub fn deinit(this: *EVP) void {
// https://github.com/oven-sh/bun/issues/3250
_ = BoringSSL.EVP_MD_CTX_cleanup(&this.ctx);
}
pub const Digest = [BoringSSL.EVP_MAX_MD_SIZE]u8;
pub const PBKDF2 = @import("./PBKDF2.zig");
pub const pbkdf2 = PBKDF2.pbkdf2;
const std = @import("std");
const bun = @import("root").bun;
const string = bun.string;
const strings = bun.strings;
const MutableString = bun.MutableString;
const stringZ = bun.stringZ;
const default_allocator = bun.default_allocator;
const JSC = bun.JSC;
const Async = bun.Async;
const ZigString = JSC.ZigString;
const JSValue = JSC.JSValue;
const JSGlobalObject = JSC.JSGlobalObject;
const CallFrame = JSC.CallFrame;
const assert = bun.assert;
const EVP = @This();
const BoringSSL = bun.BoringSSL.c;

View File

@@ -0,0 +1,56 @@
ctx: BoringSSL.HMAC_CTX,
algorithm: EVP.Algorithm,
pub usingnamespace bun.New(@This());
pub fn init(algorithm: EVP.Algorithm, key: []const u8) ?*HMAC {
const md = algorithm.md() orelse return null;
var ctx: BoringSSL.HMAC_CTX = undefined;
BoringSSL.HMAC_CTX_init(&ctx);
if (BoringSSL.HMAC_Init_ex(&ctx, key.ptr, @intCast(key.len), md, null) != 1) {
BoringSSL.HMAC_CTX_cleanup(&ctx);
return null;
}
return HMAC.new(.{
.ctx = ctx,
.algorithm = algorithm,
});
}
pub fn update(this: *HMAC, data: []const u8) void {
_ = BoringSSL.HMAC_Update(&this.ctx, data.ptr, data.len);
}
pub fn size(this: *const HMAC) usize {
return BoringSSL.HMAC_size(&this.ctx);
}
pub fn copy(this: *HMAC) !*HMAC {
var ctx: BoringSSL.HMAC_CTX = undefined;
BoringSSL.HMAC_CTX_init(&ctx);
if (BoringSSL.HMAC_CTX_copy(&ctx, &this.ctx) != 1) {
BoringSSL.HMAC_CTX_cleanup(&ctx);
return error.BoringSSLError;
}
return HMAC.new(.{
.ctx = ctx,
.algorithm = this.algorithm,
});
}
pub fn final(this: *HMAC, out: []u8) []u8 {
var outlen: c_uint = undefined;
_ = BoringSSL.HMAC_Final(&this.ctx, out.ptr, &outlen);
return out[0..outlen];
}
pub fn deinit(this: *HMAC) void {
BoringSSL.HMAC_CTX_cleanup(&this.ctx);
this.destroy();
}
const bun = @import("root").bun;
const BoringSSL = bun.BoringSSL.c;
const JSC = bun.JSC;
const EVP = JSC.API.Bun.Crypto.EVP;
const HMAC = @This();

View File

@@ -0,0 +1,265 @@
password: JSC.Node.StringOrBuffer = JSC.Node.StringOrBuffer.empty,
salt: JSC.Node.StringOrBuffer = JSC.Node.StringOrBuffer.empty,
iteration_count: u32 = 1,
length: i32 = 0,
algorithm: EVP.Algorithm,
pub fn run(this: *PBKDF2, output: []u8) bool {
const password = this.password.slice();
const salt = this.salt.slice();
const algorithm = this.algorithm;
const iteration_count = this.iteration_count;
const length = this.length;
@memset(output, 0);
assert(this.length <= @as(i32, @intCast(output.len)));
BoringSSL.ERR_clear_error();
const rc = BoringSSL.PKCS5_PBKDF2_HMAC(
if (password.len > 0) password.ptr else null,
@intCast(password.len),
salt.ptr,
@intCast(salt.len),
@intCast(iteration_count),
algorithm.md().?,
@intCast(length),
output.ptr,
);
if (rc <= 0) {
return false;
}
return true;
}
pub const Job = struct {
pbkdf2: PBKDF2,
output: []u8 = &[_]u8{},
task: JSC.WorkPoolTask = .{ .callback = &runTask },
promise: JSC.JSPromise.Strong = .{},
vm: *JSC.VirtualMachine,
err: ?u32 = null,
any_task: JSC.AnyTask = undefined,
poll: Async.KeepAlive = .{},
pub usingnamespace bun.New(@This());
pub fn runTask(task: *JSC.WorkPoolTask) void {
const job: *PBKDF2.Job = @fieldParentPtr("task", task);
defer job.vm.enqueueTaskConcurrent(JSC.ConcurrentTask.create(job.any_task.task()));
job.output = bun.default_allocator.alloc(u8, @as(usize, @intCast(job.pbkdf2.length))) catch {
job.err = BoringSSL.EVP_R_MEMORY_LIMIT_EXCEEDED;
return;
};
if (!job.pbkdf2.run(job.output)) {
job.err = BoringSSL.ERR_get_error();
BoringSSL.ERR_clear_error();
bun.default_allocator.free(job.output);
job.output = &[_]u8{};
}
}
pub fn runFromJS(this: *Job) void {
defer this.deinit();
if (this.vm.isShuttingDown()) {
return;
}
const globalThis = this.vm.global;
const promise = this.promise.swap();
if (this.err) |err| {
promise.reject(globalThis, createCryptoError(globalThis, err));
return;
}
const output_slice = this.output;
assert(output_slice.len == @as(usize, @intCast(this.pbkdf2.length)));
const buffer_value = JSC.JSValue.createBuffer(globalThis, output_slice, bun.default_allocator);
if (buffer_value == .zero) {
promise.reject(globalThis, ZigString.init("Failed to create buffer").toErrorInstance(globalThis));
return;
}
this.output = &[_]u8{};
promise.resolve(globalThis, buffer_value);
}
pub fn deinit(this: *Job) void {
this.poll.unref(this.vm);
this.pbkdf2.deinitAndUnprotect();
this.promise.deinit();
bun.default_allocator.free(this.output);
this.destroy();
}
pub fn create(vm: *JSC.VirtualMachine, globalThis: *JSC.JSGlobalObject, data: *const PBKDF2) *Job {
var job = Job.new(.{
.pbkdf2 = data.*,
.vm = vm,
.any_task = undefined,
});
job.promise = JSC.JSPromise.Strong.init(globalThis);
job.any_task = JSC.AnyTask.New(@This(), &runFromJS).init(job);
job.poll.ref(vm);
JSC.WorkPool.schedule(&job.task);
return job;
}
};
pub fn deinitAndUnprotect(this: *PBKDF2) void {
this.password.deinitAndUnprotect();
this.salt.deinitAndUnprotect();
}
pub fn deinit(this: *PBKDF2) void {
this.password.deinit();
this.salt.deinit();
}
pub fn fromJS(globalThis: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame, is_async: bool) bun.JSError!PBKDF2 {
const arg0, const arg1, const arg2, const arg3, const arg4, const arg5 = callFrame.argumentsAsArray(6);
if (!arg3.isNumber()) {
return globalThis.throwInvalidArgumentTypeValue("keylen", "number", arg3);
}
const keylen_num = arg3.asNumber();
if (std.math.isInf(keylen_num) or std.math.isNan(keylen_num)) {
return globalThis.throwRangeError(keylen_num, .{
.field_name = "keylen",
.msg = "an integer",
});
}
if (keylen_num < 0 or keylen_num > std.math.maxInt(i32)) {
return globalThis.throwRangeError(keylen_num, .{ .field_name = "keylen", .min = 0, .max = std.math.maxInt(i32) });
}
const keylen: i32 = @intFromFloat(keylen_num);
if (globalThis.hasException()) {
return error.JSError;
}
if (!arg2.isAnyInt()) {
return globalThis.throwInvalidArgumentTypeValue("iterations", "number", arg2);
}
const iteration_count = arg2.coerce(i64, globalThis);
if (!globalThis.hasException() and (iteration_count < 1 or iteration_count > std.math.maxInt(i32))) {
return globalThis.throwRangeError(iteration_count, .{ .field_name = "iterations", .min = 1, .max = std.math.maxInt(i32) + 1 });
}
if (globalThis.hasException()) {
return error.JSError;
}
const algorithm = brk: {
if (!arg4.isString()) {
return globalThis.throwInvalidArgumentTypeValue("digest", "string", arg4);
}
invalid: {
switch (try EVP.Algorithm.map.fromJSCaseInsensitive(globalThis, arg4) orelse break :invalid) {
.shake128, .shake256, .@"sha3-224", .@"sha3-256", .@"sha3-384", .@"sha3-512" => break :invalid,
else => |alg| break :brk alg,
}
}
if (!globalThis.hasException()) {
const slice = try arg4.toSlice(globalThis, bun.default_allocator);
defer slice.deinit();
const name = slice.slice();
return globalThis.ERR_CRYPTO_INVALID_DIGEST("Invalid digest: {s}", .{name}).throw();
}
return error.JSError;
};
var out = PBKDF2{
.iteration_count = @intCast(iteration_count),
.length = keylen,
.algorithm = algorithm,
};
defer {
if (globalThis.hasException()) {
if (is_async)
out.deinitAndUnprotect()
else
out.deinit();
}
}
const allow_string_object = true;
out.salt = try JSC.Node.StringOrBuffer.fromJSMaybeAsync(globalThis, bun.default_allocator, arg1, is_async, allow_string_object) orelse {
return globalThis.throwInvalidArgumentTypeValue("salt", "string or buffer", arg1);
};
if (out.salt.slice().len > std.math.maxInt(i32)) {
return globalThis.throwInvalidArguments("salt is too long", .{});
}
out.password = try JSC.Node.StringOrBuffer.fromJSMaybeAsync(globalThis, bun.default_allocator, arg0, is_async, allow_string_object) orelse {
return globalThis.throwInvalidArgumentTypeValue("password", "string or buffer", arg0);
};
if (out.password.slice().len > std.math.maxInt(i32)) {
return globalThis.throwInvalidArguments("password is too long", .{});
}
if (is_async) {
if (!arg5.isFunction()) {
return globalThis.throwInvalidArgumentTypeValue("callback", "function", arg5);
}
}
return out;
}
/// For usage in Zig
pub fn pbkdf2(
output: []u8,
password: []const u8,
salt: []const u8,
iteration_count: u32,
algorithm: Algorithm,
) ?[]const u8 {
var pbk = PBKDF2{
.algorithm = algorithm,
.password = JSC.Node.StringOrBuffer{ .encoded_slice = JSC.ZigString.Slice.fromUTF8NeverFree(password) },
.salt = JSC.Node.StringOrBuffer{ .encoded_slice = JSC.ZigString.Slice.fromUTF8NeverFree(salt) },
.iteration_count = iteration_count,
.length = @intCast(output.len),
};
if (!pbk.run(output)) {
return null;
}
return output;
}
const std = @import("std");
const bun = @import("root").bun;
const string = bun.string;
const strings = bun.strings;
const MutableString = bun.MutableString;
const stringZ = bun.stringZ;
const default_allocator = bun.default_allocator;
const JSC = bun.JSC;
const Async = bun.Async;
const ZigString = JSC.ZigString;
const JSValue = JSC.JSValue;
const JSGlobalObject = JSC.JSGlobalObject;
const CallFrame = JSC.CallFrame;
const assert = bun.assert;
const EVP = JSC.API.Bun.Crypto.EVP;
const Algorithm = EVP.Algorithm;
const BoringSSL = bun.BoringSSL.c;
const createCryptoError = JSC.API.Bun.Crypto.createCryptoError;
const VirtualMachine = JSC.VirtualMachine;
const PBKDF2 = @This();

View File

@@ -0,0 +1,772 @@
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) bun.JSError!Value {
if (value.isObject()) {
if (try value.getTruthy(globalObject, "algorithm")) |algorithm_value| {
if (!algorithm_value.isString()) {
return globalObject.throwInvalidArgumentType("hash", "algorithm", "string");
}
const algorithm_string = try algorithm_value.getZigString(globalObject);
switch (PasswordObject.Algorithm.label.getWithEql(algorithm_string, JSC.ZigString.eqlComptime) orelse {
return globalObject.throwInvalidArgumentType("hash", "algorithm", unknown_password_algorithm_message);
}) {
.bcrypt => {
var algorithm = PasswordObject.Algorithm.Value{
.bcrypt = PasswordObject.Algorithm.Value.bcrpyt_default,
};
if (try value.getTruthy(globalObject, "cost")) |rounds_value| {
if (!rounds_value.isNumber()) {
return globalObject.throwInvalidArgumentType("hash", "cost", "number");
}
const rounds = rounds_value.coerce(i32, globalObject);
if (rounds < 4 or rounds > 31) {
return globalObject.throwInvalidArguments("Rounds must be between 4 and 31", .{});
}
algorithm.bcrypt = @as(u6, @intCast(rounds));
}
return algorithm;
},
inline .argon2id, .argon2d, .argon2i => |tag| {
var argon = Algorithm.Argon2Params{};
if (try value.getTruthy(globalObject, "timeCost")) |time_value| {
if (!time_value.isNumber()) {
return globalObject.throwInvalidArgumentType("hash", "timeCost", "number");
}
const time_cost = time_value.coerce(i32, globalObject);
if (time_cost < 1) {
return globalObject.throwInvalidArguments("Time cost must be greater than 0", .{});
}
argon.time_cost = @as(u32, @intCast(time_cost));
}
if (try value.getTruthy(globalObject, "memoryCost")) |memory_value| {
if (!memory_value.isNumber()) {
return globalObject.throwInvalidArgumentType("hash", "memoryCost", "number");
}
const memory_cost = memory_value.coerce(i32, globalObject);
if (memory_cost < 1) {
return globalObject.throwInvalidArguments("Memory cost must be greater than 0", .{});
}
argon.memory_cost = @as(u32, @intCast(memory_cost));
}
return @unionInit(Algorithm.Value, @tagName(tag), argon);
},
}
unreachable;
} else {
return globalObject.throwInvalidArgumentType("hash", "options.algorithm", "string");
}
} else if (value.isString()) {
const algorithm_string = try value.getZigString(globalObject);
switch (PasswordObject.Algorithm.label.getWithEql(algorithm_string, JSC.ZigString.eqlComptime) orelse {
return globalObject.throwInvalidArgumentType("hash", "algorithm", unknown_password_algorithm_message);
}) {
.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 {
return globalObject.throwInvalidArgumentType("hash", "algorithm", "string");
}
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_512 = bun.sha.SHA512.init();
defer sha_512.deinit();
sha_512.update(password);
sha_512.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,
.silently_truncate_password = true,
},
.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 => {
var password_to_use = password;
var outbuf: [bun.sha.SHA512.digest]u8 = undefined;
// 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_512 = bun.sha.SHA512.init();
defer sha_512.deinit();
sha_512.update(password);
sha_512.final(&outbuf);
password_to_use = &outbuf;
}
pwhash.bcrypt.strVerify(previous_hash, password_to_use, .{
.allocator = allocator,
.silently_truncate_password = true,
}) 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) JSC.JSValue {
var object = JSValue.createEmptyObject(globalObject, 4);
object.put(
globalObject,
ZigString.static("hash"),
JSC.createCallback(globalObject, ZigString.static("hash"), 2, JSPasswordObject__hash),
);
object.put(
globalObject,
ZigString.static("hashSync"),
JSC.createCallback(globalObject, ZigString.static("hashSync"), 2, JSPasswordObject__hashSync),
);
object.put(
globalObject,
ZigString.static("verify"),
JSC.createCallback(globalObject, ZigString.static("verify"), 2, JSPasswordObject__verify),
);
object.put(
globalObject,
ZigString.static("verifySync"),
JSC.createCallback(globalObject, ZigString.static("verifySync"), 2, JSPasswordObject__verifySync),
);
return object;
}
const HashJob = struct {
algorithm: PasswordObject.Algorithm.Value,
password: []const u8,
promise: JSC.JSPromise.Strong,
event_loop: *JSC.EventLoop,
global: *JSC.JSGlobalObject,
ref: Async.KeepAlive = .{},
task: JSC.WorkPoolTask = .{ .callback = &run },
pub usingnamespace bun.New(@This());
pub const Result = struct {
value: Value,
ref: Async.KeepAlive = .{},
task: JSC.AnyTask = undefined,
promise: JSC.JSPromise.Strong,
global: *JSC.JSGlobalObject,
pub usingnamespace bun.New(@This());
pub const Value = union(enum) {
err: PasswordObject.HashError,
hash: []const u8,
pub fn toErrorInstance(this: Value, globalObject: *JSC.JSGlobalObject) JSC.JSValue {
const error_code = std.fmt.allocPrint(bun.default_allocator, "PASSWORD{}", .{PascalToUpperUnderscoreCaseFormatter{ .input = @errorName(this.err) }}) catch bun.outOfMemory();
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).toJS(globalObject));
return instance;
}
};
pub fn runFromJS(this: *Result) void {
var promise = this.promise;
defer promise.deinit();
this.promise = .{};
this.ref.unref(this.global.bunVM());
const global = this.global;
switch (this.value) {
.err => {
const error_instance = this.value.toErrorInstance(global);
this.destroy();
promise.reject(global, error_instance);
},
.hash => |value| {
const js_string = JSC.ZigString.init(value).toJS(global);
this.destroy();
promise.resolve(global, js_string);
},
}
}
};
pub fn deinit(this: *HashJob) void {
this.promise.deinit();
bun.freeSensitive(bun.default_allocator, this.password);
this.destroy();
}
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: *HashJob = @fieldParentPtr("task", task);
var result = Result.new(.{
.value = getValue(this.password, this.algorithm),
.task = undefined,
.promise = this.promise,
.global = this.global,
.ref = this.ref,
});
this.promise = .empty;
result.task = JSC.AnyTask.New(Result, Result.runFromJS).init(result);
this.ref = .{};
this.event_loop.enqueueTaskConcurrent(JSC.ConcurrentTask.createFrom(&result.task));
this.deinit();
}
};
pub fn hash(globalObject: *JSC.JSGlobalObject, password: []const u8, algorithm: PasswordObject.Algorithm.Value, comptime sync: bool) bun.JSError!JSC.JSValue {
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);
return globalObject.throwValue(error_instance);
},
.hash => |h| {
return JSC.ZigString.init(h).toJS(globalObject);
},
}
unreachable;
}
const promise = JSC.JSPromise.Strong.init(globalObject);
var job = HashJob.new(.{
.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) bun.JSError!JSC.JSValue {
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);
return globalObject.throwValue(error_instance);
},
.pass => |pass| {
return JSC.JSValue.jsBoolean(pass);
},
}
unreachable;
}
var promise = JSC.JSPromise.Strong.init(globalObject);
const job = VerifyJob.new(.{
.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 fn JSPasswordObject__hash(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
const arguments_ = callframe.arguments_old(2);
const arguments = arguments_.ptr[0..arguments_.len];
if (arguments.len < 1) {
return globalObject.throwNotEnoughArguments("hash", 1, 0);
}
var algorithm = PasswordObject.Algorithm.Value.default;
if (arguments.len > 1 and !arguments[1].isEmptyOrUndefinedOrNull()) {
algorithm = try PasswordObject.Algorithm.Value.fromJS(globalObject, arguments[1]);
}
// TODO: this most likely should error like `hashSync` instead of stringifying.
//
// fromJS(...) orelse {
// return globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray");
// }
const password_to_hash = try JSC.Node.StringOrBuffer.fromJSToOwnedSlice(globalObject, arguments[0], bun.default_allocator);
errdefer bun.default_allocator.free(password_to_hash);
if (password_to_hash.len == 0) {
return globalObject.throwInvalidArguments("password must not be empty", .{});
}
return hash(globalObject, password_to_hash, algorithm, false);
}
// Once we have bindings generator, this should be replaced with a generated function
pub fn JSPasswordObject__hashSync(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
const arguments_ = callframe.arguments_old(2);
const arguments = arguments_.ptr[0..arguments_.len];
if (arguments.len < 1) {
return globalObject.throwNotEnoughArguments("hash", 1, 0);
}
var algorithm = PasswordObject.Algorithm.Value.default;
if (arguments.len > 1 and !arguments[1].isEmptyOrUndefinedOrNull()) {
algorithm = try PasswordObject.Algorithm.Value.fromJS(globalObject, arguments[1]);
}
var string_or_buffer = try JSC.Node.StringOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[0]) orelse {
return globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray");
};
defer string_or_buffer.deinit();
if (string_or_buffer.slice().len == 0) {
return globalObject.throwInvalidArguments("password must not be empty", .{});
}
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: Async.KeepAlive = .{},
task: JSC.WorkPoolTask = .{ .callback = &run },
pub usingnamespace bun.New(@This());
pub const Result = struct {
value: Value,
ref: Async.KeepAlive = .{},
task: JSC.AnyTask = undefined,
promise: JSC.JSPromise.Strong,
global: *JSC.JSGlobalObject,
pub usingnamespace bun.New(@This());
pub const Value = union(enum) {
err: PasswordObject.HashError,
pass: bool,
pub fn toErrorInstance(this: Value, globalObject: *JSC.JSGlobalObject) JSC.JSValue {
const error_code = std.fmt.allocPrint(bun.default_allocator, "PASSWORD{}", .{PascalToUpperUnderscoreCaseFormatter{ .input = @errorName(this.err) }}) catch bun.outOfMemory();
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).toJS(globalObject));
return instance;
}
};
pub fn runFromJS(this: *Result) void {
var promise = this.promise;
defer promise.deinit();
this.promise = .{};
this.ref.unref(this.global.bunVM());
const global = this.global;
switch (this.value) {
.err => {
const error_instance = this.value.toErrorInstance(global);
this.destroy();
promise.reject(global, error_instance);
},
.pass => |pass| {
this.destroy();
promise.resolve(global, JSC.JSValue.jsBoolean(pass));
},
}
}
};
pub fn deinit(this: *VerifyJob) void {
this.promise.deinit();
bun.freeSensitive(bun.default_allocator, this.password);
bun.freeSensitive(bun.default_allocator, this.prev_hash);
this.destroy();
}
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: *VerifyJob = @fieldParentPtr("task", task);
var result = Result.new(.{
.value = getValue(this.password, this.prev_hash, this.algorithm),
.task = undefined,
.promise = this.promise,
.global = this.global,
.ref = this.ref,
});
this.promise = .empty;
result.task = JSC.AnyTask.New(Result, Result.runFromJS).init(result);
this.ref = .{};
this.event_loop.enqueueTaskConcurrent(JSC.ConcurrentTask.createFrom(&result.task));
this.deinit();
}
};
// Once we have bindings generator, this should be replaced with a generated function
pub fn JSPasswordObject__verify(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
const arguments_ = callframe.arguments_old(3);
const arguments = arguments_.ptr[0..arguments_.len];
if (arguments.len < 2) {
return globalObject.throwNotEnoughArguments("verify", 2, 0);
}
var algorithm: ?PasswordObject.Algorithm = null;
if (arguments.len > 2 and !arguments[2].isEmptyOrUndefinedOrNull()) {
if (!arguments[2].isString()) {
return globalObject.throwInvalidArgumentType("verify", "algorithm", "string");
}
const algorithm_string = try arguments[2].getZigString(globalObject);
algorithm = PasswordObject.Algorithm.label.getWithEql(algorithm_string, JSC.ZigString.eqlComptime) orelse {
if (!globalObject.hasException()) {
return globalObject.throwInvalidArgumentType("verify", "algorithm", unknown_password_algorithm_message);
}
return error.JSError;
};
}
// TODO: this most likely should error like `verifySync` instead of stringifying.
//
// fromJS(...) orelse {
// return globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray");
// }
const owned_password = try JSC.Node.StringOrBuffer.fromJSToOwnedSlice(globalObject, arguments[0], bun.default_allocator);
// TODO: this most likely should error like `verifySync` instead of stringifying.
//
// fromJS(...) orelse {
// return globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray");
// }
const owned_hash = JSC.Node.StringOrBuffer.fromJSToOwnedSlice(globalObject, arguments[1], bun.default_allocator) catch |err| {
bun.default_allocator.free(owned_password);
return err;
};
if (owned_hash.len == 0) {
bun.default_allocator.free(owned_password);
return JSC.JSPromise.resolvedPromiseValue(globalObject, JSC.JSValue.jsBoolean(false));
}
if (owned_password.len == 0) {
bun.default_allocator.free(owned_hash);
return JSC.JSPromise.resolvedPromiseValue(globalObject, JSC.JSValue.jsBoolean(false));
}
return verify(globalObject, owned_password, owned_hash, algorithm, false);
}
// Once we have bindings generator, this should be replaced with a generated function
pub fn JSPasswordObject__verifySync(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
const arguments_ = callframe.arguments_old(3);
const arguments = arguments_.ptr[0..arguments_.len];
if (arguments.len < 2) {
return globalObject.throwNotEnoughArguments("verify", 2, 0);
}
var algorithm: ?PasswordObject.Algorithm = null;
if (arguments.len > 2 and !arguments[2].isEmptyOrUndefinedOrNull()) {
if (!arguments[2].isString()) {
return globalObject.throwInvalidArgumentType("verify", "algorithm", "string");
}
const algorithm_string = try arguments[2].getZigString(globalObject);
algorithm = PasswordObject.Algorithm.label.getWithEql(algorithm_string, JSC.ZigString.eqlComptime) orelse {
if (!globalObject.hasException()) {
return globalObject.throwInvalidArgumentType("verify", "algorithm", unknown_password_algorithm_message);
}
return .zero;
};
}
var password = try JSC.Node.StringOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[0]) orelse {
return globalObject.throwInvalidArgumentType("verify", "password", "string or TypedArray");
};
var hash_ = try JSC.Node.StringOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[1]) orelse {
password.deinit();
return globalObject.throwInvalidArgumentType("verify", "hash", "string or TypedArray");
};
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);
}
};
const std = @import("std");
const bun = @import("root").bun;
const string = bun.string;
const strings = bun.strings;
const MutableString = bun.MutableString;
const stringZ = bun.stringZ;
const default_allocator = bun.default_allocator;
const JSC = bun.JSC;
const Async = bun.Async;
const ZigString = JSC.ZigString;
const JSValue = JSC.JSValue;
const JSGlobalObject = JSC.JSGlobalObject;
const CallFrame = JSC.CallFrame;
const assert = bun.assert;
const unknown_password_algorithm_message = "unknown algorithm, expected one of: \"bcrypt\", \"argon2id\", \"argon2d\", \"argon2i\" (default is \"argon2id\")";

View File

@@ -78,3 +78,17 @@ describe("banned words", () => {
});
}
});
describe("files that must have comments at the top", () => {
const files = ["src/bun.js/api/BunObject.zig"];
for (const file of files) {
test(file, async () => {
const joined = path.join(import.meta.dir, "..", "..", file);
const content = await Bun.file(joined).text();
if (!content.startsWith("//")) {
throw new Error(`Please don't add imports to the top of ${file}. Put them at the bottom.`);
}
});
}
});