mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 23:53:17 -05:00
feat(crypto): use native node:crypto scrypt with fallback to @noble/hashes
Add native scrypt support via node:crypto for environments like Cloudflare Workers that provide it, while preserving the @noble/hashes fallback for environments without native crypto. The hash format is unchanged so existing stored password hashes continue to work. Closes #8456 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
98cd201a17
commit
dbf2c2e87a
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<typeof import("node:crypto").scrypt | null> =
|
||||
(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<Uint8Array> {
|
||||
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<Uint8Array> {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user