先看一个在 Node.js 中使用 AES 对文件内容进行加密的例子:
const fs = require('fs')
const { createCipheriv, randomBytes } = require('crypto')
const key = randomBytes(32)
const iv = randomBytes(16)
const cipher = createCipheriv('aes-256-cbc', key, iv)
// 加密文件
fs.createReadStream('plain.txt')
.pipe(cipher)
.pipe(fs.createWriteStream('aes-256-cbc.dat'))
// 输出 key,iv
fs.writeFileSync('aes-256-cbc.key', key.toString('hex') + '|' + iv.toString('hex'))
输出的 key 和 iv 分别为:
435157a4775085cef2aa2d44dd399eeea4a3cc304f85c0ef895770943e5ec4fc
294926351aad0cc4e8ce88c8356575b5
对加密后的文件进行解密:
const fs = require('fs')
const { createDecipheriv } = require('crypto')
const [key, iv] = fs.readFileSync('aes-256-cbc.key', {encoding: 'utf8'}).split('|')
const decipher = createDecipheriv('aes-256-cbc', Buffer.from(key, 'hex'), Buffer.from(iv, 'hex'))
fs.createReadStream('aes-256-cbc.dat')
.pipe(decipher)
.pipe(fs.createWriteStream('aes-256-cbc.txt'))
如果你感兴趣,可以把代码复制出来跑一下看看。
接下来我并不想继续介绍 Node.js 中的加密、解密 API 如何,这些在 Node.js 官方文档中都有:
Crypto - Node.js v12.13.0 Documentation
在上面的例子,使用了算法 aes-256-cbc
进行加解密。AES
是一种对称密码算法,而后面的 256
和 cbc
是指什么?iv
又是什么呢?
如果你对这感兴趣,那么本文正适合你。
AES 对称密码
AES 全称为“Advanced Encryption Standard”,是美国政府于 2001 年通过公开竞标征集的对称密码算法,用于替代已经老旧的 DES 算法。
AES 有三种密钥长度:128,192 和 256 比特。上面例子中的 256
就表示密钥长度为 256 比特。
由于 AES 算法是公开的,所以决定加密数据的机密性的关键是 密钥。密钥越长,则可供选择的组合数目越多,也就是密钥空间越大,对于暴力破解来说难度也就越高。
AES 每次加密的数据块大小是确定的 128 比特,如果数据超过这个大小,就需要进行拆分,分别进行加密后再组装,解密的过程则相反。
看起来对数据进行分组加密再组合是很简单的事情,最多额外约定下如果分组数据不够 128 比特如何填补就好了。
分组工作模式
ECB 模式
前面提到的思路,就是 ECB(Electronic CodeBook)模式的实现方式:
解密过程相反:分解 - 解密 - 合并。
但是这种模式是不够安全的,我们考虑数据的发送方(Alice)和接收方(Bob)之间有一个攻击者(Mallory)的情况。
由于对称密码的存在,Mallory 并不能解密数据。但是 ECB 模式下,数据只是简单分组加密再组装,Mallory 如果改变密文分组的顺序,Bob 解密得到的明文也就被改变了。
如果 Mallory 知晓通信报文的格式,就可以通过调整密文分组的顺序达到特殊的目的。例如 Alice 发送转账数据给 Bob,Mallory 从中对调了付款人和收款人信息对应的分组,就会导致 Bob 接收到错误数据。
能够在不破译密文的情况下操纵明文,这是 ECB 模式的一大弱点。
接下来,我们看下 CBC 模式是怎么应对这种攻击的。
CBC 模式
CBC 全称 Cipher Block Chaining,也就是密文分组链接模式:
上一个分组加密结果先与下一个分组明文执行 XOR(异或)运算,然后再加密。由于第一个明文分组没有上一个密文分组可以使用,所以需要事先传人一个数据,也就是 IV(初始化向量),其长度与分组的大小相同。对于 AES 密码算法,其分组大小为 128 bit,所以使用的 IV 也就是 128 比特。
采用 CBC 模式,密文中如果有一个分组缺失,后续所有分组都无法正确解密;如果一个分组数据被篡改或损坏,由于链接关系的存在,会影响下一个分组的解密(因为解密需要使用上一分组的密文进行 XOR 运算)。
如果 Mallory 对分组顺序进行了调换,由于相邻分组链接的关系,会导致无法正常解密。
当然,确保数据在传输过程中不被篡改(完整性),使用消息认真码(MAC,Message Authentication Code)是更好的方案。
根据前一分组信息介入的时机和方式的不同,还有其他几种和 CBC 模式类似的模式。
CFB 模式
CFB,Cipher FeedBack,密文反馈模式:
看起来有点奇怪,第一个分组中,应用 AES 进行加密的其实 IV,IV 的密文再和明文进行 XOR 运算得到分组最终密文。其实 XOR 也是一种加密运算,而 IV 的密文则可以看作是生成的 一次性密码本。
OFB 模式
OFB,Output-FeedBack,输出反馈模式:
和 CFB 模式非常类似,只是提供下一组的密钥是密码算法的输出,而非最终的密文。
OFB 的优势是可以抛开明文,事先计算好每个分组需要的密钥,所以生成密钥流和对明文进行 XOR 运算是可以并行的。
CTR 模式
CTR,CounTer,计数器模式:
CTR 模式和 CBC 模式差别就更大了,CTR 模式是通过对逐次累加的计数器进行加密来生成密钥流的 流密码。
CTR 和 OFB 有点类似,真正对明文加密的过程,是基于密钥流进行 XOR 运算。而 CTR 模式更进一步,由于是采用计数器的模式,任意分组的密钥是可以提前计算的,不依赖前一分组,所以在支持并行计算的系统中,CTR 模式的速度会非常快。
小结
这里介绍的分组密码模式有很多,一般而言,ECB 模式是不应该使用的,而通常使用的是 CBC 模式和 CTR 模式。
认证加密模式
如果在加密过程中,同时还生成了有关数据的认证信息,则既可以确保数据的机密性,又能确保完整性。
认证加密的方法有:
- Encrypt-then-MAC (EtM):明文加密,密文生成 MAC
- Encrypt-and-MAC (E&M):明文加密,明文生成 MAC
- MAC-then-Encrypt (MtE):明文生成 MAC,明文和MAC一起加密
Node.js 文档中给出的一个使用 aes-192-ccm
的例子,CCM 模式就是一个满足认证加密要求的算法:
crypto_ccm_mode - crypto
总结
回过头,再看下本文开头的算法 aes-256-cbc
,我们知道:
-
AES
:对称密码算法 -
256
:选择的 AES 密钥长度 -
CBC
:分组密码模式
在使用该算法进行加密时,需要传入两个参数:
-
key
:AES 的密钥,256 比特 -
iv
:CBC 模式需要的初始化向量,与分组大小一致,AES 分组为固定的 128 比特
另外,密钥 key
是重要的机密,要妥善保存,通信双方如果需要传输该密钥,需要额外通过其他机制(密钥交换/协商机制)。而初始化向量 iv
则无需秘密保存,但要避免在反复使用相同的数据,尽量每次创建随机数据。