본문으로 건너뛰기

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주)

  1. 라이브러리 개발

    • Core Field Encryption 라이브러리 구현
    • 단위 테스트 완료 (커버리지 90%+)
  2. KMS 인프라

    • Dev 환경 KMS 키 생성
    • 소프트웨어 기반 보호
    • 7일 키 순환 주기
  3. 통합 테스트

    • Prisma 미들웨어 통합
    • API 암호화/복호화 검증

Phase 2: 스테이징 환경 (1주)

  1. 베타 테스터 검증

    • 실제 사용자 시나리오 테스트
    • 성능 모니터링
  2. 보안 검증

    • 키 순환 테스트
    • 접근 제어 검증

Phase 3: 프로덕션 환경 (1주)

  1. 프로덕션 배포

    • HSM 기반 키 보호
    • 15일/30일 키 순환
    • 교차 리전 백업
  2. 모니터링 활성화

    • 실시간 성능 모니터링
    • 보안 알림 시스템

데이터 마이그레이션

# 기존 데이터 암호화 마이그레이션
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< 80ms10 필드/초
Staging< 80ms< 60ms20 필드/초
Production< 50ms< 30ms50 필드/초

💰 비용 예상

환경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.02025-08-13bok@weltcorp.com최초 작성