perf: replace bcryptjs with native bcrypt for password hashing
Deploy / release (push) Skipped
Deploy / deploy (push) Failing after 58s

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-07-21 20:37:02 +02:00
1 parent 830d252346
commit d2120987b0
6 files changed
+47 -18

No files matched your search

+2 -1
View File
@@ -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",
+32 -7
View File
@@ -40,9 +40,9 @@ importers:
'@tanstack/react-virtual':
specifier: ^3.14.6
version: 3.14.6([email protected]([email protected]))([email protected])
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([email protected])
'@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/[email protected]':
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
'@types/[email protected]':
resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==}
'@types/[email protected]':
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'}
[email protected]:
resolution: {integrity: sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q==}
@@ -3640,6 +3646,10 @@ packages:
[email protected]:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
[email protected]:
resolution: {integrity: sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==}
engines: {node: ^18 || ^20 || >= 21}
[email protected]:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
@@ -3649,6 +3659,10 @@ packages:
encoding:
optional: true
[email protected]:
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
hasBin: true
[email protected]:
resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==}
engines: {node: '>=18'}
@@ -6117,6 +6131,10 @@ snapshots:
tslib: 2.8.1
optional: true
'@types/[email protected]':
dependencies:
'@types/node': 26.1.1
'@types/[email protected]':
dependencies:
'@types/deep-eql': 4.0.2
@@ -6500,7 +6518,10 @@ snapshots:
[email protected]: {}
bcrypt[email protected]: {}
bcrypt@6.0.0:
dependencies:
node-addon-api: 8.9.0
node-gyp-build: 4.8.4
[email protected]: {}
@@ -7291,10 +7312,14 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
dependencies:
whatwg-url: 5.0.0
[email protected]: {}
[email protected]: {}
[email protected]: {}
+1
View File
@@ -7,6 +7,7 @@ onlyBuiltDependencies:
- "@parcel/watcher"
- "@swc/core"
- "@sentry/cli"
- bcrypt
overrides:
fast-uri: "^3.1.3"
+3 -3
View File
@@ -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 },
+3 -3
View File
@@ -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);
+6 -4
View File
@@ -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<string> {
...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;
}