建议先看文章做初步了解:Password-Based Encryption (PBE)_厚积薄发者,轻舟万重山-优快云博客
补充:
- 因为具备密码学安全意义的密钥对于人来说是很难记忆的,所以密钥保存的安全性也需要慎重考虑,2022年建议使用的密钥安全强度已经是256bit起(当然128按照当前的算力也是有生之年系列,但是随着算力的提升,如果系统准备用10年以上,建议安全强度256bit,十年以内可以使用128bit);
- 但是口令很容易,一段具有意义的文本对用户来说记忆是很容易的
- 如何通过弱口令接近或达到密钥的安全强度?这就是PBE的价值
我用JDK中现役、常见的PBE算法PBEWithHmacSHA256AndAES_128为例作简述,简述思路如下:
- 先看看JDK中PBEWithHmacSHA256AndAES_128的应用代码;
- 通过应用代码来简述PBE过程;
- 通过应用代码和PBE过程来简述安全性;
应用代码
package wxy.secret;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
/**
* PBEWithHmacSHA256AndAES_128 示例代码
* @author 王大锤
* @date 2022年2月15日
*/
public class PBEDemo {
private static String PBE_MODE = "PBEWithHmacSHA256AndAES_128";
private static SecureRandom random = new SecureRandom();
private static int DEFAULT_ITERATION_COUNT = 1000;
/**
* 生成随机salt
* @param seed
* @return
*/
public static byte[] initSalt(int seed) {
return random.generateSeed(seed);
}
/**
* PBEWithHmacSHA256AndAES_128中AES默认是CBC模式,分组128bit,初始化向量也是128bit
* @param length 16字节
* @return
*/
public static byte[] initIV(int length) {
return random.generateSeed(length);
}
/**
* 虽然是生成SecretKey对象,但实际上这并不是pbe的密钥
* 你通过SecretKey.getEncoded方法拿到密钥的字节数组,你会发现和password.toCharArray是一样
* 实际上,这一步得到的密钥的字节数组就是口令password字符编码的二进制
* @param password
* @return
* @throws Exception
*/
public static SecretKey genereateKey(String password) throws Exception {
PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
return SecretKeyFactory.getInstance(PBE_MODE).generateSecret(pbeKeySpec);
}
/**
* 注意加密方法每次调用都要传入salt和iv
* @param key 其实是用户口令password的字符编码
* @param salt 随机salt
* @param iv AES CBC模式需要的初始化向量,128bit
* @param data 待加密数据
* @return pbe密文
* @throws Exception
*/
public static byte[] encryption(SecretKey key, byte[] salt, byte[] iv, byte[] data) throws Exception {
Cipher cipher = Cipher.getInstance(PBE_MODE);
IvParameterSpec ivp = new IvParameterSpec(iv);
PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, DEFAULT_ITERATION_COUNT, ivp);
cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
return cipher.doFinal(data);
}
/**
* 注意解密方法入参的salt和iv和加密的一致,都是可以公开的数据
* @param key 用户口令的字符编码的二进制
* @param salt 随机salt
* @param iv AES CBC模式需要的初始化向量
* @param data 密文
* @return 明文
* @throws Exception
*/
public static byte[] decryption(SecretKey key, byte[] salt, byte[] iv, byte[] data) throws Exception {
Cipher cipher = Cipher.getInstance(PBE_MODE);
IvParameterSpec ivp = new IvParameterSpec(iv);
PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, DEFAULT_ITERATION_COUNT, ivp);
cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec);
return cipher.doFinal(data);
}
}
看一看测试:
String password = "my_csdn_blog";
SecretKey key = PBEDemo.genereateKey(password);
byte[] salt = PBEDemo.initSalt(32);
byte[] iv = PBEDemo.initIV(16);
String message = "关于Java pbe的简述";
Charset UTF8 = Charset.forName("UTF-8");
byte[] ciphertext = PBEDemo.encryption(key, salt, iv, message.getBytes(UTF8));
HexStringTool.print(ciphertext);
byte[] plaintext = PBEDemo.decryption(key, salt, iv, ciphertext);
HexStringTool.print(plaintext);
System.out.println(new String(plaintext, UTF8));
System.out.println("-------------------");
System.out.println("基于password生成的SecretKey对象实际就是口令的字符编码的二进制:");
HexStringTool.print(password.getBytes());
HexStringTool.print(key.getEncoded());
测试结果:
833459198d56b24c7d3392035075a8c41145c75b5ab326c2687c4e482a977e5b
e585b3e4ba8e4a61766120706265e79a84e7ae80e8bfb0
关于Java pbe的简述
-------------------
基于password生成的SecretKey对象实际就是口令的字符编码的二进制:
6d795f6373646e5f626c6f67
6d795f6373646e5f626c6f67
jdk加解密各种getInstance的入参字符串不需要记忆,懂得查就行了,例如本例PBEWithHmacSHA256AndAES_128,查询页面Java Security Standard Algorithm Names (oracle.com)各种类各种密码学术语的入参,会找这个页面就行了。
更多的信息,看Java Platform, Standard Edition Security Developer’s Guide, Release 15 (oracle.com)
PBE过程
图下图:
通过上面测试代码就能知道,通过口令password得到的SecreteKey对象并非加密密钥,实际上是password的字符编码的二进制。
实际上是两个步骤:
- 用户口令password、随机salt在单向散列函数SHA256中经过iterationCount次迭代,得到AES的密钥(这里你可能不理解,但是没关系,周末我写一篇Java中diffie-hellman密钥协商的文章,那里面会给出这个过程的代码示例,就两句代码,很简单的,那里是Oracle官方推荐的转换方式);
- 然后PBE中AES默认是CBC模式,密钥+分组模式的初始化向量IV对明文进行加密得到密文;
- 将密文、salt、iv和iterationCount发送给解密方,解密方根据事先共享的password就能解密出明文;
同样的,PBE属于对称加密,一样存在密钥共享的问题,也就是加密方和解密方如何共享口令password。
安全性
对于PBEWithHmacSHA256AndAES_128而言,PBE过程是:
- 用户口令+salt经过iterationCount次的散列函数SHA256迭代,生成AES的密钥;
- 根据AES(默认是CBC模式)的模式选择好初始化向量IV,明文+密钥经过AES cipher得到密文;
如果要爆破:
A.对于攻击者而言,直接选择攻击AES 128的密钥爆破出明文是不现实的,就当前的算力而言这还属于有生之年系列(假设一秒钟能做100亿次计算,接近2^33次方,一年365*24*60*60差不多3KW秒,2^(128-33)/3KW,即2^95/3KW,你可以算算是多少年,有生之年系列)
B.用户口令就弱得多了,而且salt、iv和iterationCount都是公开的,这么来看,pbe的安全强度似乎就相当于用户的弱口令了?
实际上直接攻击用户弱口令这个选择也很难:
- iterationCount会加倍攻击者的攻击代价,iterationCount通常是1000次以上迭代,对用户来说加解密就是迭代一千次,而对攻击者来说,是要付出一千倍的代价;
- salt一直在变化,虽然salt是可以公开的,但是每次调用encryption方法输入的salt都不一样,一次通信是很多个data[]片段组成,每个data[]片段的salt都不一样,所以生成的AES密码也不一样,所以攻击者付出极大的代价爆破了一个data[]片段,也拿不到完整的信息;