2. AES

高级加密标准(AES)是数据加密标准(DES)的继任者,由美国国家标准与技术研究院(NIST)于2001年发布。它被归类为对称分组密码。

对称密码使用相同的密钥进行加密和解密。分组密码意味着它以128位为单位处理明文输入:

AES密钥

2.1. AES变体

根据密钥长度,AES支持三种变体:AES-128(128位)、AES-192(192位)和AES-256(256位)。增加密钥长度会增强加密强度,因为更大的密钥意味着可能的密钥数量更多。因此,算法执行所需的轮次也会增加,计算量相应提升:

密钥长度 分组长度 轮次
128 128 10
192 128 12
256 128 14

2.2. AES有多安全?

AES算法是公开的——真正需要保密的是AES密钥,必须知道密钥才能成功解密。因此,破解AES的关键在于破解密钥。假设密钥被安全保存,攻击者只能尝试猜测密钥。

我们来看看暴力破解在猜测密钥时的表现。

AES-128密钥是128位,这意味着有2^128种可能值。遍历这些可能性需要海量且不可行的时间和金钱。因此,AES实际上无法通过暴力破解方式攻破。

虽然出现过一些非暴力破解方法,但它们也只能将可能的密钥搜索空间减少几个比特。

所有这些都意味着:在完全不知道密钥的情况下,AES实际上是不可能被破解的。

3. 好密钥的特性

现在我们来看看生成AES密钥时需要遵循的重要指南。

3.1. 密钥长度

由于AES支持三种密钥长度,我们应该根据使用场景选择合适的长度。AES-128是商业应用中最常见的选择,它在安全性和速度之间取得了平衡。国家政府通常使用AES-192和AES-256以获得最高安全性。如果我们想拥有额外的安全层,可以使用AES-256。

量子计算机确实可能降低大密钥空间所需的计算量,因此拥有AES-256密钥更具未来兼容性——尽管目前它们还未对商业应用构成实际威胁。

3.2. 熵

熵指的是密钥的随机性。如果生成的密钥不够随机,并且与时间、机器相关或使用字典单词等存在相关性,就会变得脆弱。攻击者将能够缩小密钥搜索空间,削弱AES的强度。因此,密钥必须真正随机,这一点至关重要

4. 生成AES密钥

现在,掌握了生成AES密钥的指南后,我们来看看各种生成方法。

对于所有代码片段,我们定义加密算法为:

private static final String CIPHER = "AES";

4.1. Random类

让我们使用Java中的Random类生成密钥:

private static Key getRandomKey(String cipher, int keySize) {
    byte[] randomKeyBytes = new byte[keySize / 8];
    Random random = new Random();
    random.nextBytes(randomKeyBytes);
    return new SecretKeySpec(randomKeyBytes, cipher);
}

我们创建一个所需长度的字节数组,并用random.nextBytes()生成的随机字节填充它。然后使用该随机字节数组创建SecretKeySpec

Java的Random类是**伪随机数生成器(PRNG),也称为确定性随机数生成器**(DRNG)。这意味着它不是真正随机的。PRNG中的随机数序列可以完全由其种子决定。Java不推荐使用Random进行加密应用。

因此,绝对不要使用Random类生成密钥——这是一个典型的踩坑案例。

4.2. SecureRandom类

现在我们使用Java中的SecureRandom类生成密钥:

private static Key getSecureRandomKey(String cipher, int keySize) {
    byte[] secureRandomKeyBytes = new byte[keySize / 8];
    SecureRandom secureRandom = new SecureRandom();
    secureRandom.nextBytes(secureRandomKeyBytes);
    return new SecretKeySpec(secureRandomKeyBytes, cipher);
}

与前一个例子类似,我们实例化一个所需长度的字节数组。现在,我们使用SecureRandom而不是Random来生成随机字节填充数组。Java推荐使用SecureRandom为加密应用生成随机数。它至少符合FIPS 140-2,加密模块安全要求

显然,在Java中,SecureRandom是获取随机性的事实标准。但它是生成密钥的最佳方式吗?我们继续看下一种方法。

4.3. KeyGenerator类

接下来,我们使用KeyGenerator类生成密钥:

private static Key getKeyFromKeyGenerator(String cipher, int keySize) throws NoSuchAlgorithmException {
    KeyGenerator keyGenerator = KeyGenerator.getInstance(cipher);
    keyGenerator.init(keySize);
    return keyGenerator.generateKey();
}

我们获取当前加密算法的KeyGenerator实例。然后用所需的keySize初始化keyGenerator对象。最后调用generateKey方法生成密钥。那么,它和Random/SecureRandom方法有什么不同?

有两个关键差异值得注意。

首先,无论是Random还是SecureRandom方法,都无法验证我们生成的密钥长度是否符合加密规范。只有当我们进行加密时,如果密钥长度不受支持,才会遇到异常。

