| import crypto from 'node:crypto' |
|
|
| export function normalizeEmail(email: string) { |
| return email.trim().toLowerCase() |
| } |
|
|
| export function isValidEmail(email: string) { |
| const e = normalizeEmail(email) |
| return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e) |
| } |
|
|
| export function isStrongPassword(password: string) { |
| if (password.length < 8) return false |
| const hasLetter = /[a-zA-Z]/.test(password) |
| const hasNumber = /\d/.test(password) |
| return hasLetter && hasNumber |
| } |
|
|
| export async function hashPassword(password: string) { |
| const salt = crypto.randomBytes(16) |
| const key = await new Promise<Buffer>((resolve, reject) => { |
| crypto.scrypt(password, salt, 64, { N: 16384, r: 8, p: 1 }, (err, derivedKey) => { |
| if (err) reject(err) |
| else resolve(derivedKey as Buffer) |
| }) |
| }) |
| return `scrypt$${salt.toString('hex')}$${key.toString('hex')}` |
| } |
|
|
| export async function verifyPassword(password: string, stored: string) { |
| const parts = stored.split('$') |
| if (parts.length !== 3 || parts[0] !== 'scrypt') return false |
| const salt = Buffer.from(parts[1], 'hex') |
| const expected = Buffer.from(parts[2], 'hex') |
|
|
| const key = await new Promise<Buffer>((resolve, reject) => { |
| crypto.scrypt(password, salt, 64, { N: 16384, r: 8, p: 1 }, (err, derivedKey) => { |
| if (err) reject(err) |
| else resolve(derivedKey as Buffer) |
| }) |
| }) |
|
|
| if (key.length !== expected.length) return false |
| return crypto.timingSafeEqual(key, expected) |
| } |
|
|
| export function hashBackupCode(code: string, saltHex?: string) { |
| const salt = saltHex ? Buffer.from(saltHex, 'hex') : crypto.randomBytes(16) |
| const h = crypto.createHash('sha256').update(salt).update(code).digest('hex') |
| return `sha256$${salt.toString('hex')}$${h}` |
| } |
|
|
| export function verifyBackupCode(code: string, stored: string) { |
| const parts = stored.split('$') |
| if (parts.length !== 3 || parts[0] !== 'sha256') return false |
| const saltHex = parts[1] |
| const expected = parts[2] |
| const actual = hashBackupCode(code, saltHex).split('$')[2] |
| return crypto.timingSafeEqual(Buffer.from(actual), Buffer.from(expected)) |
| } |
|
|
| export function generateBackupCodes(count: number) { |
| const codes: string[] = [] |
| for (let i = 0; i < count; i++) { |
| const raw = crypto.randomBytes(5).toString('hex').slice(0, 10).toUpperCase() |
| codes.push(`${raw.slice(0, 5)}-${raw.slice(5)}`) |
| } |
| return codes |
| } |
|
|
|
|