openssl_encrypt() randomly fails - IV passed is only ${x} bytes long, cipher expects an IV of precisely 16 bytes
Asked Answered
G

3

8

This is the code I use to encrypt/decrypt the data:

// Set the method
$method = 'AES-128-CBC';

// Set the encryption key
$encryption_key = 'myencryptionkey';

// Generet a random initialisation vector
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($method));

// Define the date to be encrypted
$data = "Encrypt me, please!";

var_dump("Before encryption: $data");

// Encrypt the data
$encrypted = openssl_encrypt($data, $method, $encryption_key, 0, $iv);

var_dump("Encrypted: ${encrypted}");

// Append the vector at the end of the encrypted string
$encrypted = $encrypted . ':' . $iv;

// Explode the string using the `:` separator.
$parts = explode(':', $encrypted);

// Decrypt the data
$decrypted = openssl_decrypt($parts[0], $method, $encryption_key, 0, $parts[1]);

var_dump("Decrypted: ${decrypted}");

It ususaly works fine, but sometimes (1 in 10 or even less often) it fails. When it fails than the text is only partially encrypted:

This is the error message when it happens:

Warning: openssl_decrypt(): IV passed is only 10 bytes long, cipher expects an IV of precisely 16 bytes, padding with \0

And when it happens the encrypted text looks like:

Encrypt me���L�se!

I thought that it might be caused by a bug in PHP, but I've tested on different hosts: PHP 7.0.6 and PHP 5.6. I've also tried multiple online PHP parsers like phpfidle.org or 3v4l.org.

It seems that openssl_random_pseudo_bytes not always returns a string of a proper length, but I have no idea why.

Here's the sample: https://3v4l.org/RZV8d

Keep on refreshing the page, you'll get the error at some point.

Gratitude answered 25/5, 2016 at 14:21 Comment(0)
O
20

When you generate a random IV, you get raw binary. There's a nonzero chance that the binary strings will contain a : or \0 character, which you're using to separate the IV from the ciphertext. Doing so makes explode() give you a shorter string. Demo: https://3v4l.org/3ObfJ

The trivial solution would be to add base64 encoding/decoding to this process.


That said, please don't roll your own crypto. In particular, unauthenticated encryption is dangerous and there are already secure libraries that solve this problem.

Instead of writing your own, consider just using defuse/php-encryption. This is the safe choice.

Oxalate answered 25/5, 2016 at 14:36 Comment(4)
Thank you for the explanation. Now, at least, I know what caused the issue, but still I'm not sure how to solve it. If I replace: $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($method)); with: $iv = base64_encode(openssl_random_pseudo_bytes(openssl_cipher_iv_length($method))); Then the string length will not be 16 bytes long. openssl_encrypt expects it to be 16 bytes long (In case of AES-128-CBC method). Did I miss something?Gratitude
You don't encode it before you pass it to openssl_encrypt(), you change $encrypted = $encrypted . ':' . $iv; to $encrypted = $encrypted . ':' . base64_encode($iv);Oxalate
OMG, so easy. thank you very much! Works like a charm. I've also wrapped the whole encrypted data in base64_encode() in order to make it URL-friendly (like @Ryan Vincent said.). I've updated the original post.Gratitude
Hi!, $IV_64 = 'AAAAAAAAABBBBAAAAAAAA==' ; " is only 15 bytes long, cipher expects an IV of precisely 16 bytes, padding with \0 in " When I'm calling open_ssl encrypt with base64_decode($IV_64) Means that the original IV is not 16 bytes long?Sycamore
G
1

Here's the solution

I've updated the code from the first post and wrapped it in a class. This is fixed code based on the solution provided by Scott Arciszewski.

class Encryptor
{

    /**
     * Holds the Encryptor instance
     * @var Encryptor
     */
    private static $instance;

    /**
     * @var string
     */
    private $method;

    /**
     * @var string
     */
    private $key;

    /**
     * @var string
     */
    private $separator;

    /**
     * Encryptor constructor.
     */
    private function __construct()
    {
        $app = App::getInstance();
        $this->method = $app->getConfig('encryption_method');
        $this->key = $app->getConfig('encryption_key');
        $this->separator = ':';
    }

    private function __clone()
    {
    }

    /**
     * Returns an instance of the Encryptor class or creates the new instance if the instance is not created yet.
     * @return Encryptor
     */
    public static function getInstance()
    {
        if (self::$instance === null) {
            self::$instance = new Encryptor();
        }
        return self::$instance;
    }

    /**
     * Generates the initialization vector
     * @return string
     */
    private function getIv()
    {
        return openssl_random_pseudo_bytes(openssl_cipher_iv_length($this->method));
    }

    /**
     * @param string $data
     * @return string
     */
    public function encrypt($data)
    {
        $iv = $this->getIv();
        return base64_encode(openssl_encrypt($data, $this->method, $this->key, 0, $iv) . $this->separator . base64_encode($iv));
    }

    /**
     * @param string $dataAndVector
     * @return string
     */
    public function decrypt($dataAndVector)
    {
        $parts = explode($this->separator, base64_decode($dataAndVector));
        // $parts[0] = encrypted data
        // $parts[1] = initialization vector
        return openssl_decrypt($parts[0], $this->method, $this->key, 0, base64_decode($parts[1]));
    }

}

Usage

$encryptor = Encryptor::getInstance();

$encryptedData = $encryptor->encrypt('Encrypt me please!');
var_dump($encryptedData);

$decryptedData = $encryptor->decrypt($encryptedData);
var_dump($decryptedData);
Gratitude answered 25/5, 2016 at 18:19 Comment(1)
This is still vulnerable to chosen-ciphertext attacks.Oxalate
S
0

This happened to me also. I got error like

openssl_decrypt(): IV passed is only 14 bytes long, cipher expects an IV of precisely 16 bytes, padding with \0

I was using lowercase method like openssl_cipher_iv_length('aes-128-cbc')

Lowercase aes-- method gives a result of length which varies between 12 to 16. Ref: https://www.php.net/manual/en/function.openssl-cipher-iv-length.php

Making the method to uppercase openssl_cipher_iv_length('AES-128-CBC') will give consistent value which is 16.

So while encrypting & decrypting the IV length stays the same as 16.

Sharisharia answered 26/11, 2020 at 9:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.