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

wube picture wube · May 25, 2016 · Viewed 14.7k times · Source

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.

Answer

Scott Arciszewski picture Scott Arciszewski · May 25, 2016

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.