PHP Encryption The Right Way With LibSodium

Using cryptography in your PHP project

You probably already use at least one facet of cryptography in your PHP projects; password hashing via the password_hash and password_verify functions.

Hashing is a one-way process, transforming data of arbitrary length in to a fixed-length sequence of bytes. The same data with the same hashing algorithm will always produce the same hash. This makes it a good choice for passwords, as it is not reversible - it is not possible to take the hash and directly recover the original data used to produce it.

Thus we can store the hash without ever storing or knowing the original password, but retain the ability to verify if the password a user has entered is correct.

Sometimes, however, you need to store sensitive data securely in such a way that you can retrieve it later, safe in the knowledge that it hasn't been altered and that no attacker or unauthorized person is able to see it, even if they've gained access to your database.

For this, we need a reversible, two-way encryption process.

Encryption algorithms can broadly be divided into two main categories: symmetric (i.e. using a password or shared secret key) and asymmetric (i.e. using a public and private keypair).

We can also use cryptography techniques to sign data, which is a way to make sure that the data has not been tampered with when it is transmitted from one computer to another, or between independent systems.

Modern versions of PHP generally ship with two options for managing cryptography: OpenSSL and Sodium.

Prior to PHP 7.2, the mcrypt extension was commonly used. This extension is no longer supported, do not use it. If you haven't upgraded to PHP 7.2 yet, consider using a polyfill such as Paragonie's Sodium Compat. (Seriously though, upgrade as a matter of urgency - anything less than PHP 7.4 is already EOL'd and not supported for security updates.)

In this post, I'll be running through a tutorial on how to use the Sodium extension the right way to perform a range of common cryptography tasks, including symmetric and asymmetric encryption and message authentication.

Why Sodium?

OpenSSL is a good choice if you need to work with RSA keys, such as generating certificates. But for encrypting and signing data, it's easy to use OpenSSL the wrong way. Cryptography is hard and choosing the right algorithm, authenticating data to ensure it hasn't been altered, managing keys and all the rest of it is something you need to do yourself with confidence when you use the OpenSSL functions.

In contrast, Sodium is a higher level API which is designed to make it easier to use the cryptography functions and mitigate the risk of the user making bad choices. It provides a simple interface to secure and proven cryptography algorithms, and is designed to be used in a secure way.

In short, if you're not a cryptography expert (and I know I'm not), you're less likely to make a mistake when using Sodium.

Symmetric password encryption with Sodium

"Symmetric" means that the same key (or password) is used to both encrypt and decrypt the data.

Thus, symmetric encryption is a good choice if you need to encrypt data that is stored in a database, for later retrieval and decryption by the same system. Or encrypting large amounts of data such as a file, or any other case where only one party needs to know the secret key.

If only you are encrypting the data and only you need to decrypt the data, you can use symmetric encryption.

Using Sodium to encrypt data with PHP

<?php
    // The data we wish to encrypt.
    $message = 'Hello, this is a secret message!';
    // This generates a new, random secret key of the correct length (32 bytes).
    // The key is then converted to a string of hexadecimal characters (hexits) using
    // sodium_bin2hex(). This is like PHP's built-in bin2hex() function, but with
    // the benefit of being time-constant - a critical security feature. 
    $secretKey = sodium_crypto_secretbox_keygen();
    $secretKeyHex = sodium_bin2hex($secretKey);
    // You can now save $secretKeyHex key somewhere safe, such as a file in a secure location
    // outside your web root. The value of $secretKeyHex will be something like:
    // d817c88db153f90ce4a93adbd0b9519137b6e673bd0abf7ca5a6ceeb53cadc97

    // We then generate a random 24-byte nonce, which should be different for each message we
    // wish to encrypt.
    $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
    // We then encrypt the message using the secret key and nonce.
    $ciphertext = sodium_crypto_secretbox($message, $nonce, $secretKey);
    // We then convert the encrypted message with the nonce to base64 for safe transport or storage.
    // Again we use a timing-safe variant of base64_encode() to do this.
    $result = sodium_bin2base64($nonce . $ciphertext, SODIUM_BASE64_VARIANT_ORIGINAL);
    // We should then overwrite the original message, the nonce and the secret key with
    // null bytes in memory, to prevent any leakage of sensitive data.
    sodium_memzero($message);
    sodium_memzero($nonce);
    sodium_memzero($secretKey);
    sodium_memzero($secretKeyHex);
    // $result can be stored in a database, etc. and will look something like this:
    // FOgVO54jZEl+AgsqUYR6E2K054v0M2eHDHohF+DxRWQshQFwYbIP2D8whjr7I085byzBYf671aRCiWyUswa9qBrdt5fwNQT6WMHAxCJv

