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加密代码,有两处关键差异:
- 缺少Ruby Marshal序列化:Rails的
encrypt_and_sign会先对明文做Ruby Marshal序列化,你的代码直接加密原始字符串,导致解密时rbmarshal.Decode无法解析。 - 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
相关产品推荐
相关产品推荐

