Files
hotelsync/backend/src/lib/crypto.ts
HotelSync 8282155237 Add AES-256-GCM encryption for sensitive personal data fields
- New backend/src/lib/crypto.ts: encrypt/decrypt (AES-256-GCM) + passportToken (HMAC-SHA256)
- Migration 012: birth_date/passport_issue_date → TEXT, add passport_search_token indexes
- guests.ts: encrypt passport_series/number/birth_date on write, decrypt on read,
  passport search via HMAC token (?passport= param) instead of plaintext LIKE
- booking-guests.ts: encrypt all sensitive fields, search token for passport lookup
- api.ts: guests.list() supports passport= param for exact passport lookup
- BookingDetailPanel: use passport param for passport-based autocomplete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 17:19:27 +03:00

52 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createCipheriv, createDecipheriv, createHmac, randomBytes } from 'crypto'
const KEY = Buffer.from(process.env.ENCRYPTION_KEY ?? '', 'hex') // 32 bytes
const HMAC = process.env.HMAC_KEY ?? ''
if (process.env.NODE_ENV !== 'test' && (KEY.length !== 32 || !HMAC)) {
throw new Error('ENCRYPTION_KEY (64 hex chars) and HMAC_KEY must be set in environment')
}
// ── Encrypt ───────────────────────────────────────────────────────────────────
// Format: hex(iv 12b) : hex(authTag 16b) : hex(ciphertext)
export function encrypt(plaintext: string): string {
const iv = randomBytes(12)
const cipher = createCipheriv('aes-256-gcm', KEY, iv)
const enc = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()])
const tag = cipher.getAuthTag()
return `${iv.toString('hex')}:${tag.toString('hex')}:${enc.toString('hex')}`
}
// ── Decrypt ───────────────────────────────────────────────────────────────────
export function decrypt(data: string): string {
const parts = data.split(':')
if (parts.length !== 3) return data // не зашифровано — вернуть как есть
const [ivHex, tagHex, encHex] = parts
if (ivHex.length !== 24) return data // не наш формат
const iv = Buffer.from(ivHex, 'hex')
const authTag = Buffer.from(tagHex, 'hex')
const enc = Buffer.from(encHex, 'hex')
const decipher = createDecipheriv('aes-256-gcm', KEY, iv)
decipher.setAuthTag(authTag)
return Buffer.concat([decipher.update(enc), decipher.final()]).toString('utf8')
}
// ── Null-safe helpers ─────────────────────────────────────────────────────────
export function encryptField(val: string | null | undefined): string | null {
if (!val) return null
return encrypt(val)
}
export function decryptField(val: string | null | undefined): string | null {
if (!val) return null
try { return decrypt(val) } catch { return val } // старые незашифрованные данные
}
// ── Passport search token — HMAC(series+number) ───────────────────────────────
// Хранится рядом с зашифрованными полями для точного поиска без расшифровки
export function passportToken(seriesAndNumber: string): string {
return createHmac('sha256', HMAC)
.update(seriesAndNumber.toLowerCase().replace(/\s/g, ''))
.digest('hex')
}