使用 PBKDF2 和 AES256 进行加密和解密 - 需要实际示例 - 如何获取派生密钥

Posted

技术标签:

【中文标题】使用 PBKDF2 和 AES256 进行加密和解密 - 需要实际示例 - 如何获取派生密钥【英文标题】:Encryption and Decryption with PBKDF2 and AES256 - practical example needed - how do I get the Derived key 【发布时间】:2021-01-25 10:44:23 【问题描述】:

我试图了解如何使用 PBKDF2 和 SHA256 获取派生密钥。

我有点纠结,需要一个清晰易懂的例子。

到目前为止我所拥有的:

    我发现 https://en.wikipedia.org/wiki/PBKDF2 有一个示例,但使用 SHA1,具有以下值:

    密码 plnlrtfpijpuhqylxbgqiiyipieyxvfsavzgxbbcfusqkozwpngsyejqlmjsytrmd UTF8

    SALT A009C1A485912C6AE630D3E744240B04 HEX

    哈希函数 SHA1

    密钥大小 128

    迭代 1000 次

    我一直在使用https://gchq.github.io/CyberChef,可以得到输出17EB4014C8C461C300E9B61518B9A18B,它与***示例中的派生密钥字节相匹配。


我一直在使用https://mkyong.com/java/java-aes-encryption-and-decryption/,它有一个名为 getAESKeyFromPassword 的方法,在这里:

// Password derived AES 256 bits secret key
public static SecretKey getAESKeyFromPassword(char[] password, byte[] salt)
        throws NoSuchAlgorithmException, InvalidKeySpecException 

    SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
    // iterationCount = 65536
    // keyLength = 256
    KeySpec spec = new PBEKeySpec(password, salt, 65536, 256);
    SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
    return secret;


我想执行与***页面、SHA1 和 Cyber​​Chef 相同的“调查”,使用 SHA256(替换 Java 代码中的值,以匹配盐、密码、迭代,来自示例)。

这就是我的困惑开始的地方:

如果我要使用 Cyber​​Chef 处理与上述相同的值,但替换为 SHA256:

密码 plnlrtfpijpuhqylxbgqiiyipieyxvfsavzgxbbcfusqkozwpngsyejqlmjsytrmd UTF8

SALT A009C1A485912C6AE630D3E744240B04 HEX

哈希函数 SHA256

密钥大小 128

迭代 1000 次

我希望派生密钥在 Cyber​​Chef 中与 https://mkyong.com/java/java-aes-encryption-and-decryption/ 示例中的相同。

不是。

我不禁认为我的理解有缺陷。

有人可以提供一个带有 SHA256 的 PBKDF2 的简单(完整)示例,这样我就可以理解发生了什么。如果派生密钥不应该相同(与 SHA1 示例一样,请解释原因)。

是 Java SecretKey:

SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");

和派生密钥一样吗?

似乎缺乏易于理解的示例。

谢谢

英里数。

【问题讨论】:

您已经注意到 mkyong 代码使用 65536 次迭代和 256 的密钥长度?我不清楚你在比较什么 - 使用 SHA-1 哈希肯定会给出另一个使用 SHA-256 的输出。第三:“factory.generateSecret(spec).getEncoded()”为您提供了一个与“new SecretKeySpec...AES”一起使用以生成 AES 密钥的密钥。顺便说一句:“salt”在真实的单词程序中应该是随机的,即使使用相同的密码字符串也会得到不同的哈希值。 Java PBKDF2 的工作方式略有不同。您构建的密钥只是密码等输入值的容器。实际的密钥派生是由密码本身完成的。请参阅此问题及其答案:***.com/q/39954211/150978 我无法重现:使用您的值:密码:pln...,盐:0xA0...,摘要:SHA256,keySize:128 和迭代:1000 方法 getAESKeyFromPassword()返回十六进制编码的28869B5F31AE29236F164C5CB33E2E3B,等于CyberChef 结果。也许您应该发布完整的getAESKeyFromPassword()-call。 此代码中包含大量文本,但您生成的实际值或所需的值并非如此。请确保您发布了 MCVE 和要比较的实际值;非常不鼓励为那些指向外部资源。 【参考方案1】:

