AES

对称加密-AES

Posted by wantu on August 9, 2019

前言

  最近宇哥排查到了一个很匪夷所思的事情。部分小程序接口请求某个接口但是响应莫名其妙被置为空。后面排查到是被 TX 给劫持了,具体被劫持的原因,那次请求中有一个 url(指向七牛云上的图片资源),应该 TX 拿到了那个资源并且识别到了图片上的“京东”这两个字眼,然后就发生了之后的事情。心疼我东哥 1 东(1 东===2min)。后面宇哥就交代了我们这边的接口提供方要求对数据进行加密。

处理思路

需求
  1、对数据进行加密
  2、加密过程不要太过复杂
  3、对接口响应时间不会造成太大的影响(避免响应时间大幅度增加)
  4、灵活配置

着手
  1、首先确定加密方向。因为不能太过复杂,所以最好采用对称加密。个人觉得有必要像组合加密机制那样先用非对称加密加密对称加密的密钥(撇开中间人攻击),但是后面是采用在前后端都维护一个密钥数组。
  2、选中对称加密,对称加密算法选择,Aes 是典型的加密算法。而且 node 核心模块 crypto 也支持。

实现
  1、打算在服务器端和客户端这边维护一些密钥数组和加密向量数组为的是稍微提高些安全性。
  2、加密密钥和加密向量的定位值不应该是明文。采用位运算对实际下标值进行某种位运算进行偏移。

代码

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
/*
 * @Descripttion: aes加密测试
 * @version:1.0
 * @Author: want
 * @Date: 2019-08-09 10:44:25
 * @LastEditors: want
 * @LastEditTime: 2019-08-12 09:33:01
 */

var crypto = require("crypto");

/**
 * 加密方法
 * @param {String}      加密key
 * @param {String}       向量
 * @param {String}      需要加密的数据
 * @returns string 密文
 */
var encrypt = function(key, iv, data) {
  var cipher = crypto.createCipheriv("aes-128-cbc", key, iv);
  var crypted = cipher.update(data, "utf8", "binary");
  crypted += cipher.final("binary");
  crypted = new Buffer(crypted, "binary").toString("base64");
  return crypted;
};

/**
 * @name: 解密方法
 * @msg: 对密文进行解密
 * @param {String}  解密算法
 * @param {String}   解密向量
 * @param {String} 密文
 * @return: 解密后的数据
 */
var decrypt = function(key, iv, crypted) {
  crypted = new Buffer(crypted, "base64").toString("binary");
  var decipher = crypto.createDecipheriv("aes-128-cbc", key, iv);
  var decoded = decipher.update(crypted, "binary", "utf8");
  decoded += decipher.final("utf8");
  return decoded;
};

var key = "(IJf621ea5c0ok8j";
console.log(`key:${key.toString("hex")}`);
var iv = "02kk950004598718";
console.log(`iv:${iv.toString("hex")}`);
var data = "want dddd";

var crypted = encrypt(key, iv, data);

console.log("数据加密后:", crypted);
var dec = decrypt(key, iv, crypted);
console.log("数据解密后:", dec);

/**
 * k: ['751f621ea5c8f930','751f621ea5c0ok8j','@#$898js8.9ksj8j']
 *
 * iv:['2624750004598718','02kk950004598718','9s82,.9#$%hjjHJF']
 *
 */

困惑
  1、加密向量感觉很废。   2、加密向量为啥长度有要求,为啥是 16 位?

Aes 细细讲来

高级加密标准(Advanced Encryption Standard: AES)是美国国家标准与技术研究院(NIST)在 2001 年建立了电子数据的加密规范。其是对称加解密算法的最经典算法之一,它是一种分组加密标准,每个加密块大小为 128 位,允许的密钥长度为 128、192 和 256 位。

Aes 算法三个特点:
1、密钥。密钥是 AES 算法实现加密和解密的根本。对称加密算法之所以对称,是因为这类算法对明文的加密和解密需要使用同一个密钥。AES 支持三种长度的密钥。分别是:128 位、192 位、256 位。密钥位数越多加密轮数越多、也就越安全,但是也会导致性能的下降。
2、填充。常见的填充:NoPadding(要求明文必须是 16 字节的整数倍)、PKCS5Padding(如果明文块少于 16 个字节,则再明文末尾补足相应数量的字符)、ISO10126Padding(在明文块末尾补足相应数字的字节,最后一个字符值等于缺少的字符数,其他字符填充随机数)。值得注意的是在 AES 加密的时候使用了某一种填充方式,解密的时候也必须采用相同的填充方式。
3、模式。常见的模式有:ECB(电子密码本)、CBC(密码块连接)、CTR(计算器模式)、CFB(密码反馈方式) 和 OFB(输出反馈方式)、PCBC(填充密码块链接)。所有工作模式的差别体现在宏观上,即明文块与明文块之间的关联。AES 加密器的内部处理流程都是相同的。

ECB 电子密码本模式是最简单的加密模式,加密前根据加密块的大小(AES 为 128 位)分成若干块,之后将每块使用相同的密钥单独加密,解密同理。
ECB 因为每块数据的加密是独立的因此加密和解密都可以并行计算。ECB 最大的问题是相同的明文快回被加密成相同的密文,在某些特俗环境并不能提供严格的数据保密性。

CBC 模式又称之为密码分组链接:CBC 模式对于每个待加密的密码块在加密前会先与前一个密码块的密文进行异或然后再用加密器进行加密。显然第一个密码块是没有前一个的密文与之进行异或操作的,所以其采用的是第一个明文块与一个初始化向量(IV)的数据块进行异或操作。
AES_cbc_encrypt 允许 length 不是 16(128 位)的整数倍,不足的部分会用 0 填充,输出总是 16 的整数倍。完成加密或解密后会更新初始化向量 IV。CBC 模式相比 ECB 有更高的保密性,但由于对每个数据块的加密依赖与前一个数据块的加密所以加密无法并行。与 ECB 一样在加密前需要对数据进行填充,不是很适合对流数据进行加密。

