如何从Java的任意字节数组派生AES 256位密钥?

问题描述

给出一个任意的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) 0xac},StandardCharsets.UTF_8));

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

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

PBKDF2(基于密码的密钥派生功能2)旨在通过增加计算量和添加盐来使对用户提供的密码的暴力攻击更加困难。

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

您可以将输入哈希为正确的长度:

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

这里需要的是KBKDF或基于密钥的密钥派生功能。 KBKDF将包含足够熵的秘密值转换为特定大小的其他密钥。当您的密码短语使用密钥加强(使用盐和工作因子或迭代计数)对密钥的熵可能太少时,将使用PBKDF。如果输入值已经足够强大而不会被猜测/强制使用,则无需使用工作因数/迭代次数。

如果您只希望得到结果的128位值,通常

SHA-256就足够了。但是,使用密钥派生功能仍然可以带来好处。首先,它是为该函数显式定义的一个函数,因此更容易证明它是安全的。此外,通常可以将其他数据添加到密钥派生功能,以便您可以派生更多密钥或一个密钥和一个IV。或者,您可以扩展可配置的输出大小,以为不同的键或键/ IV输出足够的数据。

也就是说,如果您使用SHA-256(或SHA-512(如果您需要更多位用于密钥/ IV),则大多数密码学家不会太担心)。仍然应该使用输入中所有可能的位来使输出随机化,并且不可能对函数求逆。