- Remove .prettierrc (dead config, Biome replaces Prettier) - Rename lighthouserc.json to lighthouserc.cjs with module.exports for ESM compat - Add logger.warn to empty catch blocks in auth, register, site-settings, prisma-cache, redis, security, rate-limit - Unify ActionResult type: action-helper.ts uses 'ok' consistent with safe-action-shared.ts - Add noUnusedLocals + noUnusedParameters to tsconfig + fix 25 pre-existing unused vars - Replace barrel export src/types/index.ts with direct @/types/common imports - Make trustHost conditional (development only) in auth.ts - Add pre-flight URL validation to update-Nitrov3.sh to catch image.library.url misconfigurations - Improve NITRO_IMAGE_LIBRARY_URL content validation in pre-flight & post-compute checks
103 lines
3.3 KiB
TypeScript
103 lines
3.3 KiB
TypeScript
import { hash as bcryptHash } from "bcrypt";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
const mockEnv = vi.hoisted(() => ({
|
|
PASSWORD_HASH: undefined as string | undefined,
|
|
ARGON2_PARALLELISM: 1,
|
|
ARGON2_ITERATIONS: 4,
|
|
ARGON2_MEMORY_SIZE: 65536,
|
|
BCRYPT_ROUNDS: 12,
|
|
}));
|
|
|
|
vi.mock("@/env", () => ({
|
|
env: mockEnv,
|
|
}));
|
|
|
|
import {
|
|
checkLogin,
|
|
hashPassword,
|
|
isMd5Of,
|
|
md5Hex,
|
|
verifyPassword,
|
|
} from "./password";
|
|
|
|
describe("md5Hex", () => {
|
|
it("matches PHP md5() on canonical vectors", async () => {
|
|
expect(await md5Hex("")).toBe("d41d8cd98f00b204e9800998ecf8427e");
|
|
expect(await md5Hex("abc")).toBe("900150983cd24fb0d6963f7d28e17f72");
|
|
});
|
|
});
|
|
|
|
describe("hashPassword (default driver: bcrypt)", () => {
|
|
it("emits a PHP-style $2y$ bcrypt hash that fits varchar(64) and round-trips", async () => {
|
|
mockEnv.PASSWORD_HASH = undefined;
|
|
const h = await hashPassword("s3cret!");
|
|
expect(h).toMatch(/^\$2y\$/);
|
|
expect(h.length).toBeLessThanOrEqual(60);
|
|
expect(await verifyPassword("s3cret!", h)).toBe(true);
|
|
expect(await verifyPassword("wrong", h)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("hashPassword (PASSWORD_HASH=argon2id)", () => {
|
|
it("hashes with the AtomCMS params and round-trips", async () => {
|
|
mockEnv.PASSWORD_HASH = "argon2id";
|
|
mockEnv.ARGON2_MEMORY_SIZE = 1024;
|
|
mockEnv.ARGON2_ITERATIONS = 1;
|
|
const h = await hashPassword("s3cret!");
|
|
expect(h).toMatch(/^\$argon2id\$v=19\$m=1024,t=1,p=1\$/);
|
|
expect(await verifyPassword("s3cret!", h)).toBe(true);
|
|
expect(await verifyPassword("wrong", h)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("bcrypt", () => {
|
|
it("verifies a bcrypt hash and accepts the PHP $2y$ prefix", async () => {
|
|
mockEnv.BCRYPT_ROUNDS = 4;
|
|
const h = await bcryptHash("hunter2", 4);
|
|
expect(await verifyPassword("hunter2", h)).toBe(true);
|
|
const phpStyle = h.replace(/^\$2[ab]\$/, "$2y$");
|
|
expect(await verifyPassword("hunter2", phpStyle)).toBe(true);
|
|
expect(await verifyPassword("nope", h)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("isMd5Of", () => {
|
|
it("detects a legacy md5 password", async () => {
|
|
expect(await isMd5Of("habbo", await md5Hex("habbo"))).toBe(true);
|
|
expect(await isMd5Of("habbo", await md5Hex("other"))).toBe(false);
|
|
expect(await isMd5Of("habbo", "not-a-hash")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("checkLogin", () => {
|
|
it("upgrades a legacy md5 hash to the configured hash when conversion is enabled", async () => {
|
|
mockEnv.PASSWORD_HASH = undefined;
|
|
const stored = await md5Hex("oldpass");
|
|
const res = await checkLogin("oldpass", stored, { convertPasswords: true });
|
|
expect(res.valid).toBe(true);
|
|
expect(res.upgradedHash).toMatch(/^\$2y\$/);
|
|
expect((res.upgradedHash as string).length).toBeLessThanOrEqual(60);
|
|
expect(await verifyPassword("oldpass", res.upgradedHash as string)).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("does NOT upgrade md5 when conversion is disabled", async () => {
|
|
const stored = await md5Hex("oldpass");
|
|
const res = await checkLogin("oldpass", stored, {
|
|
convertPasswords: false,
|
|
});
|
|
expect(res.valid).toBe(false);
|
|
expect(res.upgradedHash).toBeUndefined();
|
|
});
|
|
|
|
it("validates an existing modern hash with no upgrade", async () => {
|
|
mockEnv.PASSWORD_HASH = undefined;
|
|
const stored = await hashPassword("modern");
|
|
const res = await checkLogin("modern", stored, { convertPasswords: true });
|
|
expect(res.valid).toBe(true);
|
|
expect(res.upgradedHash).toBeUndefined();
|
|
});
|
|
});
|