diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 6d66e097c5bcbe..67ad1ab7053591 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -2017,6 +2017,11 @@ declare module "bun" { * time, given in number of iterations. */ timeCost?: number; + /** + * Number of parallel lanes/threads. + * @remarks Minimum 1. Defaults to 1 (single lane). + */ + parallelism?: number; } interface BCryptAlgorithm { diff --git a/src/bun.js/api/crypto/PasswordObject.zig b/src/bun.js/api/crypto/PasswordObject.zig index 59e07638e15854..42e08e61af5fc8 100644 --- a/src/bun.js/api/crypto/PasswordObject.zig +++ b/src/bun.js/api/crypto/PasswordObject.zig @@ -83,6 +83,23 @@ pub const PasswordObject = struct { argon.memory_cost = @as(u32, @intCast(memory_cost)); } + if (try value.getTruthy(globalObject, "parallelism")) |parallelism_value| { + if (!parallelism_value.isNumber()) { + return globalObject.throwInvalidArgumentType("hash", "parallelism", "number"); + } + const parallelism = try parallelism_value.coerce(i32, globalObject); + if (parallelism < 1) { + return globalObject.throwInvalidArguments("Parallelism must be greater than 0", .{}); + } + // Number high enough that it doesn't limit thread count + // but low enough it doesn't trigger Zig truncation + const max_p = 65535; // 2^16 - 1 + if (parallelism > max_p) { + return globalObject.throwInvalidArguments("Parallelism must be between 1 and {d}", .{max_p}); + } + argon.parallelism = @as(u24, @intCast(parallelism)); + } + return @unionInit(Algorithm.Value, @tagName(tag), argon); }, } @@ -130,12 +147,13 @@ pub const PasswordObject = 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, + parallelism: u24 = pwhash.argon2.Params.interactive_2id.p, pub fn toParams(this: Argon2Params) pwhash.argon2.Params { return pwhash.argon2.Params{ .t = this.time_cost, .m = this.memory_cost, - .p = 1, + .p = this.parallelism, }; } }; diff --git a/test/js/bun/util/password.test.ts b/test/js/bun/util/password.test.ts index 3f73068f883922..657ce93c5e9352 100644 --- a/test/js/bun/util/password.test.ts +++ b/test/js/bun/util/password.test.ts @@ -52,6 +52,13 @@ describe("hash", () => { }), ).toThrow(); + expect(() => + hash(placeholder, { + algorithm: "argon2id", + memoryCost: 0, + }), + ).toThrow(); + expect(() => hash(placeholder, { algorithm: "argon2id", @@ -59,6 +66,34 @@ describe("hash", () => { }), ).toThrow(); + expect(() => + hash(placeholder, { + algorithm: "argon2id", + timeCost: 0, + }), + ).toThrow(); + + expect(() => + hash(placeholder, { + algorithm: "argon2id", + parallelism: -1, + }), + ).toThrow(); + + expect(() => + hash(placeholder, { + algorithm: "argon2id", + parallelism: 0, + }), + ).toThrow(); + + expect(() => + hash(placeholder, { + algorithm: "argon2id", + parallelism: 65536, // 2^16 + }), + ).toThrow(); + expect(() => hash(placeholder, { algorithm: "bcrypt", @@ -237,13 +272,13 @@ for (let algorithmValue of algorithms) { } async function runSlowTestWithOptions(algorithmLabel: any) { - const algorithm = { algorithm: algorithmLabel, timeCost: 5, memoryCost: 8 }; + const algorithm = { algorithm: algorithmLabel, timeCost: 5, memoryCost: 8, parallelism: 2 }; const hashed = await password.hash(input, algorithm); const prefix = "$" + algorithmLabel; expect(hashed).toStartWith(prefix); expect(hashed).toContain("t=5"); expect(hashed).toContain("m=8"); - expect(hashed).toContain("p=1"); + expect(hashed).toContain("p=2"); expect(await password.verify(input, hashed, algorithmLabel)).toBeTrue(); expect(() => password.verify(hashed, input, algorithmLabel)).toThrow(); expect(await password.verify(input + "\0", hashed, algorithmLabel)).toBeFalse();