mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
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>
297 lines
9.6 KiB
TypeScript
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();
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
});
|
|
}
|