perf: replace bcryptjs with native bcrypt for password hashing
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
830d252346
commit
d2120987b0
6 files changed
+47
-18
No files matched your search
+2
-1
@@ -32,7 +32,7 @@
|
|||||||
"@prisma/client": "^7.9.0",
|
"@prisma/client": "^7.9.0",
|
||||||
"@sentry/nextjs": "^10.67.0",
|
"@sentry/nextjs": "^10.67.0",
|
||||||
"@tanstack/react-virtual": "^3.14.6",
|
"@tanstack/react-virtual": "^3.14.6",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcrypt": "^6.0.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -73,6 +73,7 @@
|
|||||||
"@tailwindcss/forms": "^0.5.11",
|
"@tailwindcss/forms": "^0.5.11",
|
||||||
"@tailwindcss/postcss": "^4.3.3",
|
"@tailwindcss/postcss": "^4.3.3",
|
||||||
"@tailwindcss/typography": "^0.5.20",
|
"@tailwindcss/typography": "^0.5.20",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
"@types/node": "^26.1.1",
|
"@types/node": "^26.1.1",
|
||||||
"@types/nodemailer": "^7.0.12",
|
"@types/nodemailer": "^7.0.12",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "^19.2.17",
|
||||||
|
|||||||
Generated
+32
-7
@@ -40,9 +40,9 @@ importers:
|
|||||||
'@tanstack/react-virtual':
|
'@tanstack/react-virtual':
|
||||||
specifier: ^3.14.6
|
specifier: ^3.14.6
|
||||||
version: 3.14.6([email protected]([email protected]))([email protected])
|
version: 3.14.6([email protected]([email protected]))([email protected])
|
||||||
bcryptjs:
|
bcrypt:
|
||||||
specifier: ^3.0.2
|
specifier: ^6.0.0
|
||||||
version: 3.0.3
|
version: 6.0.0
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.1
|
specifier: ^0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
@@ -158,6 +158,9 @@ importers:
|
|||||||
'@tailwindcss/typography':
|
'@tailwindcss/typography':
|
||||||
specifier: ^0.5.20
|
specifier: ^0.5.20
|
||||||
version: 0.5.20([email protected])
|
version: 0.5.20([email protected])
|
||||||
|
'@types/bcrypt':
|
||||||
|
specifier: ^6.0.0
|
||||||
|
version: 6.0.0
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^26.1.1
|
specifier: ^26.1.1
|
||||||
version: 26.1.1
|
version: 26.1.1
|
||||||
@@ -2374,6 +2377,9 @@ packages:
|
|||||||
'@tybys/[email protected]':
|
'@tybys/[email protected]':
|
||||||
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
|
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
|
||||||
|
|
||||||
|
'@types/[email protected]':
|
||||||
|
resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==}
|
||||||
|
|
||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||||
|
|
||||||
@@ -2750,9 +2756,9 @@ packages:
|
|||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
bcryptjs@3.0.3:
|
bcrypt@6.0.0:
|
||||||
resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==}
|
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
|
||||||
hasBin: true
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q==}
|
resolution: {integrity: sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q==}
|
||||||
@@ -3640,6 +3646,10 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==}
|
||||||
|
engines: {node: ^18 || ^20 || >= 21}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||||
engines: {node: 4.x || >=6.0.0}
|
engines: {node: 4.x || >=6.0.0}
|
||||||
@@ -3649,6 +3659,10 @@ packages:
|
|||||||
encoding:
|
encoding:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==}
|
resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -6117,6 +6131,10 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@types/[email protected]':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 26.1.1
|
||||||
|
|
||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/deep-eql': 4.0.2
|
'@types/deep-eql': 4.0.2
|
||||||
@@ -6500,7 +6518,10 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
bcrypt[email protected]: {}
|
bcrypt@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
node-addon-api: 8.9.0
|
||||||
|
node-gyp-build: 4.8.4
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
@@ -7291,10 +7312,14 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url: 5.0.0
|
whatwg-url: 5.0.0
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ onlyBuiltDependencies:
|
|||||||
- "@parcel/watcher"
|
- "@parcel/watcher"
|
||||||
- "@swc/core"
|
- "@swc/core"
|
||||||
- "@sentry/cli"
|
- "@sentry/cli"
|
||||||
|
- bcrypt
|
||||||
|
|
||||||
overrides:
|
overrides:
|
||||||
fast-uri: "^3.1.3"
|
fast-uri: "^3.1.3"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import { hash } from "bcryptjs";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Prisma } from "@/generated/prisma/client";
|
import { Prisma } from "@/generated/prisma/client";
|
||||||
|
import { hashPassword } from "@/lib/auth/password";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { adminAction } from "@/lib/safe-action";
|
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");
|
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);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -304,7 +304,7 @@ export const resetPassword = adminAction(
|
|||||||
.randomBytes(12)
|
.randomBytes(12)
|
||||||
.toString("base64url")
|
.toString("base64url")
|
||||||
.slice(0, 16);
|
.slice(0, 16);
|
||||||
const hashed = await hash(newPassword, 10);
|
const hashed = await hashPassword(newPassword);
|
||||||
|
|
||||||
await prisma.user.update({
|
await prisma.user.update({
|
||||||
where: { id: ctx.data.userId },
|
where: { id: ctx.data.userId },
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { hash as bcryptHash } from "bcryptjs";
|
import { hash as bcryptHash } from "bcrypt";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
checkLogin,
|
checkLogin,
|
||||||
@@ -50,9 +50,9 @@ describe("hashPassword (PASSWORD_HASH=argon2id)", () => {
|
|||||||
|
|
||||||
describe("bcrypt", () => {
|
describe("bcrypt", () => {
|
||||||
it("verifies a bcrypt hash and accepts the PHP $2y$ prefix", async () => {
|
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);
|
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$");
|
const phpStyle = h.replace(/^\$2[ab]\$/, "$2y$");
|
||||||
expect(await verifyPassword("hunter2", phpStyle)).toBe(true);
|
expect(await verifyPassword("hunter2", phpStyle)).toBe(true);
|
||||||
expect(await verifyPassword("nope", h)).toBe(false);
|
expect(await verifyPassword("nope", h)).toBe(false);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { randomBytes } from "node:crypto";
|
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";
|
import { argon2id, argon2Verify, md5 } from "hash-wasm";
|
||||||
|
|
||||||
// AtomCMS hashing (config/hashing.php): argon2id with memory=65536 KiB, time=4,
|
// 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,
|
...ARGON2_PARAMS,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// bcryptjs emits $2a$/$2b$; normalise to the PHP-canonical $2y$ the emulator
|
// native bcrypt emits $2a$/$2b$; normalise to the PHP-canonical $2y$ the
|
||||||
// and existing AtomCMS rows use.
|
// emulator and existing AtomCMS rows use.
|
||||||
const h = await bcryptHash(password, BCRYPT_ROUNDS);
|
const h = await bcryptHash(password, BCRYPT_ROUNDS);
|
||||||
return h.replace(/^\$2[ab]\$/, "$2y$");
|
return h.replace(/^\$2[ab]\$/, "$2y$");
|
||||||
}
|
}
|
||||||
@@ -84,7 +84,9 @@ export async function verifyPassword(
|
|||||||
}
|
}
|
||||||
if (/^\$2[aby]\$/.test(stored)) {
|
if (/^\$2[aby]\$/.test(stored)) {
|
||||||
try {
|
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 {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user