使用SecureRandom配合无效的keySize会在初始化加密时抛出异常:

encrypt(plainText, getSecureRandomKey(CIPHER, 111));
java.security.InvalidKeyException: Invalid AES key length: 13 bytes
  at java.base/com.sun.crypto.provider.AESCrypt.init(AESCrypt.java:90)
  at java.base/com.sun.crypto.provider.GaloisCounterMode.init(GaloisCounterMode.java:321)
  at java.base/com.sun.crypto.provider.CipherCore.init(CipherCore.java:592)
  at java.base/com.sun.crypto.provider.CipherCore.init(CipherCore.java:470)
  at java.base/com.sun.crypto.provider.AESCipher.engineInit(AESCipher.java:322)
  at java.base/javax.crypto.Cipher.implInit(Cipher.java:867)
  at java.base/javax.crypto.Cipher.chooseProvider(Cipher.java:929)
  at java.base/javax.crypto.Cipher.init(Cipher.java:1299)
  at java.base/javax.crypto.Cipher.init(Cipher.java:1236)
  at com.baeldung.secretkey.Main.encrypt(Main.java:59)
  at com.baeldung.secretkey.Main.main(Main.java:51)

而使用KeyGenerator时,在密钥生成阶段就会失败,让我们能更早处理问题:

encrypt(plainText, getKeyFromKeyGenerator(CIPHER, 111));
java.security.InvalidParameterException: Wrong keysize: must be equal to 128, 192 or 256
  at java.base/com.sun.crypto.provider.AESKeyGenerator.engineInit(AESKeyGenerator.java:93)
  at java.base/javax.crypto.KeyGenerator.init(KeyGenerator.java:539)
  at java.base/javax.crypto.KeyGenerator.init(KeyGenerator.java:516)
  at com.baeldung.secretkey.Main.getKeyFromKeyGenerator(Main.java:89)
  at com.baeldung.secretkey.Main.main(Main.java:58)

另一个关键差异是默认使用SecureRandomKeyGenerator类属于Java加密包javax.crypto,确保使用SecureRandom获取随机性。我们可以查看KeyGenerator类中init方法的定义:

public final void init(int keysize) {
    init(keysize, JCAUtil.getSecureRandom());
}

因此,使用KeyGenerator作为实践,可以确保我们永远不会误用Random类生成密钥。

4.4. 基于密码的密钥

到目前为止,我们都是从随机且不友好的字节数组生成密钥。基于密码的密钥(PBK)让我们能够根据人类可读的密码生成SecretKey

private static Key getPasswordBasedKey(String cipher, int keySize, char[] password) throws NoSuchAlgorithmException, InvalidKeySpecException {
    byte[] salt = new byte[100];
    SecureRandom random = new SecureRandom();
    random.nextBytes(salt);
    PBEKeySpec pbeKeySpec = new PBEKeySpec(password, salt, 1000, keySize);
    SecretKey pbeKey = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(pbeKeySpec);
    return new SecretKeySpec(pbeKey.getEncoded(), cipher);
}

这里涉及多个步骤,我们来分解一下:

我们从人类可读的密码开始。这是机密信息,必须受到保护。密码需遵循安全指南,如最小长度8个字符、使用特殊字符、大小写字母和数字组合等。此外,OWASP指南建议检查已暴露的密码。

用户友好的密码熵不足。因此,我们添加随机生成的字节(称为salt)来增加猜测难度最小salt长度应为128位。我们使用SecureRandom生成salt。salt不是机密信息,可以明文存储。我们应该为每个密码生成唯一的salt,而不是全局使用相同的salt。这能抵御彩虹表攻击——该攻击使用预计算的哈希表来破解密码。

迭代次数是密钥生成算法应用转换函数的次数。它应该尽可能大。最小推荐迭代次数为1,000。更高的迭代次数会增加攻击者暴力破解所有可能密码时的计算复杂度。

密钥长度就是我们之前讨论的,AES可以是128、192或256位。

我们将上述四个元素封装到PBEKeySpec对象中。然后使用SecretKeyFactory获取PBKDF2WithHmacSHA256算法实例生成密钥。

最后,调用generateSecret方法并传入PBEKeySpec,我们生成了基于人类可读密码的SecretKey

5. 结论

生成密钥主要有两种基础方式:随机密钥或基于人类可读密码的密钥。我们讨论了三种生成随机密钥的方法。其中,KeyGenerator提供真正的随机性,并包含验证机制。因此,**KeyGenerator是更好的选择**。

对于基于人类可读密码的密钥,我们可以使用SecretKeyFactory配合SecureRandom生成的salt和高迭代次数。

完整代码可在GitHub上获取。


原始标题:Generating a Secure AES Key in Java