PHP AES encrypt / decrypt
Solution 1
$sDecrypted
and $sEncrypted
were undefined in your code. See a solution that works (but is not secure!):
STOP!
This example is insecure! Do not use it!
$Pass = "Passwort";
$Clear = "Klartext";
$crypted = fnEncrypt($Clear, $Pass);
echo "Encrypred: ".$crypted."</br>";
$newClear = fnDecrypt($crypted, $Pass);
echo "Decrypred: ".$newClear."</br>";
function fnEncrypt($sValue, $sSecretKey)
{
return rtrim(
base64_encode(
mcrypt_encrypt(
MCRYPT_RIJNDAEL_256,
$sSecretKey, $sValue,
MCRYPT_MODE_ECB,
mcrypt_create_iv(
mcrypt_get_iv_size(
MCRYPT_RIJNDAEL_256,
MCRYPT_MODE_ECB
),
MCRYPT_RAND)
)
), "\0"
);
}
function fnDecrypt($sValue, $sSecretKey)
{
return rtrim(
mcrypt_decrypt(
MCRYPT_RIJNDAEL_256,
$sSecretKey,
base64_decode($sValue),
MCRYPT_MODE_ECB,
mcrypt_create_iv(
mcrypt_get_iv_size(
MCRYPT_RIJNDAEL_256,
MCRYPT_MODE_ECB
),
MCRYPT_RAND
)
), "\0"
);
}
But there are other problems in this code which make it insecure, in particular the use of ECB (which is not an encryption mode, only a building block on top of which encryption modes can be defined). See Fab Sa's answer for a quick fix of the worst problems and Scott's answer for how to do this right.
Solution 2
Please use an existing secure PHP encryption library
It's generally a bad idea to write your own cryptography unless you have experience breaking other peoples' cryptography implementations.
None of the examples here authenticate the ciphertext, which leaves them vulnerable to bit-rewriting attacks.
If you can install PECL extensions, libsodium is even better
<?php
// PECL libsodium 0.2.1 and newer
/**
* Encrypt a message
*
* @param string $message - message to encrypt
* @param string $key - encryption key
* @return string
*/
function safeEncrypt($message, $key)
{
$nonce = \Sodium\randombytes_buf(
\Sodium\CRYPTO_SECRETBOX_NONCEBYTES
);
return base64_encode(
$nonce.
\Sodium\crypto_secretbox(
$message,
$nonce,
$key
)
);
}
/**
* 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);
$nonce = mb_substr($decoded, 0, \Sodium\CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
$ciphertext = mb_substr($decoded, \Sodium\CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');
return \Sodium\crypto_secretbox_open(
$ciphertext,
$nonce,
$key
);
}
Then to test it out:
<?php
// This refers to the previous code block.
require "safeCrypto.php";
// Do this once then store it somehow:
$key = \Sodium\randombytes_buf(\Sodium\CRYPTO_SECRETBOX_KEYBYTES);
$message = 'We are all living in a yellow submarine';
$ciphertext = safeEncrypt($message, $key);
$plaintext = safeDecrypt($ciphertext, $key);
var_dump($ciphertext);
var_dump($plaintext);
This can be used in any situation where you are passing data to the client (e.g. encrypted cookies for sessions without server-side storage, encrypted URL parameters, etc.) with a reasonably high degree of certainty that the end user cannot decipher or reliably tamper with it.
Since libsodium is cross-platform, this also makes it easier to communicate with PHP from, e.g. Java applets or native mobile apps.
Note: If you specifically need to add encrypted cookies powered by libsodium to your app, my employer Paragon Initiative Enterprises is developing a library called Halite that does all of this for you.
Solution 3
If you don't want to use a heavy dependency for something solvable in 15 lines of code, use the built in OpenSSL functions. Most PHP installations come with OpenSSL, which provides fast, compatible and secure AES encryption in PHP. Well, it's secure as long as you're following the best practices.
The following code:
- uses AES256 in CBC mode
- is compatible with other AES implementations, but not mcrypt, since mcrypt uses PKCS#5 instead of PKCS#7.
- generates a key from the provided password using SHA256
- generates a hmac hash of the encrypted data for integrity check
- generates a random IV for each message
- prepends the IV (16 bytes) and the hash (32 bytes) to the ciphertext
- should be pretty secure
IV is a public information and needs to be random for each message. The hash ensures that the data hasn't been tampered with.
function encrypt($plaintext, $password) {
$method = "AES-256-CBC";
$key = hash('sha256', $password, true);
$iv = openssl_random_pseudo_bytes(16);
$ciphertext = openssl_encrypt($plaintext, $method, $key, OPENSSL_RAW_DATA, $iv);
$hash = hash_hmac('sha256', $ciphertext . $iv, $key, true);
return $iv . $hash . $ciphertext;
}
function decrypt($ivHashCiphertext, $password) {
$method = "AES-256-CBC";
$iv = substr($ivHashCiphertext, 0, 16);
$hash = substr($ivHashCiphertext, 16, 32);
$ciphertext = substr($ivHashCiphertext, 48);
$key = hash('sha256', $password, true);
if (!hash_equals(hash_hmac('sha256', $ciphertext . $iv, $key, true), $hash)) return null;
return openssl_decrypt($ciphertext, $method, $key, OPENSSL_RAW_DATA, $iv);
}
Usage:
$encrypted = encrypt('Plaintext string.', 'password'); // this yields a binary string
echo decrypt($encrypted, 'password');
// decrypt($encrypted, 'wrong password') === null
edit: Updated to use hash_equals
and added IV to the hash.
Solution 4
For information MCRYPT_MODE_ECB
doesn't use the IV (initialization vector). ECB mode divide your message into blocks and each block is encrypted separately. I really don't recommended it.
CBC mode use the IV to make each message unique. CBC is recommended and should be used instead of ECB.
Example :
<?php
$password = "myPassword_!";
$messageClear = "Secret message";
// 32 byte binary blob
$aes256Key = hash("SHA256", $password, true);
// for good entropy (for MCRYPT_RAND)
srand((double) microtime() * 1000000);
// generate random iv
$iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC), MCRYPT_RAND);
$crypted = fnEncrypt($messageClear, $aes256Key);
$newClear = fnDecrypt($crypted, $aes256Key);
echo
"IV: <code>".$iv."</code><br/>".
"Encrypred: <code>".$crypted."</code><br/>".
"Decrypred: <code>".$newClear."</code><br/>";
function fnEncrypt($sValue, $sSecretKey) {
global $iv;
return rtrim(base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $sSecretKey, $sValue, MCRYPT_MODE_CBC, $iv)), "\0\3");
}
function fnDecrypt($sValue, $sSecretKey) {
global $iv;
return rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $sSecretKey, base64_decode($sValue), MCRYPT_MODE_CBC, $iv), "\0\3");
}
You have to stock the IV to decode each message (IV are not secret). Each message is unique because each message has an unique IV.
- More informations about mode of operation (wikipedia).
Solution 5
This is a working solution of AES encryption
- implemented using openssl
. It uses the Cipher Block Chaining Mode (CBC-Mode). Thus, alongside data
and key
, you can specify iv
and block size
<?php
class AESEncryption {
protected $key;
protected $data;
protected $method;
protected $iv;
/**
* Available OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING
*
* @var type $options
*/
protected $options = 0;
/**
*
* @param type $data
* @param type $key
* @param type $iv
* @param type $blockSize
* @param type $mode
*/
public function __construct($data = null, $key = null, $iv = null, $blockSize = null, $mode = 'CBC') {
$this->setData($data);
$this->setKey($key);
$this->setInitializationVector($iv);
$this->setMethod($blockSize, $mode);
}
/**
*
* @param type $data
*/
public function setData($data) {
$this->data = $data;
}
/**
*
* @param type $key
*/
public function setKey($key) {
$this->key = $key;
}
/**
* CBC 128 192 256
CBC-HMAC-SHA1 128 256
CBC-HMAC-SHA256 128 256
CFB 128 192 256
CFB1 128 192 256
CFB8 128 192 256
CTR 128 192 256
ECB 128 192 256
OFB 128 192 256
XTS 128 256
* @param type $blockSize
* @param type $mode
*/
public function setMethod($blockSize, $mode = 'CBC') {
if($blockSize==192 && in_array('', array('CBC-HMAC-SHA1','CBC-HMAC-SHA256','XTS'))){
$this->method=null;
throw new Exception('Invalid block size and mode combination!');
}
$this->method = 'AES-' . $blockSize . '-' . $mode;
}
/**
*
* @param type $data
*/
public function setInitializationVector($iv) {
$this->iv = $iv;
}
/**
*
* @return boolean
*/
public function validateParams() {
if ($this->data != null &&
$this->method != null ) {
return true;
} else {
return FALSE;
}
}
//it must be the same when you encrypt and decrypt
protected function getIV() {
return $this->iv;
}
/**
* @return type
* @throws Exception
*/
public function encrypt() {
if ($this->validateParams()) {
return trim(openssl_encrypt($this->data, $this->method, $this->key, $this->options,$this->getIV()));
} else {
throw new Exception('Invalid params!');
}
}
/**
*
* @return type
* @throws Exception
*/
public function decrypt() {
if ($this->validateParams()) {
$ret=openssl_decrypt($this->data, $this->method, $this->key, $this->options,$this->getIV());
return trim($ret);
} else {
throw new Exception('Invalid params!');
}
}
}
Sample usage:
<?php
$data = json_encode(['first_name'=>'Dunsin','last_name'=>'Olubobokun','country'=>'Nigeria']);
$inputKey = "W92ZB837943A711B98D35E799DFE3Z18";
$iv = "tuqZQhKP48e8Piuc";
$blockSize = 256;
$aes = new AESEncryption($data, $inputKey, $iv, $blockSize);
$enc = $aes->encrypt();
$aes->setData($enc);
$dec=$aes->decrypt();
echo "After encryption: ".$enc."<br/>";
echo "After decryption: ".$dec."<br/>";
Andreas Prang
Updated on February 03, 2022Comments
-
Andreas Prang over 2 years
I found an example for en/decoding strings in PHP. At first it looks very good but it wont work :-(
Does anyone know what the problem is?
$Pass = "Passwort"; $Clear = "Klartext"; $crypted = fnEncrypt($Clear, $Pass); echo "Encrypted: ".$crypted."</br>"; $newClear = fnDecrypt($crypted, $Pass); echo "Decrypted: ".$newClear."</br>"; function fnEncrypt($sValue, $sSecretKey) { return trim(base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $sSecretKey, $sDecrypted, MCRYPT_MODE_ECB, mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB), MCRYPT_RAND)))); } function fnDecrypt($sValue, $sSecretKey) { return trim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $sSecretKey, base64_decode($sEncrypted), MCRYPT_MODE_ECB, mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB), MCRYPT_RAND))); }
The result is:
Encrypted:
boKRNTYYNp7AiOvY1CidqsAn9wX4ufz/D9XrpjAOPk8=
Decrypted:
—‚(ÑÁ ^ yË~F'¸®Ó–í œð2Á_B‰Â—
-
Paul Jacobse almost 12 yearsI was using this code, and I found a bug. The trim should NOT be used!!! it should be a rtrim() with a second parameter "\0". In rare cases the first or last character of the encrypted value could be a space or return, the decryption goes wrong...
-
techie_28 over 11 yearsI am using this to store a field in the database.But it fails when It has to be matched against some user provided data even on correct input,But If I do it as a MYSQL search from the PHPMYADMIN,it works fine.Any Ideas?
-
Thunder Rabbit over 11 yearsThank you @PaulJacobse for the tweak; fixed version available here stackoverflow.com/review/suggested-edits/1314732
-
mgutt about 11 yearsYou should add an example to clearify the usage of
aes256Key
. This example shows how to use it: php.net/manual/fr/book.mcrypt.php#107483 -
Fabien Sa about 11 yearsIt's pretty the same but my example doesn't use salt for clarity.
-
Luc over 10 yearsCould you obfuscate that anymore please? I'm afraid I can, with some effort, still read what your code does. </sarcasm mode> Code should be self-descriptive. Give things like the output of mcrypt_get_iv_size an appropriate variable name and then use it. This kind of indentation is hard to read. Unless you're used to Lisp, maybe, but most PHP programmers aren't I'd say. I'm not saying the OP's (question asker's) code was any better, but as a good answer you might improve the code too.
-
Polynomial about 10 years-1 for ECB. See the Wikipedia entry for details on why it's a poor choice of block cipher mode.
-
Clément about 10 years-1: Passing different IVs in the
encrypt
anddecrypt
functions makes no sense at all. The only reason why this works is because the ECB mode does not use an initialization vector at all, so any value would do and produce the same output. -
Clément about 10 years+1. The top answer generates random IVs to feed a system (ECB) that doesn't need any.
-
Ashwin Parmar almost 10 yearsIt is working, Great Should I use md5() Value as a Secret Key?
-
Admin almost 10 yearsIf I have understood it properly, the $password = "myPassword_!" becomes part of the encryption algorithm, right?
-
Sergio Viudes almost 10 yearsUsing MCRYPT_RIJNDAEL_128 this worked for me: rtrim($output, "\x00..\x1F")
-
Maarten Bodewes over 9 yearsBe warned that the code above does not use AES nor does it use PKCS#7 padding, which means it will be incompatible with any other system out there. I'm the guy that fixed that example code for
mcrypt_encrypt
. -
Maarten Bodewes over 9 yearsThis code does not use AES, it uses Rijndael with a block cipher of 256 bits (AES uses a block size of 128 bits). This code is using ECB etc. and should not be considered a secure example on how to perform encryption/decryption.
-
wappy about 9 yearsI don't know why this answer is so up voted. Just because 'it works' it doesn't mean it secured and should be used on production environments. MCRYPT_MODE_ECB use is highly descurged and even PHP mcrypt_ecb function has been DEPRECATED as of PHP 5.5.0. Relying on this function is highly discouraged. Instead you should use MCRYPT_MODE_CBC mode: wpy.me/blog/15-encrypt-and-decrypt-data-in-php-using-aes-256
-
Andrew over 8 yearsthe library you provided, returns encrypted message in binary format - correct? Is it possible to return in a simple string format? Thanks
-
Erwin Mayer over 7 yearsA C# .NET port is available in this GitHub repo, in case anyone needs it : github.com/mayerwin/SaferCrypto. Thank you @ScottArciszewski.
-
jww almost 7 years
-
jww almost 7 years
-
Riking almost 7 yearsNo @Andrew, it returns the message with base64 encoding, which is a simple string
-
Moritz Friedrich over 6 yearsHey there, I converted this in a slightly easier to use class, see here: gist.github.com/Radiergummi/b326219f55edb33759791a78a1c134c3. Thank you for your work!
-
MBourne over 5 yearsEvery ssl_encrypt/decrypt example I come across (including the ones on this page) have the 2 processes back to back. But if you just want to decrypt some encrypted text from a file, it is not successful. Can anyone please answer my question here: stackoverflow.com/questions/53238812/…
-
blade over 5 years@MBourne this example is compatible with any standard AES-256-CBC implementation. The problem is likely somewhere else. For example you may be reading the file as a text (instead of binary), the crypto functions have quite specific requirements, like for IV length, AES supports a other algorithms besides CBC, ... a lot of factors in play here.
-
MBourne over 5 years@blade: I gave up and now use str_rot13(). I just needed jumbled text - security wasn't the issue. Thanks anyway.
-
FireEmerald about 5 yearsWhy don't you @blade also include the
$iv
while generating thehash_hmac()
? Wouldn't it be better to also verify, that the nonce wasn't changed? -
blade about 5 years@FireEmerald I don't think it's necessary, as IV is already "baked" into the ciphertext. If you changed the IV, you wouldn't be able to decipher the data, so integrity is guaranteed.
-
Yohannes Kristiawan about 5 yearsI am trying many AES-256 functions I can find on the internet with no success. This one just works! THANKS @blade!!!
-
Shahin about 5 yearsIt's not a good practice to compare hashes using the equality operator, You should use
hash_equals()
instead, as it's vulnerable to the timing attack, more information here -
Luke Joshua Park almost 5 yearsThis answer is almost there, but needs... 1) A better KDF, SHA-256 is a very poor KDF. Use PBKDF2 at the very least, but Argon2/bcrypt would be better. 2) The IV needs to be included in the HMAC - the point of an HMAC is to ensure decryption will either result in the plaintext or fail - not including the IV gives rise to a situation where the user thinks they're getting the original plaintext, but aren't. 3) Use a time safe comparison when comparing the hashes, otherwise this code could be vulnerable to timing attacks. 4) Don't use the same key for HMAC as you do for AES.
-
Luke Joshua Park almost 5 yearsThis code leaves IV handling to the user (who *will do it poorly) and also doesn't include any integrity checks. Not good crypto code.
-
blade almost 5 years@LukeJoshuaPark Thank you for the suggestions, they're all valid and I'll update the answer - one question though - when using a salted KDF like PBKDF2, is it necessary to generate salt for each key and bundle the salts with ciphertext for later decryption or can I safely reuse the IV? Argon2 is unfortunately not readily available in most PHP installations.
-
relipse about 4 yearsis it possible to base64 encode the stuff so it is actual letters and numbers characters? readable in a command line or what not
-
Mikko Rantalainen about 4 years@LukeJoshuaPark: about your point (1): why do you think one should use e.g. PBKDF2 or argon2? I think we can assume that the key is secure and sha-256 here is not about key derivation but converting string input into binary 256 bit output. It's not like output of SHA-256 is leaked to encrypted output as plaintext so where's the problem here?
-
Luke Joshua Park about 4 years@MikkoRantalainen We can't assume the key is secure, specifically because it isn't a key, it's a human-selected, low-entropy password. If we use SHA-256 to derive our encryption key, which takes a very negligible amount of time, brute force attacks on passwords are quite easy. However, if we use PBKDF2 or Argon2, where we can fine-tune the time it takes to derive a password (think a few hundred milliseconds), brute forcing becomes far less feasible. Pretty much the exact same reason we wouldn't use SHA-256 as a password hash.
-
Mikko Rantalainen about 4 years@LukeJoshuaPark: OK, I assumed that the developer would understand encryption requirements and use a key with equivalent amount of entropy to encryption used. If one uses keys such as output of
openssl rand -hex 32
there's no need to avoid SHA-256 in the above code. -
Luke Joshua Park about 4 years@MikkoRantalainen The input is defined as a password though, not a key. It could be developer chosen, or it could be a user chosen password - definitely requires a decent KDF. Assuming that "developers understand encryption requirements" is a footgun - with plenty of historical evidence. WEP, for example.
-
Luke Joshua Park about 4 yearsIt isn't enough just to hash a password to use as an encryption key, see comments on blade's answer.
-
Mikko Rantalainen about 4 years@LukeJoshuaPark: I added a new answer with tweaked implementation: stackoverflow.com/a/61016088/334451
-
Luke Joshua Park about 4 yearsNice edits, looks good! The one thing, like we previously discussed, is that this is vulnerable to brute-force through lookup tables because we trust the user to provide a "cryptokey" that has sufficient entropy. This problem could be fixed with a real KDF rather than SHA-256. Otherwise, looks good!
-
Mikko Rantalainen about 4 years@LukeJoshuaPark: Yeah, I think these methods would be the low level implementation using a real key. Perhaps I should add a method for using Key derivation function (KDF) to go from user password to encryption key. However, such method should not claim to magically have 256 bit of entropy from low quality user password. Instead, KDF logically is an injection from e.g. 32 bit key to 256 bit keyspace where attacker does not have an easy way to simply enumerate all the 2^32 possible keys out of 256 bit keyspace.
-
Mikko Rantalainen about 4 yearsAssuming that we have only password (=no storage for salt), the KDF would need to be something like
hash_pbkdf2("sha256", $password, $password, 500000)
. I'm not sure if even that's enough with low quality passwords when we consider SHA-256 performance on GPUs. -
Mikko Rantalainen about 4 years@LukeJoshuaPark do you think it would be okay to generate hash key and encryption key from the same password? For example
$hash_key = hash_pbkdf2("sha256", "$password", "hash$password", 500000)
and$encryption_key = hash_pbkdf2("sha256", $password, "enc$password", 500000)
. -
Luke Joshua Park about 4 yearsYes - although I'd recommend running PBKDF2 with SHA-512 rather than SHA-256 if you're going to do that. This allows the first 256 bits of output to be the encryption key and the last 256 bits of output to be the hash key.
-
WilliamK almost 4 yearsI found that this worked, while the others above produced strange characters and didn't decrypt to anything legible.
-
WilliamK almost 4 yearsI am finding that this works intermittently. Each time it encrypts it produces a different answer. Sometimes it does not decrypt.
-
Marco Concas over 3 yearsTry this: encryptString("mysecretText", "myPassword", "hex") | decryptString($enc, "myPassword", "hex") @WilliamK
-
Chris almost 3 yearsIf you want to store this in a database you should do base64_encode on the return value of the encrypt function and base64_decode on the $ivHashCiphertext value of the decrypt function. If not you might run into encoding problems with the database.
-
naman1994 almost 3 years@blade I used it in PHP and it works perfectly fine. But when I encrypt the string and serve it as response of an API it's not quite working. I tried to use
Content-Type: text/plain
in httpresponse, but I'm not sure what really is the issue. Any help would be appreciated. -
blade almost 3 years@naman1994 The output a binary blob, so you need to encode it into plain text - ie. using base64_encode, bin2hex, etc.
-
Jeaf Gilbert almost 3 yearsI encrypt the string in Javascript using crypto-es and want to decrypt it in PHP using your function, but it returns null. The passphrase are the same in JS and PHP. I already set encoding param using 'base64', but no luck. What am I possibly missing here?
-
Marco Concas almost 3 yearsI should check the function you use on JS to understand the problem, anyway I have tested this with C# and everything works perfectly.