最近遇到一个场景,需要在 OpenResty 里使用 AES 对数据进行加密。但是看了一下官方提供的 AES 库,如果数据密码的话,并没有返回实际使用的 key 和 IV,因此查看了一下源码,研究了一下 key 生成的方式,方便其他语言使用。
密钥生成逻辑的核心代码在这:
function _M.new(self, key, salt, _cipher, _hash, hash_rounds, iv_len, enable_padding)
...
local _hash = _hash or hash.md5
local hash_rounds = hash_rounds or 1
...
if C.EVP_BytesToKey(_cipher.method, _hash, salt, key, #key,
hash_rounds, gen_key, gen_iv)
~= _cipherLength
then
return nil, "failed to generate key and iv"
end
end
其中最核心的部分就是使用了 EVP_BytesToKey 这个函数,进行密钥扩展。这个函数是 OpenSSL 库提供的函数,相关说明在这:
https://www.openssl.org/docs/man3.1/man3/EVP_BytesToKey.html
注意!该函数并不安全!一般不建议在生产中使用该函数!应当使用安全的密钥衍生函数(KDF)!
阅读文档,该函数的主要做法就是对提供的密码进行 hash,如果长度不够 key 或 iv 的要求,则继续进行 hash,直到长足足够为止。
第一轮 hash 直接拼接给定的密码和 salt,从第二轮开始,则将上一轮的结果附加在密码和 salt 前,然后再次进行 hash 操作。注意这里从第二轮开始附加的上一轮结果是二进制结果,如果希望手动使用工具计算的话,需要将 hex 编码或 base64 编码后的结果先进行解码,再附加在输入前。
用 Go 模拟这个过程的代码如下:
package main
import (
"crypto/sha256"
"fmt"
)
// EVPBytesToKey 模拟 OpenSSL EVP_BytesToKey 方法
func EVPBytesToKey(password, salt []byte, keyLen, ivLen int) (key, iv []byte) {
var (
concatenatedHashes []byte
previousHash []byte
currentHash []byte
hash = sha256.New()
)
for len(concatenatedHashes) < keyLen+ivLen {
hash.Reset()
// 如果不是第一轮,将上一轮的哈希值加到当前哈希的输入中
if previousHash != nil {
hash.Write(previousHash)
}
// 加入密码和盐值
hash.Write(password)
hash.Write(salt)
// 当前哈希
currentHash = hash.Sum(nil)
// 添加到累积哈希中
concatenatedHashes = append(concatenatedHashes, currentHash...)
// 为下一轮准备
previousHash = currentHash
}
// 提取密钥和 IV
key = concatenatedHashes[:keyLen]
iv = concatenatedHashes[keyLen : keyLen+ivLen]
return key, iv
}
func main() {
password := []byte("password")
salt := []byte("salt")
keyLen := 32 // AES-256 需要的密钥长度
ivLen := 16 // AES 的 IV 长度
key, iv := EVPBytesToKey(password, salt, keyLen, ivLen)
fmt.Printf("Key: %x\nIV: %x\n", key, iv)
}