Go语言AES-GCM解密时消息认证失败问题求助
AES-GCM解密迁移Go后部分数据认证失败问题排查与解决
问题背景
原本在TypeScript+Next.js环境中基于web/crypto实现的AES-GCM解密方案可正常解密所有数据,迁移至Go语言实现后,仅部分数据能正常解密,其余数据触发消息认证失败,失败点位于Go标准库crypto/cipher的GCM标签校验逻辑:
if subtle.ConstantTimeCompare(expectedTag[:g.tagSize], tag) != 1 { // The AESNI code decrypts and authenticates concurrently, and // so overwrites dst in the event of a tag mismatch. That // behavior is mimicked here in order to be consistent across // platforms. for i := range out { out[i] = 0 } return nil, errOpen }
原TypeScript实现代码
/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/no-var-requires */ import { Controller, Get, Res, Param, Req, HttpStatus } from '@nestjs/common'; import { HttpService } from '@nestjs/axios'; import { response, type Response } from 'express'; import { Readable } from 'stream'; import { firstValueFrom, map } from 'rxjs'; import { FileService } from './file.service'; const fs = require('fs'); const crypto = require('crypto').webcrypto; //import crypto, { subtle } from \"crypto\"; async function generateSecretKeyForEncryption( secreteKeyString: string, userSalt: string, ) { const key = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(secreteKeyString), { name: 'PBKDF2', hash: 'SHA-256' }, false, ['deriveKey'], ); const derivedKey = await crypto.subtle.deriveKey( { name: 'PBKDF2', salt: new TextEncoder().encode(userSalt), iterations: 1000, hash: 'SHA-256', }, key, { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt'], ); return derivedKey; } const fromHexString = (hexString) => Uint8Array.from(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))); const decryptedSecretKeyAndFile = async ( data, secretKey, accessKey, iv, fileData, userSalt, ) => { console.log("🚀 ~ file: file.controller.ts:52 ~ iv:", iv) const newDataArray = fromHexString(data); console.log("🚀 ~ file: file.controller.ts:55 ~ newDataArray:", newDataArray) const key = await generateSecretKeyForEncryption(secretKey, userSalt); console.log("🚀 ~ file: file.controller.ts:57 ~ key:", key) const encryptionKey = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: new TextEncoder().encode(accessKey), tagLength: 128, }, key, newDataArray, ); console.log("🚀 ~ file: file.controller.ts:65 ~ encryptionKey:", encryptionKey) const ecnryptionKeyForFile = await crypto.subtle.importKey( 'raw', new Uint8Array(encryptionKey), { name: 'AES-GCM' }, true, ['encrypt', 'decrypt'], ); //console.log("🚀 ~ file: file.controller.ts:74 ~ ecnryptionKeyForFile:", ecnryptionKeyForFile) const encrtedData = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: new TextEncoder().encode(iv) }, ecnryptionKeyForFile, fileData, ); console.log("🚀 ~ file: file.controller.ts:84 ~ fileData:", fileData.length) console.log("🚀 ~ file: file.controller.ts:81 ~ encrtedData:", encrtedData) return encrtedData; };
Go语言实现代码(原错误版本)
// Function to decrypt filedata using decrypted key iv and filedata func decryptor(key, trimiv, data []byte) ([]byte, error) { // Create a new AES block cipher with the key b, err := aes.NewCipher(key) if err != nil { return nil, err } // Create a GCM cipher mode aesgcm, err := NewGCMWithNonceSize(b, 32) if err != nil { return nil, err } // Decrypt the data decryptedData, err := aesgcm.Open(nil, trimiv, data, nil) if err != nil { return nil, err } return decryptedData, nil } // Function to derive pbkf2 keyfrom secret key and salt func deriveKey(secretKey string, userSalt string) []byte { // Derive the key using PBKDF2 with provided salt and other parameters derivedKey := pbkdf2.Key([]byte(secretKey), []byte(userSalt), 1000, 32, sha256.New) // Return the derived key return derivedKey } // Function to decrypt key and then decrypt data using AES-GCM func DecryptedSecretKeyAndFile(data, secretKey, accessKey, iv, userSalt string, fileData []byte) ([]byte, error) { //Nonce and data to decrypt Master Key //nonce/iv to decrypt key hexaccessKey, _ := hex.DecodeString(accessKey) trimaccessKey := hexaccessKey[:32] //data to decrypt key hexdata, _ := hex.DecodeString(data) //Nonce and data to decrypt original data //nonce/iv to decrypt data hexiv, _ := hex.DecodeString(iv) trimiv := hexiv[:32] //fileData contains the original data to be decrypted //gcm method key := deriveKey(secretKey, userSalt) decryptedKey, err := decryptor(key, trimaccessKey, hexdata) if err != nil { return nil, err } fmt.Println("DecryptedKey accessed", decryptedKey) //Decrypt the Data decryptedData, err := decryptor(decryptedKey, trimiv, fileData) if err != nil { fmt.Println("Error:", err) return nil, err } fmt.Println("Decrypted Data accessed", len(fileData), len(decryptedData), decryptedData[:32]) //return Decrypted Data return decryptedData, nil }
核心原因分析
IV(Nonce)处理逻辑完全不匹配
- TypeScript中,
accessKey和iv是直接通过TextEncoder.encode()转成UTF-8字节数组作为GCM的IV(Nonce)。 - Go代码错误地将
accessKey和iv当作十六进制字符串进行hex.DecodeString()解码,再截断前32字节,这与TS的处理逻辑完全相悖,导致两端使用的IV不一致,必然触发标签校验失败。
- TypeScript中,
GCM Nonce长度设置错误
- TypeScript的web/crypto未强制指定Nonce长度,实际使用的是
accessKey/iv字符串UTF-8编码后的原始长度。 - Go代码硬编码设置了32字节的Nonce长度(
NewGCMWithNonceSize(b, 32)),若TS端的IV长度不等于32,会导致GCM模式参数不匹配,进一步加剧校验失败。
- TypeScript的web/crypto未强制指定Nonce长度,实际使用的是
错误处理缺失
- Go代码忽略了
hex.DecodeString()的错误返回(直接用_丢弃),若accessKey或iv并非合法十六进制字符串,会生成错误的IV数据,导致解密失败。
- Go代码忽略了
解决方法
修正IV处理逻辑
将Go中accessKey和iv的处理改为与TS一致的UTF-8编码,去掉错误的hex解码和截断操作:
// 原错误代码 // hexaccessKey, _ := hex.DecodeString(accessKey) // trimaccessKey := hexaccessKey[:32] // 修正后:直接将字符串转为UTF-8字节数组 trimaccessKey := []byte(accessKey) // 原错误代码 // hexiv, _ := hex.DecodeString(iv) // trimiv := hexiv[:32] // 修正后:直接将字符串转为UTF-8字节数组 trimiv := []byte(iv)
匹配GCM Nonce长度
根据TS端实际使用的IV长度,动态设置Go的GCM NonceSize,而非硬编码32字节:
func decryptor(key, nonce, data []byte) ([]byte, error) { b, err := aes.NewCipher(key) if err != nil { return nil, err } // 使用nonce的实际长度创建GCM模式 aesgcm, err := cipher.NewGCMWithNonceSize(b, len(nonce)) if err != nil { return nil, err } return aesgcm.Open(nil, nonce, data, nil) }
完善错误处理
不要忽略hex.DecodeString(data)的错误,确保输入的加密数据是合法的十六进制字符串:
hexdata, err := hex.DecodeString(data) if err != nil { return nil, fmt.Errorf("invalid hex data: %w", err) }
完整修正后的Go代码
import ( "crypto/aes" "crypto/cipher" "crypto/sha256" "encoding/hex" "fmt" "golang.org/x/crypto/pbkdf2" ) // Function to decrypt data using AES-GCM func decryptor(key, nonce, data []byte) ([]byte, error) { b, err := aes.NewCipher(key) if err != nil { return nil, err } // Create GCM mode with nonce's actual length aesgcm, err := cipher.NewGCMWithNonceSize(b, len(nonce)) if err != nil { return nil, err } decryptedData, err := aesgcm.Open(nil, nonce, data, nil) if err != nil { return nil, err } return decryptedData, nil } // Function to derive PBKDF2 key from secret key and salt func deriveKey(secretKey string, userSalt string) []byte { return pbkdf2.Key([]byte(secretKey), []byte(userSalt), 1000, 32, sha256.New) } // Function to decrypt key and then decrypt data using AES-GCM func DecryptedSecretKeyAndFile(data, secretKey, accessKey, iv, userSalt string, fileData []byte) ([]byte, error) { // Decrypt master key: use accessKey as UTF-8 nonce nonceForKey := []byte(accessKey) hexdata, err := hex.DecodeString(data) if err != nil { return nil, fmt.Errorf("invalid encrypted key hex: %w", err) } // Decrypt original data: use iv as UTF-8 nonce nonceForData := []byte(iv) // Derive root key via PBKDF2 rootKey := deriveKey(secretKey, userSalt) // Decrypt the master key decryptedKey, err := decryptor(rootKey, nonceForKey, hexdata) if err != nil { return nil, fmt.Errorf("failed to decrypt master key: %w", err) } // Decrypt the target file data decryptedData, err := decryptor(decryptedKey, nonceForData, fileData) if err != nil { return nil, fmt.Errorf("failed to decrypt file data: %w", err) } return decryptedData, nil }
内容的提问来源于stack exchange,提问作者Hamza Sajid
相关产品推荐
相关产品推荐