CFB 又称为面反馈模式,其与 ECB、CBC 不同的是 CFB 能够将密文转换为流密文。

OFB 模式又称输出反馈模式(Output feedback):OFB 是先用块加密器生成密钥流(Keystream),然后再将密钥流与明文流异或得到密文流,解密是先用块加密器生成密钥流,再将密钥流与密文流异或得到明文,由于异或操作的对称性所以加密和解密的流程是完全一样的。

关于填充
需要了解分组加密特性,何为分组加密,在密码学中分组加密又称之为分块加密或者块密码,是一种对称加密算法。它将明文分为多个等长的模块,使用确定的算法和对称密钥对每组分别加密解密。有一个问题,因为 AES 加密算法是会将明文拆分为一个个独立的明文块,那么显然可能会存在最后一个明文块长度达不到标准,如果出现这种情况就需要对明文进行填充处理。
常见的填充处理方式
1、NoPadding: 不做任何填充,但是明文必须是指定块长度的整数倍。
2、PKCS5Padding(默认):如果明文块少于 16 个字节(128bit),在明文块末尾补足相应数量的字符,且每个字节的值等于缺少的字符数。
3、ISO10126Padding:如果明文块少于 16 个字节(128bit),在明文块末尾补足相应数量的字节,最后一个字符值等于缺少的字符数,其他字符填充随机数。

AES 整体关系图
1、把明文按照 128bit 拆分为若干个明文块。
2、按照选择的填充方式来填充最后一个明文块。
3、每一个明文块利用 AES 加密器和密钥,加密成密文块。
4、拼接所有的密文块,成为最终的密文结果。
整体关系图 AES 加密器剖析 AES 不是一次把明文变成密文,而是经过很多轮加密。大致可以分为:
初始轮 1 次+普通轮 N 次+最终轮 1 次。之前提到 AES 的 key(密钥) 支持三种长度:AES128、AES192、AES256。key 的长度决定轮 AES 加密的轮数。
出去初始轮,各种 key 长度对应的轮数如下:
AES128 – 10 轮 – 8 次普通轮
AES192 – 12 轮 – 10 次普通轮
AES256 – 14 轮 – 12 次普通轮

初始轮只有一个步骤:加轮密钥。
普通轮有四个步骤:1、字节代替。2、行移位。3、列混淆。4、加轮密钥。
最终轮有三个步骤:1、字节代替。2、行移位。3、加轮密钥。

字节代替
16 个字节在明文块在每一个处理步骤中都被排列成 4*4 的二维数组。所谓的字节代替就是把明文块的每一个字节都替代成另外一个字节。替代的依据一个被称为 S 盒的 16*16 的二维常量数组。 假设明文块中 a[2][3] = 5A;那么输出值 b[2,3] = S[5][10]。

行移位
二维矩阵,第一行不动。第二行循环左移动 1 个字节。第三行循环左移 2 个字节。第 4 行循环左移 3 个字节。

列混淆
输入数组的每一列要和一个名为修补举证的二维常量数组做矩阵相乘得到对应的输出列。

加轮密钥
128bit 的密钥也被同样排列成 4*4 的矩阵。让输入数组的每一个字节 a[i][j]与密钥对应位置的字节 k[i][j]异或一次,就生成了输出值 b[i,j]。需要注意的是:加密的每一轮所用到的密钥并不是相同的,这里涉及到一个概念:扩展密钥。

扩展密钥
AES 源代码中用长度 4 _ 4 _(10+1) 字节的数组 W 来存储所有轮的密钥。W{0-15}的值等同于原始密钥的值,用于为初始轮做处理。后续每一个元素 W[i]都是由 W[i-4]和 W[i-1]计算而来,直到数组 W 的所有元素都赋值完成。W 数组当中,W{0-15}用于初始轮的处理,W{16-31}用于第 1 轮的处理,W{32-47}用于第 2 轮的处理 ……一直到 W{160-175}用于最终轮(第 10 轮)的处理。

至此加密流程全部走完。解密的过程就是加密的逆过程不再坠述。

遇到的问题

在前端同事进行联调的时候发现,数据量少的情况,前端是可以对后端的密文进行正常解密的,但是当后端提供的密文很长的时候,前端解密的时候就会出现无法解密的情况。 经过排查发现,因为后端加密采用的是 node 的核心模块中的 crypto,但是前端用的是 crypto-js。最终解决是后端也采用 crypto-js 这个模块进行加密操作。
最近加密又出了问题。原因是crypto-js这个module在针对包含%的明文的时候加密会报错。根本原因是在decodeURI转码时,通过%进行解析,如果字符串中存在%则会出现URI malformed。最后的解决办法是采用另一种加密形式。其实只要把明文转为TX识别不出的密文并且可以通过某种方式解密出来即可。

回头看看

解答
1、解答:iv 的作用是在 CBC 加密模式的情况下与第一个明文块进行异步操作的。
2、解答:AES 是一种分组加密标准,每个加密块大小为 128 位,允许的密钥长度为 128、192 和 256 位。

知识收集
1、http://www.361way.com/aes/5830.html
2、https://zhuanlan.zhihu.com/p/45155135
3、https://en.wikipedia.org/wiki/Advanced_Encryption_Standard
4、https://www.zybuluo.com/coldxiangyu/note/796980
5、https://blog.csdn.net/Vieri_32/article/details/48345023