Migrating mcrypt with Blowfish and ECB to OpenSSL - php

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="

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).

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.

Related

PHP openssl MCRYPT_RIJNDAEL_128 equivalent

I have an encrypted value, which I know has been encrypted via the following obsolete php function:
$encrypt = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, trim($encryptedValue), MCRYPT_MODE_CBC, $iv);
I'm trying to decrypt this value using openssl_decrypt with this function :
$decryptedValue = openssl_decrypt("QTu07uBvWSJHmN7gqGIaJg==", 'aes-256-cbc', $key, $options = 0, $iv);
I know that the encryptedValue should return the value '1000' but the function don't work (return false)
What I did wrong ? Is the AES mode incorrect or something like that ?
I also tried this :
$encryptedValue = "QTu07uBvWSJHmN7gqGIaJg=="; // = "1000"
if (strlen($encryptedValue) % 8) {
$encryptedValue = str_pad($encryptedValue, strlen($encryptedValue) + 8 - strlen($encryptedValue) % 8, "\0");
}
$decryptedValue = openssl_decrypt($encryptedValue, 'aes-256-cbc', $key, $options = 0, $iv);
dd($decryptedValue);
But this function still return false with the dump.
I hope you've found a better solution in the months past, as this seems outdated, but for the sake of answering the question:
The correct cipher to use with OpenSSL depends on the keysize from your original code using mcrypt. Both AES-128 and AES-256 are variants of Rijndael-128, they just differ in key size. If you have a 128-bit (16-byte) key, then you have AES-128; if it's larger than that (and ideally exactly 256 bits), then you have AES-256.
Then, seeing that your cipherText is Base64-encoded, you need to either base64_decode() it before passing to openssl_decrypt() OR don't use OPENSSL_RAW_DATA - the only thing this flag does is to tell the function to not perform Base64 decoding itself.
And finally, yes, mcrypt will apply zero-padding, but that extra step you tried is just unnecessarily adding it again, just use OPENSSL_ZERO_PADDING while decrypting. So, you end up with something like this:
$cipher = (mb_strlen($key, '8bit') <= 8) ? 'aes-128-cbc' : 'aes-256-cbc';
$plainText = openssl_decrypt($encryptedValue, $cipher, $key, OPENSSL_ZERO_PADDING, $iv);
There are other possible variables, like the key also being encoded or not, the IV being prepended or appended to the cipherText already, etc, but with the information that you provided, this should be all you need to recover the data.

Why are mcrypt and openssl_encrypt not giving the same results for blowfish with ecb if password is shorter than 16 chars?

I've read the general suggesstions:
mcrypt is deprecated, what is the alternative? and
PHP7.1 mcrypt alternative
And tested the following solutions:
Migrating mcrypt with Blowfish and ECB to OpenSSL and
php: mcrypt_encrypt to openssl_encrypt, and OPENSSL_ZERO_PADDING problems
They ain't work. This is my used code:
$message = "My secret message";
$key = "mysecretpasswor"; // <- if you add one more character here, it's working
$iv = "\0\0\0\0\0\0\0\0";
$message_padded = $message;
if(strlen($message_padded) % 8) {
$message_padded = str_pad($message_padded, strlen($message_padded) + 8 - strlen($message_padded) % 8, "\0");
}
$encrypted_mcrypt = mcrypt_encrypt(MCRYPT_BLOWFISH, $key, $message, MCRYPT_MODE_ECB, $iv);
$encrypted_openssl = openssl_encrypt($message_padded, "bf-ecb", $key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING);
printf("%s => %s\n", $message, base64_encode($encrypted_mcrypt));
printf("%s => %s\n", $message_padded, base64_encode($encrypted_openssl));
Using something like "DES-EDE3-CBC" as encryption method does work. But I cannot change the encryption that's used. I've to migrate the old code to new code. There sometimes keys used, that are shorter than 16 characters.
Any suggestions?
First of all, OPENSSL_NO_PADDING is not supposed to be used with openssl_encrypt(). Its documentation mentions OPENSSL_ZERO_PADDING, which (confusingly) means 'no padding'. That is what you want.
OPENSSL_NO_PADDING is intended for use with asymmetric cryptography. By coincidence it has a value of 3 which is equal to OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING. This the reason why you can use it incorrectly without consequence (in this case).
Your ciphertexts are different because the function openssl_encrypt() in bf-ecb mode by default will pad your key with \0's if its length is less than 16 bytes. This is not required for blowfish and mcrypt_encrypt() does not do that. In order to switch off that behavior, use the flag OPENSSL_DONT_ZERO_PAD_KEY when calling openssl_encrypt(). Since this flag does not seem to be documented, you will have to go to the source code to learn about it :-). Or read Bug #72362 OpenSSL Blowfish encryption is incorrect for short keys.
With this, the correct invocation of openssl_encrypt() becomes:
$opts = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING | OPENSSL_DONT_ZERO_PAD_KEY;
$encrypted_openssl = openssl_encrypt($message_padded, "bf-ecb", $key, $opts);
Testing it:
$ php bf.php
My secret message => JPO/tvAqFD2KCTqfv0l8uWLfPUWdZIxQ
My secret message => JPO/tvAqFD2KCTqfv0l8uWLfPUWdZIxQ

