通过阅读本文,你将了解 OpenSSL 中的 EVP_BytesToKey 和 CryptoJS.AES 中加解密函数 encrypt 和 decrypt 是如何实现的。
AES 算法(Rijndael 算法)在加解密时需要提供密钥(key)和初始向量(iv),而用 CryptoJS.AES 库在加解密时却需要提供密码(passphrase 或者 password):
var encrypted = CryptoJS.AES.encrypt("Message", "Secret Passphrase");
var decrypted = CryptoJS.AES.decrypt(encrypted, "Secret Passphrase");
在经过一番探索之后,我发现 CryptoJS.AES 库加解密本质并没有改变,只不过为了使用方便,CryptoJS.AES 库使用 OpenSSL 的 EVP_BytesToKey 函数将 passphrase 转换成 32 位 key 和 16 位 iv 以供 AES 算法使用。
要实现这个 CryptoJS.AES 库加解,首先要实现 EVP_BytesToKey 函数。找了蛮多实现,发现 browserify/EVP_BytesToKey 比较容易阅读,接下来我就用列表的形式展示这个函数的实现过程:
- 将 password 转换成二进制 buffer
- 将 salt 转换成二进制 buffer,并确保 salt 是 8 位
- 初始化 key、iv、tmp 等二进制 buffer
进入 md5 加密循环:
- 使用 md5 算法,依次传入 tmp、password 和 salt,计算得到新的 tmp
- 使用 tmp 依次填充 key 和 iv
- 如果两者没被填满,则继续循环,否则跳出循环
- 返回 key 和 iv
注意,这个实现就算不加入 salt 依然得到 key 和 iv,一般来说,我们用的都是加 salt 的,这个 salt 在加密时也可能是随机生成的,会在最后的加密字符串中体现。有 salt 参与的 AES 加密得到的加密字符串以 base64 编码储存,其所对应的字节数组由三部分组成:
Salted__
这八个字符对应的 ASCII 编码,占 8 个字节- 加密时用到的随机种子,占 8 个字节
- 用 AES 算法(Rijndael 算法,CBC 模式,256 位密钥,128 位块)加密得到的字节数组
好了,读到这里,想必你也可以自己动手实现这个函数了。感兴趣的话可以参考我写的 Rust 实现代码 felixmaker/crypto_aes。