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;