I need to encrypt some data and have it decrypted on a later point in time. The data is tied to specific users. I've gathered two possible solutions...
1: The first one is derived from the official docs (example #1 # http://php.net/manual/en/function.mcrypt-encrypt.php):
function encrypt($toEncrypt)
{
global $key;
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC);
$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
return base64_encode($iv . mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $toEncrypt, MCRYPT_MODE_CBC, $iv));
}
function decrypt($toDecrypt)
{
global $key;
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC);
$toDecrypt = base64_decode($toDecrypt);
return rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, substr($toDecrypt, $iv_size), MCRYPT_MODE_CBC, substr($toDecrypt, 0, $iv_size)));
}
The key is generated once using:
echo bin2hex(openssl_random_pseudo_bytes(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC)))
And then later referred to as this:
$key = pack('H*', [result of above]);
1.1: I've noticed that the encrypted result always ends in two equal signs ('=='). Why? - Using bin2hex() and hex2bin() in encrypt() and decrypt() instead of base64_encode()/base64_decode() respectively does not yield these results.
1.2: Will using bin2hex()/hex2bin() have any consequence on the outcome (other than length)?
1.3: There seems to be some discussion whether or not to call a trim-function on the return result when decrypting (this applies to the solution below as well). Why would this be necessary?
2: Second solution comes from here, Stackoverflow (Simplest two-way encryption using PHP):
function encrypt($key, $toEncrypt)
{
return base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, md5($key), $toEncrypt, MCRYPT_MODE_CBC, md5(md5($key))));
}
function decrypt($key, $toDecrypt)
{
return rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, md5($key), base64_decode($toDecrypt), MCRYPT_MODE_CBC, md5(md5($key))), "\0");
}
I'm aware that both approaches to the key handling is interchangeable, I purposely made them different in that respect in order to highlight possible solutions, please feel free to mix and match.
Personally I feel that the first one offers tighter security since both key and initialization vector is properly randomized. The second solution however, does offer some form of non-predictability since the key is unique for each piece of encrypted data (even though it suffers under the weak randomization of md5()).
The key could for example be the user's name.
3: So, which one is preferable? I'm slightly in the dark since the Stackoverflow answer got a whopping 105 votes. Other thoughts, tips?
4: Bonus question!: I'm not incredibly brainy on server security aspects, but obviously gaining access to the PHP files would expose the key, which as a direct result, would render the encryption useless, assuming the attacker also has access to the DB. Is there any way to obscure the key?
Thank you for reading and have a nice day!
EDIT: All things considered, this seems to be my best bet:
function encrypt($toEncrypt)
{
global $key;
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
$iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC), MCRYPT_RAND);
return base64_encode($iv . mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $toEncrypt, MCRYPT_MODE_CBC, $iv));
}
function decrypt($toDecrypt)
{
global $key;
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
$toDecrypt = base64_decode($toDecrypt);
return rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, substr($toDecrypt, $iv_size), MCRYPT_MODE_CBC, substr($toDecrypt, 0, $iv_size)));
}
Using a key created once using the following:
bin2hex(openssl_random_pseudo_bytes(32)));
The main difference between the two code samples is that the first one generates a random initialization vector (IV) for each message, while the second one always uses a fixed IV derived from the key.
If you never encrypt more than one message with the same key, both methods are OK. However, encrypting multiple messages with the same key and IV is dangerous, so you should never use the second code sample to encrypt more than one message with the same key.
Another difference is that the first code sample passes the key directly to the block cipher (Rijndael), whereas the second one first runs it through md5(), apparently in a weak attempt to use it as a key derivation function.
If the key is already a random bitstring (of suitable length), like your sample key generation code would produce, there's no need to run it through md5(). If, instead, it's something like a user-provided password, there might be some advantage to hashing it — but in that case, you really ought to use a proper key derivation function like PBKDF2 instead, e.g. like this:
$cipher = MCRYPT_RIJNDAEL_128; // = AES-256
$mode = MCRYPT_MODE_CBC;
$keylen = mcrypt_get_key_size( $cipher, $mode );
$salt = mcrypt_create_iv( $keylen, MCRYPT_DEV_URANDOM );
$iterations = 10000; // higher = slower; make this as high as you can tolerate
$key = hash_pbkdf2( 'sha256', $password, $salt, $iterations, $keylen, true );
Note that the correct $salt and $iterations values will be needed to reconstruct the key from the password for decryption, so remember to store them somewhere, e.g. by prepending them to the ciphertext. The length of the salt doesn't matter much, as long as it's not very short; making it equal to the key length is a safe enough choice.
(Incidentally, this is also a pretty good way to hash a password to verify its correctness. Obviously, you shouldn't use the same $key value for both encryption and password verification, but you could safely store, say, hash( 'sha256', $key, true ) alongside the ciphertext to let you verify that the password / key is correct.)
A few other issues I see with the two code snippets:
Both snippets use MCRYPT_RIJNDAEL_256, which is, apparently, not AES-256, but rather the non-standard Rijndael-256/256 variant, with a 256-bit block size (and key size). It's probably secure, but the 256-bit-block-size variants of Rijndael have receive much less cryptanalytic scrutiny than the 128-bit-block-size ones (which were standardized as AES), so you're taking a slightly higher risk by using them.
Thus, if you want to play it safe, need to interoperate with other software using standard AES, or just need to be able to tell your boss that, yes, you're using a standard NIST-approved cipher, the you should go with MCRYPT_RIJNDAEL_128 (which, apparently, is what mcrypt calls AES-256) instead.
In your key generation code, pack( 'H*', bin2hex( ... ) ) is a no-op: bin2hex() converts the key from binary to hexadecimal, and pack( 'H*', ... ) then does the reverse. Just get rid of both functions.
Also, you're generating a key, not an IV, so you should use mcrypt_get_key_size(), not mcrypt_get_iv_size(). As it happens, for MCRYPT_RIJNDAEL_256 there's no difference (since both the IV size and the key size are 32 bytes = 256 bits), but for MCRYPT_RIJNDAEL_128 (and many other ciphers) there is.
As owlstead notes, mcrypt's implementation of CBC mode apparently uses a non-standard zero-padding scheme. You second code sample correctly removes the padding with rtrim( $msg, "\0" ); the first one just calls rtrim( $msg ), which will also trim any whitespace off the end of the message.
Also, obviously, this zero-padding scheme won't work properly if your data can legitimately contain zero bytes at the end. You could instead switch to some other cipher mode, like MCRYPT_MODE_CFB or MCRYPT_MODE_OFB, which do not require any padding. (Out of those two, I would generally recommend CFB, since accidental IV reuse is very bad for OFB. It's not good for CFB or CBC either, but their failure mode is much less catastrophic.)
Q1: Choose this!
Disclosure: I (re-)wrote the mcrypt_encrypt code sample. So I opt for 1.
Personally I would not recommend to use MCRYPT_RIJNDAEL_256. You use AES-256 by using a key with a key size of 32 bytes (256 bit) for the MCRYPT_RIJNDAEL_128 algorithm, not by selecting a Rijndael with a block size of 256. I explicitly rewrote the sample to remove MCRYPT_RIJNDAEL_256 – among other mistakes – and put in comments why you should use MCRYPT_RIJNDAEL_128 instead.
Q 1.1: Padding byte for base64
= is a padding character for base 64 encoding. Base64 encodes 3 bytes into 4 characters. To have a number of characters that is an exact multiple of 4 they use these padding bytes, if required.
Q1.2: Will using bin2hex()/hex2bin() have any consequence on the outcome (other than length)?
No, as both hex and base64 are deterministic and fully reversible.
Q1.3: On rtrim
The same goes for the rtrim. This is required as PHP's mcrypt uses the non-standard zero padding, up to the block size (it fills the plaintext with 00 valued bytes at the right). This is fine for ASCII & UTF-8 strings where the 00 byte is not in the range of printable characters, but you may want to look further if you want to encrypt binary data. There are examples of PKCS#7 padding in the comments section of mcrypt_encrypt. Minor note: rtrim may only work for some languages such as PHP, other implementations may leave trailing 00 characters as 00 is not considered white space.
Q2: Disqualification
The other SO answer uses MD5 for password derivation and MD5 over the password for IV calculation. This fully disqualifies it as a good answer. If you have a password instead of a key, please check this Q/A.
And it doesn't use AES either, choosing to opt for MCRYPT_RIJNDAEL_256.
Q3: On the votes
As long as SO community keeps voting on answers that seem to work for a certain language/configuration instead of voting on answers that are cryptographically secure, you will find absolute trap like the answer in Q2. Unfortunately, most people that come here are not cryptographers; the other answer would be absolutely smitten on crypto.stackexchange.com.
Note that just yesterday I had to explain to somebody on SO why it is not possibly to decrypt MCRYPT_RIJNDAEL_256 using CCCrypt on iOS because only AES is available.
Q4: Obfuscation
You can obfuscate the key, but not much else if you store an AES key in software or configuration file.
Either you need to use a public key (e.g. RSA) and hybrid cryptography, or you need to store the key somewhere safe such as a HSM or smart card. Key management is a complex part of crypto, possibly the most complex part.
First of all I apologize for the length of this answer.
I just came across this thread and I hope that this class may be of help to anyone reading this thread looking for an answer and source code they can use.
Description:
This class will first take the supplied encryption key and run it through the PBKDF2 implementation using the SHA-512 algorithm at 1000 iterations.
When encrypting data this class will compress the data and compute an md5 digest of the compressed data before encryption. It will also calculate the length of the data after compression. These calculated values are then encrypted with with the compressed data and the IV is prepended to the encrypted output.
A new IV is generated using dev/urandom before each encryption operation. If the script is running on a Windows machine and the PHP version is less than 5.3, the class will use MCRYPT_RAND to generate an IV.
Depending on if parameter $raw_output is true or false, the encryption method will return lowercase hexit by default or raw binary of the encrypted data.
Decryption will reverse the encryption process and check that the computed md5 digest is equal to the stored md5 digest that was encrypted with the data. If the hashes are not the same, the decryption method will return false. It will also use the stored length of the compressed data to ensure all padding is removed before decompression.
This class uses Rijndael 128 in CBC mode.
This class will work cross platform and has been tested on PHP 5.2, 5.3, 5.4, 5.5 and 5.6
File: AesEncryption.php
<?php
/**
* This file contains the class AesEncryption
*
* AesEncryption can safely encrypt and decrypt plain or binary data and
* uses verification to ensure decryption was successful.
*
* PHP version 5
*
* LICENSE: This source file is subject to version 2.0 of the Apache license
* that is available through the world-wide-web at the following URI:
* https://www.apache.org/licenses/LICENSE-2.0.html.
*
* #author Michael Bush <michael(.)bush(#)hotmail(.)co(.)uk>
* #license https://www.apache.org/licenses/LICENSE-2.0.html Apache 2.0
* #copyright 2015 Michael Bush
* #version 1.0.0
*/
/**
* #version 1.0.0
*/
final class AesEncryption
{
/**
* #var string
*/
private $key;
/**
* #var string
*/
private $iv;
/**
* #var resource
*/
private $mcrypt;
/**
* Construct the call optionally providing an encryption key
*
* #param string $key
* #return Encryption
* #throws RuntimeException if the PHP installation is missing critical requirements
*/
public function __construct($key = null) {
if (!extension_loaded ('mcrypt')) {
throw new RuntimeException('MCrypt library is not availble');
}
if (!extension_loaded ('hash')) {
throw new RuntimeException('Hash library is not availble');
}
if (!in_array('rijndael-128', mcrypt_list_algorithms(), true)) {
throw new RuntimeException('MCrypt library does not contain an implementation of rijndael-128');
}
if (!in_array('cbc', mcrypt_list_modes(), true)) {
throw new RuntimeException('MCrypt library does not support CBC encryption mode');
}
$this->mcrypt = mcrypt_module_open('rijndael-128', '', 'cbc', '');
if(isset($key)) {
$this->SetKey($key);
}
}
/**
* #return void
*/
public function __destruct() {
if (extension_loaded ('mcrypt')) {
if (isset($this->mcrypt)) {
mcrypt_module_close($this->mcrypt);
}
}
}
/**
* Set the key to be used for encryption and decryption operations.
*
* #param string $key
* #return void
*/
public function SetKey($key){
$this->key = $this->pbkdf2('sha512', $key, hash('sha512', $key, true), 1000, mcrypt_enc_get_key_size($this->mcrypt), true);
}
/**
* Encrypts data
*
* #param string $data
* #param bool $raw_output if false this method will return lowercase hexit, if true this method will return raw binary
* #return string
*/
public function Encrypt($data, $raw_output = false) {
$data = gzcompress($data, 9);
$hash = md5($data, true);
$datalen = strlen($data);
$datalen = pack('N', $datalen);
$data = $datalen . $hash . $data;
if (version_compare(PHP_VERSION, '5.3.0', '<=')) {
if (strtolower (substr (PHP_OS, 0, 3)) == 'win') {
$this->iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($this->mcrypt), MCRYPT_RAND);
} else {
$this->iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($this->mcrypt), MCRYPT_DEV_URANDOM);
}
} else {
$this->iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($this->mcrypt), MCRYPT_DEV_URANDOM);
}
$this->initialize();
$data = mcrypt_generic($this->mcrypt, $data);
$this->deinitialize();
$data = $this->iv . $data;
$this->iv = null;
if ($raw_output) {
return $data;
}
$data = unpack('H*',$data);
$data = end($data);
return $data;
}
/**
* Decrypts data
*
* #param string $data
* #return string This method will return false if an error occurs
*/
public function Decrypt($data) {
if (ctype_xdigit($data)) {
$data = pack ('H*',$data);
}
$this->iv = substr ($data, 0, mcrypt_enc_get_iv_size($this->mcrypt));
$data = substr ($data, mcrypt_enc_get_iv_size($this->mcrypt));
$this->initialize();
$data = mdecrypt_generic($this->mcrypt, $data);
$this->deinitialize();
$datalen = substr($data, 0, 4);
$len = unpack('N', $datalen);
$len = end($len);
$hash = substr($data, 4, 16);
$data = substr($data, 20, $len);
$datahash = md5($data, true);
if ($this->compare($hash,$datahash)) {
$data = #gzuncompress($data);
return $data;
}
return false;
}
/**
* Initializes the mcrypt module
*
* #return void
*/
private function initialize() {
mcrypt_generic_init($this->mcrypt, $this->key, $this->iv);
}
/**
* Deinitializes the mcrypt module and releases memory.
*
* #return void
*/
private function deinitialize() {
mcrypt_generic_deinit($this->mcrypt);
}
/**
* Implementation of a timing-attack safe string comparison algorithm, it will use hash_equals if it is available
*
* #param string $safe
* #param string $supplied
* #return bool
*/
private function compare($safe, $supplied) {
if (function_exists('hash_equals')) {
return hash_equals($safe, $supplied);
}
$safe .= chr(0x00);
$supplied .= chr(0x00);
$safeLen = strlen($safe);
$suppliedLen = strlen($supplied);
$result = $safeLen - $suppliedLen;
for ($i = 0; $i < $suppliedLen; $i++) {
$result |= (ord($safe[$i % $safeLen]) ^ ord($supplied[$i]));
}
return $result === 0;
}
/**
* Implementation of the keyed-hash message authentication code algorithm, it will use hash_hmac if it is available
*
* #param string $algo
* #param string $data
* #param string $key
* #param bool $raw_output
* #return string
*
* #bug method returning wrong result for joaat algorithm
* #id 101275
* #affects PHP installations without the hash_hmac function but they do have the joaat algorithm
* #action wont fix
*/
private function hmac($algo, $data, $key, $raw_output = false) {
$algo = strtolower ($algo);
if (function_exists('hash_hmac')) {
return hash_hmac($algo, $data, $key, $raw_output);
}
switch ( $algo ) {
case 'joaat':
case 'crc32':
case 'crc32b':
case 'adler32':
case 'fnv132':
case 'fnv164':
case 'fnv1a32':
case 'fnv1a64':
$block_size = 4;
break;
case 'md2':
$block_size = 16;
break;
case 'gost':
case 'gost-crypto':
case 'snefru':
case 'snefru256':
$block_size = 32;
break;
case 'sha384':
case 'sha512':
case 'haval256,5':
case 'haval224,5':
case 'haval192,5':
case 'haval160,5':
case 'haval128,5':
case 'haval256,4':
case 'haval224,4':
case 'haval192,4':
case 'haval160,4':
case 'haval128,4':
case 'haval256,3':
case 'haval224,3':
case 'haval192,3':
case 'haval160,3':
case 'haval128,3':
$block_size = 128;
break;
default:
$block_size = 64;
break;
}
if (strlen($key) > $block_size) {
$key=hash($algo, $key, true);
} else {
$key=str_pad($key, $block_size, chr(0x00));
}
$ipad=str_repeat(chr(0x36), $block_size);
$opad=str_repeat(chr(0x5c), $block_size);
$hmac = hash($algo, ($key^$opad) . hash($algo, ($key^$ipad) . $data, true), $raw_output);
return $hmac;
}
/**
* Implementation of the pbkdf2 algorithm, it will use hash_pbkdf2 if it is available
*
* #param string $algorithm
* #param string $password
* #param string $salt
* #param int $count
* #param int $key_length
* #param bool $raw_output
* #return string
* #throws RuntimeException if the algorithm is not found
*/
private function pbkdf2($algorithm, $password, $salt, $count = 1000, $key_length = 0, $raw_output = false) {
$algorithm = strtolower ($algorithm);
if (!in_array($algorithm, hash_algos(), true)) {
throw new RuntimeException('Hash library does not contain an implementation of ' . $algorithm);
}
if (function_exists('hash_pbkdf2')) {
return hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output);
}
$hash_length = strlen(hash($algorithm, '', true));
if ($count <= 0) {
$count = 1000;
}
if($key_length <= 0) {
$key_length = $hash_length * 2;
}
$block_count = ceil($key_length / $hash_length);
$output = '';
for($i = 1; $i <= $block_count; $i++) {
$last = $salt . pack('N', $i);
$last = $xorsum = $this->hmac($algorithm, $last, $password, true);
for ($j = 1; $j < $count; $j++) {
$xorsum ^= ($last = $this->hmac($algorithm, $last, $password, true));
}
$output .= $xorsum;
}
if ($raw_output) {
return substr($output, 0, $key_length);
}
$output = unpack('H*',$output);
$output = end ($output);
return substr($output, 0, $key_length);
}
}
Example usage:
<?php
include 'AesEncryption.php';
$key = 'my secret key';
$string = 'hello world';
try
{
$aes = new AesEncryption($key); // exception can be thrown here if the class is not supported
$data = $aes->Encrypt($string, true); // expecting return of a raw byte string
$decr = $aes->Decrypt($data); // expecting the return of "hello world"
var_dump ($decr);
// encrypt something else with a different key
$aes->SetKey('my other secret key'); // exception can be thrown here if the class is not supported
$data2 = $aes->Encrypt($string); // return the return of a lowercase hexit string
$decr = $aes->Decrypt($data2); // expecting the return of "hello world"
var_dump ($decr);
// proof that the key was changed
$decr = $aes->Decrypt($data); // expecting return of Boolean False
var_dump ($decr);
// reset the key back
$aes->SetKey($key); // exception can be thrown here if the class is not supported
$decr = $aes->Decrypt($data); // expecting hello world
var_dump ($decr);
}
catch (Exception $e)
{
print 'Error running AesEncryption class; reason: ' . $e->getMessage ();
}
1.1 It's just padding. It happens with most input to base64, but not all.
1.2 No difference. Keep with base64, it's standard with encryption.
1.3 I don't see reason why it would be necessary. People sometimes solve problems at wrong places. Instead of fixing the input, they modify the output. Where's this discussion?
Definitely DO NOT use this. You change your key of some legnth to 128 bit MD5. And that is not secure.
Use asymetric encryption, if decrypting is on different machine, or different user.
Related
I have a legacy database with content that was encrypted with mcrypt using DES (yes, I know, it was a long time ago)
The encryption method is like this:
/**
* General encryption routine for generating a reversible ciphertext
* #param String $string the plain text to encrypt
* #param String $key the encryption key to use
* #return String the cypher text result
*/
function encrypt($string, $key)
{
srand((double) microtime() * 1000000);
/* Open module, and create IV */
$td = mcrypt_module_open('des', '', 'cfb', '');
$ksub = substr(md5($key), 0, mcrypt_enc_get_key_size($td));
$iv_size = mcrypt_enc_get_iv_size($td);
$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
/* Initialize encryption handle */
if (mcrypt_generic_init($td, $ksub, $iv) != -1)
{
/* Encrypt data */
$ctxt = mcrypt_generic($td, $string);
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
$ctxt = $iv . $ctxt;
return base64_encode($ctxt);
} //end if
}
and the decryption method is like this:
/**
* General decryption routine for recovering a plaintext
* #param String $string the cypher text to decrypt
* #param String $key the encryption key to use
* #return String the plain text result
*/
function decrypt($string, $key)
{
$ptxt = base64_decode($string);
/* Open module, and create IV */
$td = mcrypt_module_open('des', '', 'cfb', '');
$ksub = substr(md5($key), 0, mcrypt_enc_get_key_size($td));
$iv_size = mcrypt_enc_get_iv_size($td);
$iv = substr($ptxt, 0, $iv_size);
$ptxtsub = substr($ptxt, $iv_size);
/* Initialize encryption handle */
if (mcrypt_generic_init($td, $ksub, $iv) != -1)
{
/* Encrypt data */
$ctxt = mdecrypt_generic($td, $ptxtsub);
mcrypt_generic_deinit($td);
mcrypt_module_close($td);
return $ctxt;
} //end if
}
I need to extract this data in a PHP7.4 environment, even if only to re-encrypt it with something better, but I'm not sure how to reproduce the mcrypt operations with stuff that exists in PHP7.4 like sodium.
I suppose one method would be to spin up some sort of legacy PHP installation that still has mcrypt and do it offline, but is there a more direct way of coding a decryption method?
While mcrypt is not part of PHP anymore (for good reasons), it still exists as module you can install for PHP 7.4
https://pecl.php.net/package/mcrypt
Install it, make sure to re-encrypt all data, once all old data is updated, change your code to not use it anymore and remove the extension.
For those who use cPanel, you can simply do it in PHP 7.3
Go to PHP Selector, choose 7.3 PHP version if not current, then select 'mcrypt' and 'sodium' extension.
Then you can use both encryptions on the same PHP file in order to decrypt your data with 'mcrypt' and encrypt with 'sodium' on a single operation.
I'm developing a web app using Symfony 3.4 with PHP 7.2 and a SQLite db.
I want to encrypt some fields of my entity using AES-256 using standard libraries - openssl or sodium - but I'm really not sure on how to properly do it so I'm asking for your guidance before I make many big awful mistakes:
what are the best practices for storing key and IV?
is it relevant where I put it as long as it is not accessible by the web browser? I'm thinking to put them in the config.yml but it feels wrong, very wrong
which library is more secure between openssl and sodium?
I'm using the following code where my secret is stored in the parameters.yml so it will no be visible if you push it to Git.
/**
* SecurityHelper.
*
* #author Kengy Van Hijfte <development#kengy.be>
*/
class SecurityHelper
{
/** #var string $secret */
private $secret;
public function __construct($secret)
{
$this->secret = $secret;
}
/**
* #param $text
* #return string
*/
public function encrypt($text)
{
if (null == $text)
return null;
// Generate an initialization vector
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc'));
// Encrypt the data using AES 256 encryption in CBC mode using our encryption key and initialization vector.
$encrypted = openssl_encrypt($text, 'aes-256-cbc', $this->secret, 0, $iv);
// The $iv is just as important as the key for decrypting, so save it with our encrypted data using a unique separator (::)
return base64_encode($encrypted . '::' . $iv);
}
/**
* #param $text
* #return string
*/
public function decrypt($text)
{
if (null == $text)
return null;
// To decrypt, split the encrypted data from our IV - our unique separator used was "::"
list($encrypted_data, $iv) = explode('::', base64_decode($text), 2);
return openssl_decrypt($encrypted_data, 'aes-256-cbc', $this->secret, 0, $iv);
}
}
I am currently in the process of replacing Mcrypt with OpenSSL since Mcrypt will be deprecated in PHP 7.1. I need is a way to get the blocksize per algorithm like mcrypt_get_block_size().
I am wondering if there is an equivalent function to mcrypt_get_block_size() but it's pretty badly documented can't seem to find it.
php-openssl unfortunately doesn't have an API that would give you the cipher blockSize. If you really need it, you'd have to hard-code the blockSize (per algorithm).
However, typical applications would only need to support a single encryption algorithm, and in that case you should already know what the block size is for your case.
And also, the only use cases I've had for mcrypt_get_block_size(), mcrypt_enc_get_block_size() is PKCS#7 padding, which OpenSSL already does by default for block cipher algorithms. So it may be the case that you don't need this at all.
The following function can be used as a replacement for PHP >= 5.4.0.
It just bruteforces the block length out of openssl_encrypt().
if (!function_exists('openssl_cipher_block_length')) {
/**
* Returns the block length for a given cipher.
*
* #param string $cipher
* A cipher method (see openssl_get_cipher_methods()).
*
* #retval int
* The cipher's block length.
* Returns false if the actual length cannot be determined.
* Returns 0 for some cipher modes that do not use blocks
* to encrypt data.
*
* #note
* PHP >= 5.4.0 is required for this function to work.
*/
function openssl_cipher_block_length($cipher)
{
$ivSize = #openssl_cipher_iv_length($cipher);
// Invalid or unsupported cipher.
if (false === $ivSize) {
return false;
}
$iv = str_repeat("a", $ivSize);
// Loop over possible block sizes, from 1 upto 1024 bytes.
// The upper limit is arbitrary but high enough that is
// sufficient for any current & foreseeable cipher.
for ($size = 1; $size < 1024; $size++) {
$output = openssl_encrypt(
// Try varying the length of the raw data
str_repeat("a", $size),
// Cipher to use
$cipher,
// OpenSSL expands the key as necessary,
// so this value is actually not relevant.
"a",
// Disable data padding: php_openssl will return false
// if the input data's length is not a multiple
// of the block size.
//
// We also pass OPENSSL_RAW_DATA so that PHP does not
// base64-encode the data (since we just throw it away
// afterwards anyway)
OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
// Feed it with an IV to avoid nasty warnings.
// The actual value is not relevant as long as
// it has the proper length.
$iv
);
if (false !== $output) {
return $size;
}
}
// Could not determine the cipher's block length.
return false;
}
}
I think you can do something like that:
$method = 'AES-256-CBC';
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($method));
$block_size = strlen(openssl_encrypt('', $method, '', OPENSSL_RAW_DATA, $iv));
It's basically encrypting an empty string and, as the data will be padded to the block size, check the result length.
I've tested it with some different methods, and it seems to work properly.
A more general approach that might help you involves using this polyfill: mcrypt_compat. A polyfill is a library that implements functionality which is not supported yet/anymore.
Step 1: install the library with Composer
composer require phpseclib/mcrypt_compat
Step 2: require Composer's autoloader at the top of the PHP script where you use the mcrypt functions (make sure the relative path is correct)
require_once('../vendor/autoload.php');
By now, you can use functions like mcrypt_get_block_size() in this PHP script
txigreman this solution worked for me, i managed to change MCRYPT code to use OPENSSL
this is my function to encrypt which replaced MCRYPT i mixed some code from MCRYPT and new code from openssl, while decrypt is not working but i can open payment page of provider and finish payment- thats what i needed and after it redirects me to success page, NeO.network.ae payment gateway
public $method = 'aes-256-cbc';
$this->EncryptedString = $this->encryptData($this->beforeEncryptionString, $this->merchantKey, $this->method);
public function encryptData(string $data, string $key, string $method): string
{
$ivSize = openssl_cipher_iv_length($method);
$iv = 'YOUR_IV';
$size = strlen(openssl_encrypt('', $method, '', OPENSSL_RAW_DATA, $iv));
$pad = $size - ( strlen( $data ) % $size );
$padtext = $data . str_repeat( chr( $pad ), $pad );
$encrypted = openssl_encrypt($padtext, $method, base64_decode( $key ), OPENSSL_RAW_DATA, $iv);
$encrypted = base64_encode($encrypted);
return $encrypted;
}
This is the code I use to encrypt/decrypt the data:
// Set the method
$method = 'AES-128-CBC';
// Set the encryption key
$encryption_key = 'myencryptionkey';
// Generet a random initialisation vector
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($method));
// Define the date to be encrypted
$data = "Encrypt me, please!";
var_dump("Before encryption: $data");
// Encrypt the data
$encrypted = openssl_encrypt($data, $method, $encryption_key, 0, $iv);
var_dump("Encrypted: ${encrypted}");
// Append the vector at the end of the encrypted string
$encrypted = $encrypted . ':' . $iv;
// Explode the string using the `:` separator.
$parts = explode(':', $encrypted);
// Decrypt the data
$decrypted = openssl_decrypt($parts[0], $method, $encryption_key, 0, $parts[1]);
var_dump("Decrypted: ${decrypted}");
It ususaly works fine, but sometimes (1 in 10 or even less often) it fails. When it fails than the text is only partially encrypted:
This is the error message when it happens:
Warning: openssl_decrypt(): IV passed is only 10 bytes long, cipher expects an IV of precisely 16 bytes, padding with \0
And when it happens the encrypted text looks like:
Encrypt me���L�se!
I thought that it might be caused by a bug in PHP, but I've tested on different hosts: PHP 7.0.6 and PHP 5.6. I've also tried multiple online PHP parsers like phpfidle.org or 3v4l.org.
It seems that openssl_random_pseudo_bytes not always returns a string of a proper length, but I have no idea why.
Here's the sample: https://3v4l.org/RZV8d
Keep on refreshing the page, you'll get the error at some point.
When you generate a random IV, you get raw binary. There's a nonzero chance that the binary strings will contain a : or \0 character, which you're using to separate the IV from the ciphertext. Doing so makes explode() give you a shorter string. Demo: https://3v4l.org/3ObfJ
The trivial solution would be to add base64 encoding/decoding to this process.
That said, please don't roll your own crypto. In particular, unauthenticated encryption is dangerous and there are already secure libraries that solve this problem.
Instead of writing your own, consider just using defuse/php-encryption. This is the safe choice.
Here's the solution
I've updated the code from the first post and wrapped it in a class. This is fixed code based on the solution provided by Scott Arciszewski.
class Encryptor
{
/**
* Holds the Encryptor instance
* #var Encryptor
*/
private static $instance;
/**
* #var string
*/
private $method;
/**
* #var string
*/
private $key;
/**
* #var string
*/
private $separator;
/**
* Encryptor constructor.
*/
private function __construct()
{
$app = App::getInstance();
$this->method = $app->getConfig('encryption_method');
$this->key = $app->getConfig('encryption_key');
$this->separator = ':';
}
private function __clone()
{
}
/**
* Returns an instance of the Encryptor class or creates the new instance if the instance is not created yet.
* #return Encryptor
*/
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new Encryptor();
}
return self::$instance;
}
/**
* Generates the initialization vector
* #return string
*/
private function getIv()
{
return openssl_random_pseudo_bytes(openssl_cipher_iv_length($this->method));
}
/**
* #param string $data
* #return string
*/
public function encrypt($data)
{
$iv = $this->getIv();
return base64_encode(openssl_encrypt($data, $this->method, $this->key, 0, $iv) . $this->separator . base64_encode($iv));
}
/**
* #param string $dataAndVector
* #return string
*/
public function decrypt($dataAndVector)
{
$parts = explode($this->separator, base64_decode($dataAndVector));
// $parts[0] = encrypted data
// $parts[1] = initialization vector
return openssl_decrypt($parts[0], $this->method, $this->key, 0, base64_decode($parts[1]));
}
}
Usage
$encryptor = Encryptor::getInstance();
$encryptedData = $encryptor->encrypt('Encrypt me please!');
var_dump($encryptedData);
$decryptedData = $encryptor->decrypt($encryptedData);
var_dump($decryptedData);
This happened to me also. I got error like
openssl_decrypt(): IV passed is only 14 bytes long, cipher expects an IV of precisely 16 bytes, padding with \0
I was using lowercase method like openssl_cipher_iv_length('aes-128-cbc')
Lowercase aes-- method gives a result of length which varies between 12 to 16. Ref: https://www.php.net/manual/en/function.openssl-cipher-iv-length.php
Making the method to uppercase openssl_cipher_iv_length('AES-128-CBC') will give consistent value which is 16.
So while encrypting & decrypting the IV length stays the same as 16.
I need to encrypt/decrypt data with PHP. I am completely new to this, however I have read that Libsodium-PHP is the best tool for AES encryption. Much like the other PHP encryption libraries I have researched Libsoduim-PHP seemed to offer almost no documentation of how to use the library (that I was able to find). Can anyone that has experience with PHP encryption either point me in the direction of a good learning resource or write a few lines of sample code using Libsoduim-PHP?Thank you very much for the help,
Atlas
Much like the other PHP encryption libraries I have researched Libsoduim-PHP seemed to offer almost no documentation of how to use the library (that I was able to find).
From the libsodium-php Github page you will find a direct link to a free online book that covers everything you need to know to get started with libsodium.
The final chapter contains libsodium recipes, but each chapter contains detailed usage information.
If you specifically need AES, read this.
If you don't have an "AES-or-bust" requirement hanging over your head, where failure to specifically use AES means your department gets axed and your developers face a firing squad, you should consider just using crypto_secretbox which uses Xsalsa20 for encryption and attaches a Poly1305 authentication tag. (This is authenticated encryption, which you want to use almost always.)
Also look into Halite if you want easy-mode.
PHP Version >= 7.2
If you are using PHP >= 7.2 use inbuilt sodium core extension instead.
Sample implementation
<?php
//Simple Usage
/**
* Encrypt a message
*
* #param string $message - message to encrypt
* #param string $key - encryption key
* #return string
*/
function safeEncrypt($message, $key)
{
$nonce = random_bytes(
SODIUM_CRYPTO_SECRETBOX_NONCEBYTES
);
$cipher = base64_encode(
$nonce.
sodium_crypto_secretbox(
$message,
$nonce,
$key
)
);
sodium_memzero($message);
sodium_memzero($key);
return $cipher;
}
/**
* Decrypt a message
*
* #param string $encrypted - message encrypted with safeEncrypt()
* #param string $key - encryption key
* #return string
*/
function safeDecrypt($encrypted, $key)
{
$decoded = base64_decode($encrypted);
if ($decoded === false) {
throw new Exception('Scream bloody murder, the encoding failed');
}
if (mb_strlen($decoded, '8bit') < (SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES)) {
throw new Exception('Scream bloody murder, the message was truncated');
}
$nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
$ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');
$plain = sodium_crypto_secretbox_open(
$ciphertext,
$nonce,
$key
);
if ($plain === false) {
throw new Exception('the message was tampered with in transit');
}
sodium_memzero($ciphertext);
sodium_memzero($key);
return $plain;
}
//Encrypt & Decrypt your message
$key = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
$enc = safeEncrypt('Encrypt This String...', $key); //generates random encrypted string (Base64 related)
echo $enc;
echo '<br>';
$dec = safeDecrypt($enc, $key); //decrypts encoded string generated via safeEncrypt function
echo $dec;