/** * lib/crypto.mjs — 비밀번호 해시·토큰·HMAC 공통. * * - 비밀번호: scrypt(N=2^15) + 16바이트 salt. Webmaker 와 같은 계열의 알고리즘이라 SSO 계정 동기화 때 혼동이 없다. * - 토큰: 32바이트 무작위 base64url. 저장은 항상 sha256 해시만(원문은 발급 응답에 한 번). * - HMAC: Agent 요청 서명·SSO 티켓 서명. 비교는 항상 timingSafeEqual. */ import { scryptSync, randomBytes, createHash, createHmac, timingSafeEqual } from 'node:crypto'; /** 비밀번호 해시. @returns {{salt:string, hash:string}} */ export function hashPassword(password) { const salt = randomBytes(16).toString('hex'); return { salt, hash: scryptSync(String(password), salt, 64, { N: 2 ** 14, r: 8, p: 1, maxmem: 64 * 1024 * 1024 }).toString('hex') }; } /** 비밀번호 검증(상수 시간). */ export function verifyPassword(password, salt, hash) { if (!salt || !hash) return false; const a = scryptSync(String(password), salt, 64, { N: 2 ** 14, r: 8, p: 1, maxmem: 64 * 1024 * 1024 }); const b = Buffer.from(hash, 'hex'); return a.length === b.length && timingSafeEqual(a, b); } /** 무작위 토큰(원문). bytes 기본 32. */ export const randomToken = (bytes = 32) => randomBytes(bytes).toString('base64url'); /** 짧은 식별자(ULID 대용): 시각 정렬 가능한 16진 26자 — 앞 10자는 ms 시각. */ export function newId(prefix = '') { const t = Date.now().toString(16).padStart(12, '0'); return prefix + t + randomBytes(7).toString('hex'); } /** 토큰 저장용 해시 */ export const sha256 = (s) => createHash('sha256').update(String(s)).digest('hex'); /** HMAC-SHA256 (base64url) */ export const hmac = (secret, data) => createHmac('sha256', secret).update(String(data)).digest('base64url'); /** 문자열 상수 시간 비교 */ export function safeEqual(a, b) { const x = Buffer.from(String(a || '')); const y = Buffer.from(String(b || '')); return x.length === y.length && x.length > 0 && timingSafeEqual(x, y); } /** 6자리 숫자 코드(이메일 확인·MFA 복구 등) */ export const numericCode = (digits = 6) => String(parseInt(randomBytes(4).toString('hex'), 16) % 10 ** digits).padStart(digits, '0');