How to handle AES key in Symfony - php

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);
}
}

Related

PHP 7.4 direct replacement for mcrypt decryption

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.

PHP AES encryption key

I have a PHP and MySQL system and I need to encrypt user data with AES-256. I know how to encrypt and decrypt the data using AES-encrypt/decrypt but I'm not sure how to securely store the AES encryption key. Would it be recommended to store the key inside of a file outside of the public website folder, then use
<?php include('')?>
to call the key for the encryption?
Thanks
Access to your data should currently be constrained by a username/password for MySQL - Where do you store that?
Adding encryption into the mix raises the possibility of splitting the things-you-need-to-know-to-access-the-data across different substrates - with different exposures.
The link in the comment by Mehdi covers some of the options at a fairly abstract level. It doesn't mention, for example, storing the key at the client. But the choice of which method(s) you use depends on the infrastructure, code management, deployment and operational processes in place. The right choice for a low end shared web-hosting service is not the right choice for a dedicated datacentre and vice versa.
You do propose a specific method for managing the key: storing it outside the document root limits access. If you go further and store it in something which is recognized as PHP code by your webserver then access via the webserver should only expose the output of the PHP code - conversely if it were stored in a text file, and someone could get the webserver to serve the file, they would have access to the key.
OTOH its not a great solution if the key hows up in your github repository, or if other people have access to your filesystem/backups/logs.
You need to think about about how you develop code, whom should be able to use the key, whom should be able to see the key itself, whom should definitely not be able to see the key, how your backups are managed, whom has access to your storage.....
It is impossible to provide sufficient information in a question here on SO to get an informed and definitive answer.
At the bottom of the above answer,
I've added:
/*
$key will store in the database in refrence of this content and this key will use to decrypt the data as given below
$
*/
$content = 'blahlol';
$aes = new AES_Encrypt();
$encryptedData = $aes->setData($content)->encrypt()->getEncryptedString();
$key = $aes->getKey();
echo $encryptedData;
echo '<br>';
$decryptedData = $aes->setData($encryptedData)->decrypt()->getDecryptedString();
echo $decryptedData;
//The code above outputs an encrypted string followed by "blahlol" which is my $content variable.
//Below, I'm trying to grab the encrypted string from the database and decrypt it. However it outputs nothing
echo '<br><br><br><br>Database:<br><br>';
$sql = "SELECT * from data WHERE id = '1'";
$result = $con->query($sql);
while($rowLol = $result->fetch_assoc()) {
echo $rowLol['data']; //Outputs encrypted string
$aes = new AES_Encrypt($key);
$decryptedData = $aes->setData($rowLol['data'])->decrypt()->getDecryptedString();
echo $decryptedData; //Meant to output decrypted string (blahlol)
}
?>
At the top, it outputs the encrypted string of "blahlol" followed by plaintext "blahlol" after decryption. However I'm trying to decrypt it by getting the encrypted string for the database.
As noted in the code, the decrypted part outputs nothing.
There are two way you can keep encrypted data.
File
Databse
If data is less you can manage encrypted data in the database and if data is large then it is good to store encrypted data in file outside the public folder.
To make data more secure, use unique key to encrypt every data and save that unique key in the database including data reference value, so when you will decrypt data using referred unique key from the database.
Create php class to handle this.
<?php
class AES_Encrypt {
/**
* #var string
*/
private $key;
/**
*
* #var String
*/
private $string;
/**
*
* #var String
*/
private $encryptedString;
/**
*
* #var String
*/
private $decryptedString;
/**
* Constructor
*/
public function __construct($key = null) {
if (empty($key)) {
$this->setKey(md5($this->randomStr(5)) . '.' . base64_encode(openssl_random_pseudo_bytes(32)));
} else {
$this->setKey($key);
}
}
/**
*
* #param String $key
* #return \AES_Encrypt
*/
public function setKey($key) {
$this->key = $key;
return $this;
}
/**
* Return security key
* #return string
*/
public function getKey() {
return $this->key;
}
/**
*
* #param type $string
* #return \AES_Encrypt
*/
public function setData($string) {
$this->string = $string;
return $this;
}
/**
*
* #return string
*/
public function getData() {
return $this->string;
}
/**
* Convert encrypt string from plain
* #return \AES_Encrypt
*/
public function encrypt() {
$privateKey = explode('.', $this->getKey(), 2);
// Remove the base64 encoding from our key
$encryption_key = base64_decode($privateKey[1]);
// 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($this->getData(), 'aes-256-cbc', $encryption_key, 0, $iv);
// The $iv is just as important as the key for decrypting, so save it with our encrypted data using a unique separator (::)
$this->encryptedString = base64_encode($encrypted . '::' . $iv);
return $this;
}
/**
*
* #return string
*/
public function getEncryptedString() {
return $this->encryptedString;
}
/**
*
* #return string
*/
public function getDecryptedString() {
return $this->decryptedString;
}
/**
* Convert decrypt string
*/
public function decrypt() {
$privateKey = explode('.', $this->getKey(), 2);
// Remove the base64 encoding from our key
$encryption_key = base64_decode($privateKey[1]);
// To decrypt, split the encrypted data from our IV - our unique separator used was "::"
list($encrypted_data, $iv) = explode('::', base64_decode($this->getData()), 2);
$this->decryptedString = openssl_decrypt($encrypted_data, 'aes-256-cbc', $encryption_key, 0, $iv);
return $this;
}
/**
*
* #param type $length
* #return string
*/
public function randomStr($length = 5) {
$string = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$charactersLength = strlen($string);
$str = '';
for ($i = 0; $i < $length; $i++) {
$str .= $string[rand(0, $charactersLength - 1)];
}
return $str;
}
}
$aes = new AES_Encrypt();
$encryptedData = $aes->setData($content)->encrypt()->getEncryptedString();
$key = $aes->getKey();
/*
$key will store in the databse in refrence of this content and this key will use to decrypt the data as given below
*/
$aes = new AES_Encrypt($key);
$decryptedData = $aes->setData($encryptedContent)->decrypt()->getDecryptedString();

