使用 Envelope Encryption 保护敏感数据

发布: (2025年12月30日 GMT+8 21:39)
8 min read
原文: Dev.to

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 SbicegoUnsplash 上提供。

Back to Blog

相关文章

阅读更多 »