感谢大家的意见,尤其是 Topaco :)

我将回答我的问题,因为我花了一些时间在 MCVE 上工作,并设法获得了与cyberChef 相同的 SecretKey。

秘钥值为:28869b5f31ae29236f164c5cb33e2e3bb46f483867a15f8e7208e1836070f64a

这是cyberChef的输出:

这是 Java 代码,以及运行它的输出:

package crypto;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Base64;

public class EncryptDecryptAesGcmPassword 

    private static final String ENCRYPT_ALGO = "AES/GCM/NoPadding";

    private static final int TAG_LENGTH_BIT = 128; // must be one of 128, 120,     112, 104, 96
    private static final int IV_LENGTH_BYTE = 12;
    private static final int SALT_LENGTH_BYTE = 16;
    public static final int ITERATION_COUNT = 1000;
    public static final int KEY_LENGTH = 256;

    private static final Charset UTF_8 = StandardCharsets.UTF_8;

    // return a base64 encoded AES encrypted text
    public static String encrypt(byte[] salt, byte[] pText, String password) throws Exception 
        // GCM recommended 12 bytes iv?
        byte[] iv = getRandomNonce(IV_LENGTH_BYTE);

        // secret key from password
        SecretKey aesKeyFromPassword = getAESKeyFromPassword(password.toCharArray(), salt);

        Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);

        // ASE-GCM needs GCMParameterSpec
        cipher.init(Cipher.ENCRYPT_MODE, aesKeyFromPassword, new GCMParameterSpec(TAG_LENGTH_BIT, iv));

        byte[] cipherText = cipher.doFinal(pText);

        // prefix IV and Salt to cipher text
        byte[] cipherTextWithIvSalt = ByteBuffer.allocate(iv.length + salt.length + cipherText.length)
            .put(iv)
            .put(salt)
            .put(cipherText)
            .array();

        // string representation, base64, send this string to other for decryption.
        return Base64.getEncoder().encodeToString(cipherTextWithIvSalt);

    

    // we need the same password, salt and iv to decrypt it
    private static String decrypt(String cText, String password) throws Exception 
        byte[] decode = Base64.getDecoder().decode(cText.getBytes(UTF_8));

        // get back the iv and salt from the cipher text
        ByteBuffer bb = ByteBuffer.wrap(decode);

        byte[] iv = new byte[IV_LENGTH_BYTE];
        bb.get(iv);

        byte[] salt = new byte[SALT_LENGTH_BYTE];
        bb.get(salt);

        byte[] cipherText = new byte[bb.remaining()];
        bb.get(cipherText);

        // get back the aes key from the same password and salt
        SecretKey aesKeyFromPassword = getAESKeyFromPassword(password.toCharArray(), salt);

        Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);

        cipher.init(Cipher.DECRYPT_MODE, aesKeyFromPassword, new GCMParameterSpec(TAG_LENGTH_BIT, iv));

        byte[] plainText = cipher.doFinal(cipherText);

        return new String(plainText, UTF_8);

    


    public static byte hexToByte(String hexString) 
        int firstDigit = toDigit(hexString.charAt(0));
        int secondDigit = toDigit(hexString.charAt(1));
        return (byte) ((firstDigit << 4) + secondDigit);
    

    public static byte[] decodeHexString(String hexString) 
        if (hexString.length() % 2 == 1) 
            throw new IllegalArgumentException(
                "Invalid hexadecimal String supplied.");
        

        byte[] bytes = new byte[hexString.length() / 2];
        for (int i = 0; i < hexString.length(); i += 2) 
            bytes[i / 2] = hexToByte(hexString.substring(i, i + 2));
          
        return bytes;
    

    private static int toDigit(char hexChar) 
        int digit = Character.digit(hexChar, 16);
        if (digit == -1) 
            throw new IllegalArgumentException(
                "Invalid Hexadecimal Character: "+ hexChar);
        
        return digit;
    

    // Random byte[] with length numBytes
    public static byte[] getRandomNonce(int numBytes) 
        byte[] nonce = new byte[numBytes];
        new SecureRandom().nextBytes(nonce);
        return nonce;
    

    // Password derived AES 256 bits secret key
    public static SecretKey getAESKeyFromPassword(char[] password, byte[] salt)
        throws NoSuchAlgorithmException, InvalidKeySpecException 

        SecretKeyFactory factory =    SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        // iterationCount = 1000
        // keyLength = 256
        KeySpec spec = new PBEKeySpec(password, salt, ITERATION_COUNT,
            KEY_LENGTH);
        SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");

        String encodedKey = hex(secret.getEncoded());

        // print SecretKey as hex
        System.out.println("SecretKey: " + encodedKey);

        return secret;

    

    // hex representation
    public static String hex(byte[] bytes) 
        StringBuilder result = new StringBuilder();
        for (byte b : bytes) 
            result.append(String.format("%02x", b));
        
        return result.toString();
    



    public static void main(String[] args) throws Exception 
        String OUTPUT_FORMAT = "%-30s:%s";
        String PASSWORD = "plnlrtfpijpuhqylxbgqiiyipieyxvfsavzgxbbcfusqkozwpngsyejqlmjsytrmd";

        // plain text
        String pText = "AES-GSM Password-Bases encryption!";

        // convert hex string to byte[]
        byte[] salt = decodeHexString("A009C1A485912C6AE630D3E744240B04");


        String encryptedTextBase64 = EncryptDecryptAesGcmPassword.encrypt(salt, pText.getBytes(UTF_8), PASSWORD);

        System.out.println("\n------ AES GCM Password-based Encryption ------");
        System.out.println(String.format(OUTPUT_FORMAT, "Input (plain text)", pText));
        System.out.println(String.format(OUTPUT_FORMAT, "Encrypted (base64) ", encryptedTextBase64));

        System.out.println("\n------ AES GCM Password-based Decryption ------");
        System.out.println(String.format(OUTPUT_FORMAT, "Input (base64)", encryptedTextBase64));

        String decryptedText = EncryptDecryptAesGcmPassword.decrypt(encryptedTextBase64, PASSWORD);
        System.out.println(String.format(OUTPUT_FORMAT, "Decrypted (plain text)", decryptedText));
    

