Mcrypt_Encrypt to Openssl_Encrypt, and Openssl_Zero_Padding Problems

mcrypt_encrypt to openssl_encrypt, and OPENSSL_ZERO_PADDING problems

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

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

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 7.2 openssl_encrypt and mcrypt_encrypt generate different values

The difference is that mcrypt_encrypt / mcrypt_decrypt uses Zero-Padding and openssl_encrypt / openssl_decrypt uses PKCS7-Padding. This can be easily verified by applying Zero-Padding for openssl: For this, PKCS7-Padding must be disabled with the flag OPENSSL_ZERO_PADDING (important: despite the name, this flag doesn't mean that Zero-Padding is used, but that no padding is applied at all), and the plaintext must be padded with 0-values to the next integer multiple of the blocksize (8 bytes for Triple-DES) unless the length already corresponds to an integer multiple of the blocksize:

<?php

function zeroPadding($data, $size) {
$oversize = strlen($data) % $size;
return $oversize == 0 ? $data : ($data . str_repeat("\0", $size - $oversize));
}

// Something is wronguration.
$data = 'FOO';

$secret = '111222333444555666777888';
$iv = 'ABCDEFGH';

// Encrytp & decrypt with mcrypt.
$encMcrypt = bin2hex(mcrypt_encrypt(MCRYPT_3DES, $secret, utf8_encode($data), MCRYPT_MODE_CBC, $iv));
$decMcrypt = mcrypt_decrypt(MCRYPT_3DES, $secret, hex2bin($encMcrypt), MCRYPT_MODE_CBC, $iv);

// Encrytp & decrypt with openssl.
$encOpenSSLZeroPadding = bin2hex(openssl_encrypt(utf8_encode(zeroPadding($data, 8)), 'DES-EDE3-CBC', $secret, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv));
$decOpenSSLZeroPadding = openssl_decrypt(hex2bin($encOpenSSLZeroPadding), 'DES-EDE3-CBC', $secret, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv);

// Result.
echo "data padded: " . bin2hex(zeroPadding($data, 8)) . "<br>";
echo "mcrypt encrypt: $encMcrypt <br>";
echo "openssl encrypt: $encOpenSSLZeroPadding <br>";
echo "mcrypt decrypt: $decMcrypt <br>";
echo "mcrypt decrypt: " . bin2hex($decMcrypt) . "<br>";
echo "openssl decrypt: $decOpenSSLZeroPadding" . "<br>";
echo "openssl decrypt: " . bin2hex($decOpenSSLZeroPadding);

with the following output:

data padded: 464f4f0000000000
mcrypt encrypt: 3f9bd8d5f844ff67
openssl encrypt: 3f9bd8d5f844ff67
mcrypt decrypt: FOO
mcrypt decrypt: 464f4f0000000000
openssl decrypt: FOO
openssl decrypt: 464f4f0000000000

Instead of using a Zero-Padding in the openssl-context, a PKCS7-Padding could be used in the mcrypt-context. No matter which of the two variants is used, with identical padding mcrypt and openssl results are identical!

It should be noted (see the hexadecimal output) that mcrypt doesn't remove the Zero-Padding during decryption (unlike openssl which removes the PKCS7-Padding during decryption). Also, Zero-Padding is unreliable compared to PKCS7-Padding. If the requirements allow it (which in your case is probably not the case because of the external provider), PKCS7-Padding should therefore be used.

Furthermore, both posted variants use the same algorithm, namely Triple-DES in CBC-mode. Triple-DES has a blocksize of 8 bytes and a keysize of 24 bytes. Triple-DES is not identical to DES, but is based on DES in the sense that it consists of three DES-runs (encryption-decryption-encryption = ede). mcrypt specifies Triple-DES/CBC with two parameters, MCRYPT_3DES (Triple-DES) and MCRYPT_MODE_CBC (CBC-mode), while openssl uses only one parameter, DES-EDE3-CBC.

There are several Keying-Options for Triple-DES. 3TDEA uses three independent DES keys and is specified in the context of openssl with DES-EDE3-CBC, which expects a 24 bytes key, 2TDEA uses two independent keys and can be specified in the context of openssl alternatively with DES-EDE-CBC, which expects a 16 bytes key.

Triple-DES is much slower than the modern AES, but has a comparable security. As with padding, you should switch to AES if possible.

PHP's mcrypt_encrypt and openssl (AES) output are different

It is most likely the padding. Notice that the first block is the same for each and the last block is different.

The text being encrypted is 28 bytes so the the last block will have 4 bytes of padding: 16-(28%16) = 4.

PHP mcrypt does not support standard PKCS#7 (née PKCS#5) padding, only non-standard null padding.

So PHP mcrypt will appoint 4-bytes of 0x00 and openssl 4-bytes of 0x04. See PKCS#7 padding.

So if you want to create the same encrypted output with openssl_encrypt you need too specify zero padding option (OPENSSL_ZERO_PADDING) and add the null padding yourself. Note: null padding is not robust because it can not correctly handle all binary data.

Example: openssl_encrypt($data, "aes-256-cbc", $encryption_key, OPENSSL_ZERO_PADDING, $iv);


mcrypt_encrypt():

Base64: Od2i8FHmWvMeXt+HwCy7k93koPVClK1erHsZwoB6sUE=
Hex: 39DDA2F051E65AF31E5EDF87C02CBB93 DDE4A0F54294AD5EAC7B19C2807AB141

openssl_encrypt:

Base64: Od2i8FHmWvMeXt+HwCy7kyCt0nvHTaO4IdjdiF15LAc=
Hex: 39DDA2F051E65AF31E5EDF87C02CBB93 20ADD27BC74DA3B821D8DD885D792C07

Migration from mcrypt_encrypt() to openssl_encrypt()

Updated

So what you are looking for is the des-ede3-cbc Openssl algorithm.

A convenient way to get a list of all your openssl algo's that are on your server is to run:

 print_r(openssl_get_cipher_methods(TRUE));

This will generate a list that makes for a good reference.

It looks like there was a padding issue as well. Mcrypt adds padding during the encryption routine and the Openssl does not. So you have to add padding on the encryption side for the Openssl. We also need to force the no_padding in the openssl functions.

These functions should work for you now.

function encryptNew($data, $secret){

//Generate a key from a hash
$key = md5(utf8_encode($secret), true);
$data = utf8_encode($data);
$iv = utf8_encode("jvz8bUAx");

//Take first 8 bytes of $key and append them to the end of $key.
$key .= substr($key, 0, 8); //You key size has to be 192 bit for 3DES.

$method = 'des-ede3-cbc'; //<----Change you method to this...

//Mcrypt adds padding inside the function. Openssl does not. So we have to pad the data.
if (strlen($data) % 8) {

$data = str_pad($data, strlen($data) + 8 - strlen($data) % 8, "\0");

}

$encrypted = openssl_encrypt($data, $method, $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv); //Force zero padding.

$encrypted = urlencode(base64_encode($encrypted)); //Added the urlencode.....

return $encrypted;

}

function decryptNew($data, $secret){

//$data = base64_decode(urldecode($data));//<--If you have raw data coming in this needs to be commented out.
$iv = utf8_encode("jvz8bUAx");
$key = md5(utf8_encode($secret), true);

// Take first 8 bytes of $key and append them to the end of $key.
$key .= substr($key, 0, 8);

$method = 'des-ede3-cbc';

return openssl_decrypt($data, $method, $key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING, $iv); //Force zero padding.

}

Hope this helps.

PHP mcrypt to openssl BF-CBC: how to get the same encrypted value

openssl_encrypt/decrypt uses PKCS7-padding by default, mcrypt_encrypt/decrypt uses Zero-Byte-padding.

The observed extra data after decryption are the padding-bytes of the PKCS7-padding: The current code uses PKCS7-padding for the openssl_encrypt-call (OPENSSL_ZERO_PADDING-flag not set). Since no padding is used for the openssl_decrypt-call (OPENSSL_ZERO_PADDING-flag set), the padding is still present after decryption. Note, the OPENSSL_ZERO_PADDING-flag disables the padding, it does not mean Zero-Byte-padding.

Although PKCS7-padding is generally the better choice compared to Zero-Byte-padding (because the latter is unreliable), in this case it makes more sense to use the padding of the old data, i.e. Zero-Byte-padding, with regard to the compatibility of the old data. Otherwise there would be data with different padding, which generally cannot be derived from the data, e.g. the padded final block 41 42 43 44 45 46 02 02 may have been created by PKCS7-padding or by Zero-Byte-padding:

before padding             after padding
41 42 43 44 45 46 __ __ -> 41 42 43 44 45 46 02 02 PKCS7-Padding
41 42 43 44 45 46 02 02 -> 41 42 43 44 45 46 02 02 Zero-Byte-Padding (variant of mcrypt_encrypt)

This would make the un-padding more complex. Using Zero-Byte-padding avoids this problem.

Since openssl_encrypt/decrypt does not support Zero-Byte-padding, it must be explicitly implemented. It makes sense to use the Zero-Byte-padding-variant of mcrypt_encrypt: If the plaintext is already divisible by the blocksize (8 Byte for Blowfish) no additional block of zero bytes is added. Otherwise, padding is done with zero bytes until the length of the plaintext corresponds to an integer multiple of the blocksize.

The Zero-Byte-padding must have taken place before the openssl_encrypt-call. In addition, the padding in the openssl_encrypt-call itself has to be disabled (set OPENSSL_ZERO_PADDING-flag, analogous to the openssl_decrypt-call).

PHP convert MCRYPT_ENCRYPT to OPENSSL_ENCRYPT (SOAP header)

You are missing some initializator vector data.

$ivsize = openssl_cipher_iv_length('AES-128-ECB');
$iv = openssl_random_pseudo_bytes($ivsize);

$ciphertext = openssl_encrypt(
$data,
'AES-128-ECB',
$key,
OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
$iv
);

How to use OPENSSL_ZERO_PADDING in php?

What do you know, I'm Sam too!

It looks like OPENSSL_NO_PADDING won't work if the input data is not a multiple of the blocksize. You can fix this by padding the plaintext yourself:

$cipher = 'AES-256-ECB';

$key = 'd61d2cd58d01b234e1800938erf8467k';

$plaintext = "The quick brown fox jumps over the lazy dog";

if (strlen($plaintext) % 8) {
$plaintext = str_pad($plaintext, strlen($plaintext) + 8 - strlen($plaintext) % 8, "\0");
}

$chiperRaw = openssl_encrypt($plaintext, $cipher, $key, OPENSSL_NO_PADDING);

$ciphertext = trim(base64_encode($chiperRaw));

echo($ciphertext);

This will get your what you're looking for, but I think the best option for you (if you're not absolutely required to pad the string), is to use the 5th parameter of openssl_encrypt and pass an IV like the following (note the switch back to OPENSSL_RAW_DATA):

$cipher = 'AES-256-ECB';

$key = 'd61d2cd58d01b234e1800938erf8467k';

$iv_size = openssl_cipher_iv_length( $cipher );

$iv = openssl_random_pseudo_bytes( $iv_size );

$plaintext = "The quick brown fox jumps over the lazy dog";

$chiperRaw = openssl_encrypt($plaintext, $cipher, $key, OPENSSL_RAW_DATA, $iv);

$ciphertext = trim(base64_encode($chiperRaw));

echo($ciphertext);

There's great summary of why you should use an iv here: What is an openssl iv, and why do I need a key and an iv?



Related Topics



Leave a reply



Submit