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.
Related
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.
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
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.
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..
I am trying to encrypt a string using openssl_encrypt in PHP but it keeps returning FALSE.
$encrypted = openssl_encrypt('1234', 'AES-256-CBC', 'kGJeGF2hEQ', OPENSSL_ZERO_PADDING, '1234123412341234');
What am I doing wrong?
On top of answers posted, which are excellent, the code you're after, given your input parameters would be the following:
$plaintext = '1234';
$cipher = 'AES-256-CBC';
$key = 'this is a bad key';
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher));
$encrypted = openssl_encrypt($plaintext, $cipher, $key, 0, $iv);
if(false === $encrypted)
{
echo openssl_error_string();
die;
}
$decrypted = openssl_decrypt($encrypted, $cipher, $key, 0, $iv);
$result = $decrypted === $plaintext;
print $result ? 'Everything is fine' : 'Well, we did not decrypt good, did we?';
Having written the above, I advise against using it and instead, please use a tested library designed to handle the complexities of encryption and decryption for you.
I suggest using defuse/php-encryption
php > var_dump (openssl_encrypt('1234', 'AES-256-CBC', 'kGJeGF2hEQ', OPENSSL_ZERO_PADDING, '1234123412341234'));
php shell code:1:
bool(false)
php > var_dump (openssl_error_string ());
php shell code:1:
string(94) "error:0607F08A:digital envelope routines:EVP_EncryptFinal_ex:data not multiple of block length"
It seems that the cypher you're using requires that the data you're encrypting has a length that's an exact multiple of the block length. With some experimentation I found that 1234123412341234 is successfully encrypted.
I don't know if this is a universal feature of all openssl encryption schemes, or whether it's something that's specific to certain schemes. In the former case you'll need to pad the input to a multiple of the block size. If the latter is true then you can either pad, or switch to a different encryption scheme that doesn't impose the same restrictions on the input.
For padding you need to find out what the blocksize of your chosen cypher is (I don't know if there's an openssl function or constant provided for that), then work out how many characters you need to pad your input string by.
Note that the following example assumes that a) there's some way of getting the blocksize programmatically (if not then you'll have to hard-code that yourself) and b) you're working with a byte-oriented character format (unicode might cause issues!)
$plaintext = "Hello, I'm some plaintext!";
$blocksize = function_that_gets_a_blocksize_for_a_given_cypher ($cypher);
$strlen = strlen ($plaintext);
$pad = $blocksize - ($strlen % $blocksize);
// If the string length is already a multiple of the blocksize then we don't need to do anything
if ($pad === $blocksize) {
$pad = 0;
}
$plaintext = str_pad ($plaintext, $strlen + $pad);
As for your code, this suggests you need to implement some error detection into it (but be careful what you actually log/echo out as part of the error detection!).
$encrypted = openssl_encrypt('1234', 'AES-256-CBC', 'kGJeGF2hEQ', OPENSSL_ZERO_PADDING, '1234123412341234');
if (false === $encrypted) {
error_log ("Encryption failed! " . openssl_error_string ());
}
Since block ciphers such as AES require input data to be an exact multiple of the block size (16-bytes for AES) padding is necessary. The usual method is just to specify PKCS#7 (née PKCS#5) by passing it as an option and the padding will be automatically added on encryption and removed on decryption. Zero padding (OPENSSL_ZERO_PADDING) is a poor solution since it will not work for binary data.
The IV needs to be block size, 8-bytes for AES. Do not rely on the implementation for padding.
The key should be the exact size specified, valid block sizes foe AES are 128, 192 or 256 bits (16, 24 or 32 bytes). Do not rely on the implementation for padding.
Before start fixing this bug, check all extension which is required for openssl_encrypt/decrypt is enabled?
class AnswerEncryption
{
const CURRENT_ALGO = 'AES-128-ECB';
const CIPHER='A?N#G+KbPe778mYq3t6w9z$C&F!J#jcQ';
CONST IV='1234567890123455';
/**
* #param null $Value
* #param null $cipher
* #return false|string
*/
public static function Encrypt($Value=null){
$iv = substr(self::IV, 0, 16);
return (openssl_encrypt($Value,self::CURRENT_ALGO,self::CIPHER,0,$iv));
}
/**
* #param null $Value
* #return int
*/
public static function Decrypt($Value=null): int
{
$iv = substr(self::IV, 0, 16);
return intval(openssl_decrypt($Value,self::CURRENT_ALGO,self::CIPHER,0,$iv));
}
}
in the decrypt method, I want the integer value, so you can change it accordingly