PLT-SEC-003 애플리케이션 레벨 암호화 구현 가이드
🎯 개요
PLT-SEC-003 요구사항
"특별히 민감한 데이터는 추가 암호화를 고려한다"
DTx 플랫폼에서 처리하는 민감 데이터에 대한 애플리케이션 레벨 필드 암호화 구현 가이드입니다.
암호화 대상 데이터
- ✅ 의료 진단 데이터: 수면 질, 설문 응답(ISI/PHQ-9/GAD-7), AI 분석 결과
- ✅ 개인 식별 정보: 이메일, 비밀번호 해시, 2FA 비밀키, IP 주소
- ✅ API 키/토큰: 액세스 토큰, 리프레시 토큰, 서비스 간 인증 키
구현 전략
Google Cloud KMS 기반 투명한 필드 레벨 암호화
개발자 경험을 최적화하면서 최고 수준의 보안을 제공하는 데코레이터 기반 자동 암호화 시스템을 구축합니다.
📋 민감 데이터 현황 분석
1. 개인 식별 정보 (PII)
// User 테이블 민감 필드
interface UserSensitiveFields {
email: string; // 검색 필요, 고빈도 접근
passwordHash: string; // 인증용, 저빈도 접근
mfaSecret?: string; // 2FA 비밀키, 최고 보안
lastLoginIp?: string; // 추적용, 중간 보안
}
2. 의료 진단 데이터 (Medical)
// SleepLog 테이블 민감 필드
interface SleepLogSensitiveFields {
sleepQuality?: number; // 의료 진단 핵심 데이터
positiveCustomFactors: string; // JSON: 개인 맞춤 요인
negativeCustomFactors: string; // JSON: 개인 맞춤 요인
detailedAnalysis?: string; // JSON: AI 분석 결과
}
// QuestionnaireResponse 테이블 민감 필드
interface QuestionnaireSensitiveFields {
responses: string; // JSON: ISI/PHQ-9/GAD-7 응답
calculatedScore?: number; // 진단 점수
clinicalInterpretation?: string; // 임상 해석 결과
}
3. API 키/토큰 (Tokens)
// AccessCode 테이블 민감 필드
interface AccessCodeSensitiveFields {
code: string; // 등록용 액세스 코드
activationToken?: string; // 활성화 토큰
}
🏗️ 아키텍처 설계
전체 시스템 아키텍처
데이터 흐름
🔧 구현 계획
1단계: Core Field Encryption 라이브러리
디렉터리 구조
libs/core/field-encryption/
├── src/
│ ├── lib/
│ │ ├── field-encryption.service.ts # 핵심 암호화 서비스
│ │ ├── field-encryption.module.ts # NestJS 모듈
│ │ ├── decorators/
│ │ │ └── encrypted-field.decorator.ts # @EncryptedField 데코레이터
│ │ ├── interceptors/
│ │ │ └── encryption.interceptor.ts # 자동 복호화
│ │ ├── middleware/
│ │ │ └── prisma.middleware.ts # Prisma 통합
│ │ ├── types/
│ │ │ ├── sensitive-data.enum.ts # 데이터 타입 정의
│ │ │ └── interfaces.ts # 타입 정의
│ │ └── utils/
│ │ └── hash.util.ts # 검색용 해시
│ └── index.ts
├── package.json
├── README.md
└── tsconfig.lib.json
핵심 서비스 설계
// field-encryption.service.ts
export class FieldEncryptionService {
private readonly kmsClient: KeyManagementServiceClient;
private readonly encryptionPolicies: Map<SensitiveDataType, EncryptionPolicy>;
// 핵심 메서드
async encryptField(data: string, dataType: SensitiveDataType): Promise<EncryptionResult>;
async decryptField(encrypted: string, metadata: EncryptionMetadata): Promise<DecryptionResult>;
async encryptFields(fields: FieldData[]): Promise<EncryptionResult[]>; // 배치 처리
// 키 관리
needsKeyRotation(metadata: EncryptionMetadata): boolean;
// 모니터링
getMetrics(): EncryptionMetrics;
async healthCheck(): Promise<HealthStatus>;
}
// 암호화 정책
interface EncryptionPolicy {
keyRingId: string;
keyId: string;
algorithm: 'AES_256_GCM';
rotationPeriod: number; // seconds
auditRequired: boolean;
performanceThreshold: number; // ms
}
// 환경별 정책
const POLICIES = {
[SensitiveDataType.EMAIL]: {
keyId: 'pii-encryption-key',
rotationPeriod: PRODUCTION ? 2592000 : 604800, // 30일 vs 7일
auditRequired: true,
performanceThreshold: 50
},
[SensitiveDataType.MEDICAL_DIAGNOSIS]: {
keyId: 'medical-data-encryption-key',
rotationPeriod: PRODUCTION ? 1296000 : 604800, // 15일 vs 7일
auditRequired: true,
performanceThreshold: 30
}
};
데코레이터 설계
// encrypted-field.decorator.ts
@EncryptedField({
dataType: SensitiveDataType.EMAIL,
searchable: true, // email_hash 필드 자동 생성
auditRequired: true, // 감사 로깅 필수
performanceThreshold: 50 // 50ms 임계값
})
email: string;
export function EncryptedField(options: EncryptedFieldOptions) {
return function (target: any, propertyName: string) {
// Reflect 메타데이터로 암호화 필드 정보 저장
const fields = Reflect.getMetadata(ENCRYPTED_FIELD_KEY, target.constructor) || [];
fields.push({ propertyName, options });
Reflect.defineMetadata(ENCRYPTED_FIELD_KEY, fields, target.constructor);
};
}
2단계: KMS 인프라
Terraform 모듈 설계
# infrastructure/terraform/modules/field-encryption-kms/main.tf
# Key Ring 생성
resource "google_kms_key_ring" "field_encryption" {
name = "dta-wide-field-encryption-${var.environment}"
location = var.region
}
# PII 데이터 암호화 키
resource "google_kms_crypto_key" "pii_key" {
name = "pii-encryption-key"
key_ring = google_kms_key_ring.field_encryption.id
purpose = "ENCRYPT_DECRYPT"
rotation_period = var.environment == "prod" ? "2592000s" : "604800s"
version_template {
algorithm = "GOOGLE_SYMMETRIC_ENCRYPTION"
protection_level = var.environment == "prod" ? "HSM" : "SOFTWARE"
}
}
# 의료 데이터 암호화 키 (더 빈번한 순환)
resource "google_kms_crypto_key" "medical_key" {
name = "medical-data-encryption-key"
key_ring = google_kms_key_ring.field_encryption.id
rotation_period = var.environment == "prod" ? "1296000s" : "604800s"
version_template {
algorithm = "GOOGLE_SYMMETRIC_ENCRYPTION"
protection_level = var.environment == "prod" ? "HSM" : "SOFTWARE"
}
}
# 토큰 암호화 키
resource "google_kms_crypto_key" "token_key" {
name = "token-encryption-key"
key_ring = google_kms_key_ring.field_encryption.id
rotation_period = var.environment == "prod" ? "2592000s" : "1209600s"
version_template {
algorithm = "GOOGLE_SYMMETRIC_ENCRYPTION"
protection_level = "SOFTWARE"
}
}
# IAM 권한 설정
resource "google_kms_crypto_key_iam_member" "api_access" {
for_each = toset(var.authorized_services)
crypto_key_id = google_kms_crypto_key.pii_key.id
role = "roles/cloudkms.cryptoKeyEncrypterDecrypter"
member = "serviceAccount:${each.key}@${var.project_id}.iam.gserviceaccount.com"
}
환경별 Terragrunt 설정
# infrastructure/terragrunt/dev/field-encryption-kms/terragrunt.hcl
inputs = {
environment = "dev"
region = "europe-west3"
# 개발 환경 설정
authorized_services = ["dta-wide-api"]
key_protection_level = "SOFTWARE"
enable_monitoring = false
# 비용 최적화
cost_optimization = {
use_software_keys = true
disable_backup = true
reduce_monitoring = true
}
}
# infrastructure/terragrunt/prod/field-encryption-kms/terragrunt.hcl
inputs = {
environment = "prod"
region = "europe-west3"
# 프로덕션 환경 설정
authorized_services = ["dta-wide-api", "dta-wide-mcp"]
key_protection_level = "HSM"
enable_monitoring = true
enable_cross_region_backup = true
# 보안 최우선
compliance_settings = {
gdpr_compliance = true
hipaa_compliance = true
fips_140_2_level = 2
}
}
3단계: Prisma 통합
스키마 변경
// 기존 스키마 변경 사항
model User {
id String @id @default(uuid())
// 암호화된 필드들 - Json 타입으로 변경
email Json // EncryptedFieldData 구조
email_hash String @unique // 검색용 해시
passwordHash Json
mfaSecret Json?
lastLoginIp Json?
// 일반 필드들은 유지
status UserStatus
userType UserType
mfaEnabled Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
@@schema("private")
}
model SleepLog {
id String @id @default(uuid())
userId String @map("user_id")
// 기본 수면 데이터는 평문 유지
bedTime DateTime? @map("bed_time")
sleepMinutes Int? @map("sleep_minutes")
// 민감한 의료 데이터만 암호화
sleepQuality Json? @map("sleep_quality")
positiveCustomFactors Json @map("positive_custom_factors")
negativeCustomFactors Json @map("negative_custom_factors")
detailedAnalysis Json? @map("detailed_analysis")
@@map("sleep_log")
@@schema("private")
}
데이터 구조
// 암호화된 필드 저장 구조
interface EncryptedFieldData {
encryptedValue: string; // Base64 암호화 데이터
metadata: string; // JSON 직렬화된 메타데이터
version: number; // 스키마 버전
createdAt: string; // 암호화 시점
}
interface EncryptionMetadata {
algorithm: string; // "AES_256_GCM"
keyVersion: string; // KMS 키 버전
dataType: SensitiveDataType;
encryptedAt: string;
keyRingLocation: string;
}
Prisma 미들웨어
// prisma-encryption.middleware.ts
export class PrismaEncryptionMiddleware {
constructor(
private readonly encryptionService: FieldEncryptionService
) {}
createMiddleware() {
return async (params: any, next: any) => {
// CREATE, UPDATE 시 자동 암호화
if (['create', 'update', 'upsert'].includes(params.action)) {
if (params.args.data) {
params.args.data = await this.encryptModelData(
params.args.data,
params.model
);
}
}
// 쿼리 실행
return await next(params);
};
}
private async encryptModelData(data: any, modelName: string): Promise<any> {
const encryptedFields = this.getEncryptedFieldsForModel(modelName);
const encryptedData = { ...data };
// 병렬 암호화 처리
await Promise.all(
encryptedFields.map(async (field) => {
if (data[field.propertyName]) {
const encrypted = await this.encryptionService.encryptField(
data[field.propertyName],
field.options.dataType
);
encryptedData[field.propertyName] = {
encryptedValue: encrypted.encryptedData,
metadata: JSON.stringify(encrypted.metadata),
version: 1,
createdAt: new Date().toISOString()
};
// 검색 가능 필드인 경우 해시 생성
if (field.options.searchable) {
encryptedData[`${field.propertyName}_hash`] =
this.createSearchHash(data[field.propertyName]);
}
}
})
);
return encryptedData;
}
}
4단계: API 레이어 통합
자동 복호화 인터셉터
// encryption.interceptor.ts
@Injectable()
export class EncryptionInterceptor implements NestInterceptor {
constructor(
private readonly encryptionService: FieldEncryptionService
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(async (data) => {
if (!data) return data;
if (Array.isArray(data)) {
return await Promise.all(data.map(item => this.decryptObject(item)));
} else if (typeof data === 'object') {
return await this.decryptObject(data);
}
return data;
})
);
}
private async decryptObject(obj: any): Promise<any> {
const encryptedFields = this.getEncryptedFieldsFromObject(obj);
if (encryptedFields.length === 0) return obj;
const decryptedObj = { ...obj };
// 병렬 복호화 처리
await Promise.all(
encryptedFields.map(async (field) => {
const encrypted = obj[field.propertyName];
if (encrypted?.encryptedValue) {
try {
const metadata = JSON.parse(encrypted.metadata);
const decrypted = await this.encryptionService.decryptField(
encrypted.encryptedValue,
metadata
);
decryptedObj[field.propertyName] = decrypted.decryptedData;
} catch (error) {
// 복호화 실패시 null 처리
decryptedObj[field.propertyName] = null;
}
}
})
);
return decryptedObj;
}
}
모델 적용 예시
// user.model.ts
export class User {
id: string;
@EncryptedField({
dataType: SensitiveDataType.EMAIL,
searchable: true,
auditRequired: true
})
email: string;
@EncryptedField({
dataType: SensitiveDataType.PASSWORD_HASH,
auditRequired: true
})
passwordHash: string;
@EncryptedField({
dataType: SensitiveDataType.MFA_SECRET,
auditRequired: true,
performanceThreshold: 30
})
mfaSecret?: string;
// 일반 필드들
status: UserStatus;
userType: UserType;
mfaEnabled: boolean;
createdAt: Date;
updatedAt: Date;
}
// sleep-log.model.ts
export class SleepLog {
id: string;
userId: string;
// 기본 수면 데이터 (평문)
bedTime?: Date;
sleepMinutes?: number;
// 민감한 의료 데이터
@EncryptedField({
dataType: SensitiveDataType.SLEEP_QUALITY,
auditRequired: true
})
sleepQuality?: number;
@EncryptedField({
dataType: SensitiveDataType.CLINICAL_DATA,
auditRequired: true
})
positiveCustomFactors: string; // JSON
@EncryptedField({
dataType: SensitiveDataType.CLINICAL_DATA,
auditRequired: true
})
negativeCustomFactors: string; // JSON
}
서비스 활용 예시
// user.service.ts
@Injectable()
export class UserService {
constructor(
private readonly prisma: PrismaService,
private readonly encryptionHelper: EncryptionHelper
) {}
async createUser(userData: CreateUserDto): Promise<User> {
// Prisma 미들웨어가 자동으로 암호화 처리
return await this.prisma.user.create({
data: {
email: userData.email, // 자동 암호화
passwordHash: userData.hash, // 자동 암호화
status: 'ACTIVE'
}
});
}
async findUserByEmail(email: string): Promise<User | null> {
// 검색용 해시로 조회
const emailHash = this.encryptionHelper.createHashForSearch(email);
return await this.prisma.user.findUnique({
where: { email_hash: emailHash }
// 결과는 EncryptionInterceptor가 자동 복호화
});
}
}
📊 테스트 전략
단위 테스트
// field-encryption.service.spec.ts
describe('FieldEncryptionService', () => {
beforeEach(() => {
// KMS 클라이언트 모킹
mockKmsClient.encrypt.mockResolvedValue({
ciphertext: Buffer.from('encrypted_data'),
name: 'key_version_path'
});
});
it('should encrypt PII data correctly', async () => {
const result = await service.encryptField('test@example.com', SensitiveDataType.EMAIL);
expect(result.encryptedData).toBeDefined();
expect(result.metadata.dataType).toBe(SensitiveDataType.EMAIL);
});
it('should decrypt data correctly', async () => {
const encrypted = await service.encryptField('test@example.com', SensitiveDataType.EMAIL);
const decrypted = await service.decryptField(encrypted.encryptedData, encrypted.metadata);
expect(decrypted.decryptedData).toBe('test@example.com');
});
it('should enforce performance thresholds', async () => {
const start = Date.now();
await service.encryptField('medical_data', SensitiveDataType.MEDICAL_DIAGNOSIS);
const duration = Date.now() - start;
expect(duration).toBeLessThan(30); // 의료 데이터 30ms 임계값
});
});
통합 테스트
// encryption-integration.e2e-spec.ts
describe('Field Encryption E2E', () => {
it('should encrypt user data on creation', async () => {
const userData = {
email: 'test@example.com',
passwordHash: 'hashed_password'
};
const response = await request(app.getHttpServer())
.post('/users')
.send(userData)
.expect(201);
// 응답은 복호화된 데이터
expect(response.body.email).toBe(userData.email);
// DB에는 암호화된 데이터 저장 확인
const rawUser = await testDb.query('SELECT email FROM users WHERE id = $1', [response.body.id]);
expect(rawUser[0].email).toHaveProperty('encryptedValue');
});
it('should search users by email hash', async () => {
const email = 'search@test.com';
// 사용자 생성
await request(app.getHttpServer())
.post('/users')
.send({ email, passwordHash: 'hash' });
// 이메일로 검색
const response = await request(app.getHttpServer())
.get(`/users/search?email=${email}`)
.expect(200);
expect(response.body.email).toBe(email);
});
});
🚀 배포 계획
Phase 1: 개발 환경 (2주)
-
라이브러리 개발
- Core Field Encryption 라이브러리 구현
- 단위 테스트 완료 (커버리지 90%+)
-
KMS 인프라
- Dev 환경 KMS 키 생성
- 소프트웨어 기반 보호
- 7일 키 순환 주기
-
통합 테스트
- Prisma 미들웨어 통합
- API 암호화/복호화 검증
Phase 2: 스테이징 환경 (1주)
-
베타 테스터 검증
- 실제 사용자 시나리오 테스트
- 성능 모니터링
-
보안 검증
- 키 순환 테스트
- 접근 제어 검증
Phase 3: 프로덕션 환경 (1주)
-
프로덕션 배포
- HSM 기반 키 보호
- 15일/30일 키 순환
- 교차 리전 백업
-
모니터링 활성화
- 실시간 성능 모니터링
- 보안 알림 시스템
데이터 마이그레이션
# 기존 데이터 암호화 마이그레이션
echo "🔄 데이터 마이그레이션 시작"
# 1. 백업 생성
pg_dump $DATABASE_URL > backup_before_encryption.sql
# 2. 테이블별 마이그레이션
npm run migrate:encrypt-users
npm run migrate:encrypt-sleep-logs
npm run migrate:encrypt-questionnaires
# 3. 검증
npm run verify:encryption-migration
echo "✅ 마이그레이션 완료"
📈 성능 목표
| 환경 | 암호화 응답시간 | 복호화 응답시간 | 배치 처리 |
|---|---|---|---|
| Development | < 100ms | < 80ms | 10 필드/초 |
| Staging | < 80ms | < 60ms | 20 필드/초 |
| Production | < 50ms | < 30ms | 50 필드/초 |
💰 비용 예상
| 환경 | KMS 비용 | 스토리지 | 총 비용 |
|---|---|---|---|
| Development | $5/월 | $3/월 | $8/월 |
| Staging | $15/월 | $8/월 | $23/월 |
| Production | $35/월 | $15/월 | $50/월 |
🔍 모니터링 설계
알림 규칙
# 암호화 성능 알림
- alert: EncryptionLatencyHigh
expr: field_encryption_latency_p95 > 100
for: 5m
annotations:
summary: "필드 암호화 응답시간 높음"
# 키 순환 지연 알림
- alert: KeyRotationOverdue
expr: (time() - kms_key_last_rotation) > (30 * 24 * 3600)
for: 1m
annotations:
summary: "KMS 키 순환 지연"
# 무단 의료 데이터 접근
- alert: UnauthorizedMedicalAccess
expr: rate(medical_data_unauthorized_access[5m]) > 0
for: 0s
annotations:
summary: "무단 의료 데이터 접근 감지"
운영 체크리스트
# 일일 체크
./scripts/daily-encryption-health.sh
# 주간 체크
./scripts/weekly-key-rotation-audit.sh
# 월간 체크
./scripts/monthly-security-review.sh
🎯 성공 기준
기술적 목표
- 모든 식별된 민감 필드 암호화 (100%)
- 성능 목표 달성 (암호화
<50ms, 복호화<30ms) - 감사 로그 완전성 (100%)
보안 목표
- GDPR Article 32 기술적 보호조치 준수
- HIPAA 164.312(a)(2)(iv) 암호화 요구사항 충족
- 키 순환 자동화 100% 성공률
운영 목표
- 개발팀 교육 완료 (100%)
- 예산 범위 내 비용 달성 (
<$150/월) - 사용자 불편 사항 0건 (투명한 암호화)
📚 관련 문서
작성자: DTA Wide Platform Team
버전: 1.0.0
최종 업데이트: 2025년 8월 13일
이 구현 가이드는 DTx 플랫폼의 PLT-SEC-003 애플리케이션 레벨 암호화 요구사항을 체계적으로 구현하기 위한 포괄적인 계획을 제시합니다.
변경 이력
| 버전 | 날짜 | 작성자 | 변경 내용 |
|---|---|---|---|
| 0.1.0 | 2025-08-13 | bok@weltcorp.com | 최초 작성 |