咸鱼

咸鱼是以盐腌渍后,晒干的鱼

0%

AES加密实践

有项目对AES对称加密有需求,使用Java使用Aes过程中有些坑是要注意一下的。

跨平台问题

AES是对称加密,利用秘钥,可以对内容加密和解密,于是我在PC开发电脑上尝试以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static byte[] encrypt(String content, String password) {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128, new SecureRandom(password.getBytes()));
SecretKey secretKey = kgen.generateKey();
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES");
Cipher cipher = Cipher.getInstance("AES");
byte[] byteContent = content.getBytes("utf-8");
cipher.init(Cipher.ENCRYPT_MODE, key);
return cipher.doFinal(byteContent);
}

public static byte[] decrypt(byte[] content, String password) {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128, new SecureRandom(password.getBytes()));
SecretKey secretKey = kgen.generateKey();
byte[] enCodeFormat = secretKey.getEncoded();
SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
return cipher.doFinal(content);
}

这段代码在本机加密和解密都没问题,是正常的,但如果跨机器执行很大概率会解密失败,这是由于不同机器很大概率是安装了不同平台厂商的JDK和版本(特别是Android 平台)。

这是由于:上面的代码只是指定了”AES”加密,而”AES”的默认实现,不同的JDK实现不一样。

** 先了解一下AES加密:**

  • 秘钥长度:128位/192位/256位
  • 加密模式有:ECB/CBC/CTR/OFB/CFB
  • 补码填充模式有:pkcs5padding/pkcs7padding/zeropadding/nopadding/iso10126/ansix923

所以上面代码到底用的是哪一种?是由JDK决定的。 推荐的代码写法是:指定所有配置项,如”AES/ECB/PKCS5Padding”

接下来了解一下AES对称加密…..

秘钥长度

AES支持三种长度的密钥: 128位,192位,256位。
AES256安全性最高,AES128性能最高。

加密模式

AES加密模式有两个是最常用的,ECB 和 CBC,本文就说一下这两个。

  • ECB 电码本模式(Electronic Codebook Book): 简单快速利于计算的加密模式,但不太安全,加密时只需要密码。
  • CBC 密码分组链接模式(Cipher Block Chaining): 安全性很高的加密模式,是SSL、IPSec的标准,加密时需要密码和初始化向量IV。

补码填充

  • NoPadding: 不做任何填充,但是要求明文必须是16字节的整数倍,所以要自己补码。
  • PKCS5Padding(默认): 如果明文块少于16个字节(128bit),在明文块末尾补足相应数量的字符,且每个字节的值等于缺少的字符数。 比如明文:{1,2,3,4,5,a,b,c,d,e} ,缺少6个字节,则补全为 {1,2,3,4,5,a,b,c,d,e,6,6,6,6,6,6 }
  • PKCS7Padding原理与PKCS5Padding相似,区别是PKCS5Padding的blocksize为8字节,而PKCS7Padding的blocksize可以为1到255字节
  • ISO10126Padding:如果明文块少于16个字节(128bit),在明文块末尾补足相应数量的字节,最后一个字符值等于缺少的字符数,其他字符填充随机数。比如明文:{1,2,3,4,5,a,b,c,d,e},缺少6个字节,则可能补全为 {1,2,3,4,5,a,b,c,d,e,5,c,3,G,$,6}

正确示例

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
45
46
47
48

private static final int KEY_LEN = 16;
private static final String CIP_TYPE_PKCS5_PADDING = "AES/ECB/PKCS5Padding";

public static void main(String[] args) throws Exception {

// 此处使用AES-128-ECB加密模式,passwd需要为16位。
String passwd = "1234567890123456";
String src = "hello world!hello world!hello world!hello world!hello world!hello world!";

// 加密
byte[] enByte = encrypt(src, passwd,CIP_TYPE_PKCS5_PADDING);
String enString = Base64.getEncoder().encodeToString(enByte);
System.out.println("加密后的字串是:" + enString);
// 解密
String deString = decrypt(enByte, passwd,CIP_TYPE_PKCS5_PADDING);
System.out.println("解密后的字串是:" + deString);
}
// 加密
private static byte[] encrypt(String src, String passwd,String cipherType) throws Exception {
Cipher cipher = Cipher.getInstance(cipherType);
cipher.init(Cipher.ENCRYPT_MODE, generateSecretKeySpec(passwd));
return cipher.doFinal(src.getBytes(StandardCharsets.UTF_8));
}

