Golang实现AES-128 ECB加密后JS端解密失败问题排查
Golang实现AES-128加密跨端解密失败问题
现有实现代码
Go端加密工具函数
func EncryptAES(key []byte, plaintext string) (string, error) { // create cipher c, err := aes.NewCipher(key) if err != nil { return "", err } // allocate space for ciphered data out := make([]byte, len(plaintext)) for i := 1; i <= len(plaintext)/16; i++ { tempBuf := make([]byte, 16) offset := (i - 1) * 16 limit := offset + 16 // encrypt c.Encrypt(tempBuf, []byte(plaintext[offset:limit])) for j := 0; j < len(tempBuf); j++ { out[offset+j] = tempBuf[j] } } // return hex string return hex.EncodeToString(out), nil }
Go端加密调用流程
hasher := sha256.New() hasher.Write([]byte(word)) sha := hasher.Sum(nil) cypherText := word + userSessionKey for len([]byte(cypherText))%16 != 0 { cypherText += "0" } sha128 := sha[:len(sha)/2] captchaKey, err := utils.EncryptAES([]byte(hex.EncodeToString(sha128)), cypherText) if err != nil { SendErrorResponse(ctx, fasthttp.StatusInternalServerError, []byte("error generating aes session key "+err.Error())) return }
该流程逻辑:
- 对
word做SHA256哈希 - 拼接
word与userSessionKey作为待加密明文 - 明文末尾追加字符
"0",将长度补齐为16的倍数 - 取SHA256哈希结果的前半段转十六进制字符串作为AES密钥
- 调用加密函数生成十六进制格式密文
JS端解密函数
async function decryptAES128(key, cipherText){ let hash = CryptoJS.SHA256(key).toString(); hash = hash.substring(0, hash.length/2); console.log(hash); console.log(cipherText) const bytes = await CryptoJS.AES.decrypt(CryptoJS.enc.Hex.parse(cipherText), CryptoJS.enc.Hex.parse(hash), { mode: CryptoJS.mode.ECB }); return CryptoJS.enc.Utf8.stringify(bytes.words) }
故障表现
生成的密钥和密文在AES在线加解密网站测试时报错,JS端解密函数执行后无任何输出,无法正常解密得到明文。
问题原因
- 密钥长度不符合规范:AES-128要求密钥长度固定为16字节,原Go代码将16字节的SHA256片段转成32字符的十六进制字符串作为密钥传入,实际密钥长度为32字节,会被Go标准库识别为AES-256算法,和预期的AES-128不匹配。
- 填充逻辑非标准:手动追加字符
"0"的填充方式不符合块加密通用的PKCS#7填充规则,且当明文长度刚好为16的倍数时不会追加填充,会导致解密端校验块长度失败。 - 两端逻辑不匹配:JS端解密时,CryptoJS默认将传入的字符串作为口令派生密钥,而非直接作为原始密钥使用;同时JS端密文解析、结果序列化的逻辑都存在错误,且
CryptoJS.AES.decrypt是同步方法,不需要await调用。
修正后代码
Go端修正
首先引入必要依赖:
import ( "bytes" "crypto/aes" "crypto/sha256" "encoding/hex" "errors" )
修正加密函数,加入标准PKCS7填充,增加密钥长度校验:
func EncryptAES(key []byte, plaintext string) (string, error) { // 校验AES-128密钥长度必须为16字节 if len(key) != 16 { return "", errors.New("aes-128 key length must be 16 bytes") } c, err := aes.NewCipher(key) if err != nil { return "", err } // 标准PKCS7填充 plainBytes := []byte(plaintext) padLen := 16 - len(plainBytes)%16 plainBytes = append(plainBytes, bytes.Repeat([]byte{byte(padLen)}, padLen)...) out := make([]byte, len(plainBytes)) // ECB模式分块加密 blockCount := len(plainBytes) / 16 for i := 0; i < blockCount; i++ { offset := i * 16 c.Encrypt(out[offset:offset+16], plainBytes[offset:offset+16]) } return hex.EncodeToString(out), nil }
修正加密调用流程,去掉手动补0逻辑,直接取SHA256前16字节作为密钥:
hasher := sha256.New() hasher.Write([]byte(word)) sha := hasher.Sum(nil) // 直接取前16字节作为AES-128密钥,不需要转十六进制 aesKey := sha[:16] cypherText := word + userSessionKey // 加密函数内部已实现标准PKCS7填充,无需手动补"0" captchaKey, err := utils.EncryptAES(aesKey, cypherText) if err != nil { SendErrorResponse(ctx, fasthttp.StatusInternalServerError, []byte("error generating aes session key "+err.Error())) return }
JS端修正
对齐Go端密钥生成逻辑,修正CryptoJS调用参数:
function decryptAES128(key, cipherText){ // 和Go端对齐:对原始key做SHA256,取前16字节(对应十六进制前32位)作为密钥 const hash = CryptoJS.SHA256(key).toString(); const aesKey = CryptoJS.enc.Hex.parse(hash.substring(0, 32)); // ECB模式,PKCS7填充(CryptoJS默认填充规则为PKCS7,可显式声明) const bytes = CryptoJS.AES.decrypt( CryptoJS.enc.Hex.parse(cipherText).toString(CryptoJS.enc.Base64), aesKey, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 } ); return bytes.toString(CryptoJS.enc.Utf8); }
内容的提问来源于stack exchange,提问作者Danil Giniyatullin
相关产品推荐
相关产品推荐

