是五月呀!

对称加密与AES算法

对称加密

加密和解密使用同一个密钥的方式成为共享密钥加密(Common key crypto system),也被成为对称密钥加密。
以共享的方式加密时必须将密钥也发送给对方。

AES算法

高级加密标准(英语:Advanced Encryption Standard,缩写:AES),在密码学中又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。
对称加密算法也就是加密和解密用相同的密钥,具体的加密流程如下图:

  • 明文P:没有经过加密的数据。
  • 密钥K:用来加密明文的密码,在对称加密算法中,加密与解密的密钥是相同的。密钥为接收方与发送方协商产生,但不可以直接在网络上传输,否则会导致密钥泄漏,通常是通过非对称加密算法加密密钥,然后再通过网络传输给对方,或者直接面对面商量密钥。密钥是绝对不可以泄漏的,否则会被攻击者还原密文,窃取机密数据。
  • AES加密函数:设AES加密函数为E,则 C = E(K, P),其中P为明文,K为密钥,C为密文。也就是说,把明文P和密钥K作为加密函数的参数输入,则加密函数E会输出密文C。
  • 密文C:经加密函数处理后的数据。
  • AES解密函数:设AES解密函数为D,则 P = D(K, C),其中C为密文,K为密钥,P为明文。也就是说,把密文C和密钥K作为解密函数的参数输入,则解密函数会输出明文P。

密钥

AES的区块长度固定为128比特,密钥长度则可以是128,192或256比特。
以密钥长度128比特为例,密钥可以是:
(1)16个字符的随机字符串
假如我们跟客户端协商好,密钥是一个随机字符串(全英文),那么它应该包含16个字符,直接用getBytes()获取128位。
为什么是16个字符?
客户端一般将AES算法写在so库里,用C语言实现。而C语言中char是1个字节,8位,那么16x8=128位。
注意这16个字符不要包含中文等特殊字符,因为在Java中,char是两个字节。由于英文可以用1个字节表示,所以仅用低8位,可以理解为英文字符也占1个字节,8位。
而中文等字符,使用Unicode编码表示,可能占据3个甚至多个字节。
(2)32个字符的十六进制字符串
另一种约定方式,是约定一个特殊字符串,每个字符是一个十六进制,一共32个字符,作为128位的十六进制表示,4x32=128。
这样两端在使用这个密钥时,都需要将十六进制转成二进制,而不是直接getBytes()

生成一个十六进制字符串秘钥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* AES密钥生成器,生成16进制格式的密钥
* 密钥长度可以是128,192或256比特,这里生成的是128bit的16进制字符串,长度为32 (4*32=128)
*/
public static String generateAESKey() {
try {
KeyGenerator generator = KeyGenerator.getInstance("AES");
generator.init(128); //128bit
SecretKey secretKey = generator.generateKey();
return DatatypeConverter.printHexBinary(secretKey.getEncoded());//16进制编码后返回
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}

AES属于对称加密,内容传输之前,前端使用密钥将内容加密,后端拿到后,使用相同的密钥将内容解密。

模式

主要使用以下两种模式:

  • ECB:ECB是最简单的块密码加密模式,加密前根据加密块大小(如AES为128位)分成若干块,之后将每块使用相同的密钥单独加密,解密同理。ECB模式由于每块数据的加密是独立的因此加密和解密都可以并行计算,ECB模式最大的缺点是相同的明文块会被加密成相同的密文块,这种方法在某些环境下不能提供严格的数据保密性。
  • CBC:CBC模式对于每个待加密的密码块在加密前会先与前一个密码块的密文异或然后再用加密器加密。第一个明文块与一个叫初始化向量的数据块异或。CBC模式相比ECB有更高的保密性,但由于对每个数据块的加密依赖与前一个数据块的加密所以加密无法并行。与ECB一样在加密前需要对数据进行填充,不是很适合对流数据进行加密。

ECB模式

这里介绍使用16长随机字符串进行ECB模式加解密。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* aes加密
* @param content 明文
* @param aesKey 随机密钥(16字节)
* @return 密文
*/
public static String encrypt(String content, String aesKey) throws Exception {
SecretKeySpec secretKey = new SecretKeySpec(aesKey.getBytes("UTF-8"), "AES");
try {
Security.addProvider(new BouncyCastleProvider()); // java本身不支持ECB模式,所以需要引入provider
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS7Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encrypted = cipher.doFinal(content.getBytes("UTF-8"));
return Base64.encodeBase64String(encrypted);
} catch (Exception e) {
logger.warn("@@encryptParams@@ failed. content:" + content, e);
}
return "";
}
/**
* aes解密
* @param content 密文
* @param aesKey 随机密钥(16字节)
* @return 明文
*/
public static String decrypt(String content, String aesKey) throws Exception {
SecretKeySpec secretKey = new SecretKeySpec(aesKey.getBytes("UTF-8"), "AES");
try {
Security.addProvider(new BouncyCastleProvider());
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS7Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] decrypted = cipher.doFinal(Base64.decodeBase64(content));
return new String(decrypted, "UTF-8");
} catch (Exception e) {
logger.warn("@@encryptParams@@ failed. content:" + content, e);
}
return "";
}
public static void main(String[] args) throws Exception {
String key = "30023E85AE254CF4";
System.out.println(decrypt(encrypt("123abc", key), key));
}

