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
Installing/Enabling Sodium Extension
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 phpinfo()
.
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. .dll
or .so
) in extension
directives.
The following example should work most of the standard PHP setups:
extension=sodium
When compiling PHP from source, enabling Sodium extension requires the --with-sodium
flag and libsodium
library, easy installed as libsodium-dev
on Ubuntu/Debian and libsodium-dev
on Fedora/RHEL.
Symmetric and Asymmetric Encryption
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.
Symmetric Encryption/Decryption with Sodium
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.
XChaCha20 with Poly1305 MAC
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.
Create a Secret Key
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.
Generate Nonce Values
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
$nonce
is unique.
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);
Encrypting a Message
With the $key
and $nonce
generated, it is time to encrypt a message.
$message = 'Hello World';
$encrypted_text = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($message, '', $nonce, $key);
The $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.
Storing/Transmitting the Encrypted Message
Once the encrypted text is generated, it contains the authentication tag, which should prevent unexpected and malicious tampering of the encrypted text.
The $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.
Additionally, 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 base64_encode
or bin2hex
prior to printing or transmitting to a medium that cannot handle raw byte streams.
echo bin2hex($encrypted_text);
Decrypting a Message
Decrypting a message requires the original secret key ($key
) and the Nonce ($nonce
) value.
$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 false
.
Complete Example: Authenticated Symmetric Encryption/Decryption
// 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);
Migrating from Mcrypt and OpenSSL
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 mcrypt_encrypt
and openssl_encrypt
functions.
Authenticated Asymmetric Encryption/Decryption with Sodium
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
crypto_box_seal
API.
Generate Key Pairs
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_keypair
generates a random key pair containing an X25519 secret key and its corresponding X25519 public keysodium_crypto_box_secretkey
extracts the secret key portion of the key pair.sodium_crypto_box_publickey
extract the public key of the key pair.
Exchange Keys
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.
Create Nonce Values
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 24
.
$nonce = \random_bytes(\SODIUM_CRYPTO_BOX_NONCEBYTES);
Build the Encryption Key Pair
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 sodium_crypto_box_keypair_from_secretkey_and_publickey
function.
Sender:
$sender_keypair = sodium_crypto_box_keypair_from_secretkey_and_publickey($alice_secret_key, $bob_public_key);
Encrypt and Sign a Message
With the keys exchanged, nonce generated, and encryption key pair built, it's now time to encrypt a message.
$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 ($encrypted_signed_text
).
Storing/Transmitting the Encrypted and Signed 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.
The $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.
Decrypting and Authenticating a Received Message
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, sodium_crypto_box_open
returns false
.
Complete Example: Authenticated Public Key Encryption/Decryption
// 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"
Unauthenticated Asymmetric Encryption/Decryption with Sodium
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
API.
crypto_box_seal
in Sodium
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.
Sodium's crypto_box_seal
functionality is similar to OpenSSL's openssl_public_encrypt
and openssl_private_decrypt
, in that the OpenSSL function pair does not provide authentication either.
With 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.
Create a Key Pair for Recipient
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);
Distribute Public Key
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.
Encrypt a Message
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);
Store/Transmit Messages
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.
Decrypt Messages
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"
Complete Example: Unauthenticated Public Encryption
$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"