277

What is the simplest way of doing two way encryption in common PHP installs?

I need to be able to encrypt data with a string key, and use the same key to decrypt on the other end.

The security isn't as big of a concern as the portability of the code, so I'd like to be able to keep things as simple as possible. Currently, I am using an RC4 implementation, but if I can find something natively supported I figure I can save a lot of unnecessary code.

Braiam
  • 1
  • 11
  • 47
  • 78
user1206970
  • 2,797
  • 3
  • 14
  • 4
  • 6
    [Just XOR your string.](http://www.go4expert.com/forums/showthread.php?t=5555) – Thunraz Feb 13 '12 at 14:28
  • 3
    For general purpose encryption, use [defuse/php-encryption/](https://github.com/defuse/php-encryption/) instead of rolling your own. – Scott Arciszewski May 11 '15 at 04:37
  • 3
    Hands away from https://github.com/defuse/php-encryption/ - it is slower by orders of magnitude than mcrypt. – Eugen Rieck May 11 '15 at 16:12
  • `defuse/php-encryption` uses `openssl()` which can leverage `AES-NI` (which libmcrypt does not) and, depending on how much data you are encrypting, can therefore provide much faster encryption throughput than mcrypt. Please post a side-by-side benchmark to substantiate your claim. – Scott Arciszewski May 11 '15 at 16:17
  • Also, @EugenRieck, encryption probably will not be the bottleneck of your application. If it is, use libsodium ;) – Scott Arciszewski May 12 '15 at 13:40
  • 1
    @Scott Thinking along the lines of "this will probably not be the bottleneck" is what brought us a lot of bad software. – Eugen Rieck May 12 '15 at 14:30
  • 5
    If you're really encrypting/decrypting a lot of data to the point that the milliseconds it costs bogs your application down, bite the bullet and switch to libsodium. `Sodium::crypto_secretbox()` and `Sodium::crypto_secretbox_open()` are secure and performant. – Scott Arciszewski May 12 '15 at 18:53
  • Since my previous comment was posted, libsodium's PHP bindings changed. Now you want to use `\Sodium\crypto_secretbox()` and `\Sodium\crypto_secretbox_open()`. – Scott Arciszewski Oct 07 '15 at 05:25
  • I'd suggest you to use the new Sodium library in PHP 7.1+. This library only needs a secret and public key as setting, and for each row a unique nonce. This (wrapper) library might help you with that, the methods are easy to use so you don't have to figure out the Sodium library yourself: https://github.com/internetpixels/sodium-encryption – Petervw Jun 17 '18 at 10:16
  • rot13 with null pass-code, imho – Agnius Vasiliauskas Dec 14 '18 at 13:47

6 Answers6

287

Important: Unless you have a very particular use-case, do not encrypt passwords, use a password hashing algorithm instead. When someone says they encrypt their passwords in a server-side application, they're either uninformed or they're describing a dangerous system design. Safely storing passwords is a totally separate problem from encryption.

Be informed. Design safe systems.

Portable Data Encryption in PHP

If you're using PHP 5.4 or newer and don't want to write a cryptography module yourself, I recommend using an existing library that provides authenticated encryption. The library I linked relies only on what PHP provides and is under periodic review by a handful of security researchers. (Myself included.)

If your portability goals do not prevent requiring PECL extensions, libsodium is highly recommended over anything you or I can write in PHP.

Update (2016-06-12): You can now use sodium_compat and use the same crypto libsodium offers without installing PECL extensions.

If you want to try your hand at cryptography engineering, read on.


First, you should take the time to learn the dangers of unauthenticated encryption and the Cryptographic Doom Principle.

  • Encrypted data can still be tampered with by a malicious user.
  • Authenticating the encrypted data prevents tampering.
  • Authenticating the unencrypted data does not prevent tampering.

Encryption and Decryption

Encryption in PHP is actually simple (we're going to use openssl_encrypt() and openssl_decrypt() once you have made some decisions about how to encrypt your information. Consult openssl_get_cipher_methods() for a list of the methods supported on your system. The best choice is AES in CTR mode:

  • aes-128-ctr
  • aes-192-ctr
  • aes-256-ctr

There is currently no reason to believe that the AES key size is a significant issue to worry about (bigger is probably not better, due to bad key-scheduling in the 256-bit mode).

Note: We are not using mcrypt because it is abandonware and has unpatched bugs that might be security-affecting. Because of these reasons, I encourage other PHP developers to avoid it as well.

Simple Encryption/Decryption Wrapper using OpenSSL

class UnsafeCrypto
{
    const METHOD = 'aes-256-ctr';
    
    /**
     * Encrypts (but does not authenticate) a message
     * 
     * @param string $message - plaintext message
     * @param string $key - encryption key (raw binary expected)
     * @param boolean $encode - set to TRUE to return a base64-encoded 
     * @return string (raw binary)
     */
    public static function encrypt($message, $key, $encode = false)
    {
        $nonceSize = openssl_cipher_iv_length(self::METHOD);
        $nonce = openssl_random_pseudo_bytes($nonceSize);
        
        $ciphertext = openssl_encrypt(
            $message,
            self::METHOD,
            $key,
            OPENSSL_RAW_DATA,
            $nonce
        );
        
        // Now let's pack the IV and the ciphertext together
        // Naively, we can just concatenate
        if ($encode) {
            return base64_encode($nonce.$ciphertext);
        }
        return $nonce.$ciphertext;
    }
    
    /**
     * Decrypts (but does not verify) a message
     * 
     * @param string $message - ciphertext message
     * @param string $key - encryption key (raw binary expected)
     * @param boolean $encoded - are we expecting an encoded string?
     * @return string
     */
    public static function decrypt($message, $key, $encoded = false)
    {
        if ($encoded) {
            $message = base64_decode($message, true);
            if ($message === false) {
                throw new Exception('Encryption failure');
            }
        }

        $nonceSize = openssl_cipher_iv_length(self::METHOD);
        $nonce = mb_substr($message, 0, $nonceSize, '8bit');
        $ciphertext = mb_substr($message, $nonceSize, null, '8bit');
        
        $plaintext = openssl_decrypt(
            $ciphertext,
            self::METHOD,
            $key,
            OPENSSL_RAW_DATA,
            $nonce
        );
        
        return $plaintext;
    }
}

Usage Example

$message = 'Ready your ammunition; we attack at dawn.';
$key = hex2bin('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f');

$encrypted = UnsafeCrypto::encrypt($message, $key);
$decrypted = UnsafeCrypto::decrypt($encrypted, $key);

var_dump($encrypted, $decrypted);

Demo: https://3v4l.org/jl7qR


The above simple crypto library still is not safe to use. We need to authenticate ciphertexts and verify them before we decrypt.

Note: By default, UnsafeCrypto::encrypt() will return a raw binary string. Call it like this if you need to store it in a binary-safe format (base64-encoded):

$message = 'Ready your ammunition; we attack at dawn.';
$key = hex2bin('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f');

$encrypted = UnsafeCrypto::encrypt($message, $key, true);
$decrypted = UnsafeCrypto::decrypt($encrypted, $key, true);

var_dump($encrypted, $decrypted);

Demo: http://3v4l.org/f5K93

Simple Authentication Wrapper

class SaferCrypto extends UnsafeCrypto
{
    const HASH_ALGO = 'sha256';
    
    /**
     * Encrypts then MACs a message
     * 
     * @param string $message - plaintext message
     * @param string $key - encryption key (raw binary expected)
     * @param boolean $encode - set to TRUE to return a base64-encoded string
     * @return string (raw binary)
     */
    public static function encrypt($message, $key, $encode = false)
    {
        list($encKey, $authKey) = self::splitKeys($key);
        
        // Pass to UnsafeCrypto::encrypt
        $ciphertext = parent::encrypt($message, $encKey);
        
        // Calculate a MAC of the IV and ciphertext
        $mac = hash_hmac(self::HASH_ALGO, $ciphertext, $authKey, true);
        
        if ($encode) {
            return base64_encode($mac.$ciphertext);
        }
        // Prepend MAC to the ciphertext and return to caller
        return $mac.$ciphertext;
    }
    
    /**
     * Decrypts a message (after verifying integrity)
     * 
     * @param string $message - ciphertext message
     * @param string $key - encryption key (raw binary expected)
     * @param boolean $encoded - are we expecting an encoded string?
     * @return string (raw binary)
     */
    public static function decrypt($message, $key, $encoded = false)
    {
        list($encKey, $authKey) = self::splitKeys($key);
        if ($encoded) {
            $message = base64_decode($message, true);
            if ($message === false) {
                throw new Exception('Encryption failure');
            }
        }
        
        // Hash Size -- in case HASH_ALGO is changed
        $hs = mb_strlen(hash(self::HASH_ALGO, '', true), '8bit');
        $mac = mb_substr($message, 0, $hs, '8bit');
        
        $ciphertext = mb_substr($message, $hs, null, '8bit');
        
        $calculated = hash_hmac(
            self::HASH_ALGO,
            $ciphertext,
            $authKey,
            true
        );
        
        if (!self::hashEquals($mac, $calculated)) {
            throw new Exception('Encryption failure');
        }
        
        // Pass to UnsafeCrypto::decrypt
        $plaintext = parent::decrypt($ciphertext, $encKey);
        
        return $plaintext;
    }
    
    /**
     * Splits a key into two separate keys; one for encryption
     * and the other for authenticaiton
     * 
     * @param string $masterKey (raw binary)
     * @return array (two raw binary strings)
     */
    protected static function splitKeys($masterKey)
    {
        // You really want to implement HKDF here instead!
        return [
            hash_hmac(self::HASH_ALGO, 'ENCRYPTION', $masterKey, true),
            hash_hmac(self::HASH_ALGO, 'AUTHENTICATION', $masterKey, true)
        ];
    }
    
    /**
     * Compare two strings without leaking timing information
     * 
     * @param string $a
     * @param string $b
     * @ref https://paragonie.com/b/WS1DLx6BnpsdaVQW
     * @return boolean
     */
    protected static function hashEquals($a, $b)
    {
        if (function_exists('hash_equals')) {
            return hash_equals($a, $b);
        }
        $nonce = openssl_random_pseudo_bytes(32);
        return hash_hmac(self::HASH_ALGO, $a, $nonce) === hash_hmac(self::HASH_ALGO, $b, $nonce);
    }
}

Usage Example

$message = 'Ready your ammunition; we attack at dawn.';
$key = hex2bin('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f');

$encrypted = SaferCrypto::encrypt($message, $key);
$decrypted = SaferCrypto::decrypt($encrypted, $key);

var_dump($encrypted, $decrypted);

Demos: raw binary, base64-encoded


If anyone wishes to use this SaferCrypto library in a production environment, or your own implementation of the same concepts, I strongly recommend reaching out to your resident cryptographers for a second opinion before you do. They'll be able tell you about mistakes that I might not even be aware of.

You will be much better off using a reputable cryptography library.

biziclop
  • 14,466
  • 3
  • 49
  • 65
Scott Arciszewski
  • 33,610
  • 16
  • 89
  • 206
  • 3
    So, I am just trying to get the UnsafeCrypto working first. The encryption happens fine, but every time I run the decrypt, I am getting 'false' as the response. I am using the same key to decrypt, and passing true on the encode, as well as the decode. There is, what I assume is a typeo in the example, I am wondering if that is where my problem is coming from. Can you explain where the $mac variable is coming from, and should it simply be $iv? – David C Jul 06 '15 at 21:33
  • @DavidC [Yes, it should](http://stackoverflow.com/posts/30189841/revisions). Sorry for not noticing this sooner. I've also added 3v4l.org demos of the example code. – Scott Arciszewski Jul 21 '15 at 13:00
  • 1
    @EugenRieck The OpenSSL cipher implementations are probably the only parts that don't suck, and it's the only way to leverage AES-NI in vanilla PHP. If you install on OpenBSD, PHP will be compiled against LibreSSL without the PHP code noticing a difference. Libsodium > OpenSSL any day. Also, [don't use libmcrypt](https://paragonie.com/blog/2015/05/if-you-re-typing-word-mcrypt-into-your-code-you-re-doing-it-wrong). **What would you recommend PHP developers use instead of OpenSSL?** – Scott Arciszewski Jul 22 '15 at 15:43
  • @ScottArciszewski - You were quite able to read my recommendation, when you downvoted it: We are all grown-ups here, no need to play games. EOD, let's just agree to disagree. – Eugen Rieck Aug 09 '15 at 09:20
  • supported only in PHP 5.3 and above http://php.net/manual/en/function.openssl-encrypt.php any option for lower version preferably 5.2 but encrypted cipher should be pure text? – Akshay Khale Sep 06 '15 at 14:49
  • 2
    [Neither 5.2 nor 5.3 are supported anymore](https://secure.php.net/eol.php). You should instead look into updating to a [supported version of PHP](https://secure.php.net/supported-versions.php), such a 5.6. – Scott Arciszewski Sep 06 '15 at 20:19
  • Your suggested cipher method is not available on my system. How do I know which alternatives are safe? There are literally 17 different "AES" options on my server for example. – Abhi Beckert Jul 02 '16 at 08:21
  • (`aes-*-cbc` or `aes-*-ctr`) then HMAC-SHA256 is fine. `aes-*-gcm` is better, but not available before PHP 7.1. – Scott Arciszewski Jul 02 '16 at 18:25
  • @ScottArciszewski, I have published a C# .NET port of your very helpful classes on GitHub: https://github.com/mayerwin/SaferCrypto – Erwin Mayer Jan 03 '17 at 04:19
  • The **UnsafeCrypto** class not work properly with URLs, How can we make it produce encrypted strings ready to use in URLs ? – Az.Youness Apr 06 '17 at 16:10
  • 1
    @BBeta https://paragonie.com/blog/2015/09/comprehensive-guide-url-parameter-encryption-in-php – Scott Arciszewski Apr 06 '17 at 18:45
  • Why did you do that line `hex2bin('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f')` ? what could be the risks if I instead used the key directly ? Thank you very much for sharing your experience. – Accountant م Aug 26 '17 at 08:44
  • 1
    I just did it as a demonstration of **you want binary strings, not human-readabale strings, for your keys**. – Scott Arciszewski Aug 26 '17 at 16:10
  • Excellent... But why you dont combine them into one class, like this: https://pastebin.com/sDtEtgJm – T.Todua Jan 18 '18 at 17:58
  • Well, the purpose of the example code was to demonstrate, not to enable copy-paste programming. – Scott Arciszewski Jan 18 '18 at 21:34
208

Edited:

You should really be using openssl_encrypt() & openssl_decrypt()

As Scott says, Mcrypt is not a good idea as it has not been updated since 2007.

There is even an RFC to remove Mcrypt from PHP - https://wiki.php.net/rfc/mcrypt-viking-funeral

Community
  • 1
  • 1
472084
  • 17,666
  • 10
  • 63
  • 81
  • 6
    @EugenRieck Yes, that's the point. Mcrypt doesn't receive patches. OpenSSL receives patches as soon as any vulnerability is discovered, big or small. – Greg Sep 07 '16 at 08:49
  • 7
    it would be better for such high-voted answer, to be there provided simplest examples too in answer. thanks anyway. – T.Todua Jan 18 '18 at 17:33
  • guys, just FYI => MCRYPT IS DEPRECATED. capsing so everyone should know not to use it as it gave us a myriad of issues. It deprecated since PHP 7.1 if i'm not mistaken. – clusterBuddy Jun 03 '19 at 10:29
  • Since PHP 7 the mcrypt function is remove from php codebase. So when using the latest version of php (which should be standard) you are not able to use this deprecated function anymore. – Alexander Behling Sep 12 '19 at 13:09
  • You should also mention that Mcrypt has be depreciated since PHP 7.1.0 and removed as of PHP 7.2.0. – Jonathan J. Pecany Sep 09 '20 at 03:06
  • Down-voted for not actually answering the question *with code*. – John Aug 06 '21 at 00:14
22

Use mcrypt_encrypt() and mcrypt_decrypt() with corresponding parameters. Really easy and straight forward, and you use a battle-tested encryption package.

EDIT

5 years and 4 months after this answer, the mcrypt extension is now in the process of deprecation and eventual removal from PHP.

Eugen Rieck
  • 64,175
  • 10
  • 70
  • 92
  • 39
    Battle tested and not updated for more than 8 years? – Maarten Bodewes Oct 09 '14 at 09:57
  • 5
    Well, mcrypt is in PHP7 and not deprecated - that's good enough for me. Not all code is of OpenSSL's horrible quality and needs patching every few days. – Eugen Rieck May 11 '16 at 04:30
  • 4
    mcrypt is not just horrible with regards to support. It also doesn't implement best practices like PKCS#7 compliant padding, authenticated encryption. It won't support SHA-3 or any other new algorithm as nobody is maintaining it, robbing you of an upgrade path. Furthermore it used to accept things like partial keys, performing zero padding etc. There is a good reason why it is in the process of being gradually removed from PHP. – Maarten Bodewes May 12 '16 at 15:09
  • 3
    In PHP 7.1, all mcrypt_* functions will raise an E_DEPRECATED notice. In PHP 7.1+1 (be it 7.2 or 8.0), the mcrypt extension will be moved out of core and into PECL, where people who *really* want to install it may still do so if they can install PHP extensions from PECL. – Mladen Janjetovic May 20 '16 at 11:59
13

Encrypting using openssl_encrypt() The openssl_encrypt function provides a secured and easy way to encrypt your data.

In the script below, we use the AES128 encryption method, but you may consider other kind of encryption method depending on what you want to encrypt.

<?php
$message_to_encrypt = "Yoroshikune";
$secret_key = "my-secret-key";
$method = "aes128";
$iv_length = openssl_cipher_iv_length($method);
$iv = openssl_random_pseudo_bytes($iv_length);

$encrypted_message = openssl_encrypt($message_to_encrypt, $method, $secret_key, 0, $iv);

echo $encrypted_message;
?>

Here is an explanation of the variables used :

message_to_encrypt : the data you want to encrypt secret_key : it is your ‘password’ for encryption. Be sure not to choose something too easy and be careful not to share your secret key with other people method : the method of encryption. Here we chose AES128. iv_length and iv : prepare the encryption using bytes encrypted_message : the variable including your encrypted message

Decrypting using openssl_decrypt() Now you encrypted your data, you may need to decrypt it in order to re-use the message you first included into a variable. In order to do so, we will use the function openssl_decrypt().

<?php
$message_to_encrypt = "Yoroshikune";
$secret_key = "my-secret-key";
$method = "aes128";
$iv_length = openssl_cipher_iv_length($method);
$iv = openssl_random_pseudo_bytes($iv_length);
$encrypted_message = openssl_encrypt($message_to_encrypt, $method, $secret_key, 0, $iv);

$decrypted_message = openssl_decrypt($encrypted_message, $method, $secret_key, 0, $iv);

echo $decrypted_message;
?>

The decrypt method proposed by openssl_decrypt() is close to openssl_encrypt().

The only difference is that instead of adding $message_to_encrypt, you will need to add your already encrypted message as the first argument of openssl_decrypt().

Note: The secret key and iv needs to be saved in order to decrypt.

Lonare
  • 3,581
  • 1
  • 41
  • 45
  • 1
    unless I didn't read properly, I think its worth noting that the secret key and iv needs to be saved if you want to decrypt later. I couldn't get mine going until I realized this reading this link https://www.php.net/manual/en/function.openssl-encrypt.php#example-903 – gstlouis Jun 03 '21 at 13:52
  • AGREED with gstlouis, I had to retract my up-vote because the code posted failed to take this in to consideration. However it serves as a 90% foundation and doesn't dump class nonsense in to the mix. – John Aug 06 '21 at 06:38
  • 1
    Updated the code as oer @gstlouis advice. – Lonare Mar 31 '22 at 17:55
  • Sorry - how do you get the `$iv`? I have tried displaying it and saving it to file but display is not readable and the text saved looks a bit like base64 encoded or something. I guess saving that in a script is better than a clear text password at least. – Steve Oct 30 '22 at 21:57
6

PHP 7.2 moved completely away from Mcrypt and the encryption now is based on the maintainable Libsodium library.

All your encryption needs can be basically resolved through Libsodium library.

// On Alice's computer:
$msg = 'This comes from Alice.';
$signed_msg = sodium_crypto_sign($msg, $secret_sign_key);


// On Bob's computer:
$original_msg = sodium_crypto_sign_open($signed_msg, $alice_sign_publickey);
if ($original_msg === false) {
    throw new Exception('Invalid signature');
} else {
    echo $original_msg; // Displays "This comes from Alice."
}

Libsodium documentation: https://github.com/paragonie/pecl-libsodium-doc

Hemerson Varela
  • 24,034
  • 16
  • 68
  • 69
  • 2
    The `crypto_sign` API does *not* encrypt messages - that will require one of the `crypto_aead_*_encrypt` functions. – Roger Dueck Jul 17 '19 at 18:07
3

IMPORTANT this answer is valid only for PHP 5, in PHP 7 use built-in cryptographic functions.

Here is simple but secure enough implementation:

  • AES-256 encryption in CBC mode
  • PBKDF2 to create encryption key out of plain-text password
  • HMAC to authenticate the encrypted message.

Code and examples are here: https://stackoverflow.com/a/19445173/1387163

Eugene Fidelin
  • 2,049
  • 23
  • 22
  • 1
    I'm not a cryptography expert, but having a key derived directly from a password seems like terrible idea. Rainbow tables + weak password and gone is your security. Also your link point to mcrypt functions, that are deprecated since PHP 7.1 – Slava Apr 22 '18 at 10:45
  • @Alph.Dev you are correct the answer above is only valid for PHP 5 – Eugene Fidelin Jan 10 '20 at 12:44