API 키 암호화 저장 패턴
백엔드API 키 암호화 저장 패턴 — 실전 적용 구조와 코드 예시
언제 쓰나 · API 키를 DB에 평문으로 저장하면 DB 유출 시 키가 그대로 노출된다.
#backend#API#암호화
# 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는 매 암호화마다 새로 생성 (같은 평문도 다른 암호문)jt · v1 · CC0-1.0 · 복사 0