I have a PHP module that encrypts emails with aes-256-cbc
using openssl_encrypt
.
The ciphertexts generated by this module can be decrypted by this module as well.
But if I try to decrypt them with an implementation of aes-256-cbc in Go with the same IV and Key, I get a bad blocksize error. The block size is supposed to be in multiples of 16 but the ciphertext generated by PHP is not in multiples of 16.
Here is the code
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
)
var (
IV = []byte("fg3Dk54f4340fKF2JTC9")
KEY = []byte("13GsJd6076v69^f4(fdB")
)
func main() {
h := sha256.New()
h.Write(KEY)
KEY = []byte(hex.EncodeToString(h.Sum(nil))[:32])
h.Reset()
h.Write(IV)
IV = []byte(hex.EncodeToString(h.Sum(nil))[:16])
fmt.Println("Key", string(KEY))
fmt.Println("IV", string(IV))
// This ciphertext was generated by the PHP module
de := "ZHRodkpCK3R5QXlCMnh3MFdudDh3Zz09"
q, err := decrypt(KEY, IV, []byte(de))
fmt.Println(string(q), err)
}
// Returns slice of the original data without padding.
func pkcs7Unpad(data []byte, blocklen int) ([]byte, error) {
if blocklen <= 0 {
return nil, fmt.Errorf("invalid blocklen %d", blocklen)
}
if len(data)%blocklen != 0 || len(data) == 0 {
return nil, fmt.Errorf("invalid data len %d", len(data))
}
padlen := int(data[len(data)-1])
if padlen > blocklen || padlen == 0 {
return nil, fmt.Errorf("invalid padding")
}
// check padding
pad := data[len(data)-padlen:]
for i := 0; i < padlen; i++ {
if pad[i] != byte(padlen) {
return nil, fmt.Errorf("invalid padding")
}
}
return data[:len(data)-padlen], nil
}
func decrypt(key, iv, data []byte) ([]byte, error) {
var err error
data, err = base64.StdEncoding.DecodeString(string(data))
if err != nil {
return nil, err
}
if len(data) == 0 || len(data)%aes.BlockSize != 0 {
return nil, fmt.Errorf("bad blocksize(%v), aes.BlockSize = %v
", len(data), aes.BlockSize)
}
c, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
cbc := cipher.NewCBCDecrypter(c, iv)
cbc.CryptBlocks(data[aes.BlockSize:], data[aes.BlockSize:])
out, err := pkcs7Unpad(data[aes.BlockSize:], aes.BlockSize)
if err != nil {
return out, err
}
return out, nil
}
I have tried different things like using OPENSSL_RAW_DATA
and OPENSSL_ZERO_PADDING
in PHP module but absolutely nothing has worked.
The ciphertext used in above program was generated with options
field in openssl_encrypt
set to 0.
My speculation is, PHP doesn't pads input before encrypting them which generates ciphertexts that are not multiples of 16.
PHP implementation
<?php
class MBM_Encrypt_Decrypt {
const ENCRYPT_METHOD = 'AES-256-CBC'; // type of encryption
const SECRET_KEY = '13GsJd6076v69^f4(fdB'; // secret key
const SECRET_IV = 'fg3Dk54f4340fKF2JTC9'; // secret iv
public function encrypt($string) {
return $this->encrypt_decrypt('encrypt', $string);
}
public function decrypt($string) {
return $this->encrypt_decrypt('decrypt', $string);
}
private function encrypt_decrypt($action, $string)
{
$key = hash('sha256', self::SECRET_KEY);
$iv = substr(hash('sha256', self::SECRET_IV), 0, 16);
if ($action == 'encrypt') {
$output = openssl_encrypt($string, self::ENCRYPT_METHOD, $key, 0, $iv);
} else if ($action == 'decrypt') {
$output = openssl_decrypt(base64_decode($string), self::ENCRYPT_METHOD, $key, 0, $iv);
}
$output = (!empty($output)) ? $output : false;
return $output;
}
}
$class_encrypt = new MBM_Encrypt_Decrypt();
$plain_txt = "xyz@abc.com";
echo 'Plain Text: ' . $plain_txt . PHP_EOL;
$decrypted_txt = $class_encrypt->decrypt("ZHRodkpCK3R5QXlCMnh3MFdudDh3Zz09");
echo 'Decrypted Text: ' . $decrypted_txt . PHP_EOL;
if ($plain_txt === $decrypted_txt) echo 'SUCCESS' . PHP_EOL;
else echo 'FAILED' . PHP_EOL;
echo PHP_EOL . 'Length of Plain Text: ' . strlen($plain_txt);
echo PHP_EOL . 'Length of Encrypted Text: ' . strlen($encrypted_txt). PHP_EOL;
I see a few issues with your code:
The PHP hash()
function returns hex-encoded strings by default. You need to pass TRUE
as the third argument to enable "raw" mode.
Similarly no need to hex.EncodeToString
in the Go version.
The PHP openssl_encrypt()
and openssl_decrypt()
functions work with Base64-encoded strings by default. So no need to base64_decode
.
Here are the fixed versions:
PHP:
<?php
class MBM_Encrypt_Decrypt {
const ENCRYPT_METHOD = 'AES-256-CBC'; // type of encryption
const SECRET_KEY = '13GsJd6076v69^f4(fdB'; // secret key
const SECRET_IV = 'fg3Dk54f4340fKF2JTC9'; // secret iv
public function encrypt($string) {
return $this->encrypt_decrypt('encrypt', $string);
}
public function decrypt($string) {
return $this->encrypt_decrypt('decrypt', $string);
}
private function encrypt_decrypt($action, $string)
{
$key = hash('sha256', self::SECRET_KEY, true);
$iv = substr(hash('sha256', self::SECRET_IV, true), 0, 16);
if ($action == 'encrypt') {
$output = openssl_encrypt($string, self::ENCRYPT_METHOD, $key, 0, $iv);
} else if ($action == 'decrypt') {
$output = openssl_decrypt($string, self::ENCRYPT_METHOD, $key, 0, $iv);
}
$output = (!empty($output)) ? $output : false;
return $output;
}
}
$class_encrypt = new MBM_Encrypt_Decrypt();
$plain_txt = "xyz@abc.com";
echo 'Plain Text: ' . $plain_txt . PHP_EOL;
$encrypted_txt = $class_encrypt->encrypt($plain_txt);
echo 'Ciphertext: ' . $encrypted_txt . PHP_EOL;
$decrypted_txt = $class_encrypt->decrypt($encrypted_txt);
echo 'Decrypted Text: ' . $decrypted_txt . PHP_EOL;
if ($plain_txt === $decrypted_txt) echo 'SUCCESS' . PHP_EOL;
else echo 'FAILED' . PHP_EOL;
echo PHP_EOL . 'Length of Plain Text: ' . strlen($plain_txt);
echo PHP_EOL . 'Length of Encrypted Text: ' . strlen($encrypted_txt). PHP_EOL;
Go:
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
)
var (
IV = []byte("fg3Dk54f4340fKF2JTC9")
KEY = []byte("13GsJd6076v69^f4(fdB")
)
func main() {
h := sha256.New()
h.Write(KEY)
KEY = h.Sum(nil)
h.Reset()
h.Write(IV)
IV = h.Sum(nil)[:16]
fmt.Println("Key", hex.EncodeToString(KEY))
fmt.Println("IV", hex.EncodeToString(IV))
// This ciphertext was generated by the PHP module
de := "rDAnykzTorR5/SgpdD7slA=="
q, err := decrypt(KEY, IV, de)
fmt.Println(string(q), err)
}
// Returns slice of the original data without padding.
func pkcs7Unpad(data []byte, blocklen int) ([]byte, error) {
if blocklen <= 0 {
return nil, fmt.Errorf("invalid blocklen %d", blocklen)
}
if len(data)%blocklen != 0 || len(data) == 0 {
return nil, fmt.Errorf("invalid data len %d", len(data))
}
padlen := int(data[len(data)-1])
if padlen > blocklen || padlen == 0 {
return nil, fmt.Errorf("invalid padding")
}
// check padding
pad := data[len(data)-padlen:]
for i := 0; i < padlen; i++ {
if pad[i] != byte(padlen) {
return nil, fmt.Errorf("invalid padding")
}
}
return data[:len(data)-padlen], nil
}
func decrypt(key []byte, iv []byte, encrypted string) ([]byte, error) {
data, err := base64.StdEncoding.DecodeString(encrypted)
if err != nil {
return nil, err
}
if len(data) == 0 || len(data)%aes.BlockSize != 0 {
return nil, fmt.Errorf("bad blocksize(%v), aes.BlockSize = %v
", len(data), aes.BlockSize)
}
c, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
cbc := cipher.NewCBCDecrypter(c, iv)
cbc.CryptBlocks(data, data)
out, err := pkcs7Unpad(data, aes.BlockSize)
if err != nil {
return out, err
}
return out, nil
}
Output:
$ php test.php
Plain Text: xyz@abc.com
Ciphertext: rDAnykzTorR5/SgpdD7slA==
Decrypted Text: xyz@abc.com
SUCCESS
Length of Plain Text: 11
Length of Encrypted Text: 24
$ go test
Key 45ede7f4300fcc407d734020f12c8176463e7d493aa0395cdfa32e31ff914b0a
IV 9f79430dfdd761b3ed128bc38bfeadc5
xyz@abc.com <nil>