Files
seminarhof-e2e-mrfgld02/packages/functions/src/services/password.ts
2026-07-10 23:39:57 +02:00

54 lines
1.8 KiB
TypeScript

// Password hashing using WebCrypto PBKDF2-SHA256 — works on Node, browsers, and CF Workers.
// Stored format: `pbkdf2$<iterations>$<salt_b64>$<hash_b64>`
const ITERATIONS = 100_000
const KEY_LENGTH = 32 // bytes
const SALT_LENGTH = 16
const b64encode = (bytes: Uint8Array) =>
btoa(String.fromCharCode(...bytes))
const b64decode = (str: string) => {
const bin = atob(str)
const out = new Uint8Array(bin.length)
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)
return out
}
async function pbkdf2(password: string, salt: Uint8Array, iterations: number): Promise<Uint8Array> {
const enc = new TextEncoder()
const key = await crypto.subtle.importKey(
'raw',
enc.encode(password),
'PBKDF2',
false,
['deriveBits'],
)
const bits = await crypto.subtle.deriveBits(
{ name: 'PBKDF2', salt: salt as BufferSource, iterations, hash: 'SHA-256' },
key,
KEY_LENGTH * 8,
)
return new Uint8Array(bits)
}
export async function hashPassword(password: string): Promise<string> {
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH))
const hash = await pbkdf2(password, salt, ITERATIONS)
return `pbkdf2$${ITERATIONS}$${b64encode(salt)}$${b64encode(hash)}`
}
export async function verifyPassword(password: string, stored: string): Promise<boolean> {
const parts = stored.split('$')
if (parts.length !== 4 || parts[0] !== 'pbkdf2') return false
const iterations = Number(parts[1])
if (!Number.isFinite(iterations) || iterations < 1000) return false
const salt = b64decode(parts[2])
const expected = b64decode(parts[3])
const actual = await pbkdf2(password, salt, iterations)
if (actual.length !== expected.length) return false
let diff = 0
for (let i = 0; i < actual.length; i++) diff |= actual[i] ^ expected[i]
return diff === 0
}