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..
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 have written encryption functionality in Perl and I am trying to get to work same way in PHP.
In PERL:
#!/usr/bin/perl
use strict;
use warnings;
use Crypt::CBC;
use Crypt::Rijndael;
my $cryptkey = '_PRIVATE_';
my $cipher = Crypt::CBC->new( -key => $cryptkey,
-salt => 1,
-cipher => 'Rijndael',
);
my $data = "hello";
my $ciphertext = $cipher->encrypt_hex($data);
print "HEX_KEY: '$ciphertext' \n";
Output:
HEX_KEY: '53616c7465645f5fc36630f5364619c31ac26e44809c81bf84ae995c22be45ce'
I am trying to get to work in PHP and output same HEX but it is not the same, what went wrong?
class Test {
public function Encypt($data, $cryptkey) {
$encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $cryptkey, $data, MCRYPT_MODE_CBC);
return bin2hex($encrypted);
}
}
$data = "hello";
$test = new Test();
$cryptkey = "_PRIVATE_";
$hex = $test->Encypt($data, $cryptkey);
echo $hex;
Output
2bab1b8874692176d213e4c23565b304
Crypt::CBC and mcrypt_encrypt use different defaults, which lead to this mismatch.
For mcrypt_encrypt, the documentation offers this information:
string mcrypt_encrypt ( string $cipher , string $key , string $data , string $mode [, string $iv ] )
$cipher is the algorithm name.
$key is the encryption key, if it is too short it will be padded with NUL bytes.
$data is the data to be encrypted, if it is too short it will be padded with NUL bytes.
$mode is the mode of operation, here "cbc" is correct.
$iv is the initialization vector, if not provided it will be initialized as NUL bytes.
For Crypt::CBC, we get this behaviour instead:
Crypt::CBC->new(KEYWORD_OPTIONS)
-key => STRING is the passphrase from which the encryption key is generated by some hashing operations.
-literal_key => BOOL If set, this skips the hashing for the -key and uses it as a literal key.
-cipher => CIPHER the name of a cipher, or a pre-initialized cipher object.
-salt => 1 | STRING If set to "1", this will produce a random salt. Any other values are taken as the literal salt. This defaults to -salt => 1 if a salt is needed. Or something like that, the docs are a bit confusing here. The salt is not needed if both the -iv and literal_key options are set.
-iv => STRING is the initialization vector, which is usually generated from the salt.
-header => STRING controls what kind of header is prepended to the output. This defaults to "salt", but can also be set to "none".
Note further that RIJNDAEL_128 implies a keylength of 16, whereas Crypt::CBC assumes a keylength of 32.
Using Crypt::Rijndael without the Crypt::CBC wrapper is probably preferable, because this allows us to easily set the required options to the same defaults which PHP uses:
use Crypt::Rijndael;
my $key = "_PRIVATE_";
my $data = "hello";
# pad the $key to 16 bytes
$key .= "\0" x (16 - length $key);
# pad the $data to a multiple of 16 bytes:
if (my $distance = length($data) % 16) {
$data .= "\0" x (16 - $distance);
}
my $crypt = Crypt::Rijndael->new($key, Crypt::Rijndael::MODE_CBC);
$crypt->set_iv("\0" x 16);
my $binary = $crypt->encrypt($data);
print unpack("H*", $binary), "\n";
Which then outputs 2bab1b8874692176d213e4c23565b304.
I'm working on a cross language project wrapping a ruby/Sinatra API in PHP to be consumed by another team. None of the information exposed by the API is sensitive, but we would prefer it not be easily accessible to a casual observer guessing the URL.
private function generateSliceIDToken($key){
$currentEpoch = time();
$ivSize = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
$iv = mcrypt_create_iv($ivSize, MCRYPT_RAND);
$encryptedBytes = mcrypt_encrypt(
MCRYPT_RIJNDAEL_128,
$key,
$currentEpoch.**Passcode**,
MCRYPT_MODE_CBC, $iv
);
$ivAndEncryptedBytes = $iv . $encryptedBytes;
return urlencode(urlencode(base64_encode($ivAndEncryptedBytes)));
The code above Encrypts a password and time stamp using mcrypt's RIJNDAEL implementation and encodes it to send off to the ruby API
if identifier.validate_token Base64.decode64(URI.unescape( URI.unescape(params[:token])))
Sinatra grabs it and decodes it
def validate_token(token)
cipher = OpenSSL::Cipher::AES.new(128, 'CBC')
cipher.decrypt
cipher.key = **key**
cipher.iv = token[0,16]
plain = cipher.update(token[16..-1]) + cipher.final
return plain[10,8] == **Passcode**
end
and passes it along to be decrypted
The problem is, the decryption fails with a 'Bad Decrypt' Error
I was lead to believe Mcrypt's RIJNDAEL and Cipher's AES were compatible, but is this assumption incorrect? Any help I can get one this would be most helpful.
I was lead to believe Mcrypt's RIJNDAEL and Cipher's AES were compatible, but is this assumption incorrect?
You need to slightly tweak data being encoded to make it AES compatible. Data must be right padded, with character and amount depending of its current width:
$encode = $currentEpoch.'**Passcode**';
$len = strlen($encode);
$pad = 16 - ($len % 16);
$encode .= str_repeat(chr($pad), $pad);
Also remember to have $key exactly 16 characters long. If it is shorter, ruby throws CipherError, while php pads key with null bytes. If it is longer, ruby uses only first 16 character but php pads it again, and uses last 16 characters.