在尝试将遗留代码从 Node.js 迁移到 Golang 时,我正在尝试 AES 加密和解密。以下是问题陈述。
我尝试理解并复制 Golang 中解密的要求(随机数/初始化向量)。以下是我正在使用的代码片段。代码有问题吗?任何帮助表示赞赏。蒂亚!
加密代码:
const crypto = require('crypto');
const createKey = secret => secret.padEnd(32, secret);
const randBytes = crypto.randomBytes(16);
const createIv = () => {
let randStr = Buffer.from("1234567890123456").toString('base64');
return randStr.slice(0,16);
}
const b64urlSafe = str => str.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '');
const b64urlUnsafe = str => {
let decoded = str;
if (decoded.length % 4 !== 0) {
decoded += ('===').slice(0, 4 - (decoded.length % 4));
}
return decoded.replace(/-/g, '+').replace(/_/g, '/');
};
const defaultSecret = 'goodthingstaketimesometime123456';
const defaultKey = createKey(defaultSecret);
/**
* Creates a cipher using AES-256-GCM
*
* @param {string} text the plaintext
* @param {string} secret the secret (optional)
* @returns a ciphertext (including an auth tag, separated by an underscore)
*/
const createCipher = function (text, secret = null) {
const iv = createIv();
const key = secret ? createKey(secret) : defaultKey;
let cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let crypted = cipher.update(text, 'utf8', 'base64');
crypted += cipher.final('base64');
cipher.getAuthTag()
return `${b64urlSafe(crypted)}.${b64urlSafe(cipher.getAuthTag().toString('base64'))}.${b64urlSafe(iv)}`;
};
解密代码:
func decryptTokenFromNode() {
fmt.Println("======decryptTokenFromNode function ======")
token := "KZhf9KXZUKmH2jfhYIc68M4x/60gzx6+5aYujPI8ZYc4xaO16mVdtpOXKRjP+cPAk9ftNzFOrngll4sqK0jPYDqJkVdBv+9Kw==="
iv := "MTIzNDU2Nzg5MDEy"
ciphertext, _ := base64.StdEncoding.DecodeString(token)
nonce, _ := base64.StdEncoding.DecodeString(iv)
key := []byte("goodthingstaketimesometime123456")
block, err := aes.NewCipher(key)
if err != nil {
panic(err.Error())
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
panic(err.Error())
}
fmt.Println("nonce length:", len(nonce))
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
panic(err.Error())
}
fmt.Printf("%s\n", plaintext)
}
错误:
panic: cipher: message authentication failed
goroutine 1 [running]:
main.decryptTokenFromNode()
/Users/santhosh/Quizizz/auth-service/decrypt.go:120 +0x208
main.main()
/Users/santhosh/Quizizz/auth-service/decrypt.go:128 +0x20
exit status 2
您发布的密文无法使用 NodeJS 代码生成,因为 NodeJS 代码使用 Base64url(并且发布的密文应用标准 Base64)。另外,三重
=
无效。encrypting this string to verify seal and open in golang
如果用 NodeJS 代码重新加密该明文,结果就是密文:
KZhf9KXZUKmH2jfhYIc68M4x_60gzx6-5aYujPI8ZYc4xaO16mVdtpOXKRjP-cPAk9ftNzFOrng.ll4sqK0jPYDqJkVdBv-9Kw.MTIzNDU2Nzg5MDEy
其前两部分基本上与您发布的密文相同(除了
.
分隔符、Base64的两个不同字符(+
和/
)和Base64url(-
和_
) ) 字母,以及不正确的填充。也许密文中的差异是复制/粘贴错误。
为了能够解密密文,必须将这三个部分分开。第一部分对应于 Base64url 编码的密文,第二部分对应于 Base64url 编码的身份验证标签,第三部分是随机数(与前两部分相反,Base64url 编码值用作随机数,长度为 16 个字节,并且不是原始值)。
分离后,密文和标签将进行Base64url解码并按顺序连接
ciphertext|tag
。
使用此数据,将使用 AES-GCM 进行解密 - Go 代码:
package main
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"fmt"
"strings"
)
func main() {
decryptTokenFromNode()
}
func decryptTokenFromNode() {
fmt.Println("======decryptTokenFromNode function ======")
// Separate Base64url encoded ciphertext, tag and nonce
token := "KZhf9KXZUKmH2jfhYIc68M4x_60gzx6-5aYujPI8ZYc4xaO16mVdtpOXKRjP-cPAk9ftNzFOrng.ll4sqK0jPYDqJkVdBv-9Kw.MTIzNDU2Nzg5MDEy"
data := strings.Split(token, ".")
ciphertext, _ := base64.RawURLEncoding.DecodeString(data[0])
tag, _ := base64.RawURLEncoding.DecodeString(data[1])
nonce := ([]byte)(data[2])
// Concatenate raw cipheretext and tag
ciphertext = append(ciphertext, tag...)
key := []byte("goodthingstaketimesometime123456")
block, err := aes.NewCipher(key)
if err != nil {
panic(err.Error())
}
// Import 16 bytes nonce
aesgcm, err := cipher.NewGCMWithNonceSize(block, 16)
if err != nil {
panic(err.Error())
}
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
panic(err.Error())
}
fmt.Printf("%s\n", plaintext)
}
有了这段代码,就可以成功解密密文了。
安全:
NodeJS 代码中引人注目的是随机数的生成。首先,使用16字节随机数,这与GCM推荐的12字节长度不同。因此,必须使用
NewGCMWithNonceSize()
。关于长度,出于兼容性和效率原因,建议使用 12 字节随机数。第二个漏洞是使用字符串作为密钥。键应该是随机字节序列而不是字符串。如果要使用字符串作为密钥材料,则应应用密钥派生函数(至少 PBKDF2 或更好的更现代的 Argon2)。