douyan1944 2018-11-25 17:12
浏览 554

解密由openssl_encrypt在PHP中加密的AES-256-CBC密文时出现错误的块大小错误

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;
  • 写回答

1条回答 默认 最新

  • douzi0609 2018-11-26 10:35
    关注

    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>
    
    评论

报告相同问题?

悬赏问题

  • ¥15 drone 推送镜像时候 purge: true 推送完毕后没有删除对应的镜像,手动拷贝到服务器执行结果正确在样才能让指令自动执行成功删除对应镜像,如何解决?
  • ¥15 求daily translation(DT)偏差订正方法的代码
  • ¥15 js调用html页面需要隐藏某个按钮
  • ¥15 ads仿真结果在圆图上是怎么读数的
  • ¥20 Cotex M3的调试和程序执行方式是什么样的?
  • ¥20 java项目连接sqlserver时报ssl相关错误
  • ¥15 一道python难题3
  • ¥15 牛顿斯科特系数表表示
  • ¥15 arduino 步进电机
  • ¥20 程序进入HardFault_Handler