diff --git a/packages/better-auth/src/crypto/password.test.ts b/packages/better-auth/src/crypto/password.test.ts index d77b439074..1b60ed4f3e 100644 --- a/packages/better-auth/src/crypto/password.test.ts +++ b/packages/better-auth/src/crypto/password.test.ts @@ -1,3 +1,5 @@ +import { hex } from "@better-auth/utils/hex"; +import { scryptAsync } from "@noble/hashes/scrypt.js"; import { describe, expect, it } from "vitest"; import { hashPassword, verifyPassword } from "./password"; @@ -60,4 +62,28 @@ describe("Password hashing and verification", () => { const isValid = await verifyPassword({ hash, password }); expect(isValid).toBe(true); }); + + it("should verify a password hashed with @noble/hashes (cross-compatibility)", async () => { + const password = "crossCompatTest!"; + const salt = hex.encode(crypto.getRandomValues(new Uint8Array(16))); + const key = await scryptAsync(password.normalize("NFKC"), salt, { + N: 16384, + p: 1, + r: 16, + dkLen: 64, + maxmem: 128 * 16384 * 16 * 2, + }); + const nobleHash = `${salt}:${hex.encode(key)}`; + const isValid = await verifyPassword({ hash: nobleHash, password }); + expect(isValid).toBe(true); + }); + + it("should verify a known test vector", async () => { + const password = "betterAuthTestVector"; + const hash = await hashPassword(password); + const isValid = await verifyPassword({ hash, password }); + expect(isValid).toBe(true); + const isInvalid = await verifyPassword({ hash, password: "wrong" }); + expect(isInvalid).toBe(false); + }); }); diff --git a/packages/better-auth/src/crypto/password.ts b/packages/better-auth/src/crypto/password.ts index 7d30523c5b..8e1434b3b6 100644 --- a/packages/better-auth/src/crypto/password.ts +++ b/packages/better-auth/src/crypto/password.ts @@ -11,8 +11,51 @@ const config = { dkLen: 64, }; -async function generateKey(password: string, salt: string) { - return await scryptAsync(password.normalize("NFKC"), salt, { +const nativeScryptFn: Promise = + (async () => { + try { + const { scrypt } = await import("node:crypto"); + if (typeof scrypt === "function") return scrypt; + return null; + } catch { + return null; + } + })(); + +function nativeGenerateKey( + scryptFn: typeof import("node:crypto").scrypt, + password: string, + salt: string, +): Promise { + return new Promise((resolve, reject) => { + scryptFn( + password, + salt, + config.dkLen, + { + N: config.N, + r: config.r, + p: config.p, + maxmem: 128 * config.N * config.r * 2, + }, + (err, derivedKey) => { + if (err) reject(err); + else resolve(new Uint8Array(derivedKey)); + }, + ); + }); +} + +async function generateKey( + password: string, + salt: string, +): Promise { + const normalizedPassword = password.normalize("NFKC"); + const scryptFn = await nativeScryptFn; + if (scryptFn) { + return nativeGenerateKey(scryptFn, normalizedPassword, salt); + } + return scryptAsync(normalizedPassword, salt, { N: config.N, p: config.p, r: config.r,