Add PostgreSQL and Redis integration for users and sessions Implement password and WebAuthn passkey login flows Add Docker stack, super-admin seeding, and protected routes
53 lines
1.3 KiB
TypeScript
53 lines
1.3 KiB
TypeScript
import { randomBytes, scrypt as scryptCallback, timingSafeEqual } from 'node:crypto'
|
|
import { promisify } from 'node:util'
|
|
|
|
import { decodeBase64Url, encodeBase64Url } from './base64url'
|
|
|
|
const scrypt = promisify(scryptCallback)
|
|
const SCRYPT_COST = 16_384
|
|
const SCRYPT_BLOCK_SIZE = 8
|
|
const SCRYPT_PARALLELIZATION = 1
|
|
const KEY_LENGTH = 64
|
|
|
|
export async function hashPassword(password: string): Promise<string> {
|
|
const salt = encodeBase64Url(randomBytes(16))
|
|
const derivedKey = await scrypt(password, salt, KEY_LENGTH, {
|
|
N: SCRYPT_COST,
|
|
r: SCRYPT_BLOCK_SIZE,
|
|
p: SCRYPT_PARALLELIZATION
|
|
}) as Buffer
|
|
|
|
return [
|
|
'scrypt',
|
|
SCRYPT_COST,
|
|
SCRYPT_BLOCK_SIZE,
|
|
SCRYPT_PARALLELIZATION,
|
|
salt,
|
|
encodeBase64Url(derivedKey)
|
|
].join('$')
|
|
}
|
|
|
|
export async function verifyPassword(password: string, storedHash: string): Promise<boolean> {
|
|
const [algorithm, cost, blockSize, parallelization, salt, key] = storedHash.split('$')
|
|
|
|
if (
|
|
algorithm !== 'scrypt'
|
|
|| !cost
|
|
|| !blockSize
|
|
|| !parallelization
|
|
|| !salt
|
|
|| !key
|
|
) {
|
|
return false
|
|
}
|
|
|
|
const expectedKey = decodeBase64Url(key)
|
|
const derivedKey = await scrypt(password, salt, expectedKey.length, {
|
|
N: Number(cost),
|
|
r: Number(blockSize),
|
|
p: Number(parallelization)
|
|
}) as Buffer
|
|
|
|
return timingSafeEqual(expectedKey, derivedKey)
|
|
}
|