CBC模式

这里介绍使用16字节初始向量vi和32长十六进制密钥字符串进行CBC模式加解密。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* AES加密
* @param hexStringKey 16进制密钥(32长)
* @param initVector 初始向量 iv
* @param value 待加密内容
* @return Base64编码的加密内容
*/
public static String encryptIVHexKey(String hexStringKey, String initVector, String value) {
try {
IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
byte[] decodedKey = DatatypeConverter.parseHexBinary(hexStringKey);
SecretKey secretKey = new SecretKeySpec(decodedKey, 0, decodedKey.length, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
byte[] encrypted = cipher.doFinal(value.getBytes());
return Base64.encodeBase64String(encrypted);
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
/**
* AES解密
* @param hexStringKey 16进制密钥(32长)
* @param initVector 初始向量 iv
* @param encrypted 待解密内容,Base64编码
* @return 解密后的内容
*/
public static String decryptIVHexKey(String hexStringKey, String initVector, String encrypted) {
try {
IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
byte[] decodedKey = DatatypeConverter.parseHexBinary(hexStringKey);
SecretKey secretKey = new SecretKeySpec(decodedKey, 0, decodedKey.length, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
byte[] original = cipher.doFinal(Base64.decodeBase64(encrypted));
return new String(original);
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
public static void main(String[] args) throws Exception {
String key = "30023E85AE254CF431FAAB0E3325B3D6"; // 128 bit key
String initVector = "1234567890123456"; // 16 bytes IV
System.out.println(decryptIVHexKey(key, initVector,
encryptIVHexKey(key, initVector, "123abc")));
}

对应的js代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<!DOCTYPE html>
<html>
<head>
<title>aes demo</title>
<meta charset="utf-8"/>
<style>
*{margin:0;padding:0}
.demo-wrap{width: 400px;height: 50px;margin: 50px auto auto auto}
</style>
<script src="./rollups/aes.js"></script>
</head>
<body>
<div class="demo-wrap">
<input type="text" id="data-ipt"/>
<button onclick="getAES();">AES加密</button>
<button onclick="getDAes();">AES解密</button>
<br/>
加密后的数据:
<p id = "encrypted"></p>
解密后的数据:
<p id="decrypted"></p>
</div>
<script>
function getAesString(data,key,iv){//加密
var key = CryptoJS.enc.Hex.parse(key); //由于是16进制密钥,所以需要先解析出来
var iv = CryptoJS.enc.Utf8.parse(iv);
var encrypted = CryptoJS.AES.encrypt(data,key,
{
iv:iv,
mode:CryptoJS.mode.CBC,
padding:CryptoJS.pad.Pkcs7
});
return encrypted;
}
function getDAesString(encrypted,key,iv){//解密
var key = CryptoJS.enc.Hex.parse(key);
var iv = CryptoJS.enc.Utf8.parse(iv);
var decrypted = CryptoJS.AES.decrypt(encrypted,key,
{
iv:iv,
mode:CryptoJS.mode.CBC,
padding:CryptoJS.pad.Pkcs7
});
return decrypted.toString(CryptoJS.enc.Utf8);
}
function getAES(){ //加密
var data = document.getElementById("data-ipt").value;//明文
var key = '12345678901234567890123456789012'; //密钥
var iv = '1234567890123456';
var encrypted = getAesString(data,key,iv); //密文
document.getElementById("encrypted").innerHTML = encrypted;
}
function getDAes(){//解密
var encrypted = document.getElementById("encrypted").innerHTML; //密文
var key = '12345678901234567890123456789012';
var iv = '1234567890123456';
var decryptedStr = getDAesString(encrypted,key,iv);
document.getElementById("decrypted").innerHTML = decryptedStr;
}
</script>
</body>
</html>

参考资料:
AES加密算法的详细介绍与实现
高级加密标准AES的工作模式(ECB、CBC、CFB、OFB)