目录
- 基于口令(PBE)加密
- 1 定义
- 2 加密过程
- 3 解密过程
- 5 PBE加密算法会话密钥保存
- 4 使用场景
- 5 JDK支持的PBE加密算法
- 6 Bouncy Castle 支持的PBE加密算法
- 7 算法调用示例
基于口令(PBE)加密
1 定义
PBE(Password Based Encryption,基于口令加密)算法是一种基于口令的加密算法,其特点在于口令是由用户自己掌握的,采用随机数杂凑多重加密等方法保证数据的安全性。
PBE算法没有密钥的概念,密钥在其它对称加密算法中是经过算法计算得出来的,PBE算法则是使用口令替代了密钥。
密钥的长短直接影响了算法的安全性,但不方便记忆。即便是我们将密钥经过Base64编码转换为一个可见字符,长密钥一样不容易记忆。因此,在这种情况下密钥是需要存储的,但是口令则不然。比如一般人天天开关电脑,进入操作系统的唯一途径就是输入口令。口令是我们便于记忆的一种凭证,基于这一点,PBE算法使用口令替代了密钥。
由于这种加密方式的口令容易记忆,不用放在物理媒体上,因此增加了口令的安全性。
但是单纯的口令很容易被字典法给穷举出来,所以我们需要给口令加了点“盐”,这个盐和口令组合,想破解就难了。 同时我们将盐和口令合并后用消息摘要算法进行迭代很多次来构建密钥初始化向量的基本材料,使破译更加难了。
PBE算法并没有真正构建新的加密/解密算法,而是对我们已经知道的对称加密算法(如DES算法)做了包装。使用PBE算法对数据做加密/解密操作的时候,其实是使用了DES或者是AES等其它对称加密算法做了相应的操作。 既然PBE算法使用我们较为常用的对称加密算法,那就无法回避密钥的问题。
真正用来做对称加密的密钥和盐,还是需要持久化保存的。不过被保存的是用口令+盐加密后的密文。
2 加密过程
PBE 的加密可以用下图来表示:
主要有三个步骤:
- 生成 KEK 首先,通过伪随机数生成器生成一个被称为盐(salt)的随机数。然后,将盐和口令一起输入单向散列函数,输出的结果就是 KEK。盐是一种用于防御字典攻击的机制。
通过多次单向散列函数迭代可以提高安全性。
用于加密密钥的密钥则称为KEK(Key Encrypting Key,密钥加密密钥) - 生成会话密钥并加密。会话密钥 CEK 也是通过伪随机数生成器来生成,生成之后使用 KEK 对其进行加密,然后将加密后的会话密钥和盐一起保存在安全的地方。
用于加密内容的密钥称为CEK(Contents Encrypting Key,内容加密密钥)
一次性密钥,称为会话密钥,只在特定的会话中使用。 - 加密消息。最后,使用 CEK 对消息进行加密。
3 解密过程
而 PBE 解密的过程则如下图:
解密主要也是有三个步骤:
- 重建KEK 将之前保存下来的盐和口令一起输入单向散列函数,得到的散列值就是 KEK 了。
- 解密会话密钥 再将之前保存下来的已加密的会话密钥用 KEK 进行解密,就能得到会话密钥 CEK 了。
- 解密消息 最后,用已解密的 CEK 对密文进行解密即可。
5 PBE加密算法会话密钥保存
在上文中提到,会话密钥和盐需要保存到一个安全的地方,然后用来解密。但是在JCE提供的PBE方法的调用过程中,并没有返回会话密钥让我们进行保存。目前我还没有弄清楚JCE如何处理这个会话密钥?还是JCE的实现与书中描述的过程不一致。
所以单独作为一个小节,找到答案之后再来更新。如果知道答案的朋友也可以添加评论。
4 使用场景
消息传递过程:
- 由消息传递双方约定口令,这里由甲方构建口令。
- 由口令构建者发布口令,即本系统的服务器将口令发送给系统的客户端使用者。
- 由口令构建者构建本次消息传递使用的盐,这里由甲方构建盐。
- 由消息发送方使用口令、盐对数据加密,这里由甲方对数据加密。
- 由消息发送者将盐、加密数据放松给消息接收者,这里由甲方将盐、加密数据发送给乙方。
- 由消息接收方使用盐、口令对加密数据解密,这里由乙方完成数据解密。
5 JDK支持的PBE加密算法
JDK8原生算法列表,可参第一篇博文: https://blog.csdn.net/yunyun1886358/article/details/128592503#311_JDK_Provider_63
6 Bouncy Castle 支持的PBE加密算法
Bouncy Castle算法列表,可参第一篇博文:
https://editor.csdn.net/md/?articleId=128592503#323_Bouncy_Castle_Provider_568
7 算法调用示例
下面的代码将JDK提供的几种PBE加密算法用枚枚举类进行了封装,调用encrypt()和decrypt()方法可以实现加密和解。
package com.qupeng.crypto.algorithm.oop;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.spec.InvalidKeySpecException;
public class PBEAlgorithmTest {
@Rule
public final ExpectedException exception = ExpectedException.none();
@Test
public void encrypt() throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeySpecException, NoSuchProviderException {
String cipherText = PBEAlgorithm.PBE_With_MD5_And_DES.encrypt("a", "abcde", "12345678", 100);
Assert.assertEquals("a", PBEAlgorithm.PBE_With_MD5_And_DES.decrypt(cipherText, "abcde", "12345678", 100));
cipherText = PBEAlgorithm.PBE_WITH_SHA1_AND_DESEDE.encrypt("a", "abcde", "12345678", 100);
Assert.assertEquals("a", PBEAlgorithm.PBE_WITH_SHA1_AND_DESEDE.decrypt(cipherText, "abcde", "12345678", 100));
cipherText = PBEAlgorithm.PBE_WITH_SHA1_AND_RC2_40.encrypt("a", "abcde", "12345678", 100);
Assert.assertEquals("a", PBEAlgorithm.PBE_WITH_SHA1_AND_RC2_40.decrypt(cipherText, "abcde", "12345678", 100));
cipherText = PBEAlgorithm.PBE_WITH_MD5_AND_RC2.encrypt("a", "abcde", "12345678", 100);
Assert.assertEquals("a", PBEAlgorithm.PBE_WITH_MD5_AND_RC2.decrypt(cipherText, "abcde", "12345678", 100));
cipherText = PBEAlgorithm.PBE_WITH_SHA1_AND_DES.encrypt("a", "abcde", "12345678", 100);
Assert.assertEquals("a", PBEAlgorithm.PBE_WITH_SHA1_AND_DES.decrypt(cipherText, "abcde", "12345678", 100));
cipherText = PBEAlgorithm.PBE_WITH_SHA1_AND_RC2.encrypt("a", "abcde", "12345678", 100);
Assert.assertEquals("a", PBEAlgorithm.PBE_WITH_SHA1_AND_RC2.decrypt(cipherText, "abcde", "12345678", 100));
cipherText = PBEAlgorithm.PBE_WITH_SHA_AND_IDEA_CBC.encrypt("a", "abcde", "12345678", 100);
Assert.assertEquals("a", PBEAlgorithm.PBE_WITH_SHA_AND_IDEA_CBC.decrypt(cipherText, "abcde", "12345678", 100));
cipherText = PBEAlgorithm.PBE_WITH_SHA_AND_2_KEY_TRIPLE_DES_CBC.encrypt("a", "abcde", "12345678", 100);
Assert.assertEquals("a", PBEAlgorithm.PBE_WITH_SHA_AND_2_KEY_TRIPLE_DES_CBC.decrypt(cipherText, "abcde", "12345678", 100));
cipherText = PBEAlgorithm.PBE_WITH_SHA_AND_3_KEY_TRIPLE_DES_CBC.encrypt("a", "abcde", "12345678", 100);
Assert.assertEquals("a", PBEAlgorithm.PBE_WITH_SHA_AND_3_KEY_TRIPLE_DES_CBC.decrypt(cipherText, "abcde", "12345678", 100));
cipherText = PBEAlgorithm.PBE_WITH_SHA_AND_128_BIT_RC2_CBC.encrypt("a", "abcde", "12345678", 100);
Assert.assertEquals("a", PBEAlgorithm.PBE_WITH_SHA_AND_128_BIT_RC2_CBC.decrypt(cipherText, "abcde", "12345678", 100));
cipherText = PBEAlgorithm.PBE_WITH_SHA_AND_40_BIT_RC2_CBC.encrypt("a", "abcde", "12345678", 100);
Assert.assertEquals("a", PBEAlgorithm.PBE_WITH_SHA_AND_40_BIT_RC2_CBC.decrypt(cipherText, "abcde", "12345678", 100));
cipherText = PBEAlgorithm.PBE_WITH_SHA_AND_128_BIT_RC4.encrypt("a", "abcde", "12345678", 100);
Assert.assertEquals("a", PBEAlgorithm.PBE_WITH_SHA_AND_128_BIT_RC4.decrypt(cipherText, "abcde", "12345678", 100));
cipherText = PBEAlgorithm.PBE_WITH_SHA_AND_40_BIT_RC4.encrypt("a", "abcde", "12345678", 100);
Assert.assertEquals("a", PBEAlgorithm.PBE_WITH_SHA_AND_40_BIT_RC4.decrypt(cipherText, "abcde", "12345678", 100));
}
@Test
public void encryptTripeDes() throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeySpecException, NoSuchProviderException {
exception.expect(NoSuchAlgorithmException.class);
exception.expectMessage("PBEWithMD5AndTripeDES SecretKeyFactory not available");
String cipherText = PBEAlgorithm.PBE_WITH_MD5_AND_TRIPE_DES.encrypt("a", "abcde", "12345678", 100);
Assert.assertEquals("a", PBEAlgorithm.PBE_WITH_MD5_AND_TRIPE_DES.decrypt(cipherText, "abcde", "12345678", 100));
}
@Test
public void encryptTripeBlowfish() throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeySpecException, NoSuchProviderException {
exception.expect(NoSuchAlgorithmException.class);
exception.expectMessage("no such algorithm: PBEWithSHAAndBlowfish for provider BC");
String cipherText = PBEAlgorithm.PBE_WITH_SHA_AND_BLOWFISH.encrypt("a", "abcde", "12345678", 100);
Assert.assertEquals("a", PBEAlgorithm.PBE_WITH_SHA_AND_BLOWFISH.decrypt(cipherText, "abcde", "12345678", 100));
}
}
package com.qupeng.crypto.algorithm.oop;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.*;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;
public enum PBEAlgorithm {
PBE_With_MD5_And_DES("PBEWithMD5andDES", "default"),
PBE_WITH_MD5_AND_TRIPE_DES("PBEWithMD5AndTripeDES", "default"),
PBE_WITH_SHA1_AND_DESEDE("PBEWithSHA1AndDESede", "default"),
PBE_WITH_SHA1_AND_RC2_40("PBEWithSHA1AndRC2_40", "default"),
PBE_WITH_MD5_AND_RC2("PBEWithMD5AndRC2", "BC"),
PBE_WITH_SHA1_AND_DES("PBEWithSHA1AndDES", "BC"),
PBE_WITH_SHA1_AND_RC2("PBEWithSHA1AndRC2", "BC"),
PBE_WITH_SHA_AND_IDEA_CBC("PBEWithSHAAndIDEA-CBC", "BC"),
PBE_WITH_SHA_AND_2_KEY_TRIPLE_DES_CBC("PBEWithSHAAnd2-KeyTripleDES-CBC", "BC"),
PBE_WITH_SHA_AND_3_KEY_TRIPLE_DES_CBC("PBEWithSHAAnd3-KeyTripleDES-CBC", "BC"),
PBE_WITH_SHA_AND_128_BIT_RC2_CBC("PBEWithSHAAnd128BitRC2-CBC", "BC"),
PBE_WITH_SHA_AND_40_BIT_RC2_CBC("PBEWithSHAAnd40BitRC2-CBC", "BC"),
PBE_WITH_SHA_AND_128_BIT_RC4("PBEWithSHAAnd128BitRC4", "BC"),
PBE_WITH_SHA_AND_40_BIT_RC4("PBEWithSHAAnd40BitRC4", "BC"),
PBE_WITH_SHA_AND_BLOWFISH("PBEWithSHAAndBlowfish", "BC");
static {
Security.addProvider(new BouncyCastleProvider());
}
private String algorithm = "";
private String providerName = "";
PBEAlgorithm(String algorithm, String providerName) {
this.algorithm = algorithm;
this.providerName = providerName;
}
public String encrypt(String plainText, String password, String saltStr, int iterationCount) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, NoSuchProviderException {
PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
SecretKeyFactory factory = null;
if ("default".equals(this.providerName)) {
factory = SecretKeyFactory.getInstance(this.algorithm);
} else {
factory = SecretKeyFactory.getInstance(this.algorithm, this.providerName);
}
SecretKey secretKey = factory.generateSecret(keySpec);
PBEParameterSpec parameterSpec = new PBEParameterSpec(saltStr.getBytes(), iterationCount);
Cipher cipher = Cipher.getInstance(this.algorithm);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
byte[] bytes = cipher.doFinal(plainText.getBytes());
String cipherText = Base64.getEncoder().encodeToString(bytes);
System.out.println(String.format("%s(%s-%d) plain text: %s -> cipher text: %s", this.algorithm, saltStr, iterationCount, plainText, cipherText));
return cipherText;
}
public String decrypt(String base64CipherText, String password, String saltStr, int iterationCount) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, NoSuchProviderException {
PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
SecretKeyFactory factory = null;
if ("default".equals(this.providerName)) {
factory = SecretKeyFactory.getInstance(this.algorithm);
} else {
factory = SecretKeyFactory.getInstance(this.algorithm, this.providerName);
}
SecretKey secretKey = factory.generateSecret(keySpec);
PBEParameterSpec parameterSpec = new PBEParameterSpec(saltStr.getBytes(), iterationCount);
Cipher cipher = Cipher.getInstance(this.algorithm);
cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
byte[] cipherBytes = Base64.getDecoder().decode(base64CipherText);
byte[] plainBytes = cipher.doFinal(cipherBytes);
String plainText = new String(plainBytes).trim();
System.out.println(String.format("%s(%s-%d) cipher text: %s -> plain text: %s", this.algorithm, saltStr, iterationCount, base64CipherText, plainText));
return plainText;
}
}