> 文章列表 > crypto-js AES-CTR 实现密文前缀式局部解密细节 踩坑点

crypto-js AES-CTR 实现密文前缀式局部解密细节 踩坑点

crypto-js AES-CTR 实现密文前缀式局部解密细节 踩坑点

项目有需求,长明文经过AES-CTR模式加密后,在解密的时候,密文不能直接得到,每次通过某些方法尝试后,只能得到一块密文(按顺序),所以只能一块一块的拼接解密。在使用crypto-js这个库的时候,发送不能直接实现这种局部解密,踩了个大坑,最后经过调试源码,查看文档,花了大半天时间才解决,在此分享一下解决方案。


文章目录

  • 1.使用crypto-js 进行aes-ctr加密的流程
  • 2.尝试局部解密失败
  • 3.问题分析:padding
  • 4.正解:采用ZeroPadding方式
  • 参考

1.使用crypto-js 进行aes-ctr加密的流程

直接看代码:

import CryptoJS from 'crypto-js';//参数定义
const key = CryptoJS.enc.Utf8.parse("hv9BbgGupxMIFjFB8hn4KPX9If5G8yw8");
const iv = CryptoJS.enc.Utf8.parse("eUGFebpHnoBov3Pz");
var msg = CryptoJS.enc.Utf8.parse("atfwus_test_test_____"); // 21bytes//加密
const encrypted = CryptoJS.AES.encrypt(msg, key, { mode: CryptoJS.mode.CTR, iv: iv});
const cipher = encrypted.toString();
const decrypt = CryptoJS.AES.decrypt(cipher, key, { mode: CryptoJS.mode.CTR, iv: iv});//解密
const deMsg = decrypt.toString(CryptoJS.enc.Utf8);
console.log(deMsg);

可以发现解密成功。

2.尝试局部解密失败

上面的明文加密后得到了两个16字节的密文块,理论上我只给出第一块明文,也是可以正常解密的。但是尝试后,发现并不可以:

//参数定义
const key = CryptoJS.enc.Utf8.parse("hv9BbgGupxMIFjFB8hn4KPX9If5G8yw8");
const iv = CryptoJS.enc.Utf8.parse("eUGFebpHnoBov3Pz");
var msg = CryptoJS.enc.Utf8.parse("atfwus_test_test_____"); // 21bytes//加密
const encrypted = CryptoJS.AES.encrypt(msg, key, { mode: CryptoJS.mode.CTR, iv: iv});
var cipher = encrypted.toString();//解密
var decrypt = CryptoJS.AES.decrypt(cipher, key, { mode: CryptoJS.mode.CTR, iv: iv});
var deMsg = decrypt.toString(CryptoJS.enc.Utf8);
console.log(deMsg);// 尝试局部解密 解密第一个密文块
cipher = CryptoJS.enc.Base64.stringify(CryptoJS.lib.WordArray.create(encrypted.ciphertext.words.slice(0, 4)));
var decrypt = CryptoJS.AES.decrypt(cipher, key, { mode: CryptoJS.mode.CTR, iv: iv});
var deMsg = decrypt.toString(CryptoJS.enc.Utf8);
console.log(deMsg);

可以发现解密失败。

3.问题分析:padding

我们知道AES要求明文必须是16字节的整数倍,而上述的msg是21位,能正常加密,说明CryptoJS 有默认的padding策略。

查阅官方文档可以看出,默认使用的是Pkcs7这个策略。

crypto-js AES-CTR 实现密文前缀式局部解密细节 踩坑点
Pkcs7 是一种常用的填充方式,也是默认的填充方式。它在数据块的末尾添加了几个字节,使得数据块的长度是加密算法所要求的整数倍。这些字节的值等于添加的字节数。特别需要注意的是,就算明文是16字节的整数倍,也会在后面填充16个字节的’\\x10’。

这样牺牲了数据长度的做法是为了更为灵活透明的去解包数据,发送端和接收端不需要约定好blockSize,接收端总能通过数据包的最后一个字符得到填充的数据长度。

到这里,我们大概清楚了不能局部解密的原因了:

  • 我们加密和解密的时候并未指定padding方式,默认使用Pkcs7。
  • 而Pkcs7这种方式下,需要得到末尾的填充字符下才能确定填充数据长度。
  • 如果我们只给出第一块密文,那么无法确定填充数据长度。
  • 在crypto-js里面,这样解密的密文就不可用。

4.正解:采用ZeroPadding方式

ZeroPadding 是一种简单的填充方式,它在数据块的末尾添加零或多个字节 0x00,以使得数据块长度为加密算法所要求的整数倍。

只要业务里面不涉及数据末尾零字节的加解密,那么这种填充方式也是可行的,并且可以实现上述的局部解密。

代码如下:

//参数定义
const key = CryptoJS.enc.Utf8.parse("hv9BbgGupxMIFjFB8hn4KPX9If5G8yw8");
const iv = CryptoJS.enc.Utf8.parse("eUGFebpHnoBov3Pz");
var msg = CryptoJS.enc.Utf8.parse("atfwus_test_test_____"); // 21bytes//加密
const encrypted = CryptoJS.AES.encrypt(msg, key, { mode: CryptoJS.mode.CTR, iv: iv, padding: CryptoJS.pad.ZeroPadding});
var cipher = encrypted.toString();//解密
var decrypt = CryptoJS.AES.decrypt(cipher, key, { mode: CryptoJS.mode.CTR, iv: iv, padding: CryptoJS.pad.ZeroPadding});
var deMsg = decrypt.toString(CryptoJS.enc.Utf8);
console.log(deMsg);// 尝试局部解密 解密第一个密文块
cipher = CryptoJS.enc.Base64.stringify(CryptoJS.lib.WordArray.create(encrypted.ciphertext.words.slice(0, 4)));
var decrypt = CryptoJS.AES.decrypt(cipher, key, { mode: CryptoJS.mode.CTR, iv: iv, padding: CryptoJS.pad.ZeroPadding});
var deMsg = decrypt.toString(CryptoJS.enc.Utf8);
console.log(deMsg);

可以发现,局部解密成功了。


参考

  • crypto-js官方文档:https://cryptojs.gitbook.io/docs/
  • pkcs7填充方式:http://eleaction01.spaces.eepw.com.cn/articles/article/item/212414

ATFWUS 2023-04-10