共计 2541 个字符,预计需要花费 7 分钟才能阅读完成。
上一节我们讲的 AES 加密,细心的童鞋可能会发现,密钥长度是固定的 128/192/256 位,而不是我们用 WinZip/WinRAR 那样,随便输入几位都可以。
这是因为对称加密算法决定了口令必须是固定长度,然后对明文进行分块加密。又因为安全需求,口令长度往往都是 128 位以上,即至少 16 个字符。
但是我们平时使用的加密软件,输入 6 位、8 位都可以,难道加密方式不一样?
实际上用户输入的口令并不能直接作为 AES 的密钥进行加密(除非长度恰好是 128/192/256 位),并且用户输入的口令一般都有规律,安全性远远不如安全随机数产生的随机口令。因此,用户输入的口令,通常还需要使用 PBE 算法,采用随机数杂凑计算出真正的密钥,再进行加密。
PBE 就是 Password Based Encryption 的缩写,它的作用如下:
key = generate(userPassword, secureRandomPassword);
PBE 的作用就是把用户输入的口令和一个安全随机的口令采用杂凑后计算出真正的密钥。以 AES 密钥为例,我们让用户输入一个口令,然后生成一个随机数,通过 PBE 算法计算出真正的 AES 口令,再进行加密,代码如下:
public class Main {public static void main(String[] args) throws Exception {// 把 BouncyCastle 作为 Provider 添加到 java.security:
Security.addProvider(new BouncyCastleProvider());
// 原文:
String message = "Hello, world!";
// 加密口令:
String password = "hello12345";
// 16 bytes 随机 Salt:
byte[] salt = SecureRandom.getInstanceStrong().generateSeed(16);
System.out.println(HexFormat.of().formatHex(salt));
// 加密:
byte[] data = message.getBytes("UTF-8");
byte[] encrypted = encrypt(password, salt, data);
System.out.println("encrypted:" + Base64.getEncoder().encodeToString(encrypted));
// 解密:
byte[] decrypted = decrypt(password, salt, encrypted);
System.out.println("decrypted:" + new String(decrypted, "UTF-8"));
}
// 加密:
public static byte[] encrypt(String password, byte[] salt, byte[] input) throws GeneralSecurityException {PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
SecretKeyFactory skeyFactory = SecretKeyFactory.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
SecretKey skey = skeyFactory.generateSecret(keySpec);
PBEParameterSpec pbeps = new PBEParameterSpec(salt, 1000);
Cipher cipher = Cipher.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
cipher.init(Cipher.ENCRYPT_MODE, skey, pbeps);
return cipher.doFinal(input);
}
// 解密:
public static byte[] decrypt(String password, byte[] salt, byte[] input) throws GeneralSecurityException {PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
SecretKeyFactory skeyFactory = SecretKeyFactory.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
SecretKey skey = skeyFactory.generateSecret(keySpec);
PBEParameterSpec pbeps = new PBEParameterSpec(salt, 1000);
Cipher cipher = Cipher.getInstance("PBEwithSHA1and128bitAES-CBC-BC");
cipher.init(Cipher.DECRYPT_MODE, skey, pbeps);
return cipher.doFinal(input);
}
}
使用 PBE 时,我们还需要引入 BouncyCastle,并指定算法是 PBEwithSHA1and128bitAES-CBC-BC
。观察代码,实际上真正的 AES 密钥是调用Cipher
的init()
方法时同时传入 SecretKey
和PBEParameterSpec
实现的。在创建 PBEParameterSpec
的时候,我们还指定了循环次数1000
,循环次数越多,暴力破解需要的计算量就越大。
如果我们把 salt 和循环次数固定,就得到了一个通用的“口令”加密软件。如果我们把随机生成的 salt 存储在 U 盘,就得到了一个“口令”加 USB Key 的加密软件,它的好处在于,即使用户使用了一个非常弱的口令,没有 USB Key 仍然无法解密,因为 USB Key 存储的随机数密钥安全性非常高。
小结
PBE 算法通过用户口令和安全的随机 salt 计算出 Key,然后再进行加密;
Key 通过口令和安全的随机 salt 计算得出,大大提高了安全性;
PBE 算法内部使用的仍然是标准对称加密算法(例如 AES)。