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
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:
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.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.