AES是对称的加密算法,对称意味着加密解密都是使用相同的密钥。一般来说AES不涉及到位的扩展,其实现主要涉及异或和移位运算,所以一般来说可以理解为密文长度是与明文是一致的。
而在AES当中,涉及到两个概念,一为加密模式,二为填充模式。
0x00 背景
根据AES密钥长度的不同,有三种主要的实现
- AES-128(常用)
- AES-192
- AES-256
从加密的模式上来说,包括以下几种模式
- ECB
- CBC
- CTR
- OCF
- CFB
在以上的加密模式中,有一些加密模式只要求提供key,然后根据key(密钥)对明文进行加密解密(如ECB),安全性比较低,而有一些模式除了要求key外,还要求输入IV(向量),用以增强安全性(如CBC),其优劣可以自查对比。
而在ECB和CBC模式中,明文数据要求填充至长度为分组长度(16)的整数倍,于是就诞生了对原文数据进行填充的算法
- PKCS5Padding
- PKCS7Padding
- NoPadding
当然,不同的填充算法,会直接影响加密出来的数据,而解密方法也必须要与加密方法一样,否则就解不出来,这就是“对称加密”。
AES算法的各种花式实现网上都有,Java中支持的常用AES加密模式是”AES/CBC/PKCS5Padding”,意为使用CBC加密模式和PKCS#5方法填充的AES加/解密;而在native层,却没有很好的一套实现(包括NDK),而我需要实现一套可以在native与Java中想到加解密的流程(所谓对称)。
0x01 Java实现
java实现很简单1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//加密
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec iv = new IvParameterSpec(IV);
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
byte[] encrypted = cipher.doFinal(sSrc.getBytes());
//解密
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec iv = new IvParameterSpec(IV);
cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
byte[] encrypted1 = Base64.decode(sSrc);//先用base64解密
try {
byte[] original = cipher.doFinal(encrypted1);
String originalString = new String(original);
return originalString;
} catch (Exception e) {
System.out.println(e.toString());
return null;
}
0x02 C实现
而C层我没想过自己实现一套,采用了github上的开源方案AES-C,总的来说这是一套不错的实现,它支持在C层里进行ECB,CTR和CBC的AES加密。
然而我在使用的过程中却遇到了困难,因为我用C加密过后,在使用相同KEY和IV的前提下,使用Java无法解开。
在确保KEY与IV一致,加密方式一致(CBC)的情况下,我开始怀疑数据的填充方式上。
直到我在README里,看到了作者留了这么一句
No padding is provided so for CBC and ECB all buffers should be multiples of 16 bytes. For padding PKCS7 is recommendable.
……敢情作者是没实现填充算法,需要用的人自己去填充,于是开始研究填充算法。
0x021 PKCS#5
PKCS5填充的原则是,如果明文长度少于16个字节(的倍数),则将其补满16个字节(的倍数),而其补充的字节的值即为需要补充的字节数。
如现有10个字节,需要填充(16-10)= 6 个字节,而这6个字节的值,即为0x06。
使用JNI实现一个函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16jbyteArray encrypt(JNIEnv *env, jclass thiz, jbyteArray src) {
unsigned char *str = NULL;
jbyte *bytes;
bytes = env->GetByteArrayElements(src, 0);//将src转为字节数组
int srcLength = env->GetArrayLength(src);//计算明文长度
int encs_length = ((srcLength + AES_BLOCKLEN) / AES_BLOCKLEN) * AES_BLOCKLEN;//计算需要填充的字节数,AES_BLOCKLEN=16
str = (unsigned char *) malloc(encs_length);
int sum = encs_length - srcLength;//计算填充字节的值
memset(str, sum, encs_length);//将扩展后的字节数组空间统一重置为sum
memcpy(str, bytes, srcLength);//将原字节数组复制到新数组空间
env->ReleaseByteArrayElements(src, bytes, 0);
.......
如此实现填充加密。
0x022 PKCS#7
事实上,PKCS#5与PKCS#7是同种填充算法,只是在PKCS#5中,块的大小是确定的(blockSize=8,AES中为16),而在PKCS#7中,块大小是不确定的(1-255),但除此之外两者都是一致的,扩充的方法和填充的方法都一致。可以说PKCS#5是PKCS#7在blockSize=8时的特例。
设 1 <= N <= 255 ,原始的明文数据必须是N的倍数,然后需要填充的字节数为
1 | ((strlen(plainText) + N)/N) * N |
需要填充的字节数与#5也是一致的,或者说,#5跟#7是一致的。
WIKI#PKCS7)
0x023 Zero-Padding
需要补多少与#7一致,但值统一填充为0
WIFI#Zero_padding)
0x03 实现
AES-C加上上述的明文填充后,就可以实现native加密与java的互通了。关键还是实现”对称”,这里的对称,包括几个方面
- 加密算法
- 加密模式
- 填充方法
- 密钥(KEY)
- 向量(IV)