Issue
I want to be able to decrypt in Go what was encrypted in Python. The encrypting/decrypting functions work respectively in each language but not when I am encrypting in Python and decrypting in Go, I am guessing there is something wrong with the encoding because I am getting gibberish output:
Rx����d��I�K|�ap���k��B%F���UV�~d3h�����|�����>�B��B�
Encryption/Decryption in Python
def encrypt(plaintext, key=config.SECRET, key_salt='', no_iv=False):
"""Encrypt shit the right way"""
# sanitize inputs
key = SHA256.new((key key_salt).encode()).digest()
if len(key) not in AES.key_size:
raise Exception()
if isinstance(plaintext, string_types):
plaintext = plaintext.encode('utf-8')
# pad plaintext using PKCS7 padding scheme
padlen = AES.block_size - len(plaintext) % AES.block_size
plaintext = (chr(padlen) * padlen).encode('utf-8')
# generate random initialization vector using CSPRNG
if no_iv:
iv = ('\0' * AES.block_size).encode()
else:
iv = get_random_bytes(AES.block_size)
log.info(AES.block_size)
# encrypt using AES in CFB mode
ciphertext = AES.new(key, AES.MODE_CFB, iv).encrypt(plaintext)
# prepend iv to ciphertext
if not no_iv:
ciphertext = iv ciphertext
# return ciphertext in hex encoding
log.info(ciphertext)
return ciphertext.hex()
def decrypt(ciphertext, key=config.SECRET, key_salt='', no_iv=False):
"""Decrypt shit the right way"""
# sanitize inputs
key = SHA256.new((key key_salt).encode()).digest()
if len(key) not in AES.key_size:
raise Exception()
if len(ciphertext) % AES.block_size:
raise Exception()
try:
ciphertext = codecs.decode(ciphertext, 'hex')
except TypeError:
log.warning("Ciphertext wasn't given as a hexadecimal string.")
# split initialization vector and ciphertext
if no_iv:
iv = '\0' * AES.block_size
else:
iv = ciphertext[:AES.block_size]
ciphertext = ciphertext[AES.block_size:]
# decrypt ciphertext using AES in CFB mode
plaintext = AES.new(key, AES.MODE_CFB, iv).decrypt(ciphertext).decode()
# validate padding using PKCS7 padding scheme
padlen = ord(plaintext[-1])
if padlen < 1 or padlen > AES.block_size:
raise Exception()
if plaintext[-padlen:] != chr(padlen) * padlen:
raise Exception()
plaintext = plaintext[:-padlen]
return plaintext
Encryption/Decryption in Go
// PKCS5Padding adds padding to the plaintext to make it a multiple of the block size
func PKCS5Padding(src []byte, blockSize int) []byte {
padding := blockSize - len(src)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(src, padtext...)
}
// Encrypt encrypts the plaintext,the input salt should be a random string that is appended to the plaintext
// that gets fed into the one-way function that hashes it.
func Encrypt(plaintext) string {
h := sha256.New()
h.Write([]byte(os.Getenv("SECRET")))
key := h.Sum(nil)
plaintextBytes := PKCS5Padding([]byte(plaintext), aes.BlockSize)
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
// The IV needs to be unique, but not secure. Therefore it's common to
// include it at the beginning of the ciphertext.
ciphertext := make([]byte, aes.BlockSize len(plaintextBytes))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
panic(err)
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintextBytes)
// return hexadecimal representation of the ciphertext
return hex.EncodeToString(ciphertext)
}
func PKCS5UnPadding(src []byte) []byte {
length := len(src)
unpadding := int(src[length-1])
return src[:(length - unpadding)]
}
func Decrypt(ciphertext string) string {
h := sha256.New()
// have to check if the secret is hex encoded
h.Write([]byte(os.Getenv("SECRET")))
key := h.Sum(nil)
ciphertext_bytes := []byte(ciphertext)
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
log.Print(aes.BlockSize)
// The IV needs to be unique, but not secure. Therefore it's common to
// include it at the beginning of the ciphertext.
iv := ciphertext_bytes[:aes.BlockSize]
if len(ciphertext) < aes.BlockSize {
panic("ciphertext too short")
}
ciphertext_bytes = ciphertext_bytes[aes.BlockSize:]
stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(ciphertext_bytes, ciphertext_bytes)
plaintext := PKCS5UnPadding(ciphertext_bytes)
return string(plaintext)
}
CodePudding user response:
The CFB mode uses a segment size which corresponds to the bits encrypted per encryption step, see CFB.
Go only supports a segment size of 128 bits (CFB128), at least without deeper modifications (s. here and here). In contrast, the segment size in PyCryptodome is configurable and defaults to 8 bits (CFB8), s. here. The posted Python code uses this default value, so the two codes are incompatible. Since the segment size is not adjustable in the Go code, it must be set to CFB128 in the Python code:
cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=128)
Also, the ciphertext is hex encoded in the Python code, so it must be hex decoded in the Go code, which does not yet happen in the posted code.
With these both changes, the ciphertext produced with the Python code can be decrypted.
The ciphertext in the following Go Code was created with the Python code using a segment size of 128 bits and the passphrase my passphrase
and is successfully decrypted:
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/hex"
"fmt"
)
func main() {
ciphertextHex := "546ddf226c4c556c7faa386940f4fff9b09f7e3a2ccce2ed26f7424cf9c8cd743e826bc8a2854bb574df9f86a94e7b2b1e63886953a6a3eb69eaa5fa03d69ba5" // Fix 1: Apply CFB128 on the Python side
fmt.Println(Decrypt(ciphertextHex)) // The quick brown fox jumps over the lazy dog
}
func PKCS5UnPadding(src []byte) []byte {
length := len(src)
unpadding := int(src[length-1])
return src[:(length - unpadding)]
}
func Decrypt(ciphertext string) string {
h := sha256.New()
//h.Write([]byte(os.Getenv("SECRET")))
h.Write([]byte("my passphrase")) // Apply passphrase from Python side
key := h.Sum(nil)
//ciphertext_bytes := []byte(ciphertext)
ciphertext_bytes, _ := hex.DecodeString(ciphertext) // Fix 2. Hex decode ciphertext
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
iv := ciphertext_bytes[:aes.BlockSize]
if len(ciphertext) < aes.BlockSize {
panic("ciphertext too short")
}
ciphertext_bytes = ciphertext_bytes[aes.BlockSize:]
stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(ciphertext_bytes, ciphertext_bytes)
plaintext := PKCS5UnPadding(ciphertext_bytes)
return string(plaintext)
}
Security:
- Using a digest as key derivation function is insecure. Apply a dedicated key derivation function like PBKDF2.
- A static or missing salt is also insecure. Use a randomly generated salt for each encryption. Concatenate the non-secret salt with the ciphertext (analogous to the IV), e.g.
salt|IV|ciphertext
. - The variant
no_iv=True
applies a static IV (zero IV), which is insecure and should not be used. The correct way is described with the variantno_iv=False
. - CFB is a stream cipher mode and therefore does not require padding/unpadding, which can therefore be removed on both sides.