PHP blowfish cbc VS Perl Crypt

I'm encrypting client numbers in PHP using openssl_encrypt.
$value = '01715034842';
$key = 'pi3kn3W#k#cj3';
$iv = 'Toy#dtv!';
$cipher = 'bf-cbc';
$crypted = openssl_encrypt($value, $cipher, $key, true, $iv);
$hashValue = unpack('H*',$crypted);
The result is: 0b6b81176ac7c298ebcb294f0a581539
Also my friend programmed the other part in Perl. And he also encoded the same number using same keys and using Blowfish to (he is using Perl library: https://metacpan.org/pod/release/LDS/Crypt-CBC-2.30/CBC.pm ):
use Crypt::CBC;
use Crypt::Blowfish;
## szyfrowanie
my $key = 'pi3kn3W#k#cj3';
my $iv = 'Toy#dtv!';
my $cipher = Crypt::CBC->new( -key => $key,
-iv => $iv,
-header => 'none',
-cipher => 'Blowfish'
);
sub mkHash {
my $crypt = $cipher->encrypt_hex($_[0]);
# print 'Hash: '.$crypt."\n";
return $crypt;
}
sub deHash {
my $crypt = $cipher->decrypt_hex($_[0]);
# print 'string: '.$crypt."\n";
return $crypt;
}
my $clientHash = mkHash($smc);
And getting a different result for the same set of data:
c5377bcf0f55af641709c35928350576
So we can't use this language.
Does it depend on programming language differences? Is this is a bug in my code or language?
I think that when using the same set of data and the same encrypting (BlowFish CBC) we should get the same results in every language.
Looking forward for opinion on this case.
The working scripts
The following PHP and Perl scripts show how to achieve the same output for the two languages. I will explain some of the details below that.
PHP:
$value = '01715034842';
$cipher = 'bf-cbc';
$key = '12345678901234567890123456789012345678901234567890123456';
$option = OPENSSL_RAW_DATA;
$iv = 'Toy#dtv!';
$crypted = openssl_encrypt($value, $cipher, $key, $option, $iv);
echo($crypted)
Perl:
use Crypt::CBC;
use Crypt::Blowfish;
my $value = '01715034842';
my $key = '12345678901234567890123456789012345678901234567890123456';
my $iv = 'Toy#dtv!';
my $cipher = Crypt::CBC->new( -literal_key => 1,
-key => $key,
-iv => $iv,
-header => 'none',
-cipher => 'Blowfish'
);
my $crypted = $cipher->encrypt($value);
print $crypted;
Using diff on the two outputs results in no difference, showing they are the same:
$ diff <(php encrypt.php) <(perl encrypt.pl)
$
Details explaining the required changes
The following sections explain the required changes, compared to your original code.
Encryption key
The PHP openssl_encrypt() function always expects a raw key. The bytes that you give it, are the bytes used as the encryption key. The Perl CBC class on the other hand expects a passphrase by default, from which it will derive the encryption key by doing an MD5 hash. If you want the class to use your raw bytes as the encryption key, you have to set the parameter literal_key to 1.
After you have done that, the CBC class expects the key to be the exact number of bytes needed for the encryption scheme, which the CBC class assumes to be 56 for the Crypt::Blowfish implementation. Hence the adjusted key in the scripts. The error you will get otherwise is If specified by -literal_key, then the key length must be equal to the chosen cipher's key length of 56 bytes
Output format
The PHP openssl_encrypt() function by default returns a base64 encoded string, the CBC class returns the raw bytes. One way to make this consistent is by setting the OPENSSL_RAW_DATA option in PHP.
Inspecting the ciphertext
If you want to inspect the ciphertext in a readable format, you can add your own print routines at the end or pipe the output into a tool like hexdump or xxd
$ php encrypt.php | xxd
00000000: 5f35 3205 74e8 dcaa 2f05 9aa4 366e ef8b _52.t.../...6n..
$ perl encrypt.pl | xxd
00000000: 5f35 3205 74e8 dcaa 2f05 9aa4 366e ef8b _52.t.../...6n..

How to convert mcrypt_generic to openssl_encrypt?

this is my php code in PHP7.0.30
$key = strtoupper(md5('799ae002c7e940ef8a890b3a428f8f458e3f7c39d1cc2bf24390f0c46cf932c8'));
$text ='name=王星星&mobile=15212345678&idNumber=620402198709215456&bankName=招商银行&bankNum=6214830100799652';
$plaintext = $text;
$size = mcrypt_get_block_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
//PKCS5Padding
$padding = $size - strlen($plaintext) % $size;
$plaintext .= str_repeat(chr($padding), $padding);
$module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
$iv = str_repeat("\0", $size);
/* Intialize encryption */
mcrypt_generic_init($module, base64_decode($key), $iv);
/* Encrypt data */
$encrypted = mcrypt_generic($module, $plaintext);
/* Terminate encryption handler */
mcrypt_generic_deinit($module);
mcrypt_module_close($module);
var_dump(base64_encode($encrypted));
/* openssl_encrypt */
$encrypted = openssl_encrypt($plaintext, 'AES-256-CBC',base64_decode($key), OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,$iv);
var_dump(base64_encode($encrypted));
mcrypt_generic output: 8LZZEXRhAfeeQOxF1iI9GpBcA2hSCelrUq2OimhSgZly6RfRonzGiE32vHh/JkdK+X5N5hFBMKz+iOmWAbgL9BIu2GIAxBIXCOusxFU4eDJ/5uy7F9vR9EE5NqOAiHBZhTP3pzMtEc0fLAzg8Tsngg==
but openssl_encrypt output:
g/YBzu+SGy9jfR+DIVY2S0iGM2O0QEs+J3IEv7bNAoz7+3iX9FboJZT0h+OH6uUeQBoSsD+eAga69U5C86Ibcp5Q2ay1FzfDFG/eGBtUaAJxRBwhxiNeBxPw2jBar2fR42wZUZOGTjlT5Ujgz+s/Iw==
I don't know how to convert mcrypt_generic to openssl_encrypt , thanks!
You need a 256 bit key. Your current "key" is 32 bytes which is 256 bits.
You problem is that your are decoding the key. When you decode the key you are reducing the key's size to 24 bytes or 192 bits.
So you have two options.
Increase the key size by another 8 bytes before you base64_encode() it.
OR
Just remove the base64_decode() functions.
Secondly, I hope you are using the md5 for testing purposes and not for your application. The md5 hash is not suited to give you a hash that is designed for modern cryptography.
You should use something like $key = openssl_random_pseudo_bytes($size); to generate a key with.
I would also like to point you in the direction of the Libsodium library. It is now native on the latest version of PHP.
UPDATED
If your goal is to have AES encryption with a 256 bit key like your code suggest, then you will have to do what I stated above. If that is not one of your requirements then the only thing you have to do is change the AES-256-CBC to AES-192-CBC in the openssl_encrypt() function like so.
$encrypted = openssl_encrypt($plaintext, 'AES-192-CBC', base64_decode($key), OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv);
One thing to point out is that the "128" in the name MCRYPT_RIJNDAEL_128 refers to the block size not the key size, where as the "256" in AES-256-CBC actually refers to the key size. Which is why the AES-192-CBC will work for your current key size of 192 bits. AES encryption uses a standard of a 128 bit block size, so that is compatible the MCRYPT_RIJNDAEL_128 block size.
Cheers

mcrypt_encrypt to openssl_encrypt, and OPENSSL_ZERO_PADDING problems

I have this mcrypt_encrypt call, for a given $key, $message and $iv:
$string = mcrypt_encrypt(MCRYPT_3DES, $key, $message, MCRYPT_MODE_CBC, $iv);
I'd like to change the mcrypt_encrypt call to an openssl_encrypt one, to future-proof this.
By having $mode = 'des-ede3-cbc' or $mode = '3DES'; and $options = true I get the more similar response, but not identical. Is there other way to call it to get a perfect match?
I am getting this (base64_encoded) for a lorem-ipsum $message+$key combinations, so I am starting to believe one function or the other are padding somewhat the message before encrypting...
for mcrypt:
"Y+JgMBdfI7ZYY3M9lJXCtb5Vgu+rWvLBfjug2GLX7uo="
for for openssl:
"Y+JgMBdfI7ZYY3M9lJXCtb5Vgu+rWvLBvte4swdttHY="
Tried using $options to pass OPENSSL_ZERO_PADDING, but passing anything but 1 (OPENSSL_RAW_DATA, or true) results in an empty string ...
Neither using OPENSSL_ZERO_PADDING nor OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING work... :(
I'm using "OpenSSL 1.0.2g 1 Mar 2016".
Already read this q&a, but it does not help me. Not the only one with padding troubles, but no solution in sight so far. Second answer talks about adding padding to mcrypt call, I would really want to remove padding from openssl encryption call...
mcrypt_encrypt zero-pads input data if it's not a multiple of the blocksize. This leads to ambiguous results if the data itself has trailing zeroes. Apparently OpenSSL doesn't allow you to use zero padding in this case, which explains the false return value.
You can circumvent this by adding the padding manually.
$message = "Lorem ipsum";
$key = "123456789012345678901234";
$iv = "12345678";
$message_padded = $message;
if (strlen($message_padded) % 8) {
$message_padded = str_pad($message_padded,
strlen($message_padded) + 8 - strlen($message_padded) % 8, "\0");
}
$encrypted_mcrypt = mcrypt_encrypt(MCRYPT_3DES, $key,
$message, MCRYPT_MODE_CBC, $iv);
$encrypted_openssl = openssl_encrypt($message_padded, "DES-EDE3-CBC",
$key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $iv);
printf("%s => %s\n", bin2hex($message), bin2hex($encrypted_mcrypt));
printf("%s => %s\n", bin2hex($message_padded), bin2hex($encrypted_openssl));
This prints both as equal.
4c6f72656d20697073756d => c6fed0af15d494e485af3597ad628cec
4c6f72656d20697073756d0000000000 => c6fed0af15d494e485af3597ad628cec
mcrypt_encrypt uses zeroes to pad message to the block size. So you can add zeroes to the tail of your raw data, and then encrypt the block.
Using OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDING should work. If it doesn't, then you can remove padding from the decrypted data by yourself.

Categories