再谈Java、AndroidAES加密算法填充⽅式
今天发布的博客有些临时赶⼯成分
天⽓⼀如既往的炎热,下班到了家习惯性的打开空调,然后从冰箱掏出冰棒享受着透⼼凉的赶脚。待⾝上的⾼能辐射褪去殆尽,便去开锅做起⽜⾁粉丝汤,嗯,今天的晚餐。做好,盛碗,端进卧室,在空调的风⼝下吃⼝味更佳,嗯,还不起劲,拧开了“王者农药”进⾏⼀场刺激的峡⾕之战,边吃边玩,意境更佳。
完事,洗碗刷锅后看看⼿机上的时间,打算时间充⾜的话就去游个泳,然⽽“⼀不⼩⼼”看到了今天的⽇期,F**K!31号了!这个⽉⼀篇⽂章都没发!
本可以度过⼀个轻松愉快的夜晚,然⽽…
赶紧开机⼲活,于是有了今天的意外产出,下⾯正式开始
此前写过⼀篇关于AES加密的⽂章,介绍了相关概念,使⽤以及脱坑姿势。
之所以写续篇,是因为最近项⽬中出现了AES加密的bug,折腾了许久最终确认为Android端错误地使⽤PKCS5Padding填充⽅式,⽽服务端则是PKCS7Padding,改为PKCS7Padding即可,还好bug只会出现在Android4.3和4.4的设备上,仅影响了极少数的⽤户。呵呵呵…
希望这篇⽂章没有被领导看到…看到也没事,反正⼜不是我弄的
java源代码加密
嗯哼,求知欲强的童鞋可能会抛出以下问题:
1. 为什么Android可以使⽤PKCS7Padding?
2. 为什么PKCS5Padding可以解密PKCS7Padding加密的数据?
接下来⼀⼀作答
Android可以使⽤PKCS7Padding
我们知道Java是不⽀持PKCS7Padding填充的,标准的⽅式是PKCS5Padding。⽽Android实现了Java SE API,但没有使⽤相同的源代码,没有使⽤相同的提供程序,也没有作AES限制的实现。
这句话引⾃某
Android implements the Java SE API, but does not use the same source code, not the same provider and the AES restrictions are not implemented either.
⼀句话概括:Android没有使⽤标准Java的AES加密,⽽是⾃⼰实现了⼀套,顺便实现了PKCS7Padding。PKCS5Padding可以解密PKCS7Padding加密的数据
从AES加密原理说起
加密流程
1. 把明⽂按照128bit(16byte)拆分成若⼲个明⽂块
2. 按照选择的填充⽅式来填充最后⼀个明⽂块
3. 利⽤AES加密器和密钥将每⼀个明⽂块加密成密⽂块
4. 拼接所有的密⽂块,成为最终的密⽂结果
流程图⽰
填充原理
需要填充的字节长度 = (块长度 - (数据长度 % 块长度))
假定块长度为8,数据长度为3,则填充字节数等于5
原数据为: FF FF FF
填充结果: FF FF FF 05 05 05 05 05
假定块长度为8,数据长度为9,则填充字节数等于7
原数据为:FF FF FF FF FF FF FF FF FF
填充结果:FF FF FF FF FF FF FF FF FF 07 07 07 07 07 07 07
假定块长度为8,数据长度为8,则填充字节数等于8
原数据为:FF FF FF FF FF FF FF FF
填充结果:FF FF FF FF FF FF FF FF 08 08 08 08 08 08 08 08
总结如下:
1. 填充的字节都是⼀个相同的字节
2. 该字节的值,就是要填充的字节的个数
3. 数据长度是块长度的整数倍时,还需另补块长度个值为块长度的字节
这样如果填充1个字节,那么填充的字节的值就是0x01;要填充7个字节,那么填充的字节的值全是0×07;长度恰好8的整数倍时,还要补8个值为0×08的字节。
思考⼀个问题:
为啥为整数倍时,还是要多补8个0x08?
答案:解密的需要!
嗯哼,⼀脸懵逼?看下⾯⾼能举栗:
假设当某数据是8的整数倍,没有填充8个0x08:
解密过程中,程序检查数据末尾的最后⼀个字节发现这个字节恰好是0x01,那这个字节是填充上去的呢?
还是实际的数据呢?要不要做去填充操作呢?此时程序⼼⾥⼀万个。
填充⽅式和填充结果
假设某明⽂字节长度为m,拆分密⽂块后剩余字节e=m%16,最终密⽂长度为l
算法/模式/填充e=0时密⽂长度e!=0密⽂长度
AES/CBC/NoPadding l=m不⽀持
AES/CBC/PKCS5Padding l=m+(16-e)=m+16l=m+(16-e)
AES/CFB/NoPadding l=m l=m
AES/CFB/PKCS5Padding l=m+(16-e)=m+16l=m+(16-e)
AES/ECB/NoPadding l=m不⽀持
AES/ECB/PKCS5Padding l=m+(16-e)=m+16l=m+(16-e)
AES/OFB/NoPadding l=m l=m
AES/OFB/PKCS5Padding l=m+(16-e)=m+16l=m+(16-e)
………
PKCS5Padding与PKCS7Padding
PKCS5Padding填充和PKCS7Padding填充算法没有任何区别。
PKCS5Padding在填充⽅⾯,是PKCS7Padding的⼀个⼦集:
PKCS5Padding只是对于8字节(BlockSize=8)进⾏填充,填充内容为0x01-0x08
但是PKCS7Padding不仅仅是对8字节填充,其BlockSize范围是1-255字节
所以,PKCS5Padding可以向上转换为PKCS7Padding,但是PKCS7Padding不⼀定可以转换到PKCS5Padding。⼀定条件下,两者可以互换使⽤的。
参考资料
拓展
关于NoPadding
顾名思义,不填充。数据分完块,不去做填充操作,直接加密。
嗯哼,想到⼀个骚操作,就是让NoPadding转换成PKCS5Padding效果
NoPadding加密 →→→ PKCS5Padding解密
贴代码
public class AESUtils {
private static final String AES_CBC_PKCS5_PADDING ="AES/CBC/PKCS5Padding";
private static final String AES_CBC_NO_PADDING ="AES/CBC/NoPadding";
private static final String AES ="AES";
private static final String UTF_8 ="UTF-8";
private static final String AES_KEY ="286751244B391A6DBB84778E0D6A8926";
public static void main(String[] args){
String plaintext ="12345678";
String encrypt =encryptByNoPadding(plaintext, AES_KEY);
System.out.println("          数据加密前:"+ plaintext);
System.out.println("  `NoPadding`加密后:"+ encrypt);
String desEncrypt =decryptByPKCS5Padding(encrypt, AES_KEY);
System.out.println("`PKCS5Padding`解密后:"+ desEncrypt);
}
private static String encryptByNoPadding(String plaintext, String key){
try{
// 加密初始化实例,NoPadding形式
final Cipher cipher = Instance(AES_CBC_NO_PADDING);
final int blockSize = BlockSize();
final byte[] dataBytes = Bytes(UTF_8);
final int length = dataBytes.length;
final int remainder = length % blockSize;
//计算需要填充的字节长度
final int paddingLength = blockSize - remainder;
/
/计算填充后的字节长度
int newLength = length + paddingLength;
final byte[] newDataBytes =new byte[newLength];
//填充
for(int i =0; i < paddingLength; i++){
// 填充的字节长度也是填充的值,需要转成字节(byte) paddingLength
newDataBytes[length + i]=(byte) paddingLength;
}
//将⽼数据迁移到新数据字节数组⾥
System.arraycopy(dataBytes,0, newDataBytes,0, dataBytes.length);
// 还原密钥对象
SecretKey secretKey =new Bytes(UTF_8), AES);
// 偏移量
IvParameterSpec ivParameterSpec =new IvParameterSpec(new BlockSize()]);
// CBC模式需要添加⼀个参数IvParameterSpec
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec);
byte[] result = cipher.doFinal(newDataBytes);
return parseByte2HexStr(result);
}catch(Exception e){
e.printStackTrace();
return"";
}
}
private static String decryptByPKCS5Padding(String data, String key){
try{
byte[] encryp =parseHexStr2Byte(data);
Cipher cipher = Instance(AES_CBC_PKCS5_PADDING);
// 还原密钥对象
SecretKeySpec keySpec =new Bytes(UTF_8), AES);
// 偏移量
IvParameterSpec ivParameterSpec =new IvParameterSpec(new BlockSize()]);
// CBC模式需要添加⼀个参数IvParameterSpec
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivParameterSpec);
byte[] original = cipher.doFinal(encryp);
return new String(original);
}catch(Exception e){
}
return null;
}
private static String parseByte2HexStr(byte[] buf){
StringBuffer sb =new StringBuffer();
for(int i =0; i < buf.length;++i){
String hex = HexString(buf[i]&255);
if(hex.length()==1){
hex ='0'+ hex;
}
sb.UpperCase());
}
String();
}
private static byte[]parseHexStr2Byte(String hexStr){
if(hexStr.length()<1){
return null;
}else{
byte[] result =new byte[hexStr.length()/2];
for(int i =0; i < hexStr.length()/2;++i){
int high = Integer.parseInt(hexStr.substring(i *2, i *2+1),16);
int low = Integer.parseInt(hexStr.substring(i *2+1, i *2+2),16);
result[i]=(byte)(high *16+ low);
}
return result;
}
}
}
输出结果
数据加密前:12345678
`NoPadding`加密后:76D277F787AB75E354061B2319CAE945
`PKCS5Padding`解密后:12345678
ok,转换成功了。⾄此,本⽂就告⼀段落了。
写在最后
⽂章写得时间⽐较赶,逻辑还不够紧凑清晰,希望童鞋们能指出问题或给出宝贵的意见,以帮助我更好地完善本⽂。另外,上⽂提到bug只会出现在Android4.3和4.4的设备上的具体分析将在后期的⽂章给出。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。