diff --git a/package.json b/package.json index afa99f2f..39aedf8e 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "@prisma/client": "^7.9.0", "@sentry/nextjs": "^10.67.0", "@tanstack/react-virtual": "^3.14.6", - "bcryptjs": "^3.0.2", + "bcrypt": "^6.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -73,6 +73,7 @@ "@tailwindcss/forms": "^0.5.11", "@tailwindcss/postcss": "^4.3.3", "@tailwindcss/typography": "^0.5.20", + "@types/bcrypt": "^6.0.0", "@types/node": "^26.1.1", "@types/nodemailer": "^7.0.12", "@types/react": "^19.2.17", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7879e11a..fefebfd2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,9 +40,9 @@ importers: '@tanstack/react-virtual': specifier: ^3.14.6 version: 3.14.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - bcryptjs: - specifier: ^3.0.2 - version: 3.0.3 + bcrypt: + specifier: ^6.0.0 + version: 6.0.0 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -158,6 +158,9 @@ importers: '@tailwindcss/typography': specifier: ^0.5.20 version: 0.5.20(tailwindcss@4.3.3) + '@types/bcrypt': + specifier: ^6.0.0 + version: 6.0.0 '@types/node': specifier: ^26.1.1 version: 26.1.1 @@ -2374,6 +2377,9 @@ packages: '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@types/bcrypt@6.0.0': + resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -2750,9 +2756,9 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - bcryptjs@3.0.3: - resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} - hasBin: true + bcrypt@6.0.0: + resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==} + engines: {node: '>= 18'} better-result@2.9.2: resolution: {integrity: sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q==} @@ -3640,6 +3646,10 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-addon-api@8.9.0: + resolution: {integrity: sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==} + engines: {node: ^18 || ^20 || >= 21} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -3649,6 +3659,10 @@ packages: encoding: optional: true + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + node-releases@2.0.51: resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} engines: {node: '>=18'} @@ -6117,6 +6131,10 @@ snapshots: tslib: 2.8.1 optional: true + '@types/bcrypt@6.0.0': + dependencies: + '@types/node': 26.1.1 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -6500,7 +6518,10 @@ snapshots: baseline-browser-mapping@2.10.43: {} - bcryptjs@3.0.3: {} + bcrypt@6.0.0: + dependencies: + node-addon-api: 8.9.0 + node-gyp-build: 4.8.4 better-result@2.9.2: {} @@ -7291,10 +7312,14 @@ snapshots: node-addon-api@7.1.1: {} + node-addon-api@8.9.0: {} + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 + node-gyp-build@4.8.4: {} + node-releases@2.0.51: {} nodemailer@7.0.13: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c0ed5d60..fcfb9ce1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,6 +7,7 @@ onlyBuiltDependencies: - "@parcel/watcher" - "@swc/core" - "@sentry/cli" + - bcrypt overrides: fast-uri: "^3.1.3" diff --git a/src/actions/users.ts b/src/actions/users.ts index 1a162628..9ae2bff3 100644 --- a/src/actions/users.ts +++ b/src/actions/users.ts @@ -1,9 +1,9 @@ "use server"; import crypto from "node:crypto"; -import { hash } from "bcryptjs"; import { z } from "zod"; import { Prisma } from "@/generated/prisma/client"; +import { hashPassword } from "@/lib/auth/password"; import { PERMS } from "@/lib/permissions"; import { prisma } from "@/lib/prisma"; import { adminAction } from "@/lib/safe-action"; @@ -30,7 +30,7 @@ export const createUser = adminAction( throw new ActionError("Cannot assign rank equal or higher than your own"); } - const hashedPassword = await hash(password, 12); + const hashedPassword = await hashPassword(password); const now = Math.floor(Date.now() / 1000); try { @@ -304,7 +304,7 @@ export const resetPassword = adminAction( .randomBytes(12) .toString("base64url") .slice(0, 16); - const hashed = await hash(newPassword, 10); + const hashed = await hashPassword(newPassword); await prisma.user.update({ where: { id: ctx.data.userId }, diff --git a/src/lib/auth/password.test.ts b/src/lib/auth/password.test.ts index 26fa30d6..c70be9ad 100644 --- a/src/lib/auth/password.test.ts +++ b/src/lib/auth/password.test.ts @@ -1,4 +1,4 @@ -import { hash as bcryptHash } from "bcryptjs"; +import { hash as bcryptHash } from "bcrypt"; import { describe, expect, it } from "vitest"; import { checkLogin, @@ -50,9 +50,9 @@ describe("hashPassword (PASSWORD_HASH=argon2id)", () => { describe("bcrypt", () => { it("verifies a bcrypt hash and accepts the PHP $2y$ prefix", async () => { - const h = await bcryptHash("hunter2", 10); // bcryptjs emits $2a$ + const h = await bcryptHash("hunter2", 10); // native bcrypt emits $2a$/$2b$ expect(await verifyPassword("hunter2", h)).toBe(true); - // PHP stores $2y$ — bcryptjs must accept it as equivalent. + // PHP stores $2y$ — bcrypt must accept it as equivalent. const phpStyle = h.replace(/^\$2[ab]\$/, "$2y$"); expect(await verifyPassword("hunter2", phpStyle)).toBe(true); expect(await verifyPassword("nope", h)).toBe(false); diff --git a/src/lib/auth/password.ts b/src/lib/auth/password.ts index cfd0d0b6..8786ded7 100644 --- a/src/lib/auth/password.ts +++ b/src/lib/auth/password.ts @@ -1,5 +1,5 @@ import { randomBytes } from "node:crypto"; -import { compare as bcryptCompare, hash as bcryptHash } from "bcryptjs"; +import { compare as bcryptCompare, hash as bcryptHash } from "bcrypt"; import { argon2id, argon2Verify, md5 } from "hash-wasm"; // AtomCMS hashing (config/hashing.php): argon2id with memory=65536 KiB, time=4, @@ -49,8 +49,8 @@ export async function hashPassword(password: string): Promise { ...ARGON2_PARAMS, }); } - // bcryptjs emits $2a$/$2b$; normalise to the PHP-canonical $2y$ the emulator - // and existing AtomCMS rows use. + // native bcrypt emits $2a$/$2b$; normalise to the PHP-canonical $2y$ the + // emulator and existing AtomCMS rows use. const h = await bcryptHash(password, BCRYPT_ROUNDS); return h.replace(/^\$2[ab]\$/, "$2y$"); } @@ -84,7 +84,9 @@ export async function verifyPassword( } if (/^\$2[aby]\$/.test(stored)) { try { - return await bcryptCompare(password, stored); + // PHP/AtomCMS store $2y$; native bcrypt only accepts $2a$/$2b$. + const normalized = stored.replace(/^\$2y\$/, "$2a$"); + return await bcryptCompare(password, normalized); } catch { return false; }