Files
bun.sh/test/js/bun/util/password.test.ts
190n de4182f305 chore: upgrade zig to 0.14.0 (#17820)
Co-authored-by: 190n <7763597+190n@users.noreply.github.com>
Co-authored-by: Zack Radisic <56137411+zackradisic@users.noreply.github.com>
Co-authored-by: pfg <pfg@pfg.pw>
Co-authored-by: pfgithub <6010774+pfgithub@users.noreply.github.com>
Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
2025-03-14 22:13:31 -07:00

297 lines
9.6 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { password } from "bun";
const placeholder = "hey";
describe("hash", () => {
describe("arguments parsing", () => {
for (let hash of [password.hash, password.hashSync]) {
test("no blank password allowed", () => {
expect(() => hash("")).toThrow("password must not be empty");
});
test("password is required", () => {
// @ts-expect-error
expect(() => hash()).toThrow();
});
test("invalid algorithm throws", () => {
// @ts-expect-error
expect(() => hash(placeholder, "scrpyt")).toThrow();
// @ts-expect-error
expect(() => hash(placeholder, 123)).toThrow();
expect(() =>
hash(placeholder, {
// @ts-expect-error
toString() {
return "scrypt";
},
}),
).toThrow();
expect(() =>
hash(placeholder, {
// @ts-expect-error
algorithm: "poop",
}),
).toThrow();
expect(() =>
hash(placeholder, {
algorithm: "bcrypt",
cost: Infinity,
}),
).toThrow();
expect(() =>
hash(placeholder, {
algorithm: "argon2id",
memoryCost: -1,
}),
).toThrow();
expect(() =>
hash(placeholder, {
algorithm: "argon2id",
timeCost: -1,
}),
).toThrow();
expect(() =>
hash(placeholder, {
algorithm: "bcrypt",
cost: -999,
}),
).toThrow();
});
test("coercion throwing doesn't crash", () => {
// @ts-expect-error
expect(() => hash(Symbol())).toThrow();
expect(() =>
// @ts-expect-error
hash({
toString() {
throw new Error("toString() failed");
},
}),
).toThrow();
});
for (let ArrayBufferView of [
Uint8Array,
Uint16Array,
Uint32Array,
Int8Array,
Int16Array,
Int32Array,
Float16Array,
Float32Array,
Float64Array,
ArrayBuffer,
]) {
test(`empty ${ArrayBufferView.name} throws`, () => {
expect(() => hash(new ArrayBufferView(0))).toThrow("password must not be empty");
});
}
}
});
});
describe("verify", () => {
describe("arguments parsing", () => {
for (let verify of [password.verify, password.verifySync]) {
test("minimum args", () => {
// @ts-expect-error
expect(() => verify()).toThrow();
// @ts-expect-error
expect(() => verify("")).toThrow();
});
test("empty values return false", async () => {
expect(await verify("", "$")).toBeFalse();
expect(await verify("$", "")).toBeFalse();
});
test("invalid algorithm throws", () => {
// @ts-expect-error
expect(() => verify(placeholder, "$", "scrpyt")).toThrow();
// @ts-expect-error
expect(() => verify(placeholder, "$", 123)).toThrow();
expect(() =>
// @ts-expect-error
verify(placeholder, "$", {
toString() {
return "scrypt";
},
}),
).toThrow();
});
test("coercion throwing doesn't crash", () => {
// @ts-expect-error
expect(() => verify(Symbol(), Symbol())).toThrow();
expect(() =>
verify(
// @ts-expect-error
{
toString() {
throw new Error("toString() failed");
},
},
"valid",
),
).toThrow();
expect(() =>
// @ts-expect-error
verify("valid", {
toString() {
throw new Error("toString() failed");
},
}),
).toThrow();
});
for (let ArrayBufferView of [
Uint8Array,
Uint16Array,
Uint32Array,
Int8Array,
Int16Array,
Int32Array,
Float32Array,
Float64Array,
ArrayBuffer,
]) {
test(`empty ${ArrayBufferView.name} returns false`, async () => {
expect(await verify(new ArrayBufferView(0), new ArrayBufferView(0))).toBeFalse();
expect(await verify("", new ArrayBufferView(0))).toBeFalse();
expect(await verify(new ArrayBufferView(0), "")).toBeFalse();
});
}
}
});
});
test("bcrypt uses the SHA-512 of passwords longer than 72 characters", async () => {
const boop = Buffer.from("hey".repeat(100));
const hashed = await password.hash(boop, "bcrypt");
expect(await password.verify(boop, hashed, "bcrypt")).toBeTrue();
const boop2 = Buffer.from("hey".repeat(24));
expect(await password.verify(boop2, hashed, "bcrypt")).toBeFalse();
});
test("bcrypt pre-hashing does not break compatibility across Bun versions", async () => {
// hash generated by Bun 1.2.4
// if we change the mechanism used to pre-hash long passwords so bcrypt doesn't truncate them,
// then this hash will not be considered valid by later versions of Bun.
const hash = "$2b$10$PsJ3/W82mzNJoP0rSblfvet2ab9jZg2aH7tIxr1B8uFLJwuWk/jTi";
const secret = "hello".repeat(100);
expect(await password.verify(secret, hash)).toBeTrue();
});
const defaultAlgorithm = "argon2id";
const algorithms = [undefined, "argon2id", "bcrypt"];
const argons = ["argon2i", "argon2id", "argon2d"];
for (let algorithmValue of algorithms) {
const prefix = algorithmValue === "bcrypt" ? "$2" : "$" + (algorithmValue || defaultAlgorithm);
describe(algorithmValue ? algorithmValue : "default", () => {
const hash = (value: string | TypedArray) => {
return algorithmValue ? password.hashSync(value, algorithmValue as any) : password.hashSync(value);
};
const hashSync = (value: string | TypedArray) => {
return algorithmValue ? password.hashSync(value, algorithmValue as any) : password.hashSync(value);
};
const verify = (pw: string | TypedArray, value: string | TypedArray) => {
return algorithmValue ? password.verify(pw, value, algorithmValue as any) : password.verify(pw, value);
};
const verifySync = (pw: string | TypedArray, value: string | TypedArray) => {
return algorithmValue ? password.verifySync(pw, value, algorithmValue as any) : password.verifySync(pw, value);
};
for (let input of [placeholder, Buffer.from(placeholder)]) {
describe(typeof input === "string" ? "string" : "buffer", () => {
test("password sync", () => {
const hashed = hashSync(input);
expect(hashed).toStartWith(prefix);
expect(verifySync(input, hashed)).toBeTrue();
expect(() => verifySync(hashed, input)).toThrow();
expect(verifySync(input + "\0", hashed)).toBeFalse();
});
describe("password", async () => {
async function runSlowTest(algorithm = algorithmValue as any) {
const hashed = await password.hash(input, algorithm);
const prefix = "$" + algorithm;
expect(hashed).toStartWith(prefix);
expect(await password.verify(input, hashed, algorithm)).toBeTrue();
expect(() => password.verify(hashed, input, algorithm)).toThrow();
expect(await password.verify(input + "\0", hashed, algorithm)).toBeFalse();
}
async function runSlowTestWithOptions(algorithmLabel: any) {
const algorithm = { algorithm: algorithmLabel, timeCost: 5, memoryCost: 8 };
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(await password.verify(input, hashed, algorithmLabel)).toBeTrue();
expect(() => password.verify(hashed, input, algorithmLabel)).toThrow();
expect(await password.verify(input + "\0", hashed, algorithmLabel)).toBeFalse();
}
async function runSlowBCryptTest() {
const algorithm = { algorithm: "bcrypt", cost: 4 } as const;
const hashed = await password.hash(input, algorithm);
const prefix = "$" + "2b";
expect(hashed).toStartWith(prefix);
expect(await password.verify(input, hashed, "bcrypt")).toBeTrue();
expect(() => password.verify(hashed, input, "bcrypt")).toThrow();
expect(await password.verify(input + "\0", hashed, "bcrypt")).toBeFalse();
}
if (algorithmValue === defaultAlgorithm) {
// these tests are very slow
// run the hashing tests in parallel
for (const a of argons) {
test(`${a}`, async () => {
await runSlowTest(a);
await runSlowTestWithOptions(a);
});
}
return;
}
async function defaultTest() {
const hashed = await hash(input);
expect(hashed).toStartWith(prefix);
expect(await verify(input, hashed)).toBeTrue();
expect(() => verify(hashed, input)).toThrow();
expect(await verify(input + "\0", hashed)).toBeFalse();
}
if (algorithmValue === "bcrypt") {
test("bcrypt", async () => {
await defaultTest();
await runSlowBCryptTest();
});
} else {
test("default", async () => {
await defaultTest();
});
}
});
});
}
});
}