Modern PHP data Encryption/Decryption with Sodium extension
Throughout the years PHP has added support for several extensions, libraries, and algorithms to encrypt and decrypt data. With several libraries and extensions with various levels of maintenance, several algorithms each potentially carrying pros and cons, some even inherently being insecure, it is very difficult to select the appropriate PHP extension, library, encryption constructs, and balance the security and performance.
mcrypt is one of the oldest PHP extensions to bring encryption/decryption capabilities to PHP. It is no longer maintained, and PHP un-bundled it in PHP 7.2.
OpenSSL is another library that is more widely adopted, and is actively supported. OpenSSL offers a wide range of cipher, key exchange, and authentication algorithms, and
some most of them can be insecure if used at the wrong use use-case. For example, the most common encryption algorithm OpenSSL offers is AES (Advanced Encryption Standard ) has several operations modes and key sizes that leaves rooms for insecure uses of it. From the outset, AES modes such as ECB (Electronic codebook) are not semantically secure, and some modes such as CBC (Cipher block chaining) require authenticating the encrypted messages to be completely secure, and still could be vulnerable to padding oracle attacks, such as POODLE.
Libsodium, a fork of NaCl is a more modern and heavily opinionated cryptography library. It offers secure and sensible defaults, and takes away a lot of decision making from the end user to library maintainers. PHP. Libsodium is available as a PECL extension, but PHP also includes the extension in PHP core since PHP 7.2.
- Installing/Enabling Sodium Extension
- Symmetric and Asymmetric Encryption
- Symmetric Encryption/Decryption with Sodium
- Authenticated Asymmetric Encryption/Decryption with Sodium
- Unauthenticated Asymmetric Encryption/Decryption with Sodium
Since PHP 7.2, Sodium extension is included in PHP core. It is likely that the Sodium extension is already available and enabled, which can be confirmed from
Alternately, it is possible to list the PHP extensions in PHP CLI, and inspect the output:
php -m | grep sodium
If the Sodium extension is not available, enable it by adding an
extension directive. PHP 7.2 and later do not require the file extension (e.g.
.so ) in
The following example should work most of the standard PHP setups:
With symmetric encryption and decryption, the same key is used to encrypt and decrypt. In real-life, this is similar to using a door lock that locks and unlocks a door using the key.
If a message is encrypted and decrypted on the same device, symmetric encryption is more appropriate. Some example use case on using symmetric encryption:
- Encrypting a browser cookie before sending it to the user, and decrypting incoming cookies.
- Encrypting a storage drive and decrypting it with the same key.
- Zip/Rar file encryption.
Asymmetric encryption involves a pair of keys: A public key and its private key.
The asymmetry here is that the messages are encrypted with the public key, and it can only be decrypted with the private key. As the name suggests, the public can be freely and public distributed. When the key pair is generated, it is generated in a way that it is mathematically possible for the private key — and private key alone — to decrypt a message encrypted with the public key.
This asymmetry makes it possible to share the public key with any interested party, and have them send encrypted messages that nobody else can read without the private key.
Some of the use cases for asymmetric use cases include:
- Encrypting server logs, and sending them to a remote server, so only the remote server can read them.
- SSL/TLS handshake.
- Sending an encrypted by recipient's public key.
PHP Sodium extension provides a few algorithms with optimal defaults and opinionated key sizes to encrypt/decrypt data using a key.
All of the algorithms provided by Sodium extension provide authenticated encryption, which means that the encrypted text will be authenticated against tampering. This prevents Chosen-ciphertext attacks. With approaches such as mcrypt or most ciphers from OpenSSL, it is up to the caller to generate an HMAC/signature and protect against such attacks.
Applications that need to store the authentication tag (MAC) and encrypted text may do so by using the "detached" API variants provided Sodium.
At the moment, Sodium provides four ciphers to choose from:
All of the four ciphers provide authentication by default, are safe to be used. Choosing the appropriate cipher is a balance between compatibility with other libraries and programming languages and tooling.
AES256-GCM is widely supported in most CPUs (AES-NI instructions set), and is supported in other extensions such as OpenSSL.
If compatibility between other programming languages and tooling is not a concern,
XChaCha20-Poly1305 - IETF' is the safest choice, and is the focus of the rest of this section.
Sodium extension provides functions to easily generate a key, encrypt, and decrypt a message. In addition, the
random_bytes function comes in handy to generate a random nonce value.
sodium_crypto_aead_xchacha20poly1305_ietf_keygen: Generates the secret key of required length.
random_bytes: Generates random bytes.
sodium_crypto_aead_xchacha20poly1305_ietf_encrypt: Encrypts a message.
sodium_crypto_aead_xchacha20poly1305_ietf_decrypt: Decrypts a message.
The secret key for symmetric encryption is generated using a Cryptographically-secure pseudorandom number generator.
Sodium provides an easy function to generate the key with required length:
$key = sodium_crypto_aead_xchacha20poly1305_ietf_keygen();
This key must be securely stored, and will be used to decrypt messages as well.
To prevent replay attacks, each encrypted message must be different even if the source data is same. This is achieved by generating a random value called nonce. This value is used once, and generated for each encrypted message.
A nonce does not necessarily has to be random, but it must be unique. Using a random value with an adequate length makes it not necessary to check against existing nonce values to ensure the generated
XChaCha20-Poly1305 cipher requires a nonce length of 192 bits, and is made easier with the built-in constant :
$nonce = \random_bytes(\SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
$nonce generated, it is time to encrypt a message.
$message = 'Hello World'; $encrypted_text = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($message, '', $nonce, $key);
$encrypted_text variable now holds the encrypted message. It contains the authentication tag (MAC), and Sodium uses it automatically to authenticate a message.
The second parameter of
sodium_crypto_aead_xchacha20poly1305_ietf_decrypt accepts a
string containing additional data. This value is not encrypted or stored alongside the
$encrypted_text, but is used as additional for authentication. In this example, this value intentionally left black (
""). If additional authentication data must be used (such as user ID or an IP address), use make use of this parameter.
Once the encrypted text is generated, it contains the authentication tag, which should prevent unexpected and malicious tampering of the encrypted text.
$nonce value is required to decrypt the message, and must be stored along with the encrypted message. An example of this would be the
$key (secret) key stored in a file only accessible to the process that encrypts/decrypts messages, and
$nonce (nonce) and
$encrypted_text stored in a database.
Depending on the
$additional_data parameter values at the encryption time, the they may need to be stored as well.
sodium_crypto_aead_xchacha20poly1305_ietf_encrypt function returns a byte stream. It cannot be directly printed (on a web page or a JSON response for example), and must be converted to a text format using functions such as
bin2hex prior to printing or transmitting to a medium that cannot handle raw byte streams.
Decrypting a message requires the original secret key (
$key) and the Nonce (
$original_message = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($encrypted_text, '', $nonce, $key);
If the provided key, nonce, or the additional data are invalid, this function returns
// Generate a secret key. This value must be stored securely. $key = sodium_crypto_aead_xchacha20poly1305_ietf_keygen(); // Generate a nonce for EACH MESSAGE. This can be public, and must be provided to decrypt the message. $nonce = \random_bytes(\SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES); // Text to encrypt. $message = 'Hello World'; // Encrypt $encrypted_text = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt($message, '', $nonce, $key); // Decrypt $original_message = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($encrypted_text, '', $nonce, $key);
mcrypt and OpenSSL extensions also provide symmetric encryption. However, they have varying levels of support for authentication, key lengths, and nonce lengths.
Generally speaking, the
sodium_crypto_aead_xchacha20poly1305_ietf_* functions are better alternatives to
Sodium also provides a way to asymmetrically encrypt and decrypt messages.
Asymmetric encryption involves two parties. Both parties create a key pair, containing a private key and a public key. The public key is distributed to other parties that need to communicate with.
For example, if Alice and Bob want to communicate securely, and must ensure that:
- Messages are indeed from Alice/Bob, and not from someone else.
- Messages are private, nobody can read or tamper them.
Sodium extension provides a Crypto Box API that fulfills the authenticity and privacy of such messages. Prior to sending messages, Alice and Bob (or all parties involved) must generate a key pair, and securely exchange the public key.
Sodium also provides secure key exchange protocols, but it is not covered by this article.
When a message is sent, the sender encrypts it with recipient's public key, and signs it with the sender's private key. Once the message is transmitted, the recipient authenticates the messages with the sender's public key, and decrypts with the recipient's private key.
If the recipient does not need to authenticate the received messages — make sure that the message was indeed sent by the sender — Sodium extension also Unauthenticated Asymmetric Encryption/Decryption using
All parties that need to communicate must generate a key pair. The secret must be kept private, as it is used to sign and decrypt messages.
Individually on both Alice's and Bob's devices
$alice_keypair = sodium_crypto_box_keypair(); $alice_secret_key = sodium_crypto_box_secretkey($alice_keypair); $alice_public_key = sodium_crypto_box_publickey($alice_keypair);
$bob_keypair = sodium_crypto_box_keypair(); $bob_secret_key = sodium_crypto_box_secretkey($bob_keypair); $bob_public_key = sodium_crypto_box_publickey($bob_keypair);
sodium_crypto_box_keypairgenerates a random key pair containing an X25519 secret key and its corresponding X25519 public key
sodium_crypto_box_secretkeyextracts the secret key portion of the key pair.
sodium_crypto_box_publickeyextract the public key of the key pair.
For authenticated asymmetric encryption and decryption, both parties involved must exchange their public keys securely. This can be done using a key-exchange protocol, or transmitting the keys using another secure channel, such as an HTTPS request.
Sodium provides key-exchange functionality with its
sodium_crypto_kx_* functions. However, this article does not cover those APIs at this point ensure the brevity of it.
After a successful exchange keys:
- Alice must possess Alice's public key, Alice's private key, and Bob's public key.
- Bob must possess Bob's public key, Bob's private key, and Alice's public key.
Similar to creating a nonce value for each message with symmetric encryption, an authenticated asymmetric message must use a nonce value as well.
The nonce value must of 192 bits (24 bytes), which can easily be created using
random_bytes function and built-in
SODIUM_CRYPTO_BOX_NONCEBYTES constant, assigned
$nonce = \random_bytes(\SODIUM_CRYPTO_BOX_NONCEBYTES);
Before encrypting a message, the sender (Alice in this example) must create a new key pair, containing the recipient's public key and sender's private key. This can be done by concatenating the public key and private key, or using the
$sender_keypair = sodium_crypto_box_keypair_from_secretkey_and_publickey($alice_secret_key, $bob_public_key);
$message = "Hi Bob, I'm Alice"; $encrypted_signed_text = sodium_crypto_box($message, $nonce, $sender_keypair);
sodium_crypto_box function encrypts and signs a message using the key pair (
$sender_keypair). Sender's private key is used to create a signature, and the recipient's public key is used to encrypt the actual message. The authentication tag is stored alongside the encrypted text (
Once the message is encrypted and signed, only the recipient can with the private key can decrypt the message. Anyone with the sender's public key can authenticate the message, but not read the contents without the recipient's private key.
$nonce nonce value must be stored/transmitted alongside the ciphertext, and it may be public.
Make sure to regenerate the
$nonce value for the next message to prevent replay attacks.
The recipient can authenticate a message and make sure it is signed by the sender if the recipient has the public key of the sender (public key of Alice in this example).
$recipient_keypair = sodium_crypto_box_keypair_from_secretkey_and_publickey($bob_secret_key, $alice_public_key); $orig_msg = sodium_crypto_box_open($encrypted_signed_text, $nonce, $sender_keypair); var_dump($orig_msg); // "Hi Bob, I'm Alice"
sodium_crypto_box_keypair_from_secretkey_and_publickey function is used again to create a new key pair, but this time, it is created from the recipient's private key, and the sender's public key.
Once the key pair is generated, it is possible to authenticate and decrypt a message.
sodium_crypto_box_open function authenticates that the message is signed by the sender (signed using sender's private key), and decrypts the message using recipient's private key.
If there is a key or nonce mismatch,
// On Alice's device $alice_keypair = sodium_crypto_box_keypair(); $alice_secret_key = sodium_crypto_box_secretkey($alice_keypair); $alice_public_key = sodium_crypto_box_publickey($alice_keypair); // On Bob's device $bob_keypair = sodium_crypto_box_keypair(); $bob_secret_key = sodium_crypto_box_secretkey($bob_keypair); $bob_public_key = sodium_crypto_box_publickey($bob_keypair); // Exchange keys: // - Send Alice's public key to Bob. // - Send Bob's public key to Alice. // On sender: // Create nonce $nonce = \random_bytes(\SODIUM_CRYPTO_BOX_NONCEBYTES); // Create enc/sign key pair. $sender_keypair = sodium_crypto_box_keypair_from_secretkey_and_publickey($alice_secret_key, $bob_public_key); $message = "Hi Bob, I'm Alice"; // Encrypt and sign the message $encrypted_signed_text = sodium_crypto_box($message, $nonce, $sender_keypair); // On recipient: $recipient_keypair = sodium_crypto_box_keypair_from_secretkey_and_publickey($bob_secret_key, $alice_public_key); // Authenticate and decrypt message $orig_msg = sodium_crypto_box_open($encrypted_signed_text, $nonce, $sender_keypair); var_dump($orig_msg); // "Hi Bob, I'm Alice"
If the recipient does not need to authenticate the incoming messages, but only decrypt them, it might be a use case Sodium's
crypto_box_seal functions are used to encrypt and decrypt a message using a pair of public and private keys. The major difference between Authenticated Asymmetric Encryption/Decryption is that
crypto_box_seal does not authenticate the messages.
crypto_box_seal functionality is similar to OpenSSL's
openssl_private_decrypt, in that the OpenSSL function pair does not provide authentication either.
crypto_box_seal, only the recipient needs generate a key pair. The sender can obtain the recipient's public key, and encrypt a message.
The recipient can decrypt any messaged encrypted with its public key, but has no way to identify or authenticate the sender's identity.
With unauthenticated asymmetric encryption, only the recipient is required to generate a key pair:
$recipient_keypair = sodium_crypto_box_keypair(); $recipient_public_key = sodium_crypto_box_publickey($recipient_keypair);
The recipient must keep the private key portion of the key pair stored securely. The public key (
$recipient_public_key) can then be distributed over a secure channel.
The easiest and most common way to distribute a public key is over an HTTPS connection. For example, senders can download the public key of the sender from recipient's web site served over HTTPS.
Note that although anonymous asymmetric encryption does not provide authentication for messages, the public key must be transmitted securely, and the sender must validate the recipient's public key corresponds with the actual recipient, and the key is not tampered during transmission.
With the recipient's public key, any sender can encrypt a message:
$message = "Hi Bob, you don't know who I am"; $encrypted_text = sodium_crypto_box_seal($message, $recipient_public_key);
Encrypted messages can only be opened by the recipient holding the private key. However, because there is no there is no authentication or nonce values involved, this is prone to replay attacks.
To decrypt a message encrypted with a public key, the recipient must possess the corresponding private key.
sodium_crypto_box_seal_open decrypts an encrypted message using the secret key.
$original_message = sodium_crypto_box_seal_open($encrypted_text, $recipient_keypair); var_dump($original_message); // "Hi Bob, you don't know who I am"
$recipient_keypair = sodium_crypto_box_keypair(); $recipient_public_key = sodium_crypto_box_publickey($recipient_keypair); $message = "Hi Bob, you don't know who I am"; $encrypted_text = sodium_crypto_box_seal($message, $recipient_public_key); $original_message = sodium_crypto_box_seal_open($encrypted_text, $recipient_keypair); var_dump($original_message); // "Hi Bob, you don't know who I am"