Björn Tantau
Björn Tantau

Reputation: 1614

Migrating mcrypt with Blowfish and ECB to OpenSSL

I can't for the life of me figure out how to migrate my legacy mcrypt code to OpenSSL. I got it working for Blowfish with CBC and for Rijndael with CBC, but Blowfish with ECB is eluding me.

And yes, I read Moving from mcrypt with Blowfish & ECB to OpenSSL and I tried zero-padding the data, not zero-padding the data, zero-padding the key, cycling over the key and any combination of them and nothing seems to work.

This is my code:

<?php
function encrypt_with_mcrypt($data, $key) {
        return mcrypt_encrypt(MCRYPT_BLOWFISH, $key, $data, MCRYPT_MODE_ECB);
}

function encrypt_with_openssl($data, $key) {
        return openssl_encrypt($data, 'BF-ECB', $key, OPENSSL_RAW_DATA | OPENSSL_DONT_ZERO_PAD_KEY);
}

$data = 'foobar';
$key = 'supersecretkey';

var_dump(base64_encode(encrypt_with_mcrypt($data, $key)));
var_dump(base64_encode(encrypt_with_openssl($data, $key)));

And this is the output:

test.php:13:
string(12) "5z0q3xNnokw="
test.php:14:
string(12) "1zyqavq7sCk="

Upvotes: 4

Views: 1653

Answers (2)

Reinier Torenbeek
Reinier Torenbeek

Reputation: 17383

I do not have much to add to Maarten's answer, except I thought it would be nice to show some the code that illustrates his words.

mcrypt adds zeroes to fill up the plaintext to a multiple of the BF blocksize of 8 bytes, which can be shown by printing hexdumps of both the plaintext and the decrypted ciphertext:

$key = "supersecretkey";
$data = "foobar";
$ctxt = mcrypt_encrypt(MCRYPT_BLOWFISH, $key, $data, MCRYPT_MODE_ECB);
$ptxt = mcrypt_decrypt(MCRYPT_BLOWFISH, $key, $ctxt, MCRYPT_MODE_ECB);
echo bin2hex($data).PHP_EOL;
echo bin2hex($ptxt).PHP_EOL;

gives the following hexdumps:

666f6f626172
666f6f6261720000

openssl by default uses PKCS#5 padding which, in this case, adds 2 bytes with value 2 at the end of the block:

$key = "supersecretkey";
$data = "foobar";
$opts = OPENSSL_RAW_DATA | OPENSSL_DONT_ZERO_PAD_KEY;
$ctxt = openssl_encrypt($data, 'BF-ECB', $key, $opts);
$ptxt = mcrypt_decrypt(MCRYPT_BLOWFISH, $key, $ctxt, MCRYPT_MODE_ECB);
echo bin2hex($data).PHP_EOL;
echo bin2hex($ptxt).PHP_EOL;

gives

666f6f626172
666f6f6261720202

The ciphertext for mcrypt and openssl can be made consistent by manually adding the padding bytes. Note the OPENSSL_ZERO_PADDING options and the addition of "\0\0":

$key = "supersecretkey";
$data = "foobar";
$ctxt_mc = mcrypt_encrypt(MCRYPT_BLOWFISH, $key, $data, MCRYPT_MODE_ECB);
$opts = OPENSSL_RAW_DATA | OPENSSL_DONT_ZERO_PAD_KEY | OPENSSL_ZERO_PADDING;
$ctxt_os = openssl_encrypt($data."\0\0", 'BF-ECB', $key, $opts);
echo bin2hex($ctxt_mc).PHP_EOL;
echo bin2hex($ctxt_os).PHP_EOL;

gives:

e73d2adf1367a24c
e73d2adf1367a24c

Alternatively, manually inserting PKCS#5 padding bytes at the end when using mcrypt:

$key = "supersecretkey";
$data = "foobar";
$ctxt_mc = mcrypt_encrypt(MCRYPT_BLOWFISH, $key, $data."\2\2", MCRYPT_MODE_ECB);
$opts = OPENSSL_RAW_DATA | OPENSSL_DONT_ZERO_PAD_KEY;
$ctxt_os = openssl_encrypt($data, 'BF-ECB', $key, $opts);
echo bin2hex($ctxt_mc).PHP_EOL;
echo bin2hex($ctxt_os).PHP_EOL;

gives

d73caa6afabbb029
d73caa6afabbb029

Finally, trying to invoke openssl_encrypt() with padding disabled and a length that is not a multiple of the block size:

$key = "supersecretkey";
$data = "foobar";
$opts = OPENSSL_RAW_DATA | OPENSSL_DONT_ZERO_PAD_KEY | OPENSSL_ZERO_PADDING;
$ctxt = openssl_encrypt($data, 'BF-ECB', $key, $opts);
echo(openssl_error_string().PHP_EOL)

gives

error:0607F08A:digital envelope routines:EVP_EncryptFinal_ex:data not multiple of block length

Remark: the name OPENSSL_ZERO_PADDING is confusing, but it means "no padding". You might be tempted to use the flag OPENSSL_NO_PADDING, but that one is not intended to be used with openssl_encrypt(). Its value is 3, which is the same as OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING. In stead, it is intended for use with asymmetric cryptography.

Upvotes: 4

Maarten Bodewes
Maarten Bodewes

Reputation: 94038

The mcrypt library / wrapper defaults to zero-byte padding (only when required) while OpenSSL library / wrapper defaults to PKCS#5 padding. That means that the single block gets padded differently and will therefore show a different block of ciphertext.


A common trick is to decrypt the resulting ciphertext without any unpadding and then check the padding bytes by viewing the plaintext + padding in hexadecimals.

This will show you:

5z0q3xNnokw=
666f6f6261720000

for mcrypt and

1zyqavq7sCk=
666f6f6261720202

for OpenSSL.

Using a larger plaintext message that requires multiple blocks to be encrypted would also have shown you that encryption is going fine except for the last block.


First zero-pad your data if and only if the mcrypt input is not a multiple of 8 bytes (the block size of Blowfish), then use OPENSSL_ZERO_PADDING as the padding mode.

Note that looking at the source code shows that OPENSSL_ZERO_PADDING for some unspecified reason seems to mean "no padding" for the wrapper and OPENSSL_NO_PADDING does seem to conflict with other settings - this I regard as a rather bad design and implementation mistake by the developers of the PHP OpenSSL wrapper API.

More info can be found by the great research performed by Reinier that shows how the API pads / unpads (or forgets to pad / unpad, depending on where you stand).

Upvotes: 4

Related Questions