PHP : mcrypt & openssl producing different output - php

Hoping someone can shed some light on this.
I am updating some older code which uses the mcrypt_generic function, utilizing a DES-CBC cipher
When I update this code to use the openssl_encrypt, I get the same output, but with 8 bytes appended to the end of my encoded string.
Before
$this->_cipher = mcrypt_module_open(MCRYPT_DES, '', MCRYPT_MODE_CBC,'');
mcrypt_generic_init($this->_cipher, $this->_key, $this->_iv)
mcrypt_generic($this->_cipher, $text)
Before method output:
27049189e7e08db6
After
openssl_encrypt($text, "DES-CBC", $this->_key, OPENSSL_RAW_DATA , $this->_iv);
After method output:
27049189e7e08db6d504d16516e1c567
Why is this happening, and what (other then a substring) can be done to prevent it?

According to this:
1.3. Data Size
The DES algorithm operates on blocks of eight octets. This often requires padding after the end of the unencrypted payload data.
It's possible that it's simply padding the input.
mcrypt_encrypt() seems to pad by 0 by default (based on code example in docs):
# creates a cipher text compatible with AES (Rijndael block size = 128)
# to keep the text confidential
# only suitable for encoded input that never ends with value 00h
# (because of default zero padding)
$ciphertext = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key,
$plaintext, MCRYPT_MODE_CBC, $iv);
which would explain the difference in length.

So I manage to find a solution
openssl_encrypt($text, "DES-CBC", $this->_key, OPENSSL_NO_PADDING, $this->_iv);
This mode,
OPENSSL_NO_PADDING
seems to produce the output I was expecting

Related

Encrypting SOAP envelope using AES

I am working on a project that requires the AES encryption of a soap envelope using the requirements below.
Encryption key: myKey-1234567abcdef
AES-256 encryption
128 block size
PKCS7 padding
16 bit vector (vector is attached before encrypted message)
Cipher Block Chaining (CBC)
This is what I tried:
$key = 'myKey-1234567abcdef';
$encryptionMethod = "AES-256-CBC";
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC);
$iv = substr(mcrypt_create_iv($iv_size, MCRYPT_RAND), 0, 16);
$xml = openssl_encrypt($xml,$encryptionMethod, $key, 0, $iv);
I am currently getting a 400 bad request error and there seems to be lots of options for argument constants mcrypt functions, was wondering if my implementation satisfies the needs for padding, block size and vector?
I appreciate any suggestions, thanks in advance!
First: Your key isn't an appropriate length for AES-256. I realize the key you're using here is an example, but make sure that the one you've been provided is 32 characters (256 bits) long. If it isn't, ask the recipient for clarification.
Second: You're mixing the mcrypt and openssl extensions inappropriately here. You shouldn't be using mcrypt anyway, as it's unmaintained, sometimes broken, and will be removed entirely in PHP 7.2. Instead, hard-code the IV size as 16 and use openssl_random_pseudo_bytes to generate it:
$iv = openssl_random_pseudo_bytes(16);
$xml = openssl_encrypt($xml, $encryptionMethod, $key, 0, $iv);
Third: By default, openssl_encrypt() encodes its output as Base64. The recipient may not be expecting this. Pass OPENSSL_RAW_DATA as the fourth argument to openssl_encrypt() (replacing 0) to get unencoded output.

Why does mcrypt encryption includes a lot of null chars in the final string?

