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

Go实现AES-GCM加密后解密失败,需兼容现有Rails解密逻辑

问题:Go实现Rails ActiveSupport加密逻辑不兼容现有解密代码

我需要用Go实现Ruby ActiveSupport的加密逻辑,现有Go解密代码能正常解密Rails加密的数据,但自己写的Go加密代码生成的内容用该解密代码解密时出错。要求实现Go端的加密解密,且不修改现有Go解密代码。


现有可正常解密Rails数据的Go代码

import (
    "bytes"
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "crypto/sha1"
    "encoding/base64"
    "encoding/hex"
    "fmt"
    "log"
    "strings"

    rbmarshal "github.com/dozen/ruby-marshal"
    "golang.org/x/crypto/pbkdf2"
)

// DecryptGCM
// 参考Rails 5.2-stable实现
func DecryptGCM(encryptedText string, secretKeyBase string) (string, error) {
    encryptText := strings.Split(encryptedText, "$$")
    saltHex := encryptText[0]
    encodedText := encryptText[1]

    splitEncodedText := strings.Split(encodedText, "--")
    encodedText = splitEncodedText[0]
    ivText := splitEncodedText[1]
    authTagText := splitEncodedText[2]

    decodeText, err := base64.StdEncoding.DecodeString(encodedText)
    if err != nil {
        return "", fmt.Errorf(`err b64 decode text got %v`, err)
    }

    ivDecodeText, err := base64.StdEncoding.DecodeString(ivText)
    if err != nil {
        return "", fmt.Errorf(`err b64 iv got %v`, err)
    }

    authTagTextDecoded, err := base64.StdEncoding.DecodeString(authTagText)
    if err != nil {
        return "", fmt.Errorf(`err b64 auth tag got %v`, err)
    }

    key := GenerateKey(secretKeyBase, saltHex)

    block, err := aes.NewCipher(key)
    if err != nil {
        return "", fmt.Errorf(`err aesNewCipher got %v`, err)
    }

    aesGCM, err := cipher.NewGCM(block)
    if err != nil {
        return "", fmt.Errorf(`err chipperNewGCM got %v`, err)
    }

    plaintext, err := aesGCM.Open(nil, ivDecodeText, append(decodeText, authTagTextDecoded...), nil)
    if err != nil {
        return "", fmt.Errorf(`err aesGCMOpen got %v`, err)
    }

    var v string
    rbmarshal.NewDecoder(bytes.NewReader(plaintext)).Decode(&v)
    return string(v), nil
}

func GenerateKey(secretKeyBase string, saltHex string) []byte {
    key := pbkdf2.Key([]byte(secretKeyBase), []byte(saltHex), 65536, 32, sha1.New)
    return key
}

我编写的Go加密代码(解密失败)

// EncryptGCM
// 参考Rails 5.2-stable实现
func EncryptGCM(text string, secretKeyBase string) (string, error) {
    authTagSize := 16

    salt := make([]byte, 32)
    rand.Read(salt)

    saltKey := hex.EncodeToString(salt)

    key := GenerateKey(secretKeyBase, saltKey)

    block, err := aes.NewCipher(key)
    if err != nil {
        return "", fmt.Errorf(`err aesNewCipher got %v`, err)
    }

    aesGCM, err := cipher.NewGCM(block)

    aesGCM.NonceSize()
    if err != nil {
        return "", fmt.Errorf(`err chipperNewGCM got %v`, err)
    }

    iv := make([]byte, aesGCM.NonceSize())
    rand.Read(iv)

    ciphertext := aesGCM.Seal(nil, iv, []byte(text), nil)
    textEncode := base64.StdEncoding.EncodeToString(ciphertext)
    ivEncode := base64.StdEncoding.EncodeToString(iv)

    authTagEncode := base64.StdEncoding.EncodeToString(ciphertext[len(ciphertext)-authTagSize:])

    return fmt.Sprintf("%s$$%s--%s--%s", saltKey, textEncode, ivEncode, authTagEncode), nil
}

密钥信息

SECRET_KEY = "3ae9b0ce19316f877554a0427044180e27267fb9798db9147feeb318865b3a52f79824201608f6e4e10dc8e3f29e5bf4b83e46c4103ff8d98b99903d054d721a"

Rails端加密代码