openssl_encrypt() randomly fails - IV passed is only ${x} bytes long, cipher expects an IV of precisely 16 bytes

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.

Two takes on PHP two way encryption - which one is preferable?

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.

What is the best way to store e-mail accounts data used for sending mailing to customers in PHP?

I'm using PEAR::Mail to send a lot of e-mails to our customers. I want to be able to send those e-mails using different SMTP accounts (because of different mailing types). We have like 6-7 accounts, and in the future there might be more. Each of them has different password and we want to be able to store those passwords in database so it's not hardcoded and so you can add them more easily with administrator panel.
I know I want to use encryption for password storing, but imo hashing is not an option here. I want to be able to read those passwords, not only compare the hashes.
I would like to do it with storing encrypted passwords in database but encrypt them with some algorithm. And that's where I have problem - I don't know much about that. I'm attaching my test code for encryption, but I would like your opinion on how should I improve it:
if (!function_exists('hex2bin')) {
function hex2bin($data) {
$len = strlen($data);
return pack('H' . $len, $data);
}
}
$key = $_GET['key'];
$text = $_GET['text'];
$encr = $_GET['encr'];
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB);
$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
if ($text != null) {
echo bin2hex(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $text, MCRYPT_MODE_ECB, $iv));
}
if ($encr != null) {
echo mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, hex2bin($encr), MCRYPT_MODE_ECB);
}
ECB mode is insecure and the IV is ignored with this mode. You should really use CBC (MCRYPT_MODE_CBC) instead.
When using CBC an IV is required for encryption and the same IV is required for decryption, so you need to hang on to this value (but don't use the same IV for all encryption/decryption, generating a random one as in your code example is correct). The IV does not need to be stored securely (any more securely than the encrypted data), and it's standard proceedure to prepend the IV to the encrypted data.
bin2hex($iv . mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $text, MCRYPT_MODE_CBC, $iv));
When decrypting you strip off the IV and pass it in to mcrypt_decrypt.
$cipherTextDecoded = hex2bin($encr);
$iv = substr($cipherTextDecoded, 0, $iv_size);
$cipherText = substr($cipherTextDecoded, $iv_size);
mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $cipherText, MCRYPT_MODE_CBC, $iv);
Also note that you should be using a binary key. $_GET['key'] is returning a string of text, and because you're not hex decoding it your keyspace is limited to all possible 256-bit strings rather than all 256-bit binary values.
Further, it's a bit misleading in PHP, but the 256 in MCRYPT_RIJNDAEL_256 refers to the block size, not the strength of the encryption. If you want to use 256 bit encryption, just pass a 256 bit key to the mcrypt functions. If that was your goal I'd consider using MCRYPT_RIJNDAEL_128 instead, this will make the encrypted text compatible with AES-128. If you ever need to decrypt the data in some other system (unlikely I know), it's much easier to find an AES-128 imeplementation than Rijindael 256.
I created a class which does everything what Syon mentioned. I'm attaching it here for future reference, if anyone would like to use it then feel free.
<?php
if (!function_exists('hex2bin')) {
function hex2bin($data) {
$len = strlen($data);
return pack('H' . $len, $data);
}
}
/**
* Encipherer - Class used for encoding and decoding
*
* #author kelu
* #version $Id$
*
*/
class Encipherer {
private $key;
private $iv_size;
private $mode = MCRYPT_MODE_CBC;
private $algorithm = MCRYPT_RIJNDAEL_256;
private $rand = MCRYPT_RAND;
/**
* returns singleton
*
* #return Encipherer
*/
public static function Instance()
{
static $inst = null;
if ($inst === null) {
$inst = new Encipherer;
}
return $inst;
}
private function __construct($key = '') {
$this->iv_size = mcrypt_get_iv_size($this->algorithm, $this->mode);
$this->key = $this->key = hex2bin($key);
}
private function __clone()
{
return Encipherer::Instance();
}
public function setKey($key) {
$this->key = $this->key = hex2bin($key);
}
public function encrypt($text) {
$iv = mcrypt_create_iv($this->iv_size, $this->rand);
return bin2hex($iv . mcrypt_encrypt($this->algorithm, $this->key, $text, $this->mode, $iv));
}
public function decrypt($text) {
$cipherTextDecoded = hex2bin($text);
$iv = substr($cipherTextDecoded, 0, $this->iv_size);
$cipherText = substr($cipherTextDecoded, $this->iv_size);
return mcrypt_decrypt($this->algorithm, $this->key, $cipherText, $this->mode, $iv);
}
}
?>
Example usage:
<?
$enc = Encipherer::Instance();
$enc->setKey('1234qwerty');
$encrypted = $enc->encrypt('secret message');
$decrypted = $enc->decrypt($encrypted);
?>

Categories