My question is why simply decrypting the encrypted string using PHP's mcrypt functions causes to have a lot of null (\0) chars the final result? and how to get the actual first string with the exact same length?
Example code to test
// Mcrypt Encrypt
function en_mcrypt($string)
{
return bin2hex(mcrypt_encrypt(MCRYPT_BLOWFISH, 'Y6r6gYNR5xG6Ou55q2Vf83G31t4KG24j', $string, 'ecb'));
}
// Mcrypt Decrypt
function de_mcrypt($string)
{
return mcrypt_decrypt(MCRYPT_BLOWFISH, 'Y6r6gYNR5xG6Ou55q2Vf83G31t4KG24j', hex2bin($string), 'ecb');
}
$string = 'test-1234';
$encrypted = en_mcrypt($string);
$decrypted = de_mcrypt($encrypted);
echo "{$string} (".strlen($string).")<br>";
echo "{$encrypted} (".strlen($encrypted).")<br>";
echo "{$decrypted} (".strlen($decrypted).")<br>";
PHP Output:
test-1234 (9)
e9fc266f9a2f275ca3f4435c53c662a1 (32)
test-1234 (16)
P.S. I'm aware of EBC encryption security concerns, so lets skip that part here.
Block ciphers require the input to be an exact multiple of the block size, 8-bytes for Blowfish. If padding is not supplied it is likely that bytes following the data to be encrypted will be used as pad and they may well be 0's.
"test-1234" is 9-bytes so somehow 7-bytes of padding must be added.
The usual solution is to specify a padding option to the encryption and decryption functions and they will silently add/remove the padding. Note that mcrypt only supports non-standard 0 padding, adding confusion.

Decrypt mcrypt with openssl

Since mcrypt is considered obsolete, my task is upgrading the current code to use openssl. Sounds simple, but ... after a few days of try and failure I feel like going insane.
My question to you is: Is there any way you can decrypt with openssl data previously encrypted with mcrypt? I've read so many posts on this matter and most of them say that a previous manual padding of the data was/is necessary before running mcrypt on it.
The issue is that the mcrypt-ed data is already encrypted (with the automatic null padding mcrypt provides) and resides in a database, so modification of that is not possible and/or desired.
Mentions:
the algorithm used is rijndael-128 cbc with a 32-byte key (so I'm using aes-256-cbc for openssl).
I'm using an openssl wrapper for php (php-crypto).
I've managed to make the inverse operation work (decode openssl with mcrypt) by simply stripping the end decoded characters if they were non alpha-numerical.
Manually padding the data before mcrypt-ing and then decrypting it using openssl works like a charm, but that's not the problem here.
Some code snippets:
// Simple mcrypt encrypt, decrypt with php-crypto example
// This doesn't work and produces a "Finalizing of cipher failed" error
$data = "This is a text";
$strMcryptData=mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, MCRYPT_MODE_CBC, $iv);
$algorithm = 'aes-256-cbc';
$cipher = new Cipher($algorithm);
$sim_text = $cipher->decrypt($strMcryptData, $key, $iv);
// Simple mcrypt encrypt with padding, decrypt with php-crypto
// Works and produces the correct text on decryption
$pad = $blocksize - (strlen($data) % $blocksize);
$text = $data;
$text .= str_repeat(chr($pad), $pad);
$strPaddedData=mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $text, MCRYPT_MODE_CBC, $iv);
$sim_text = $cipher->decrypt($strPaddedData, $key, $iv);
Slightly old, but you can solve this with a bit of work. You can tell PHP's OpenSSL that the encrypted string is not padded, and tell it to give you the raw output (So you don't have to base64 decode it, either). You can then strip nulls from the end of the resulting string if the length of the string happens to be perfectly divisible by the IV (This is a sanity check, as if the resulting string isn't divisible by the IV then it wasn't padded at all).
Be aware, this code has two major limitations:
If, at any point, you encrypted a legitimate string that ended in two or more NULL bytes then this code will not give you the same output.
If the padding of the string needed only one null byte, then this code won't strip it.
You can solve both of these if you know for a FACT that you didn't encrypt anything that ends in null bytes, you can alter the code that strips the nulls to just do a preg_replace; just make sure you anchor the regex to the end of the string so it only strips from the end.
<?php
$message = 'test';
$key = openssl_random_pseudo_bytes(16);
$iv = openssl_random_pseudo_bytes(16);
$cipher = mcrypt_encrypt(
MCRYPT_RIJNDAEL_128,
$key,
$message,
MCRYPT_MODE_CBC,
$iv
);
$plain = openssl_decrypt(
$cipher,
'aes-128-cbc',
$key,
OPENSSL_RAW_DATA | OPENSSL_NO_PADDING,
$iv
);
//try to detect null padding
if (mb_strlen($iv, '8bit') % mb_strlen($plain, '8bit') == 0) {
preg_match_all('#([\0]+)$#', $plain, $matches);
if (mb_strlen($matches[1][0], '8bit') > 1) {
$plain = rtrim($plain, "\0");
trigger_error('Detected and stripped null padding. Please double-check results!');
}
}
var_dump(
$message,
bin2hex($cipher),
$plain,
mb_strlen($message, '8bit'),
mb_strlen($plain, '8bit'),
$message === $plain
);
http://3v4l.org/kYAXn
Obviously this code comes with no major disclaimer and please test it in your use case, but someone might hopefully find this useful.
If you encrypt in mcrypt without adding PKCS7 manually, mcrypt will happily pad your plaintext with NUL bytes.
OpenSSL will do PKCS7 padding for you whenever using aes-X-cbc. The unfortunate consequence of this is that if you have AES-CBC(NULL_PADDED(plaintext)) and try to decrypt it, openssl_decrypt will attempt to remove the padding and fail.
Compare http://3v4l.org/bdQe9 vs http://3v4l.org/jr68f and http://3v4l.org/K6ZEU
The OpenSSL extension does not currently offer you a way to say "This string is not padded, please don't strip the padding for me" and then remove the NUL bytes on your own. You must encrypt with PKCS7 padding in order for decryption to succeed.
Although this is a limitation of OpenSSL, it bears emphasizing that the only reason you're running into it is because mcrypt is terrible.
There shouldn't be any major differences except for the padding. You should be able to call EVP_CIPHER_CTX_set_padding if you use the higher level OpenSSL (EVP) constructs directly. I presume that the padding argument should be zero, although it is not documented. You need a preconfigured encryption/decryption context for this.
Afterwards you will have your plaintext of the same length as the ciphertext. Zero to fifteen bytes at the end will be set to zero. You need to remove these bytes manually. If the plaintext happens to end with zero bytes then those will also be removed; that's however never the case if the plaintext is a printable string (that uses 8 bit encoding). You may want to ensure that you don't remove more than 15 bytes.
If you get completely random plaintext then your key or ciphertext is incorrect. If you get readable plaintext but for the first 16 bytes then your IV handling is incorrect.

