如何从任意字节数组中导出 AES 256 位密钥 Java?

How to derive an AES 256 bit key from an arbitary byte array it Java?

给定一个任意 Java 字节数组,例如 1024 字节数组,我想导出一个 AES-256 位密钥。该阵列是通过 javax.crypto.KeyAgreement 使用 byte[] secret = keyAgreement.generateSecret()

从 ECHD 生成的

我目前的解决方案是将输入字节数组视为密码。使用 PBKDF2 密钥派生函数输入数组作为密码和盐,如下所示。

更新:我已将 UTF-8 设置为编码以解决评论和答案中指出的问题。

private byte[] deriveAes256bitKey(byte[] secret)
    throws NoSuchAlgorithmException, InvalidKeySpecException {

    var secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
    var password = new String(secret, UTF_8).toCharArray();
    var keySpec = new PBEKeySpec(password, secret, 1024, 256);
    return secretKeyFactory.generateSecret(keySpec).getEncoded();
}

有没有更好的方法来获取 Java 中的字节数组并将其转换为 AES-256 位密钥?

我会谨慎使用 new String(input).toCharArray() 创建密码。它不可移植(它使用平台默认编码),如果输入中存在无效字符序列,它的行为是未定义的。

考虑一下:

    System.out.println(new String(new byte[] {(byte) 0xf0, (byte) 0x82, (byte) 0x82, (byte) 0xac}, StandardCharsets.UTF_8));

f08282ac 是欧元符号 (€) 的超长编码。它被解码为替换字符 (�; 0xfffd),因为它是一个非法序列。 所有 个非法的 UTF-8 序列将最终作为替换字符,这不是您想要的。

您可以通过在将字节数组传递给 SecretKeyFactory 之前序列化字节数组来避免解码问题(base64 对其进行编码,或者简单地 new BigInteger(input).toString(Character.MAX_RADIX))。但是,如果您不使用 SecretKeyFactory,则可以避免这种情况。没必要。

PBKDF2(Password-Based 密钥派生函数 2)旨在通过计算成本高和添加盐来使针对用户提供的密码的暴力攻击更加困难。

您在这里不需要它(您的输入很大且随机;没有人会对其进行字典攻击)。您的问题只是输入长度与所需的密钥长度不匹配。

您可以将输入散列到正确的长度:

MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] keyBytes = md.digest(input);

这里需要的是一个 KBKDF 或者 Key Based Key Derivation Function。 KBKDF 将包含足够熵的秘密值转换为特定大小的不同密钥。当您使用密钥强化(使用盐和工作因子或迭代计数)将密码短语的熵可能太少时,将使用 PBKDF。如果输入值已经足够强而不会被猜测/暴力破解,则不需要使用工作因子/迭代计数。

如果您只需要一个 128 位的结果值,SHA-256 通常就足够了。然而,使用密钥派生函数可能仍然有好处。首先,它是一个为函数显式定义的函数,所以更容易证明它是安全的。此外,通常可以向密钥派生函数添加额外的数据,以便您可以,例如导出更多密钥或密钥和 IV。或者您可以扩展可配置的输出大小,为不同的键或键/IV 输出足够的数据。

也就是说,如果您使用 SHA-256(或 SHA-512,以防您需要更多位的密钥/IV),大多数密码学家都不会皱眉。输出仍然应该使用输入中所有可能的位进行随机化,并且不可能反转函数。