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: