diff options
| author | 2023-06-04 18:20:04 -0700 | |
|---|---|---|
| committer | 2023-06-04 18:20:04 -0700 | |
| commit | 9b996e702ef32d03b01b745642292e7a747485fa (patch) | |
| tree | 4b49c2010c694afe5cf6ef738402f80e0ab7c5b8 /test | |
| parent | 2cb1376a93a59acca548769155e0d3b6110a7bd2 (diff) | |
| download | bun-9b996e702ef32d03b01b745642292e7a747485fa.tar.gz bun-9b996e702ef32d03b01b745642292e7a747485fa.tar.zst bun-9b996e702ef32d03b01b745642292e7a747485fa.zip | |
Implement `Bun.password` and `Bun.passwordSync` (#3204)
* Implement `Bun.password.{verify, hash}` and `Bun.passwordSync.{verify, hash}`
* flip the booleans
* delete unused
* Add `cost` for `"bcrypt"`, add `"memoryCost"` and `"timeCost'` for argon2, use SHA512
* Update bun.zig
---------
Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
Diffstat (limited to 'test')
| -rw-r--r-- | test/js/bun/util/password.test.ts | 279 |
1 files changed, 279 insertions, 0 deletions
diff --git a/test/js/bun/util/password.test.ts b/test/js/bun/util/password.test.ts new file mode 100644 index 000000000..67ea332c4 --- /dev/null +++ b/test/js/bun/util/password.test.ts @@ -0,0 +1,279 @@ +import { test, expect, describe } from "bun:test"; + +import { Password, password, passwordSync } from "bun"; + +const placeholder = "hey"; + +describe("hash", () => { + describe("arguments parsing", () => { + for (let { hash } of [password, passwordSync]) { + 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, + 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, passwordSync]) { + 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 longer than 72 characters is the SHA-512", async () => { + const boop = Buffer.from("hey".repeat(100)); + const hashed = await password.hash(boop, "bcrypt"); + expect(await password.verify(Bun.SHA512.hash(boop), hashed, "bcrypt")).toBeTrue(); +}); + +test("bcrypt shorter than 72 characters is NOT the SHA-512", async () => { + const boop = Buffer.from("hey".repeat(3)); + const hashed = await password.hash(boop, "bcrypt"); + expect(await password.verify(Bun.SHA512.hash(boop), hashed, "bcrypt")).toBeFalse(); +}); + +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 ? passwordSync.hash(value, algorithmValue as any) : passwordSync.hash(value); + }; + + const hashSync = (value: string | TypedArray) => { + return algorithmValue ? passwordSync.hash(value, algorithmValue as any) : passwordSync.hash(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 ? passwordSync.verify(pw, value, algorithmValue as any) : passwordSync.verify(pw, value); + }; + + for (let input of [placeholder, Buffer.from(placeholder)]) { + describe(typeof input === "string" ? "string" : "buffer", () => { + test("passwordSync", () => { + const hashed = hashSync(input); + expect(hashed).toStartWith(prefix); + expect(verifySync(input, hashed)).toBeTrue(); + expect(() => verifySync(hashed, input)).toThrow(); + expect(verifySync(input + "\0", hashed)).toBeFalse(); + }); + + test("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: 4 }; + const hashed = await password.hash(input, algorithm); + const prefix = "$" + algorithmLabel; + expect(hashed).toStartWith(prefix); + expect(hashed).toContain("t=5"); + expect(hashed).toContain("m=4"); + 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 + await Promise.all([...argons.map(runSlowTest), ...argons.map(runSlowTestWithOptions)]); + 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") { + await Promise.all([defaultTest(), runSlowBCryptTest()]); + } else { + await defaultTest(); + } + }); + }); + } + }); +} |