Using Sodium to decrypt data with PHP

Let's say we've stored the example above in a database, and we want to retrieve the message.

<?php
    // Load up the hex key we saved earlier, using file_get_contents() to read the file or whatever.
    $secretKeyHex = file_get_contents('/path/to/key');
    // Convert the hex key to a binary key using sodium_hex2bin().
    $secretKey = sodium_hex2bin($secretKeyHex);
    // Grab the base64 encoded message from the database or wherever.
    $encrypted = file_get_contents('/path/to/message');

    // Convert the base64 encoded message to binary using sodium_base642bin().
    $ciphertext = sodium_base642bin($encrypted, SODIUM_BASE64_VARIANT_ORIGINAL);

    // Now we need to extract the nonce from the beginning of the message.
    // We simply take the first 24 bytes of the message.
    $nonce = mb_substr($ciphertext, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
    // And the message is the rest of the ciphertext.
    $ciphertext = mb_substr($ciphertext, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');

    // Now we can decrypt the message with the secret key and nonce.
    $plaintext = sodium_crypto_secretbox_open($ciphertext, $nonce, $secretKey);

    // If the plaintext is false, it means the message was corrupted.
    if ($plaintext === false) {
        die('Could not decrypt');
    }

    // Now we overwrite the nonce and secret key with null bytes in memory, to prevent any leakage of sensitive data.
    sodium_memzero($nonce);
    sodium_memzero($secretKey);
    sodium_memzero($secretKeyHex);
    sodium_memzero($ciphertext);

    // Finally, we can output the plaintext.
    echo $plaintext, PHP_EOL;    

Asymmetric encryption - using keys

"Asymmetric" means that one key is used to encrypt data, and another key is used to decrypt it. These are known as public and private keys. The public key is so-called because you can freely share it, and someone else can use it to encrypt data that only you can decrypt, using your corresponding private key. Your public key cannot be used to decrypt the data, so does not need to be kept confidential.

This form of encryption is useful if you want to send an encrypted message to another system or to a third party that only they can decrypt.

We can also do this either authenticated - meaning that our identity as the sender can also be verified, or unauthenticated - meaning that the sender's identity is anonymous.

In order to use asymmetric encryption, we need a keypair - a public key to encrypt data and a private key to decrypt it. If we are sending a message to a third party, we will use their public key to encrypt the message and our private key to sign the message.

If someone else is to send an encrypted message to us, they will use our public key to encrypt the message and we will use our private key to decrypt it.

If you need to encrypt some data and someone else needs to decrypt it, you should use asymmetric encryption.

Encrypt an authenticated message with PHP

<?php
    // Alice will send an authenticated encrypted message to Bob.
    // In real life, Bob's public key would be stored in a database, etc. but
    // we'll just generate a new keypair for Bob here.
    $bobKeypair = sodium_crypto_box_keypair();
    $bobPublicKey = sodium_crypto_box_publickey($bobKeypair);
    // Generate a keypair for Alice and get her private key.
    $aliceKeypair = sodium_crypto_box_keypair();
    $alicePrivateKey = sodium_crypto_box_secretkey($aliceKeypair);

    $message = "Hi Bob, this is an authenticated secret message from Alice!";

    // Now we encrypt the message with Bob (the recipient's) public key and
    // sign it with Alice's private key.
    // To do this, we create a combined key using these two keys.
    $key = sodium_crypto_box_keypair_from_secretkey_and_publickey($alicePrivateKey, $bobPublicKey);
    $nonce = random_bytes(SODIUM_CRYPTO_BOX_NONCEBYTES);
    $ciphertext = sodium_crypto_box($message, $nonce, $key);
    sodium_memzero($key);
    sodium_memzero($message)
    sodium_memzero($alicePrivateKey);

    // And as with the symmetric example, we base64 encode the ciphertext and nonce.
    $result = sodium_bin2base64($nonce . $ciphertext, SODIUM_BASE64_VARIANT_ORIGINAL);
    sodium_memzero($nonce);
    // $result can now be transmitted to Bob.

Decrypt an authenticated message with PHP

Bob receives the base64 encoded message from Alice. Now to decrypt it, Bob needs his private key and Alice's public key to verify the signature.

    // Bob loads his private key and Alice's public key from a database, etc.
    $bobKeypair = '...';
    $bobPrivateKey = sodium_crypto_box_secretkey($bobKeypair);
    $alicePublicKey = '...';

    // $message is the encrypted message sent by Alice.
    $message = '...';

    // Now Bob can decrypt the message. First, he needs to base64 decode the message
    // and separate out the nonce.
    $message = sodium_base642bin($message, SODIUM_BASE64_VARIANT_ORIGINAL);
    $nonce = mb_substr($message, 0, SODIUM_CRYPTO_BOX_NONCEBYTES, '8bit');
    $ciphertext = mb_substr($message, SODIUM_CRYPTO_BOX_NONCEBYTES, null, '8bit');

    // Bob creates a composite keypair using his private key and Alice's public key.
    $key = sodium_crypto_box_keypair_from_secretkey_and_publickey($bobPrivateKey, $alicePublicKey);

    // Now Bob can decrypt the message.
    $plaintext = sodium_crypto_box_open($ciphertext, $nonce, $key);
    // And clean up.
    sodium_memzero($key);
    sodium_memzero($ciphertext);
    sodium_memzero($nonce);
    sodium_memzero($bobPrivateKey);

    // $plaintext is now the original message, or false if decryption and verification failed.
    if ($plaintext === false) {
        die('Decryption failed!');
    }

    echo $plaintext;

Encrypt an anonymous (unauthenticated) message with PHP

To send an anonymous encrypted message, we only need the public key of the recipient.

The recipient will be able to decrypt the message with their private key, but will not know who sent it.

<?php
    // Load Bob's public key from a database, etc.
    $bobPublicKey = '...';
    $message = "Hi Bob, this is an anonymous secret message!";
    $encrypted = sodium_crypto_box_seal($message, $bobPublicKey);
    sodium_memzero($message);    
    // Again, for transmission, we base64 encode the ciphertext.
    $result = sodium_bin2base64($encrypted, SODIUM_BASE64_VARIANT_ORIGINAL);

Decrypt an anonymous (unauthenticated) message with PHP

Now Bob can decrypt this message with his private key.

<?php
    // Load Bob's keypair from whatever secure location.
    $bobKeypair = file_get_contents('/path/to/bob.key');
    $message = sodium_base642bin($message, SODIUM_BASE64_VARIANT_ORIGINAL);
    $plaintext = sodium_crypto_box_seal_open($message, $bobKeypair);
    sodium_memzero($message);
    sodium_memzero($bobKeypair);
    if ($plaintext === false) {
        die('Decryption failed!');
    }
    echo $plaintext;

Signing and verifying messages

Sometimes, we don't need to encrypt our data, we just want the recipient to be able to verify that it was sent by us and/or that it hasn't been tampered with in transit.

Just like encryption, we can generate message signatures using either a symmetric or asymmetric algorithm.

Symmetric message authentication

<?php
    // The message we want to sign.
    $message = 'Hello, I have not been tampered with.';

    // Generate a new secret for calculating the Message Authentication Code (MAC).
    $secret = sodium_crypto_auth_keygen();
    // In the real world, you may want to convert this secret to hexits and save somewhere secure.
    // $storableSecret = sodium_bin2hex($secret);

    // Get the message signature.
    $signature = sodium_crypto_auth($message, $secret);

    // We can later verify the signature with the same secret to confirm the integrity of the message.
    if (sodium_crypto_auth_verify($signature, $message, $secret)) {
        echo 'The message is authentic.';
    } else {
        echo 'The message has been tampered with!';
    }

Asymmetric message authentication

<?php
    $message = 'Hello, this is a secure message signed by Alice.';

    // Let's generate a new signing keypair for Alice.
    $aliceKeypair = sodium_crypto_sign_keypair();
    // And extract Alice's private key to use for signing.
    $alicePrivateKey = sodium_crypto_sign_secretkey($aliceKeypair);
    // And extract Alice's public key to separately send to Bob.
    $alicePublicKey = sodium_crypto_sign_publickey($aliceKeypair);

    // Now we can sign the message.
    $signature = sodium_crypto_sign_detached($message, $alicePrivateKey);

    // The message can be transmitted to Bob, who can verify the signature using Alice's public key.
    if (sodium_crypto_sign_verify_detached($signature, $message, $alicePublicKey)) {
        echo 'The message is authentic from Alice.';
    } else {
        echo 'The message has been tampered with!';
    }    

Instead of the _detached functions in the examples above, you can also calculate a signature string which includes the original message and later decode it.

To do this we can use the sodium_crypto_sign and sodium_crypto_sign_open functions in place of sodium_crypto_sign_detached and sodium_crypto_sign_verify_detached. The message itself, however, is not encrypted; only its authenticity can be verified.

Wrapping it up - PHP encryption libraries

I have created a lightweight, simple to use wrapper class for each of the PHP cryptography techniques you've seen covered in this post.

You can install it via Composer:

$ composer require dwgebler/encryption

You can see the usage examples in the README on the project's GitHub page.

https://github.com/dwgebler/php-encryption

Alternatively, for a more feature-complete library, have a look at Paragonie's Halite package.

Further reading


Comments

Add a comment

All comments are pre-moderated and will not be published until approval.
Moderation policy: no abuse, no spam, no problem.

You can write in _italics_ or **bold** like this.

Richard Thursday 13 April 2023, 21:11

I am doing a symmetric password encryption and writing the $secretKeyHex and $result to text files. Then I read the data from the text files and go through the steps to decrypt the password. It fails where you make the comment:

// If the plaintext is false, it means the message was corrupted.

When I display each step of the process with "echo" in PHP, it all looks good, but fails at the last step.

Any suggestions on what could cause it to get corrupted, or how to troubleshoot at this step?


Hi Richard, it's difficult to say with certainty off your message alone, as there is no way to get a specific indication as to the reason decryption has failed from sodium_crypto_secretbox_open, but one thing to check is are you converting that hex key back to binary before trying to decrypt? Here is a complete sample script for encrypting, writing to file and decrypting I hope is helpful - ensure you are following each of these steps:

<?php
    $message = file_get_contents('http://loripsum.net/api/10/plaintext');
    $secretKey = sodium_crypto_secretbox_keygen();
    $secretKeyHex = sodium_bin2hex($secretKey);

    file_put_contents('secret.key', $secretKeyHex);

    $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
    $ciphertext = sodium_crypto_secretbox($message, $nonce, $secretKey);
    $result = sodium_bin2base64($nonce . $ciphertext, SODIUM_BASE64_VARIANT_ORIGINAL);

    file_put_contents('message.txt', $result);

    sodium_memzero($message);
    sodium_memzero($nonce);
    sodium_memzero($secretKey);
    sodium_memzero($secretKeyHex);

    $secretKeyHex = file_get_contents('secret.key');
    $secretKey = sodium_hex2bin($secretKeyHex);

    $message = file_get_contents('message.txt');
    $ciphertext = sodium_base642bin($message, SODIUM_BASE64_VARIANT_ORIGINAL);
    $nonce = mb_substr($ciphertext, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
    $ciphertext = mb_substr($ciphertext, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');
    $plaintext = sodium_crypto_secretbox_open($ciphertext, $nonce, $secretKey);

    echo $plaintext;
Carlos De Albuquerque Friday 24 March 2023, 18:39

Thank you for this clearly written article.

Susan Wednesday 07 December 2022, 19:20

Great article! Totally new at sodium.

In your "Using Sodium to encrypt data with PHP" you show how to use a message. but not store it in a folder?

In your "Using Sodium to decrypt data with PHP" you use folders to decrypt?

Could you explain how to use your encrypt code with folders, so the decrypt matches for a total newbie?

Thank you!


Editor's reply: Hi Susan, thanks for your comment; what do you mean by folders exactly? This word is normally a synonym for directory. If you want to encrypt the contents of a file, you just need to read the file in (using file_get_contents or other normal method of your choice), encrypt the data in memory as per the tutorial, then write the encrypted contents back to the same file (file_put_contents for example). This tutorial doesn't show you what you do with encrypted data in respect of storing it or transmitting it somewhere because that's entirely up to you; put it in a file, send it in an email, store in a database, do whatever you want. By having a base64 encoded representation of the data you can store it anywhere you can store ordinary text and then decrypt it later. Hope that helps.

Adam Jones Tuesday 07 June 2022, 21:43

Really great tutorial on encryption. Just wondered if the library on GitHub is available under the MIT license?

Editor's reply: Yes it is, see https://github.com/dwgebler/php-encryption

Recent posts


Saturday 10 February 2024, 17:18

The difference between failure and success isn't whether you make mistakes, it's whether you learn from them.

musings coding

Monday 22 January 2024, 20:15

Recalling the time I turned down a job offer because the company's interview technique sucked.

musings

SPONSORED AD

Buy this advertising space. Your product, your logo, your promotional text, your call to action, visible on every page. Space available for 3, 6 or 12 months.

Get in touch

Friday 19 January 2024, 18:50

Recalling the time I was rejected on the basis of a tech test...for the strangest reason!

musings

Monday 28 August 2023, 11:26

Why type hinting an array as a parameter or return type is an anti-pattern and should be avoided.

php

Saturday 17 June 2023, 15:49

Leveraging the power of JSON and RDBMS for a combined SQL/NoSQL approach.

php