Envelope Encryption을 이용한 민감한 데이터 보호
Source: Dev.to
Envelope 암호화는 민감한 데이터를 보호하기 위해 두 겹의 암호화를 사용하는 기술입니다.
- 첫 번째 계층: 무작위로 생성된 **데이터 암호화 키 (DEK)**를 사용하여 대칭 암호화 알고리즘으로 데이터를 암호화하며, 실제 평문을 보호합니다.
- 두 번째 계층: **키 암호화 키 (KEK)**를 사용해 DEK를 암호화(또는 래핑)합니다. KEK는 일반적으로 KMS 또는 HSM 내부에 저장·보호되는 장기 키이며, 시스템에 따라 대칭키 또는 비대칭키일 수 있습니다.
이와 같은 2단계 접근 방식은 DEK와 암호화된 데이터 모두의 보안을 보장합니다. Envelope 암호화는 주요 클라우드 플랫폼이 제공하는 중앙 집중식 키 관리 시스템(KMS)의 일부로 널리 사용됩니다.
이 글에서는 간단한 예제 구현을 통해 이 기술이 어떻게 작동하는지 살펴보겠습니다.
래핑 암호화의 장점은 무엇인가요?
-
강화된 보안
대부분의 프로덕션 환경에서 KEK는 HSM을 기반으로 하는 KMS 내부에 저장 및 보호되어, 보안 하드웨어 경계를 벗어나지 않도록 보장됩니다. KEK가 DEK를 풀어내는 데 필요하기 때문에, 공격자는 암호화된 데이터를 입수하더라도 암호문을 복호화할 수 없습니다—KEK 없이는 DEK 자체를 복구할 수 없습니다. -
간소화된 키 관리
래핑 암호화는 조직이 하나 이상의 KEK 아래에서 다수의 DEK를 관리할 수 있게 하여 운영 복잡성을 크게 줄여줍니다. 기존 암호화된 DEK는 이전 KEK 버전과 계속 호환되므로, 모든 데이터를 다시 암호화하지 않고도 KEK를 교체할 수 있습니다. 이는 대규모 키 교체를 가능하게 하고 기본 데이터 저장소에 미치는 영향을 최소화합니다.
암호화 흐름
이제 우리는 envelope encryption의 기본을 다루었으니, 이 기술을 사용하여 신용카드 번호와 같은 민감한 데이터를 암호화하는 간단한 구현을 살펴보겠습니다.
// Fake card number
String plainText = "5105105105105100";
1. 무작위 데이터 암호화 키 (DEK) 생성
private static final String KEY_ALGORITHM = "AES";
private static final int AES_KEY_LENGTH = 256;
// ...
KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
keyGenerator.init(AES_KEY_LENGTH);
SecretKey dataEncryptionKey = keyGenerator.generateKey();
2. AES‑GCM으로 평문 암호화
private static final String ENC_ALGORITHM = "AES/GCM/NoPadding";
// ...
byte[] bPlainText = plainText.getBytes(StandardCharsets.UTF_8);
Cipher cipher = Cipher.getInstance(ENC_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, dataEncryptionKey);
byte[] iv = cipher.getIV(); // Initialization vector
byte[] cipherText = cipher.doFinal(bPlainText);
3. IV와 암호문을 Base64로 인코딩
String encodedIv = Base64.getEncoder().encodeToString(iv);
String encodedCipherText = Base64.getEncoder().encodeToString(cipherText);
샘플 출력
Base64 Encoded IV: g4xPa5FCgkw+KzmT
Base64 Encoded Cipher: fWegEZo4dlmm1YHvAe+njcZrcz10r5W9ntEEpQK5OgA=
데이터 암호화 키 (DEK) 래핑
지금까지 입력 데이터를 암호화하는 첫 번째 레이어인 봉투 암호화(envelope encryption)를 다뤘습니다. 이제 두 번째 레이어인 DEK 자체를 래핑할 차례입니다.
실제 운영 환경에서는 KEK가 HSM을 기반으로 하는 KMS 서비스 안에서 생성·저장됩니다. KEK가 보안 하드웨어를 떠나지 않기 때문에 애플리케이션이 직접 접근할 수 없습니다.
예시를 위해 아래에 보여지는 인‑메모리 KMS를 사용해 보겠습니다.
package com.example;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class InMemoryKMS {
private static final String KEY_ALGORITHM = "AES";
private static final int AES_KEY_LENGTH = 256;
private static final String WRAP_TRANSFORMATION = "AESWrap";
private final Map<String, List<SecretKey>> keyStore = new HashMap<>();
public String createKEK() {
SecretKey key = generateKey();
String keyId = UUID.randomUUID().toString();
keyStore.computeIfAbsent(keyId, k -> new ArrayList<>()).add(key);
return keyId;
}
public void rotateKEK(String keyId) {
List<SecretKey> versions = keyStore.get(keyId);
if (versions == null) {
throw new IllegalArgumentException("Unknown keyId: " + keyId);
}
SecretKey newKek = generateKey();
versions.add(newKek);
}
public byte[] wrapDEK(String keyId, SecretKey dataEncryptionKey) {
try {
SecretKey activeKek = getLatestKey(keyId);
Cipher cipher = Cipher.getInstance(WRAP_TRANSFORMATION);
cipher.init(Cipher.WRAP_MODE, activeKek);
return cipher.wrap(dataEncryptionKey);
} catch (NoSuchPaddingException | IllegalBlockSizeException |
NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException(e);
}
}
// (truncated for brevity)
}
InMemoryKMS 구현의 나머지 부분은 간결성을 위해 생략되었습니다.
Source: …
Java에서 봉투 암호화 – 정리된 마크다운
unwrapDEK 메서드
SecretKey unwrapDEK(String keyId, byte[] wrappedDataEncryptionKey) {
try {
List<SecretKey> versions = keyStore.get(keyId);
if (versions == null) {
throw new IllegalArgumentException("Unknown keyId: " + keyId);
}
// Try each KEK version (newest first), just like cloud KMS
for (int i = versions.size() - 1; i >= 0; i--) {
SecretKey kek = versions.get(i);
try {
Cipher cipher = Cipher.getInstance(WRAP_TRANSFORMATION);
cipher.init(Cipher.UNWRAP_MODE, kek);
return (SecretKey) cipher.unwrap(
wrappedDataEncryptionKey,
KEY_ALGORITHM,
Cipher.SECRET_KEY);
} catch (GeneralSecurityException ignored) {
// Try next version
}
}
throw new IllegalStateException("Unable to unwrap DEK with any KEK version");
} catch (Exception e) {
throw new IllegalStateException("Failed to unwrap DEK", e);
}
}
도우미 메서드
private SecretKey generateKey() {
try {
KeyGenerator generator = KeyGenerator.getInstance(KEY_ALGORITHM);
generator.init(AES_KEY_LENGTH);
return generator.generateKey();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private SecretKey getLatestKey(String keyId) {
List<SecretKey> versions = keyStore.get(keyId);
if (versions == null || versions.isEmpty()) {
throw new IllegalArgumentException("Unknown keyId: " + keyId);
}
return versions.get(versions.size() - 1);
}
데이터 암호화 키(DEK) 래핑
InMemoryKMS kms = new InMemoryKMS();
String keyId = kms.createKEK();
byte[] wrappedDEK = kms.wrapDEK(keyId, dataEncryptionKey);
저장을 위한 래핑된 DEK 인코딩
String encodedWrappedDEK = Base64.getEncoder().encodeToString(wrappedDEK);
샘플 암호화 출력
Base64 Encoded IV: +W1d4YVNBBJFDuaB
Base64 Encoded Cipher: Wib4Hx96rSBn7dTXrrdKEb3BTxbUdDm7PPCxybc20/I=
KEK ID: eeeb0c35-b166-4680-a800-3a28641fb03c
Base64 Encoded WrappedDEK: dzQCY+x+JsDdHpscjf1qiWgm2utGEwNFIBIX6QiWqW/mjIRP42jFgw==
복호화 흐름
암호화 시에 다음을 저장했습니다:
- IV(초기화 벡터)
- 암호문
- 래핑된 DEK
- KEK 식별자
KEK는 KMS를 떠나지 않으므로, 애플리케이션은 암호문을 복호화하기 전에 KMS에 DEK를 언래핑하도록 요청해야 합니다.
DEK 언래핑
byte[] wrappedDek = Base64.getDecoder().decode(encodedWrappedDEK);
SecretKey dataEncryptionKey = kms.unwrapDEK(keyId, wrappedDek);
Note: DEK는 절대로 영구 저장되지 않아야 하며, 가능한 한 짧은 시간 동안 메모리에서만 유지하십시오.
암호문 복호화
byte[] iv = Base64.getDecoder().decode(encodedIv);
byte[] cipherText = Base64.getDecoder().decode(encodedCipherText);
Cipher cipher = Cipher.getInstance(ENC_ALGORITHM);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, dataEncryptionKey, gcmParameterSpec);
byte[] decrypted = cipher.doFinal(cipherText);
String decryptedText = new String(decrypted, StandardCharsets.UTF_8);
System.out.println("Decrypted Text: " + decryptedText);
결과
Decrypted Text: 5105105105105100
키 회전에 대한 메모
키 회전은 더 넓은 주제이지만 기본 아이디어는 다음과 같습니다:
- KMS가 KEK를 회전시킬 때, 새로운 KEK 버전을 생성하고 이를 향후 래핑 작업을 위한 활성 키로 표시합니다.
- 이전 KEK 버전은 이전에 래핑된 DEK를 언래핑할 수 있도록 계속 사용 가능하므로, 기존 데이터를 재암호화 없이도 복호화할 수 있습니다.
최종 생각
Envelope encryption은 민감한 데이터를 보호하면서 키 관리를 단순화하는 강력하고 확장 가능한 방법을 제공합니다. 이는 대부분의 최신 클라우드 KMS 솔루션의 기반이 됩니다.
아래 저장소에서 전체 코드 예제를 확인할 수 있습니다:
읽어 주셔서 감사합니다!
크레딧
- AES/GCM encryption in Java
- Envelope encryption
- Hybrid cryptosystem – Envelope encryption (Wikipedia)
표지 사진: Mauro Sbicego on Unsplash.