From time to time I come around the task of creating functions for encrypting/decrypting strings and files in PHP.
I decided to finally nail those functions and did some searching but I couldn't find enough resources to confirm the security of these functions.
Please note that I don't want to use another full-blown library unless necessary and I don't see why PHP provides OpenSSL & mcrypt functions but nobody really implements them.
I was able to find these functions but they are not commented and some steps were unclear (also they do not generate a key but use a predefined one).
Following these functions I also found this stackoverflow question but the first answer uses another library while the second one uses ECB.
edit: I updated the code sample which previously utilized mcrypt to using only OpenSSL as suggested in the comments:
function generate_key($cipher = 'AES-256-CBC')
{
return base64_encode(openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher))); // Generate a random key - currently using the function for the vector length
}
function encrypt($data, $key, $cipher = 'AES-256-CBC')
{
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher)); // Generate a random initialization vector
return base64_encode($iv) . '$' . openssl_encrypt($data, $cipher, base64_decode($key), false, $iv); // Return a base64 encoded string containing the iv and the encrypted data
}
function decrypt($data, $key, $cipher = 'AES-256-CBC')
{
$data = explode('$', $data); // Explode the previously encoded string
if(count($data) == 2)
return openssl_decrypt($data[1], $cipher, base64_decode($key), false, base64_decode($data[0])); // Decrypt the data given key and the iv
else
return false;
}
I tested encryption and decryption using these function like this:
$input = 'Hello world!';
echo 'Original data: ' . $input . '<br /><br />';
$key = generate_key();
$encrypted = encrypt($input, $key);
echo 'Key used for encryption: ' . $key . '<br />';
echo 'Encrypted data: ' . $encrypted . '<br /><br />';
$decrypted = decrypt($encrypted, $key);
echo 'Decrypted data: ' . $decrypted . '<br />';
The question: Is OpenSSL properly implemented as shown above? Can they be used for files too?
These are the old functions using mcrypt. Don't use them anymore.
function generate_key($cipher = MCRYPT_RIJNDAEL_256)
{
return bin2hex(openssl_random_pseudo_bytes(mcrypt_get_key_size($cipher, MCRYPT_MODE_CBC))); // Generate a random key using OpenSSL with size given from mcrypt depending on cipher
}
function encrypt($data, $key, $cipher = MCRYPT_RIJNDAEL_256)
{
$iv = mcrypt_create_iv(mcrypt_get_iv_size($cipher, MCRYPT_MODE_CBC)); // Generate random initialization vector with size given from mcrypt depending on cipher
return bin2hex($iv) . '$' . bin2hex(mcrypt_encrypt($cipher, pack('H*', $key), $data, MCRYPT_MODE_CBC, $iv)); // Return the initialization vector and encrypted data as ASCII string
}
function decrypt($data, $key, $cipher = MCRYPT_RIJNDAEL_256)
{
$data = explode('$', $data); // Split the input data by $ to retrieve the initialization vector and the encrypted data
if(count($data) == 2) // Check if there are 2 parts after splitting by $
return mcrypt_decrypt($cipher, pack('H*', $key), pack('H*', $data[1]), MCRYPT_MODE_CBC, pack('H*', $data[0])); // Return the decrypted string
else
return false; // Return false if the given data was not properly formatted (no $)
}
Current best practise is to avoid using mcrypt in favor of openssl.
And the speed benchmark, where openssl is pretty much faster.
Related
We use the Sage Pay / Opayo form integration to pass customer orders to Sage Pay for payment, then to process the report that comes back.
Sage Pay's example code relies on the PHP mcrypt functions which are no longer supported from PHP 7.2, so we need to update the scripts to OpenSSL.
Encryption was covered by a great post here:
Upgrade mcrypt to OpenSSL encryption in SagePay form
this works fine for me:
function encryptAes_new ($string, $key) {
$key = str_pad($key,16,"\0"); # if supplied key is, or may be, less than 16 bytes
$crypt = openssl_encrypt($string, 'aes-128-cbc', $key, OPENSSL_RAW_DATA, $key);
// Perform hex encoding and return.
return "#" . strtoupper(bin2hex($crypt));
}
...but I can't find a matching code to fix the decrypt function.
The current code is:
function decryptAes($strIn, $password)
{
// HEX decoding then AES decryption, CBC blocking with PKCS5 padding.
// Use initialization vector (IV) set from $str_encryption_password.
$strInitVector = $password;
// Remove the first char which is # to flag this is AES encrypted and HEX decoding.
$hex = substr($strIn, 1);
$strIn = pack('H*', $hex);
// Perform decryption with PHP's MCRYPT module.
$string = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $password, $strIn, MCRYPT_MODE_CBC, $strInitVector);
return removePKCS5Padding($string);
}
Could anyone help with an OpenSSL version, please?
When decrypting, the # prefix must first be removed, then hex decoding must be done, and finally decryption can be performed. openssl_decrypt() implicitly removes the padding, so that no explicit removal is required. This is implemented in decryptAes_new() below:
<?php
function encryptAes_new ($string, $key) {
$key = str_pad($key,16,"\0"); # if supplied key is, or may be, less than 16 bytes
$crypt = openssl_encrypt($string, 'aes-128-cbc', $key, OPENSSL_RAW_DATA, $key);
// Perform hex encoding and return.
return "#" . strtoupper(bin2hex($crypt));
}
function decryptAes_new($string, $key) {
$key = str_pad($key,16,"\0");
$binary = hex2bin(substr($string, 1));
return openssl_decrypt($binary, 'aes-128-cbc', $key, OPENSSL_RAW_DATA, $key);
}
$plaintext = 'The quick brown fox jumps over the lazy dog';
$key = '0123456789012345';
$ciphertext = encryptAes_new($plaintext, $key);
$decrypted = decryptAes_new($ciphertext, $key);
print('Ciphertext: ' . $ciphertext . PHP_EOL);
print('Plaintext: ' . $decrypted . ' - Size: ' . strlen($decrypted) . PHP_EOL);
?>
which produces the output:
Ciphertext: #3089E6BC224BD95B85CF56F4B967118AAA4705430F25B6B4D953188AD15DD78F3867577E7D58E18C9CB340647C8B4FD8
Plaintext: The quick brown fox jumps over the lazy dog - Size: 43
Checking the length of the plaintext after decryption, 43 bytes, shows that the padding has been automatically removed.
Thank you, Mike, for asking the question and user16205441 for your answer.
I found the answer was applicable to my problem.
I have been trying for weeks to do the following:
the string must be encrypted using AES (block size 128-bit) in CBC mode with PKCS#5 padding. Use the provided password as both the key and initialisation vector and encode the result in hex (making sure the letters are in upper case).
When I operated the following code:
<?php
function encryptAes_new ($string, $key) {
$key = str_pad($key,16,"\0"); # if supplied key is, or may be, less than 16 bytes
$crypt = openssl_encrypt($string, 'aes-128-cbc', $key, OPENSSL_RAW_DATA, $key);
// Perform hex encoding and return.
return "#" . strtoupper(bin2hex($crypt));
}
function decryptAes_new($string, $key) {
$key = str_pad($key,16,"\0");
$binary = hex2bin(substr($string, 1));
return openssl_decrypt($binary, 'aes-128-cbc', $key, OPENSSL_RAW_DATA, $key);
}
$plaintext = 'theLongStringIwasTryingToSend';
$key = 'abcdefghijklmnop';
$ciphertext = encryptAes_new($plaintext, $key);
$decrypted = decryptAes_new($ciphertext, $key);
print('Ciphertext: ' . $ciphertext . PHP_EOL);
print('Plaintext: ' . $decrypted . ' - Size: ' . strlen($decrypted) . PHP_EOL);
?>
It produced the encrypted version the Opayo team were saying my string should have produced.
I am a little surprised as the Opayo team mention the initialisation vector ($iv) which user16205441's answer doesn't mention explicitly. But if it works, that's all that matters to me.
I shall now try the form integration process again and see what the team says!
All the best
NeverSayDai
I'm working on a small program that has to encrypt a basic sentence and echo the result. Then I want to be able to copy paste the result and be able to decode the text in the same way I encoded it. It doesn't need to be too safe, so therefore I opted to use Mcrypt.
When I try to decrypt, it gives weird (ASCII?) letters and I don't understand where they come from. Tried to solve it with a base64 encode/decode, but that didn't help either. What do I need to change to get it to work properly?
<?php
// Define Mcrypt variables
$enc = MCRYPT_RIJNDAEL_128;
$mode = MCRYPT_MODE_CBC;
$key = 'SanderDitIsNodigVoor16bi';
$iv = mcrypt_create_iv(mcrypt_get_iv_size($enc, $mode), MCRYPT_DEV_URANDOM);
// Check if 'submit' is set
if (isset($_POST['submit'])) {
// Check if 'text' is set
if (!empty($_POST['text'])) {
// Check if 'crypt' is set
if (isset($_POST['crypt'])) {
// Retrieve 'text'
$input = $_POST['text'];
// Check for encrypt/decrypt
switch ($_POST['crypt']) {
case 'encrypt':
$result = encrypt();
break;
case 'decrypt':
$result = decrypt();
break;
}
echo $result;
}
// If 'crypt' is not set
else {
echo 'Please select encrypt or decrypt.';
}
}
// If 'text' is not set
else {
echo 'Please fill in some text.';
}
}
function encrypt() {
global $enc, $key, $input, $mode, $iv;
$encrypted = mcrypt_encrypt($enc, $key, $input, $mode, $iv);
$output = base64_encode($encrypted);
return $output;
}
function decrypt() {
global $enc, $key, $input, $mode, $iv;
$decrypted = base64_decode($input);
$output = mcrypt_decrypt($enc, $key, $decrypted, $mode, $iv);
return $output;
}
?>
To be clear, if I include the mcrypt_decrypt in the encryption to make sure it's not something I messed up in the function itself, it does decrypt it properly. But when I try to separate the two, it doesn't. I'm stumped.
you are sending to both encrypt and decrypt the same input , which is :
$input = $_POST['text'];
encrypt will encrypt successfully , but you are always trying to decrypt 'decrypted' phrase !
you must pass the encrypted phrase to decrypt function
and don't forget about the important note that mcrypt_* extension has been deprecated :
This extension rely in libmcrypt which is dead, unmaintained since
2007.
Please don't rely on it, consider switching to well maintained
alternatives (openssl, crypt, password hashing functions, phpseclib,
password_compat...)
and , try to stop using global variables , it isn't recommended .
i used openssl_encrypt and openssl_decrypt function but the decrypt part is not returning any value, whereas using the same key Encrypt is working fine.
here is the function which i used. the variable $decrypted always return a null .
every small help will be appreciated
function deCryption($value)
{
$methods = openssl_get_cipher_methods();
$clefSecrete = "flight";
echo '<pre>';
foreach ($methods as $method) {
//$encrypted = openssl_encrypt($texteACrypter, $method, $clefSecrete); ----this was used for encryption
$decrypted = openssl_decrypt($value, $method, $clefSecrete);
echo "value=".$decrypted;
echo $method . ' : '. $decrypted . "\n";
break;
}
echo '</pre>';
return $decrypted;
}
I had exactly the same problem, I then googled my question and ended up here, on the same question that I had asked. So I had to search elsewhere.
I found this article useful in explaining the shortcoming of the official php documentation. Another article with similar content is here.
In the end it boils down to the key/password. What the openssl_encrypt library expects is a key NOT A PASSWORD. And the size of key must be the size of cipher’s intrinsic key size. The first article says if you provide a longer key, the excess is discarded and a key shorter than expected is padded with zero, i.e. \x00 bytes. I have not tested this fact.
I have edited your code to read as below.
The idea I have used is that the size of the initial vector that a cipher expects is also the size of the key it expects. So here, I am passing a key not a password as you were doing. Simply find a way turning your password into a key.
In your code, you did not pass options and the iv (initialization vector).
The iv is a string the cipher 'mixes' with the plaintext before encryption. So what the cipher encrypts is this 'mixture'. Is this important? Yes! Without this 'mixing', a pair of identical plaintexts would result into a pair of identical ciphertexts, which can lead to an attack; if two identical plaintext-ciphertext pairs are not from the same user, these two users are using the same key! A unique iv for each plaintext therefore ensures that no two plaintexts result into identical ciphertexts. In other words, the iv is a salt.
$plaintext = 'Testing OpenSSL Functions';
$methods = openssl_get_cipher_methods();
//$clefSecrete = 'flight';
echo '<pre>';
foreach ($methods as $method) {
$ivlen = openssl_cipher_iv_length($method);
$clefSecrete = openssl_random_pseudo_bytes($ivlen);
$iv = openssl_random_pseudo_bytes($ivlen);
$encrypted = openssl_encrypt($plaintext, $method, $clefSecrete, OPENSSL_RAW_DATA, $iv);
$decrypted = openssl_decrypt($encrypted, $method, $clefSecrete, OPENSSL_RAW_DATA, $iv);
echo 'plaintext='.$plaintext. "\n";
echo 'cipher='.$method. "\n";
echo 'encrypted to: '.$encrypted. "\n";
echo 'decrypted to: '.$decrypted. "\n\n";
}
echo '</pre>';
I am new to AES but from what I have found there are several modes (ECB,CBC, etc.) and different modes need different initialization vector requirements, blocks, and encodings. I am trying to decode the following
Xrb9YtT7cHUdpHYIvEWeJIAbkxWUtCNcjdzOMgyxJzU/vW9xHivdEDFKeszC93B6MMkhctR35e+YkmYI5ejMf5ofNxaiQcZbf3OBBsngfWUZxfvnrE2u1lD5+R6cn88vk4+mwEs3WoAht1CAkjr7P+fRIaCTckWLaF9ZAgo1/rvYA8EGDc+uXgWv9KvYpDDsCd1JStrD96IACN3DNuO28lVOsKrhcEWhDjAx+yh72wM=
using php and the (text) key "043j9fmd38jrr4dnej3FD11111111111" with mode CBC and an IV of all zeros. I am able to get it to work with this tool but can't get it in php. Here is the code I am using:
function decrypt_data($data, $iv, $key) {
$data = base64_decode($data);
$cypher = mcrypt_module_open(MCRYPT_RIJNDAEL_256, '', MCRYPT_MODE_CBC, '');
// initialize encryption handle
if (mcrypt_generic_init($cypher, $key, $iv) != -1) {
// decrypt
$decrypted = mdecrypt_generic($cypher, $data);
// clean up
mcrypt_generic_deinit($cypher);
mcrypt_module_close($cypher);
return $decrypted;
}
return false;
}
I think I may be missing something relating to base 64 encoding or turning the key into binary first. I have tried decoding many things and all I can produce is gibberish. Any help would be very appreciated.
Well the tool itself does not say how exactly it's encrypted. And you can't set the IV either so it's hard to get the parameters right (because they have to be equal).
After some guesswork I found out the following:
The IV is prepended to the ciphertext
The ciphertext is encrypted with aes-128-cbc
So you have to modify the code:
function decrypt_data($data, $iv, $key) {
$cypher = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
if(is_null($iv)) {
$ivlen = mcrypt_enc_get_iv_size($cypher);
$iv = substr($data, 0, $ivlen);
$data = substr($data, $ivlen);
}
// initialize encryption handle
if (mcrypt_generic_init($cypher, $key, $iv) != -1) {
// decrypt
$decrypted = mdecrypt_generic($cypher, $data);
// clean up
mcrypt_generic_deinit($cypher);
mcrypt_module_close($cypher);
return $decrypted;
}
return false;
}
$ctext = "Xrb9YtT7cHUdpHYIvEWeJIAbkxWUtCNcjdzOMgyxJzU/vW9x" .
"HivdEDFKeszC93B6MMkhctR35e+YkmYI5ejMf5ofNxaiQcZb" .
"f3OBBsngfWUZxfvnrE2u1lD5+R6cn88vk4+mwEs3WoAht1CA" .
"kjr7P+fRIaCTckWLaF9ZAgo1/rvYA8EGDc+uXgWv9KvYpDDs" .
"Cd1JStrD96IACN3DNuO28lVOsKrhcEWhDjAx+yh72wM=";
$key = "043j9fmd38jrr4dnej3FD11111111111";
$res = decrypt_data(base64_decode($ctext), null, $key);
I'm not sure why the key length is not used to encrypt it with aes-256-cbc - I've checked out the source of that as3crypto-library and it kind of supported it, but I would have to debug it to really verify it.
I am coding a Drupal payment method module and within this I need to generate a hash to send to a bank. Bank asks me to code certain strings into the DES/ECB hash. They also provide test environment and here comes my problem. With the string B7DC02D5D6F2689E and key 7465737465703031 I should get result hash 3627C7356B25922B (after bin2hex, of course). This is by the bank test page and I have also checked this on this page: http://www.riscure.com/tech-corner/online-crypto-tools/des.html (encryption java applet).
My problem is that whatever I do I cant get my PHP code to provide the correct result. This is a simple function I am trying to use:
function encrypt($hash, $key)
{
$hash = strtoupper(substr(sha1($hash), 0, 16));
$key = strtoupper(bin2hex($key));
$block = mcrypt_get_block_size('des', 'ecb');
if (($pad = $block - (strlen($hash) % $block)) < $block) {
$hash .= str_repeat(chr($pad), $pad);
}
$sig = strtoupper(bin2hex(mcrypt_encrypt(MCRYPT_DES, $key, $hash, MCRYPT_MODE_ECB)));
return $sig;
}
and I have been trying sth like this as well:
function encrypt( $value, $key) {
$hash = strtoupper(substr(sha1($value), 0, 16));
$key = strtoupper(substr(bin2hex($key), 0, 16));
// encrypt hash with key
if (function_exists('mcrypt_module_open')) { // We have mcrypt 2.4.x
$td = mcrypt_module_open(MCRYPT_DES, "", MCRYPT_MODE_ECB, "");
$iv = mcrypt_create_iv(mcrypt_enc_get_iv_size ($td), MCRYPT_RAND);
mcrypt_generic_init($td, $key, $iv);
$signature = strtoupper(bin2hex(mcrypt_generic ($td, $hash)));
mcrypt_generic_end ($td);
}
else
{ // We have 2.2.x only
$signature = strtoupper(bin2hex(mcrypt_ecb (MCRYPT_3DES, $key, $hash, MCRYPT_ENCRYPT)));
}
return $signature;
}
None of these gave the correct signature. Any idea what's wrong? For now I am dealing with this issue more than 3 hrs, so I appreciate any help. I am not very familiar with this encryption stuff. Thanks a lot.
Btw.: Those $hash and $key mentioned above are after the strtoupper, substr and bin2hex functions at the beginning of my code snippets.
Simple solution:
function encrypt($hash, $key) {
return mcrypt_encrypt("des", pack("H*", $key), pack("H*", $hash), "ecb");
}
print bin2hex(encrypt("B7DC02D5D6F2689E", "7465737465703031"));
This prints 3627c7356b25922b for me, so it looks like that's working.
You were on the right track with bin2hex(), but that was converting in the wrong direction. (There's unfortunately no hex2bin() function, so you have to use pack() instead.)
You also don't need an IV for a single-block encryption like this.
You plaintext, B7DC02D5D6F2689E, is 8 bytes = 64 bits. This is an exact block for DES so you don't need any padding in ECB mode. I suggest that you remove the padding code entirely. All DEC-ECB needs in this case is the block to encrypt and the key; no padding and no IV.