使用 Envelope Encryption 保护敏感数据
Source: Dev.to
Envelope encryption 是一种使用两层加密来保护敏感数据的技术。
- 第一层: 使用对称加密算法和随机生成的 数据加密密钥 (DEK) 对数据进行加密,以保护实际的明文。
- 第二层: 使用 密钥加密密钥 (KEK) 对 DEK 进行加密(或 包装)。KEK 通常是存放在 KMS 或 HSM 中并受到保护的长期密钥,且可以是对称的也可以是非对称的,具体取决于系统。
这种两级方案确保了 DEK 和已加密数据的安全性。Envelope encryption 被广泛用于主要云平台提供的集中式密钥管理系统(KMS)中。
在本文中,我们将通过一个简单的示例实现来探讨该技术的工作原理。
什么是信封加密的好处?
-
更强的安全性
在大多数生产环境中,KEK 存储并受托管在由 HSM 支持的 KMS 中,确保它永远不会离开安全硬件边界。由于解封 DEK 需要 KEK,攻击者即使获取了加密数据也无法解密密文——没有 KEK 无法恢复 DEK 本身。 -
简化的密钥管理
信封加密允许组织在一个或多个 KEK 下管理大量 DEK,显著降低运营复杂性。KEK 可以在不重新加密所有数据的情况下进行轮换,因为已有的加密 DEK 仍可使用旧的 KEK 版本。这使得大规模密钥轮换成为可能,并将对底层数据存储的影响降至最低。
加密流程
现在我们已经介绍了信封加密的基础概念,下面通过一个简单的实现来演示如何使用该技术加密敏感数据——例如信用卡号。
// 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. 使用 Base64 对 IV 和密文进行编码
String encodedIv = Base64.getEncoder().encodeToString(iv);
String encodedCipherText = Base64.getEncoder().encodeToString(cipherText);
示例输出
Base64 Encoded IV: g4xPa5FCgkw+KzmT
Base64 Encoded Cipher: fWegEZo4dlmm1YHvAe+njcZrcz10r5W9ntEEpQK5OgA=
包装数据加密密钥(DEK)
到目前为止,我们已经介绍了信封加密的第一层,即对输入数据进行加密。现在我们准备进行第二层——对 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 实现的其余部分已省略,以保持简洁。
Java 中的信封加密 – 清理后的 Markdown
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);
注意: 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 加密提供了一种强大且可扩展的方式来保护敏感数据,同时简化密钥管理。它是大多数现代云 KMS 解决方案的基础。
您可以在下面的仓库中找到完整的代码示例:
感谢阅读!
致谢
封面照片由 Mauro Sbicego 在 Unsplash 上提供。