# API 키 암호화 저장 패턴 ## 문제 API 키를 DB에 평문으로 저장하면 DB 유출 시 키가 그대로 노출된다. ## 해결: AES-256-GCM 암호화 ### 구조 ``` 저장: 평문 키 → AES-256-GCM 암호화 → DB에 "iv:authTag:encrypted" 형태로 저장 읽기: DB에서 읽기 → AES-256-GCM 복호화 → 평문 키로 사용 ``` ### 암호화 키 관리 - `.env`의 기존 시크릿(예: `AUTH_SECRET`)을 `scrypt`로 파생하여 암호화 키로 사용 - 별도의 암호화 키를 관리할 필요 없음 - `.env`만 안전하면 DB가 유출돼도 API 키는 안전 ### 구현 (Node.js / TypeScript) ```typescript // lib/crypto.ts import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto"; const ALGORITHM = "aes-256-gcm"; function getKey(): Buffer { const secret = process.env.AUTH_SECRET; if (!secret) throw new Error("AUTH_SECRET 필요"); return scryptSync(secret, "app-salt", 32); // 32바이트 키 파생 } export function encrypt(plaintext: string): string { const key = getKey(); const iv = randomBytes(12); // GCM은 12바이트 IV 권장 const cipher = createCipheriv(ALGORITHM, key, iv); let encrypted = cipher.update(plaintext, "utf8", "hex"); encrypted += cipher.final("hex"); const authTag = cipher.getAuthTag().toString("hex"); return `${iv.toString("hex")}:${authTag}:${encrypted}`; } export function decrypt(ciphertext: string): string { const key = getKey(); const [ivHex, authTagHex, encrypted] = ciphertext.split(":"); const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(ivHex, "hex")); decipher.setAuthTag(Buffer.from(authTagHex, "hex")); let decrypted = decipher.update(encrypted, "hex", "utf8"); decrypted += decipher.final("utf8"); return decrypted; } export function isEncrypted(value: string): boolean { return /^[0-9a-f]{24}:[0-9a-f]{32}:.+$/.test(value); } ``` ### 적용 포인트 1. **저장 시** (관리자 설정 API): ```typescript const value = SENSITIVE_KEYS.includes(key) ? encrypt(body[key]) : body[key]; await db.setting.upsert({ where: { key }, update: { value }, create: { key, value } }); ``` 2. **읽기 시** (AI API 호출 등): ```typescript function decryptIfNeeded(value: string): string { return isEncrypted(value) ? decrypt(value) : value; } const apiKey = decryptIfNeeded(dbValue); ``` 3. **조회 시** (관리자 페이지): ```typescript // 절대 복호화하지 않고 마스킹 map[key] = "••••••••" + dbValue.slice(-8); ``` ### 마이그레이션 기존 평문 → 암호화로 전환 시 일괄 마이그레이션 스크립트 실행: ```bash AUTH_SECRET=xxx node scripts/migrate-encrypt-keys.mjs ``` - `isEncrypted()`로 이미 암호화된 값은 스킵 - 평문만 암호화하여 업데이트 ### 체크리스트 - [ ] 암호화 키는 `.env`에만 존재 (코드에 하드코딩 금지) - [ ] 관리자 페이지에서 API 키는 항상 마스킹 표시 - [ ] 마스킹된 값(`••••`)이 들어오면 저장 스킵 (기존 값 유지) - [ ] `isEncrypted()`로 평문/암호화 자동 판별 (마이그레이션 호환) - [ ] GCM 모드 사용 (인증 태그로 위변조 감지) ### 주의사항 - `AUTH_SECRET` 변경 시 모든 암호화된 값을 재암호화해야 함 - salt 값(`"app-salt"`)은 프로젝트마다 다르게 설정 - IV는 매 암호화마다 새로 생성 (같은 평문도 다른 암호문)