前言
最近始终在折腾 Golang 的 AES 加密解密,最后的一个小需要只是寻求一个简略间接的加密工具而已,然而找着找着发现外面的坑太深了 …
吐槽:对于加密解密,其实咱们很多时候并没有特地高的要求(简单)。一开始,我最间接的一个想法就是:
- 调用一个办法,传递一个秘钥,实现加密;
- 调用一个办法,传递一个秘钥,实现解密,
就能够了,但事实网上纷繁复杂的实现让我头疼。难道,就没有一个让我最省心、简略、最快、实现一个加解密的办法吗?
指标
- 我要一个对称加密,加解密用的 key 统一
- 加密后的数据 = 加密办法(数据, key)
- 解密后的数据 = 解密办法 (数据, key)
仅此而已,但寻变网络各种类库,没意外,各有各的问题,上面我列举几个我在做的过程中遇到的问题和坑
问题
- AES 有各种加密模式 CBC、ECB、CTR、OCF、CFB 选哪个?都平安吗?
- AES 在某些加密模式下须要指定 IV 也就是初始向量(那我岂不是又要弄一个配置项?)
- AES 对于 key 的长度 和 IV 的长度都有要求(这个很烦,就像我定一个明码还非得是固定长度的)
- AES 须要加密的数据不是 16 的倍数的时候,须要对原来的数据做 padding 操作(能够简略了解为补充长度到固定的位数)好嘛,padding 还有不同的形式:Zero padding、ANSI X.923、PKCS7…
- js 罕用 crypto-js 进行加密解密操作(我这边还想有个特地需要能保障 js 加密统一)
上代码
show me your code 先来看下最终实现状况如何,而后再来说原理和问题
Golang 实现
package main
import (
"fmt"
"github.com/LinkinStars/go-scaffold/contrib/cryptor"
)
func main() {
key := "1234"
e := cryptor.AesSimpleEncrypt("Hello World!", key)
fmt.Println("加密后:", e)
d := cryptor.AesSimpleDecrypt(e, key)
fmt.Println("解密后:", d)
iv := cryptor.GenIVFromKey(key)
fmt.Println("应用的 IV:", iv)
}
// 输入
// 加密后:NHlpzbcTvOj686VaF7fU7g==
// 解密后:Hello World!
// 应用的 IV:03ac674216f3e15c
对,这就是我想要的,输出须要加密的内容和 key,给我出加密后的后果就好
crypto-js 实现
解密也是相似的,这里我就不反复代码了
import CryptoJS from 'crypto-js'
var data = "Hello World!"
var keys = [
"1234",
"16bit secret key",
"16bit secret key1234567",
"16bit secret key12345678",
"16bit secret key16bit secret ke",
"16bit secret key16bit secret key",
"16bit secret key16bit secret key1",
]
function aesEncrypt(data, key) {if (key.length > 32) {key = key.slice(0, 32);
}
var cypherKey = CryptoJS.enc.Utf8.parse(key);
CryptoJS.pad.ZeroPadding.pad(cypherKey, 4);
var iv = CryptoJS.SHA256(key).toString();
var cfg = {iv: CryptoJS.enc.Utf8.parse(iv) };
return CryptoJS.AES.encrypt(data, cypherKey, cfg).toString();}
for (let i = 0; i < keys.length; i++) {console.log(aesEncrypt(data, keys[i]))
}
// 输入
// NHlpzbcTvOj686VaF7fU7g==
// PuMhKY8ZFLnDAwlQ7v/2SQ==
// ZG9JUBvEXrXwSS2RIHvpog==
// pbvDuBOV3tJrlPV0xdmbKQ==
// uAeg71zBzFeUfEMHJqCSxw==
// j9SbFFEEFX4dT9VaDAzsCg==
// j9SbFFEEFX4dT9VaDAzsCg==
问题与解决方案
抉择什么加密模式
加密模式有 CBC、ECB、CTR、OCF、CFB,其中 ECB 有平安问题,所以肯定不抉择,而罕用的是 CBC,并且 crypto-js 默认也用了 CBC 所以就无脑抉择了 CBC
密钥的长度问题
AES 须要你指定的 密钥长度 必须为 128 位、192 位或 256 位,即字符串长度为:16、24 或 32。
对于晓得 AES 算法的人来说,其实这很好了解,并且很容易接受,然而对于一个齐全不晓得你程序或者利用的内部使用者来说,必须写一个长度固定的明码很难了解 。
所以对与 key(密钥) 我做了如下解决:
- 长度超过 32,间接截取后面 32
-
长度不满足要求的,应用
ZeroPadding
形式补全(小于 16 的补充到 16,大于 16 小于 24 的补充到 24)ZeroPadding 其实实现非常简单,就是将长度有余的开端补 0 补足就能够
初始向量 IV 的问题
首先来解释为什么须要 IV
其实很好了解,AES 的加密形式是将原数据拆分成一块一块,每一块独自进行加密,最初组合到一起,而在 ECB 模式下,每块加密应用的 key 都是一样的,所以有平安危险,而为了解决这个问题,和 MD5 相似就是给你的加“盐”,咱们晓得失常的 hash 容易碰撞被猜到,而加了盐之后,相当于给了一个偏移量,使得后果不可被预测。而 CBC 模式下,第一块加密数据所需的这个盐就是 IV,前面几块加密所需的盐都是通过后面来失去的。
那如何发明 IV 呢?
再次从使用者的角度登程,我既然曾经提供了一个 key 去加密了,为什么还要提供一个与 key 相似的货色去加密呢?就相当于我须要记住两个明码,很麻烦。并且通常如果作为配置项呈现的话,两个 key 必定是配置在一起的,配置文件外面个别不会为了平安而特地的将两个明码离开寄存。
所以我在思考如何发明一个 IV 呢?
首先,必定这个 IV 须要从 key 登程,因为解密也须要,随机或固定必定不可能,所以我的第一想法就是 IV 与 key 统一,当然我置信很多人都有和我一样的想法,然而,道歉,不行。
📢 留神!!!IV 与 key 统一在某些加密模式下相当于你间接将 key 裸露给了用户
所以我参考了老版本 node 的实现,并且改良了一下
The password is used to derive the cipher key and initialization vector (IV). The value must be either a 'latin1' encoded string, a Buffer, a TypedArray, or a DataView.
The implementation of crypto.createCipher() derives keys using the OpenSSL function EVP_BytesToKey with the digest algorithm set to MD5, one iteration, and no salt. The lack of salt allows dictionary attacks as the same password always creates the same key. The low iteration count and non-cryptographically secure hash algorithm allow passwords to be tested very rapidly.
In line with OpenSSL's recommendation to use a more modern algorithm instead of EVP_BytesToKey it is recommended that developers derive a key and IV on their own using crypto.scrypt() and to use crypto.createCipheriv() to create the Cipher object. Users should not use ciphers with counter mode (e.g. CTR, GCM, or CCM) in crypto.createCipher(). A warning is emitted when they are used in order to avoid the risk of IV reuse that causes vulnerabilities. For the case when IV is reused in GCM, see Nonce-Disrespecting Adversaries for details.
老版本 node 外面就间接将 key MD5 了一下作为了 IV,那显然 MD5 是容易被碰撞的。那么好,既然 MD5 不行,那我间接 SHA256 总能够了吧(目前实践平安)。
于是,对于 IV 的生成我就采取了 SHA256 的形式,对 key 做了一次 hash 并且因为 IV 长度固定为 16,所以我又做了一次截取,这下你总不可能还原了吧。
原数据处理模式
下面咱们晓得,AES 应用 CBC 模式进行加密的时候,须要将数据拆分成一块一块的,那么问题就是,每块长度为 16,当拆分到最初长度有余的时候又须要补充,也叫 padding。padding 还有不同的形式:Zero padding、ANSI X.923、PKCS7…
这里,相似的,因为 crypto-js 默认应用 PKCS7 所以就用它了。
其余问题
- 我在寻找工具的过程中看过很多办法,发现都会在加密的时候返回 error,我就很好受,我也明确他们返回 error 通常是因为 key 长度不满足要求的时候返回,所以我这里间接解决,当 error 呈现间接返回空字符串。
- crypto-js 在应用的时候肯定记得须要应用办法转换
CryptoJS.enc.Utf8.parse
否则会导致加密不统一的状况 CryptoJS.pad.ZeroPadding.pad(cypherKey, 4);
这里的 4 的起因是外部办法计算时 乘以了 4,其实是 block 的大小也就是 16,这也是一个坑,不看源码也不晓得的坑。我一开始传递的就是 16 😭 源码地位:https://github.com/brix/crypto-js/blob/develop/src/pad-zeropadding.js
总结
代码实现在:
https://github.com/LinkinStars/go-scaffold/blob/main/contrib/cryptor/aes.go
如果须要,你不肯定须要间接援用,拷贝对应办法到本人的我的项目中进行应用就能够了,心愿能帮忙到你。同时也有反对自定义指定 IV 的办法 AesCBCEncrypt
,但绝对应的你须要本人去保障 key 和 iv 的长度正确了。
最初要揭示一下,尽管我应用了 crypto-js
进行加密,但因为是业务须要,如果你在应用的话肯定要留神不要将 key 给前端页面进行解密,毕竟 AES 是对称加密。
参考链接
- https://gist.github.com/xsephiroth/ed645eb6f8289c983ee70ec3da2fcbb5
- https://github.com/forgoer/openssl 这个库的其实,实现的根本很靠近了
- AES 对称 Crypto-JS 加密和 Go 解密
- JS 中利用 CryptoJS 进行 MD5/SHA256/BASE64/AES 加解密的办法与示例