mission-control/src/lib/password.ts

46 lines
1.9 KiB
TypeScript

import { randomBytes, scryptSync, timingSafeEqual } from 'crypto'
// Password hashing using Node.js built-in scrypt
const SALT_LENGTH = 16
const KEY_LENGTH = 32
const SCRYPT_COST = 65536
const SCRYPT_MAXMEM = 128 * SCRYPT_COST * 8 * 2 // ~128MB headroom for N=65536
// Previous cost factor — used to verify passwords hashed before the upgrade
const LEGACY_SCRYPT_COST = 16384
export function hashPassword(password: string): string {
const salt = randomBytes(SALT_LENGTH).toString('hex')
const hash = scryptSync(password, salt, KEY_LENGTH, { N: SCRYPT_COST, maxmem: SCRYPT_MAXMEM }).toString('hex')
return `${salt}:${hash}`
}
/**
* Verify a password against a stored hash.
* Tries current cost first, then falls back to legacy cost for pre-upgrade hashes.
* Returns { valid, needsRehash } so callers can progressively upgrade hashes.
*/
export function verifyPasswordWithRehashCheck(password: string, stored: string): { valid: boolean; needsRehash: boolean } {
const [salt, hash] = stored.split(':')
if (!salt || !hash) return { valid: false, needsRehash: false }
const storedBuf = Buffer.from(hash, 'hex')
// Try current cost first
const derived = scryptSync(password, salt, KEY_LENGTH, { N: SCRYPT_COST, maxmem: SCRYPT_MAXMEM })
if (derived.length === storedBuf.length && timingSafeEqual(derived, storedBuf)) {
return { valid: true, needsRehash: false }
}
// Fall back to legacy cost for passwords hashed before the upgrade
const legacyDerived = scryptSync(password, salt, KEY_LENGTH, { N: LEGACY_SCRYPT_COST })
if (legacyDerived.length !== storedBuf.length) return { valid: false, needsRehash: false }
if (timingSafeEqual(legacyDerived, storedBuf)) {
return { valid: true, needsRehash: true }
}
return { valid: false, needsRehash: false }
}
export function verifyPassword(password: string, stored: string): boolean {
return verifyPasswordWithRehashCheck(password, stored).valid
}