class Crypton
  SECRET_KEY_BASE = ENV["SECRET_KEY_BASE"]
  class << self
    def encrypt text
      raise 'Encypt failed, secret_key_base not found' unless SECRET_KEY_BASE.present?
      text = text.to_s unless text.is_a? String

      len   = ActiveSupport::MessageEncryptor.key_len
      salt  = SecureRandom.hex len
      key   = ActiveSupport::KeyGenerator.new(SECRET_KEY_BASE).generate_key salt, len
      crypt = ActiveSupport::MessageEncryptor.new key
      encrypted_data = crypt.encrypt_and_sign text
      "#{salt}$$#{encrypted_data}"
    end

    def decrypt text
      raise 'Decrypt failed, secret_key_base not found' unless SECRET_KEY_BASE.present?
      salt, data = text.split "$$"
      len   = ActiveSupport::MessageEncryptor.key_len
      key   = ActiveSupport::KeyGenerator.new(SECRET_KEY_BASE).generate_key salt, len
      crypt = ActiveSupport::MessageEncryptor.new key
      crypt.decrypt_and_verify data
    end
  end
end

问题原因

对比Rails逻辑和你的Go加密代码,有两处关键差异:

  1. 缺少Ruby Marshal序列化:Rails的encrypt_and_sign会先对明文做Ruby Marshal序列化,你的代码直接加密原始字符串,导致解密时rbmarshal.Decode无法解析。
  2. GCM输出拆分错误:你的代码把「密文+AuthTag」整体作为encodedText,但解密代码会将encodedText解码后再追加一次AuthTag,导致GCM解密时数据长度不匹配。

修复后的Go加密代码

import (
    "bytes"
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "crypto/sha1"
    "encoding/base64"
    "encoding/hex"
    "fmt"
    "strings"

    rbmarshal "github.com/dozen/ruby-marshal"
    "golang.org/x/crypto/pbkdf2"
)

// EncryptGCM
// 完全匹配Rails 5.2-stable ActiveSupport::MessageEncryptor逻辑
func EncryptGCM(text string, secretKeyBase string) (string, error) {
    // 1. 对明文做Ruby Marshal序列化,和Rails处理逻辑一致
    var buf bytes.Buffer
    enc := rbmarshal.NewEncoder(&buf)
    if err := enc.Encode(text); err != nil {
        return "", fmt.Errorf("marshal text failed: %v", err)
    }
    marshaledText := buf.Bytes()

    // 生成盐值
    salt := make([]byte, 32)
    _, err := rand.Read(salt)
    if err != nil {
        return "", fmt.Errorf("generate salt failed: %v", err)
    }
    saltKey := hex.EncodeToString(salt)

    // 生成加密密钥
    key := GenerateKey(secretKeyBase, saltKey)

    block, err := aes.NewCipher(key)
    if err != nil {
        return "", fmt.Errorf("aes new cipher failed: %v", err)
    }

    aesGCM, err := cipher.NewGCM(block)
    if err != nil {
        return "", fmt.Errorf("new gcm failed: %v", err)
    }

    // 生成IV
    iv := make([]byte, aesGCM.NonceSize())
    _, err = rand.Read(iv)
    if err != nil {
        return "", fmt.Errorf("generate iv failed: %v", err)
    }

    // Seal输出为:密文 + AuthTag
    ciphertextWithTag := aesGCM.Seal(nil, iv, marshaledText, nil)
    // 拆分密文和AuthTag(AuthTag长度由GCM Overhead获取)
    authTag := ciphertextWithTag[len(ciphertextWithTag)-aesGCM.Overhead():]
    ciphertext := ciphertextWithTag[:len(ciphertextWithTag)-aesGCM.Overhead()]

    // 分别Base64编码各部分
    ciphertextEncode := base64.StdEncoding.EncodeToString(ciphertext)
    ivEncode := base64.StdEncoding.EncodeToString(iv)
    authTagEncode := base64.StdEncoding.EncodeToString(authTag)

    return fmt.Sprintf("%s$$%s--%s--%s", saltKey, ciphertextEncode, ivEncode, authTagEncode), nil
}

// 复用现有GenerateKey函数
func GenerateKey(secretKeyBase string, saltHex string) []byte {
    key := pbkdf2.Key([]byte(secretKeyBase), []byte(saltHex), 65536, 32, sha1.New)
    return key
}

修复点说明

  • 添加Ruby Marshal序列化:加密前对明文做序列化,确保解密时rbmarshal.Decode能正确解析。
  • 正确拆分密文与AuthTag:用aesGCM.Overhead()获取标准AuthTag长度,拆分后分别编码,避免解密时重复追加AuthTag。
  • 补充错误检查:修复rand.Read返回值忽略问题,提升代码健壮性。

内容的提问来源于stack exchange,提问作者lnxsr

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 01:35:05