ruby-on-railsgocryptographyaes-gcm

Go aes gcm encryption got error when it's decrypted


I have an issue when trying to decrypt from encrypted text using Golang. basically I try to rewrite ruby activesupport encryption

here is the decrytion code. this code works well when I try to decrypt encrypted from rails

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
// reference on Rails 5.2-stable:
// https://github.com/rails/rails/blob/5-2-stable/activesupport/lib/active_support/message_encryptor.rb#L183
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) // Fix 1
    if err != nil {
        return "", fmt.Errorf(`err aesGCMOpen got %v`, err)
    }

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

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

and this is my encrytion golang code

// EncryptGCM
// reference on Rails 5.2-stable:
// https://github.com/rails/rails/blob/5-2-stable/activesupport/lib/active_support/message_encryptor.rb#L166C6-L166C6
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

SECRET_KEY = "3ae9b0ce19316f877554a0427044180e27267fb9798db9147feeb318865b3a52f79824201608f6e4e10dc8e3f29e5bf4b83e46c4103ff8d98b99903d054d721a"

And below is my rails encryption code

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

I want to be able to encrypt and decrypt in golang code, but no change in golang decryption, because it works well decrypt encrypted data from rails


Solution

  • The Go code for encryption is missing the Ruby serialization and the correct separation of ciphertext and authentication tag.

    A possible fix is:

    ...
    w := bytes.NewBuffer([]byte{})
    rbmarshal.NewEncoder(w).Encode(&text) // Fix 1
    textSerialized := w.Bytes()
    
    ciphertextTag := aesGCM.Seal(nil, iv, textSerialized, nil)
    border := len(ciphertextTag) - authTagSize
    ciphertext := ciphertextTag[:border] // Fix 2
    authTag := ciphertextTag[border:]
    
    textEncode := base64.StdEncoding.EncodeToString(ciphertext)
    ivEncode := base64.StdEncoding.EncodeToString(iv)
    authTagEncode := base64.StdEncoding.EncodeToString(authTag)
    ...
    

    Details:

    • The Rails encryption code, which is the reference for the Go encryption code, performs the encryption in the encrypt_and_sign() method, which in turn calls the create_message() method, whose source code shows that the data is serialized before encryption. Details on this Ruby serialization can be found e.g. in the post A little dip into Ruby's Marshal format. A Go library that implements this serialization is ruby-marshal.
      In the original Go code the serialization is missing. This must be corrected by adding the serialization (see Fix 1).
    • The Seal() method in the Go code provides the concatenation of ciphertext and authentication tag as return value. However, since both parts are contained separately in the result string, it is necessary to separate the two.
      In the original Go code, the authentication tag is correctly determined and returned, but for the ciphertext, the concatenated value is returned. This must be corrected, only the actual ciphertext (i.e. without the authentication tag) may be returned as ciphertext (see Fix 2).