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.
Related
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
I have a string encrypted with AES 128 CBC, which I need to decrypt. I have the key which seems to work fine. The problem is with the initialization vector (IV).
The IV is 16 bytes long,
B409678003171307B8B8B8B8B8B8B8B8
but when I add it to my script, OpenSSL truncates it saying it's 32 long like so:
openssl_decrypt(): IV passed is 32 bytes long which is longer than the 16 expected by selected cipher, truncating
I guess it means it is 32 characters long - but how do I make it understand it's just 16 bytes?
UPDATE: using hex2bin on the IV solved the truncating - but my openssl_decrypt yields nothing. Also did the hex2bin on the key, still no output. Simplified the code to make it easier to find the problem:
<?php
$str = "7F53B967F1BF7C9EC26B0C405E453ABD";
$k = "F71D4590A6E6E219EBBE8BFE9D3DC21A";
$intv = "B409678003171307B8B8B8B8B8B8B8B8";
$key = hex2bin($k);
$iv = hex2bin($intv);
$plaintext = openssl_decrypt($str, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
print_r($plaintext);
?>
So, is the hex2bin the wrong way to go? Or is there something wrong in how I use the openssl_decrypt? There are NO errors in the PHP error_log.
Thanks in advance!
OK this appears to achieve the same results as the web-based service linked by the OP. The key steps are
a) in addition to $k and $intv, make sure you also convert the encrypted $str to binary from its hex representation
b) supply the extra flag OPENSSL_ZERO_PADDING
c) when you echo or var_dump or print_r the output, make sure you do a conversion back to hex so the output is readable
$encrypted = "7F53B967F1BF7C9EC26B0C405E453ABD";
$k = "F71D4590A6E6E219EBBE8BFE9D3DC21A";
$intv = "B409678003171307B8B8B8B8B8B8B8B8";
$str = hex2bin($encrypted);
$key = hex2bin($k);
$iv = hex2bin($intv);
$decrypted = openssl_decrypt($str, 'AES-128-CBC', $key, OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDING, $iv);
$str_decrypted = bin2hex($decrypted);
var_dump($str_decrypted);
output:
string(32) "2f2f0c1335000000046d372b27230f15"
NOTE: I can't be sure that this is in fact the decrypted form of the originally encrypted data. It just matches the web-based service. I'm assuming the value you linked is in fact the correct value. Simply adding the OPENSSL_ZERO_PADDING flag to your original code can get rid of the errors but the output will be different. Maybe try some experimenting.
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.
I was recently asked to create a method for encryption / decryption of a URL string. I quickly produced a 1 liner and shot it out.
I was then provided with the code from another developer and asked my opinion. I looked at his to find a much more complex function.
My questions:
What are the specific differences here?
Are there shortfalls found in the short solution?
We are encrypting a json encoded array and passing it via query string URL.
Long solution:
public function Encrypt($message, $key = 'defaultkey') {
//Create an instance of the mcrypt resource
$td = mcrypt_module_open('tripledes', '', 'ecb', '');
//Create a random intialization vector and initialize
$iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($td), MCRYPT_RAND);
mcrypt_generic_init($td, $key, $iv);
// Create a Timestamp and add it.
$T = new \DateTime('NOW');
$message = $T->format("YmdHis") . $message;
// PKCS7 Padding
//get the block size of the cipher
$b = mcrypt_get_block_size('tripledes', 'ecb');
//What is the purpose?
$dataPad = $b-(strlen($message)%$b);
$message .= str_repeat(chr($dataPad), $dataPad);
//convert to hexidec string
$encrypted_data = bin2hex(mcrypt_generic($td, $message));
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
return $encrypted_data;
}
Short Solution:
public function Encrypt($message, $key = 'defaultkey') {
$T = new \DateTime('NOW');
return bin2hex(mcrypt_encrypt(MCRYPT_3DES, $key, $T->format("YmdHis").$message, 'ecb'));
}
The only real difference is the padding. Triple DES is a symmetric block cipher and such only operates on a single full block (8 byte). A mode of operation like ECB enables it to encrypt many full blocks. When your data is not a multiple of the block size, it has to be padded for it to be encrypted.
MCrypt uses zero padding by default. It will fill up the plaintext with 0x00 bytes until a multiple of the block size is reached. Those additional padding bytes have to be removed during decryption (usually done with rtrim()). This means that if the plaintext ends with 0x00 bytes, those will also be removed which might break your plaintext.
PKCS#5/PKCS#7 padding on the other hand pads with a byte that represents the number of padding bytes. If the plaintext is already a multiple of the block size, it will add a full block of padding. Doing it this way enables it to only remove the padding and not additional plaintext bytes during decryption.
Whether mcrypt_generic_init() or mcrypt_encrypt() was used doesn't really make a difference.
You should never use ECB mode. It is not semantically secure. It means that the same plaintext block will always result in the same ciphertext block. Since you're encrypting URLs the first couple of blocks will stay the same for similar URLs after observing many ciphertexts. An attacker might get additional information out of this.
Use at least CBC mode with a random IV. The IV doesn't need to be hidden, so it can be easily prepended to the ciphertext and sliced off during decryption.
It is also best to have the ciphertext authenticated to detect manipulation. You can use a message authentication code such as HMAC-SHA256 with a different key. A better way would be to simply use an authenticated mode such as GCM or EAX.
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.