diff --git a/docs/guides/util/hash-a-password.mdx b/docs/guides/util/hash-a-password.mdx index ebdb75ebea9a..54f5a1fc729e 100644 --- a/docs/guides/util/hash-a-password.mdx +++ b/docs/guides/util/hash-a-password.mdx @@ -23,7 +23,7 @@ const password = "super-secure-pa$$word"; // use argon2 (default) const argonHash = await Bun.password.hash(password, { algorithm: "argon2id", - memoryCost: 8, // memory usage in kibibytes (minimum 8) + memoryCost: 8, // memory usage in kibibytes timeCost: 3, // the number of iterations }); ``` diff --git a/docs/runtime/hashing.mdx b/docs/runtime/hashing.mdx index 07b4ede03c30..611723e733ae 100644 --- a/docs/runtime/hashing.mdx +++ b/docs/runtime/hashing.mdx @@ -32,7 +32,7 @@ const password = "super-secure-pa$$word"; // use argon2 (default) const argonHash = await Bun.password.hash(password, { algorithm: "argon2id", // "argon2id" | "argon2i" | "argon2d" - memoryCost: 8, // memory usage in kibibytes (minimum 8) + memoryCost: 8, // memory usage in kibibytes timeCost: 3, // the number of iterations }); diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index d8c9402e2871..8db14963cd03 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3402,7 +3402,7 @@ declare module "bun" { algorithm: "argon2id" | "argon2d" | "argon2i"; /** - * Memory usage, in kibibytes. Minimum 8. + * Memory usage, in kibibytes. Values below 8 still use 8 KiB. */ memoryCost?: number; /** diff --git a/src/runtime/crypto/PasswordObject.rs b/src/runtime/crypto/PasswordObject.rs index 4f7d6a218504..93528240d611 100644 --- a/src/runtime/crypto/PasswordObject.rs +++ b/src/runtime/crypto/PasswordObject.rs @@ -129,18 +129,17 @@ impl AlgorithmValue { let memory_cost = memory_value.as_number(); - // argon2 requires `memoryCost >= 8 * parallelism`; - // Bun hard-codes `parallelism = 1` (see - // `Argon2Params::to_params`), so the floor is 8. - if memory_cost < 8.0 || memory_cost.is_nan() { + // Values below 8 are computed with 8 blocks (see the + // vendored rust-argon2 patch), matching pre-Rust Bun. + if memory_cost < 1.0 || memory_cost.is_nan() { return Err(global_object.throw_invalid_arguments(format_args!( - "Memory cost must be at least 8" + "Memory cost must be greater than 0" ))); } if memory_cost.fract() != 0.0 || memory_cost > f64::from(u32::MAX) { return Err(global_object.throw_invalid_arguments(format_args!( - "Memory cost must be an integer between 8 and 4294967295" + "Memory cost must be an integer between 1 and 4294967295" ))); } diff --git a/test/js/bun/util/password.test.ts b/test/js/bun/util/password.test.ts index e00e78de09e1..9d4682a1ef8d 100644 --- a/test/js/bun/util/password.test.ts +++ b/test/js/bun/util/password.test.ts @@ -112,17 +112,12 @@ describe("hash", () => { }), ).toThrow(); - // argon2 requires `memoryCost >= 8 * parallelism`; Bun hard-codes - // `parallelism = 1`, so anything below 8 must throw rather than be - // silently clamped (regression coverage for #30960). - for (const invalid of [1, 3, 7]) { - expect(() => - hash(placeholder, { - algorithm: "argon2id", - memoryCost: invalid, - }), - ).toThrow("Memory cost must be at least 8"); - } + expect(() => + hash(placeholder, { + algorithm: "argon2id", + memoryCost: 0, + }), + ).toThrow("Memory cost must be greater than 0"); expect(() => hash(placeholder, { @@ -175,14 +170,14 @@ describe("hash", () => { algorithm: "argon2id", memoryCost: 2 ** 32 + 4608, }), - ).toThrow("Memory cost must be an integer between 8 and 4294967295"); + ).toThrow("Memory cost must be an integer between 1 and 4294967295"); expect(() => hash(placeholder, { algorithm: "argon2id", memoryCost: 8.5, }), - ).toThrow("Memory cost must be an integer between 8 and 4294967295"); + ).toThrow("Memory cost must be an integer between 1 and 4294967295"); // Non-finite values: NaN and -Infinity fail the lower-bound check, // +Infinity fails the integer/upper-bound check. @@ -196,11 +191,11 @@ describe("hash", () => { ); for (const memoryCost of [NaN, -Infinity]) { expect(() => hash(placeholder, { algorithm: "argon2id", memoryCost })).toThrow( - "Memory cost must be at least 8", + "Memory cost must be greater than 0", ); } expect(() => hash(placeholder, { algorithm: "argon2id", memoryCost: Infinity })).toThrow( - "Memory cost must be an integer between 8 and 4294967295", + "Memory cost must be an integer between 1 and 4294967295", ); }); @@ -345,8 +340,8 @@ test.concurrent("argon2 memoryCost at the 8 minimum is encoded faithfully (regre expect(await password.verify("test", hashed)).toBeTrue(); }); -describe.concurrent("argon2 hashes with memoryCost below 8 from earlier Bun versions still verify", () => { - // Generated by Bun 1.3.14, which accepted memoryCost < 8. +describe.concurrent("argon2 memoryCost below 8", () => { + // Generated by Bun 1.3.14. const legacy = { argon2id: "$argon2id$v=19$m=4,t=1,p=1$jaFm03353WIBtbqnvp4hx6Pd0Pk2keYfomedORTs6bI$Q+62iWiDQhCP3VFQvnMnGptmDAHFQGqY3d/dmRcGVOw", @@ -359,7 +354,7 @@ describe.concurrent("argon2 hashes with memoryCost below 8 from earlier Bun vers }; for (const [name, hash] of Object.entries(legacy)) { - test(name, async () => { + test(`verifies legacy ${name}`, async () => { expect(await password.verify("hello", hash)).toBeTrue(); expect(await password.verify("hellp", hash)).toBeFalse(); expect(password.verifySync("hello", hash)).toBeTrue(); @@ -367,11 +362,16 @@ describe.concurrent("argon2 hashes with memoryCost below 8 from earlier Bun vers }); } - test("hashing with memoryCost below 8 is still rejected", () => { - expect(() => password.hashSync("hello", { algorithm: "argon2id", memoryCost: 4 })).toThrow( - "Memory cost must be at least 8", - ); - }); + for (const memoryCost of [1, 4, 7]) { + for (const algorithm of ["argon2id", "argon2i", "argon2d"] as const) { + test(`hashes with ${algorithm} m=${memoryCost} as written`, async () => { + const hashed = await password.hash("hello", { algorithm, memoryCost, timeCost: 1 }); + expect(hashed).toStartWith(`$${algorithm}$v=19$m=${memoryCost},t=1,p=1$`); + expect(await password.verify("hello", hashed)).toBeTrue(); + expect(await password.verify("hellp", hashed)).toBeFalse(); + }); + } + } }); const defaultAlgorithm = "argon2id";