使用 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 和 CyberChef 相同的“调查”,但使用 SHA256(替换 Java 代码中的值,以匹配盐、密码、迭代,来自示例)。
这就是我的困惑开始的地方:
如果我要使用 CyberChef 处理与上述相同的值,但替换为 SHA256:
密码 plnlrtfpijpuhqylxbgqiiyipieyxvfsavzgxbbcfusqkozwpngsyejqlmjsytrmd UTF8
SALT A009C1A485912C6AE630D3E744240B04 HEX
哈希函数 SHA256
密钥大小 128
迭代 1000 次
我希望派生密钥在 CyberChef 中与 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!
谢谢
英里数。
【讨论】:
我知道您编写了与 CyberChef 相等的示例,但为了更好的安全性,您应该将迭代次数增加到(最小)10.000,更好。其次(出于同样的原因)盐应该是随机的,这样你就不会得到相同的密钥和相同的密码。 感谢您抽出宝贵时间提出这些观点。非常感谢:)以上是关于使用 PBKDF2 和 AES256 进行加密和解密 - 需要实际示例 - 如何获取派生密钥的主要内容,如果未能解决你的问题,请参考以下文章