Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | 5x 5x 5x 5x 26x 624x 13x 13x 13x 13x 13x 13x 13x 9x 4x 9x 9x 7x 7x 7x 6x 6x 6x 6x 6x 6x 6x 6x 168x 6x 4x 4x 3x 3x 3x 3x 3x 192x 3x | // Custom password hashing using Web Crypto PBKDF2
// Replaces Better Auth's default scrypt (N=16384, r=16) which is too CPU-heavy
// for Cloudflare Workers cold starts (~3s CPU time, causing 503 errors).
//
// PBKDF2-SHA256 with 100,000 iterations is NIST-approved and executes in <50ms
// on Workers thanks to native Web Crypto hardware acceleration.
//
// Hash format: "pbkdf2:iterations:salt:key" (all hex-encoded)
// Legacy format (Better Auth default scrypt): "salt:key"
import { scryptAsync } from "@noble/hashes/scrypt.js";
import { hexToBytes } from "@noble/hashes/utils.js";
const PBKDF2_ITERATIONS = 100_000;
const SALT_LENGTH = 16; // bytes
const KEY_LENGTH = 32; // bytes
const PBKDF2_PREFIX = "pbkdf2";
function hexEncode(bytes: Uint8Array): string {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
/** Hash a password using PBKDF2-SHA256 via Web Crypto */
export async function hashPassword(password: string): Promise<string> {
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
"raw",
encoder.encode(password.normalize("NFKC")),
"PBKDF2",
false,
["deriveBits"],
);
const derivedBits = await crypto.subtle.deriveBits(
{
name: "PBKDF2",
salt,
iterations: PBKDF2_ITERATIONS,
hash: "SHA-256",
},
keyMaterial,
KEY_LENGTH * 8,
);
const key = new Uint8Array(derivedBits);
return `${PBKDF2_PREFIX}:${PBKDF2_ITERATIONS}:${hexEncode(salt)}:${hexEncode(key)}`;
}
/** Verify a password against a hash (supports both PBKDF2 and legacy scrypt) */
export async function verifyPassword({
hash,
password,
}: { hash: string; password: string }): Promise<boolean> {
if (hash.startsWith(`${PBKDF2_PREFIX}:`)) {
return verifyPbkdf2(hash, password);
}
// Legacy Better Auth scrypt format: "salt:key"
return verifyLegacyScrypt(hash, password);
}
async function verifyPbkdf2(hash: string, password: string): Promise<boolean> {
const parts = hash.split(":");
if (parts.length !== 4) return false;
const [, iterStr, saltHex, keyHex] = parts;
const iterations = Number.parseInt(iterStr, 10);
if (Number.isNaN(iterations)) return false;
const salt = hexToBytes(saltHex);
const expectedKey = hexToBytes(keyHex);
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
"raw",
encoder.encode(password.normalize("NFKC")),
"PBKDF2",
false,
["deriveBits"],
);
const derivedBits = await crypto.subtle.deriveBits(
{
name: "PBKDF2",
salt,
iterations,
hash: "SHA-256",
},
keyMaterial,
expectedKey.length * 8,
);
const derivedKey = new Uint8Array(derivedBits);
// Constant-time comparison
let diff = 0;
for (let i = 0; i < derivedKey.length; i++) {
diff |= derivedKey[i] ^ expectedKey[i];
}
return diff === 0;
}
/** Verify against Better Auth's default scrypt format (salt:hex-key) */
async function verifyLegacyScrypt(
hash: string,
password: string,
): Promise<boolean> {
const [salt, keyHex] = hash.split(":");
if (!salt || !keyHex) return false;
// Better Auth default scrypt config
const config = { N: 16384, r: 16, p: 1, dkLen: 64 };
const derivedKey = await scryptAsync(
password.normalize("NFKC"),
salt,
{
N: config.N,
p: config.p,
r: config.r,
dkLen: config.dkLen,
maxmem: 128 * config.N * config.r * 2,
},
);
const expectedKey = hexToBytes(keyHex);
// Constant-time comparison
let diff = 0;
for (let i = 0; i < derivedKey.length; i++) {
diff |= derivedKey[i] ^ expectedKey[i];
}
return diff === 0;
}
|