You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
}

核心原因分析

  1. IV(Nonce)处理逻辑完全不匹配

    • TypeScript中,accessKey和iv是直接通过TextEncoder.encode()转成UTF-8字节数组作为GCM的IV(Nonce)。
    • Go代码错误地将accessKey和iv当作十六进制字符串进行hex.DecodeString()解码,再截断前32字节,这与TS的处理逻辑完全相悖,导致两端使用的IV不一致,必然触发标签校验失败。
  2. GCM Nonce长度设置错误

    • TypeScript的web/crypto未强制指定Nonce长度,实际使用的是accessKey/iv字符串UTF-8编码后的原始长度。
    • Go代码硬编码设置了32字节的Nonce长度(NewGCMWithNonceSize(b, 32)),若TS端的IV长度不等于32,会导致GCM模式参数不匹配,进一步加剧校验失败。
  3. 错误处理缺失

    • Go代码忽略了hex.DecodeString()的错误返回(直接用_丢弃),若accessKey或iv并非合法十六进制字符串,会生成错误的IV数据,导致解密失败。

解决方法

修正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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 06:35:54