32-character PHP AES Key for mcrypt_encrypt

Consider the following PHP code:
<?php
$key = "1234567812345678";
$iv = "1234567812345678";
$data = "Test string";
$encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128,
$key,
$data,
MCRYPT_MODE_CBC,
$iv);
print "Encoded1: " . base64_encode($encrypted) . "\n";
$key = "12345678123456781234567812345678";
$encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128,
$key,
$data,
MCRYPT_MODE_CBC,
$iv);
print "Encoded2: " . base64_encode($encrypted) . "\n";
When run, this produces the output:
Encoded1: iz1qFlQJfs6Ycp+gcc2z4w==
Encoded2: n3D26h/m8CSH0CE+z6okkw==
Note that I stole the first bit of code from PHP Java AES CBC Encryption Different Results
Now - here's the question:
In the first case, the key that was passed in was a string of 16 characters. If each of the individual characters was interpreted as an 8-bit quantity, this gives the 128-bit key size that one would expect. Indeed, the Java code that's on the StackOverflow page that I referenced above does exactly that, and obtains the same result as the PHP.
In the second call to mcrypt_encrypt above, I have doubled the length of the key. mcrypt_encrypt accepts this happily, but produces a different encrypted output than in the first case. Clearly, therefore, it considers this a different key - it does not, for example take only the first 128 bits and discard any past that.
So, how does mcrypt_encrypt process the input key string to come up with the 128-bit key that the MCRYPT_RIJNDAEL_128 algorithm requires?
If it makes any difference, the case I'm specifically interested in is when a 32-character string is passed in like my second example - I have to create a matching decryption routine (in Java), so I need to figure out how the key is actually generated in this case. The page I cited has perfectly-good Java code (which works with all my test cases) - I'm just missing the proper set of key bytes.
There are two important parameters for the Rijndael algorithm. There is the key size (128-bit, 192-bit and 256-bit) and then there is the block size (128-bit, 192-bit and 256-bit). The 128 in MCRYPT_RIJNDAEL_128 refers to the block size. The key size is variable.
When you pass keys of different lengths into MCrypt, it will select the appropriate key size automatically, so you don't and can't set it. MCRYPT_RIJNDAEL_128 is AES (AES-128, AES-192, AES-256). MCRYPT_RIJNDAEL_192 and MCRYPT_RIJNDAEL_256 are not AES anymore.
If the Java code produced a matching result for the 128-bit key, then it will produce a matching result for the 256-bit key as well.
MCrypt is a little strange. Before PHP version 5.6.0 it would take any key length and not just 128-bit, 192-bit or 256-bit. The key would be filled up with 0x00 bytes up to the next valid key length.
Since Java doesn't support ZeroPadding out of the box, you should use a proper padding scheme such as PKCS#5/PKCS#7 padding in PHP. This answer has a very good implementation of it.