// 解密
private static String decrypt(byte[] encrypt, String passwd,String cipherType) {
try {

Cipher cipher = Cipher.getInstance(cipherType);
cipher.init(Cipher.DECRYPT_MODE, generateSecretKeySpec(passwd));
byte[] original = cipher.doFinal(encrypt);
return new String(original,StandardCharsets.UTF_8);
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}

private static SecretKeySpec generateSecretKeySpec(String passwd) throws UnsupportedEncodingException {

// 判断Key是否为16位
if (passwd == null ||passwd.length() != KEY_LEN) {
System.out.print("passwd长度不是" + KEY_LEN);
return null;
}
return new SecretKeySpec(passwd.getBytes(StandardCharsets.UTF_8), "AES");
}

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

private static final int KEY_LEN = 16;
private static final String CIP_TYPE_CBC_PKCS5_PADDING = "AES/CBC/PKCS5Padding";

public static void main(String[] args) throws Exception {

// 此处使用AES-128-CBC加密模式,passwd需要为16位。
String passwd = "1234567890123456";
//密钥默认偏移,最少16位
String iv = "abcdabcdabcdabcd";
String src = "hello world!hello world!hello world!hello world!hello world!hello world!";//长度非16倍数

// 加密
byte[] enByte = encrypt(src, passwd,iv,CIP_TYPE_CBC_PKCS5_PADDING);
String enString = Base64.getEncoder().encodeToString(enByte);
System.out.println("加密后的字串是:" + enString);
// 解密
String deString = decrypt(enByte, passwd,iv,CIP_TYPE_CBC_PKCS5_PADDING);
System.out.println("解密后的字串是:" + deString);
}

private static byte[] encrypt(String src, String passwd, String iv, String cipherType) throws Exception {
Cipher c = Cipher.getInstance(cipherType);
c.init(Cipher.ENCRYPT_MODE, generateSecretKeySpec(passwd), new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8)));
return c.doFinal(src.getBytes(StandardCharsets.UTF_8));
}

private static String decrypt(byte[] encrypt,String passwd, String iv, String cipherType) {
try {
Cipher c = Cipher.getInstance(cipherType);
c.init(Cipher.DECRYPT_MODE, generateSecretKeySpec(passwd), new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8)));
return new String(c.doFinal(encrypt),StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

private static SecretKeySpec generateSecretKeySpec(String passwd)throws UnsupportedEncodingException{

// 判断Key是否为16位
if (passwd == null ||passwd.length() != KEY_LEN) {
System.out.print("passwd长度不是" + KEY_LEN);
return null;
}
return new SecretKeySpec(passwd.getBytes(StandardCharsets.UTF_8), "AES");
}

** NoPadding 示例 **
NoPadding 要手动补码,也就是当明文的长度不是 cipher.getBlockSize() 的倍数,就手动补齐。

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
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class TestAesCbcNoPadding {
public static byte[] key = "1234567890123456".getBytes(StandardCharsets.UTF_8);

public static void main(String[] args) throws Exception{

byte[] textBytes = "Input length not multiple of 16 bytes".getBytes(StandardCharsets.UTF_8);

byte[] enByte = encrypt(textBytes);
String enString = Base64.getEncoder().encodeToString(enByte).trim();
System.out.println("加密后的字串是:" + enString);
// 解密
String deString = decrypt(enByte);
System.out.println("解密后的字串是:" + deString);
}
public static String decrypt(byte[] enByte) throws Exception{
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");

byte[] iv = new byte[16];
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec,new IvParameterSpec(iv));
byte[] plaintextBytes = cipher.doFinal(enByte);

return new String(plaintextBytes,StandardCharsets.UTF_8).trim();
}

public static byte[] encrypt(byte[] textBytes) throws Exception{
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");

int blockSize = cipher.getBlockSize();
int length = textBytes.length;
// 计算需填充长度( blockSize 的倍数)
if (length % blockSize != 0) {
length += (blockSize - (length % blockSize));
}
System.out.println("length = " + length);
// 创建倍数数据
byte[] plaintextNoPadding = new byte[length];
//填充
System.arraycopy(textBytes, 0, plaintextNoPadding, 0, textBytes.length);

byte[] iv = new byte[16];
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec,new IvParameterSpec(iv));
return cipher.doFinal(plaintextNoPadding);
}
}

注意:手动补码的数据0x00JDK 库会自动去除,但如果用的是C语言库解密后可能会有补码的 0x00数据是无用数据。