运行此代码,会产生以下结果:

SecretKey: 28869b5f31ae29236f164c5cb33e2e3bb46f483867a15f8e7208e1836070f64a

------ AES GCM Password-based Encryption ------
Input (plain text)            :AES-GSM Password-Bases encryption!
Encrypted (base64)            :/PuTLBTKVWgJB2iMoAnBpIWRLGrmMNPnRCQLBABOkwNeY8BrrdtoRNVFqZ+xmUjvF2PET6Ne2+PAp34QLCUFjQodTMdmzaNAfzcLWOf4

------ AES GCM Password-based Decryption ------
Input (base64)               :/PuTLBTKVWgJB2iMoAnBpIWRLGrmMNPnRCQLBABOkwNeY8BrrdtoRNVFqZ+xmUjvF2PET6Ne2+PAp34QLCUFjQodTMdmzaNAfzcLWOf4
SecretKey: 28869b5f31ae29236f164c5cb33e2e3bb46f483867a15f8e7208e1836070f64a
Decrypted (plain text)        :AES-GSM Password-Bases encryption!

谢谢

英里数。

【讨论】:

我知道您编写了与 Cyber​​Chef 相等的示例,但为了更好的安全性,您应该将迭代次数增加到(最小)10.000,更好。其次(出于同样的原因)盐应该是随机的,这样你就不会得到相同的密钥和相同的密码。 感谢您抽出宝贵时间提出这些观点。非常感谢:)

以上是关于使用 PBKDF2 和 AES256 进行加密和解密 - 需要实际示例 - 如何获取派生密钥的主要内容,如果未能解决你的问题,请参考以下文章

AES 自定义密码密钥

Android 中的 AES 密钥生成

在 Java 中使用 PBKDF2 进行密码验证

AES GCM 使用 web 微妙加密进行加密并使用颤振加密进行解密

JS前端接口加密/解密

如何在 Android 中使用自己的密钥进行 AES-256 加密?