PHP: mcrypt mangles beginning of string to garbage

I need medium to strong encryption on serverside, so I thought I would use mcrypt with PHP. If I use the functions below the beginning of my original string turns into binary garbage after decryption. (This is not the usual problem of getting appended additional garbage, instead my string is altered.) According to the documentation, mcrypt_encrypt() should have padded enough characters to match the block size of the selected algorithm but I suspect it does not work.
However, if I pad it manually to the block size of 128 bit (16 bytes) of Rijndael, it doesn't work either. The only way I can get this to work is by prepending some string long enough to (likely) cover the garbaged block and add a known prefix like "DATA#" between that string and my data. After decryption that block has been partially mangled but my prefix and all data after that has been correctly decrypted.
$GLOBALS['encryptionmarker'] = 'DATA#';
function encrypt($plain, $key) {
/*
// workaround because beginning of decrypted string is being mangled
// so we simply prefix with some text plus marker
$prefix = str_pad('', 128, '#', STR_PAD_RIGHT).$GLOBALS['encryptionmarker'];
$plain = $prefix.$plain;
*/
$encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $plain, MCRYPT_MODE_CFB,
mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CFB),
MCRYPT_DEV_URANDOM));
return $encrypted;
}
function decrypt($encrypted, $key) {
$decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $encrypted, MCRYPT_MODE_CFB,
mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CFB),
MCRYPT_DEV_URANDOM));
/*
// workaround: remove garbage
$pos = strpos($decrypted, $GLOBALS['encryptionmarker']);
$decrypted = trim(substr($decrypted, $pos + strlen($GLOBALS['encryptionmarker'])));
*/
return $decrypted;
}
What's wrong with my functions? Why do I have to prefix my data like that (I consider it a dirty workaround, so I would like to fix it)?
Storing the encrypted data is not the problem; decrypting it immediately after encryption without storing it to a database results in the same errors.
Your problem is that you are generating a new, different, random IV on the receiving side. This doesn't work, as you've seen.
The receiver needs to know the IV that the sender used; so you have to send it along with the encrypted data and pass it to mcrypt_decrypt().
Note that you must also use mhash() with a key (a different key to the encryption key) to generate an HMAC over the message, and check it on the receiving side. If you do not, a man-in-the-middle can trivially modify parts of your message without you detecting it.
Use the same IV in en- and decryption. The IV is not a shared secret, but has to be shared. You may consult Wikipedia: IV
$IV = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CFB),
MCRYPT_DEV_URANDOM));
The IV must be transferred ONCE. You may want to increment the value of IV for each packet. But this can be done on both sides independently.

Categories