AES Key Wrapping

Last Modified: 2023/09/21

概述

AES Key Wrapping 翻译为中文为密钥封装,这是一种加密密钥的技术,该技术可以让通信双方安全地传输对称密钥,被加密的密钥可以被接收方放心存储而不用担心密钥泄露。

密钥封装原理

先附图一张,方便理解

密钥封装使用到一把主密钥,图中为 wrapping key(KWK)。主密钥加密数据密钥(DEK)得到 wrapped key(WK),应用程序使用 DEK 加密数据,同时存储密文和 WK。应用程序需要解密数据时,将存储的 WK 传递给密钥管理系统(例如阿里云的 KMS),KMS 返回 DEK,使用 DEK 可以解密之前存储的密文。

上图中的 Trusted Environment 可以认为是密钥管理系统。密钥封装的安全性的关键在于这个 KMS。试想一下,主密钥必须仅存储在 kms 中,而不能硬编码在应用程序中。

由于加密和解密数据密钥均需要使用主密钥,因此加密和解密数据密钥也只能发生在 kms 中。

密钥封装方便密钥轮转

一个密钥的安全性和密钥加密的数据量成反比,也就是说密钥加密的数据越多,密钥泄露产生的安全威胁就越大,因此需要密钥轮转。但是密钥轮转不是一个简单的问题,换了密钥之后,原先的数据应该怎么办?

解密后使用新的密钥重新加密?这显然不现实。生产环境的数据量往往很大,全部解密后再轮转密钥听起来就离离原上谱。但轮转密钥明明是一个现实需求,该这么实现呢?

这就要说到密钥封装的另一个好处:有了密钥封装后,密钥轮转就不再是问题了。 由于应用程序同时存储了加密数据和加密的密钥,这两个数据可以保证即便密钥轮转之后,使用老密钥加密的数据仍然可以解密。只要主密钥不变,使用主密钥便可以解密老的数据密钥,拿到 DEK 后便可以正常解密数据。

用 Java 实现 AES Key Wrapping

大概分为以下几步:

  • 生成数据密钥 DEK
  • 生成主密钥,并使用主密钥加密数据密钥 DEK 得到 wrapped key(WK)
  • 使用 DEK 加密数据(这一步应用存储数据密文和 WK)
  • 解密数据

生成数据密钥 DEK

// 生成一个数据密钥(dataEncKey)用于加密数据
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(256); // Key size in bits
SecretKey dataEncKey = keyGenerator.generateKey();

生成主密钥,并使用主密钥加密数据密钥 DEK

// 生成主密钥(wrappingKey)用于加密数据密钥
KeyGenerator wrappingKeyGenerator = KeyGenerator.getInstance("AES");
wrappingKeyGenerator.init(256); // Key size in bits
SecretKey wrappingKey = wrappingKeyGenerator.generateKey();

// 使用主密钥加密数据密钥得到加密后的数据密钥(wrappedKey)
Cipher wrappingCipher = Cipher.getInstance("AESWrap");
wrappingCipher.init(Cipher.WRAP_MODE, wrappingKey);
byte[] wrappedKey = wrappingCipher.wrap(dataEncKey);

使用 DEK 加密数据

// 使用数据密钥加密 plainText
String plainText = "Hello, World!";
// encrypt 见后文中的加解密方法
String encryptedData = encrypt(plainText, dataEncKey.getEncoded());

解密数据

// 使用主密钥解密数据密钥,然后使用解密后的数据密钥(unwrappedKey)解密 encryptedData,然后验证解密后的数据是否和 plainText 相同
wrappingCipher.init(Cipher.UNWRAP_MODE, wrappingKey);
SecretKey unwrappedKey = (SecretKey) wrappingCipher.unwrap(wrappedKey, "AES", Cipher.SECRET_KEY);
// 解密数据,decrypt 见后文中的加解密方法
String decryptedData = decrypt(encryptedData, unwrappedKey.getEncoded());
// 输出 true
System.out.println(Objects.equals(plainText, decryptedData));

加解密方法

private static String encrypt(String plainText, byte[] key) throws Exception {
  SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
  Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
  cipher.init(Cipher.ENCRYPT_MODE, secretKey);
  byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
  return Base64.getEncoder().encodeToString(encryptedBytes);
}

private static String decrypt(String encryptedText, byte[] key) throws Exception {
  SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
  Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
  cipher.init(Cipher.DECRYPT_MODE, secretKey);
  byte[] encryptedBytes = Base64.getDecoder().decode(encryptedText);
  byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
  return new String(decryptedBytes, StandardCharsets.UTF_8);
}

好了,最后如果想看完整代码,请移步 github

有问题吗?点此反馈!

温馨提示:反馈需要登录