PHP
Article
By Miguel Ibarra Romero

Using Halite for Privacy and Two-Way Encryption of Emails

By Miguel Ibarra Romero

Cryptography is a complex matter. In fact, there is one golden rule:

* Don’t implement cryptography yourself *

The reason for this is that so many things can go wrong while implementing it, the slightest error can generate a vulnerability and if you look away, your precious data can be read by someone else. Whilst this is not an exhaustive list, there are several important guidelines to follow when using cryptography:

  • Don’t use the same key to encrypt everything
  • Don’t use a generated key directly to encrypt
  • When generating values that you don’t want to be guessable, use a cryptographically secure pseudo random number generator (CSPRNG)
  • Encrypt, then MAC (or the Cryptographic Doom Principle)
  • Kerckhoffs’s principle: A crypto system should be secure even if everything about the system, except the key, is public knowledge

Picture of keys hanging on strings

Some of the cryptographic terms used in this article can be defined as follow:

  • Key: a piece of information that determines the functional output of a cryptographic algorithm.
  • CSPRNG: also known as a deterministic random bit generator, is an algorithm for generating a sequence of numbers whose properties approximate the properties of sequences of random numbers (or bytes). To be cryptographically secure, a PRNG must:
    • Pass statistical randomness tests
    • Hold up well under serious attack, even when part of their initial or running state becomes available to an attacker.
  • MAC: is a short piece of information used to confirm that the message came from the stated sender (its authenticity) and has not been changed in transit (its integrity). It accepts as input a secret key and an arbitrary-length message to be authenticated, and outputs a MAC.

To further read about cryptography and have a better understanding, you can take a look at the following pages:

Some libraries out there implement cryptography primitives and operations, and leave a lot of decisions to the developer. Examples of those are php’s own crypto library, or Defuse’s php-encryption. Some PHP frameworks implement their own crypto like Zend Framework’s zend-crypt or Laravel.

Nevertheless, there is one library that stands out from the rest for its simplicity and takes a lot of responsibility from the developer on the best practices, in addition to using the libsodium library. In this article we are going to explore Halite.

halite

The assumption is that you already have PHP 7, a web server and a MySQL server installed and running. You might want to take a look at Homestead Improved for an environment like this. In order to be able to use Halite, our system must have the libsodium library, as well as the libsodium PHP extension. Those can be installed by using the following commands. As always, depending on your system configuration and installed packages, your mileage may vary:

Install in Ubuntu

sudo apt-get install build-essential
wget https://github.com/jedisct1/libsodium/releases/download/1.0.10/libsodium-1.0.10.tar.gz
tar -xvf libsodium-1.0.10.tar.gz
cd libsodium-1.0.10/
sudo ./configure
sudo make
sudo make install
sudo apt-get install php-pear php7.0-dev
sudo pecl install libsodium

Install in CentOS

sudo yum groupinstall "Development tools"
wget https://github.com/jedisct1/libsodium/releases/download/1.0.10/libsodium-1.0.10.tar.gz
tar -xvf libsodium-1.0.10.tar.gz
cd libsodium-1.0.10/
sudo ./configure
sudo make
sudo make install
yum install php-pear php-devel
sudo pecl install libsodium

You will have to edit your php.ini file to include the extension=libsodium.so line and restart your web server or php-fpm.

The application

To showcase some of Halite’s basic features, let’s create a set of RESTful services for a simple “e-mail like” messaging application, where we want to encrypt messages sent between users. Please take into consideration that this will be just a tiny and simple example, a lot of standard email features will be missing. Should you like to browse the full source code, please take a look at the github repo.

The database tables have the following schema:

Database schema

The most relevant fields are:

table field purpose
users salt holds a random binary string for each user
messages users_id the user id to which the message is intended to
messages fromUserId the user id from which the message is sent

Let’s begin by initializing a composer.json file, and then declaring the dependency of our project on the Halite library using Composer.

composer init
composer require paragonie/halite

For this example, I’ll be using silex for routing requests into an MVC style application, phpdotenv to store and load environment variables and doctrine orm to interact with the database using php objects.

composer require silex/silex:~2.0 vlucas/phpdotenv doctrine/orm

After successfully downloading the libraries, the composer.json file should be modified to look like the following:

{
    "name": "yourName/projectName",
    "description": "Sitepoint tutorial for Halite",
    "authors": [
        {
            "name": "yourName",
            "email": "yourEmail"
        }
    ],
    "require": {
        "silex/silex": "^1.3",
        "vlucas/phpdotenv": "^2.2",
        "paragonie/halite": "^2.1",
        "doctrine/orm": "^2.5"
    },
    "autoload": {
        "psr-4": {"Acme\\": "src/"}
    }
}

The Acme namespace will hold our custom classes.

The RESTful services in our sample app have the following structure:
RESTful services

Method url description
GET /users Get a list of users
GET /users/{userid} Get the details of a given user
GET /users/{userid}/messages Get a list of messages sent to a given user
POST /users/{userid}/messages Send a message to a user
GET /users/{userid}/messages/{messageid} Retrieve a message from a given user

For this application, we desire to encrypt the subject and the message, so the urls in bold are the ones that use Halite’s features.

For this article, we will be using symmetric encryption. That is, we use the same key to encrypt and decrypt a message.

Given that we have the following users in the system (by invoking the users list service):

#Request
GET /users HTTP/1.1
Host: yourhost.dev
Cache-Control: no-cache

#Response
[
    {
        "id": 1,
        "name": "John Smith"
    },
    {
        "id": 2,
        "name": "Jane Doe"
    }
]

When we want to sent a message to John, from Jane, then we should send the following request:

POST /users/1/messages HTTP/1.1
Host: yourhost.dev
Cache-Control: no-cache

{
  "from": "2",
  "subject": "This is my subject",
  "message": "super secret information!!!"
}

The sample project uses Silex and Doctrine to do some heavy lifting, however, it is not the scope of this article to show how they work. For that, please see this intro to Silex and some of our Doctrine posts.

Roughly speaking, Silex will forward HTTP requests to a controller. The controller will invoke a service, where things begin to get interesting. Let’s look at the code from the \Acme\Service\Message::save method.

<?php
namespace Acme\Service;

use Acme\Model\Message as MessageModel;
use Acme\Model\Repository\Message as MessageRepository;
use Acme\Model\User as UserModel;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;
use ParagonIE\Halite\KeyFactory;
use ParagonIE\Halite\Symmetric\AuthenticationKey;
use ParagonIE\Halite\Symmetric\Crypto;
use ParagonIE\Halite\Symmetric\EncryptionKey;

/**
 * Class Message
 * @package Acme\Service
 */
class Message
{

// ... SOME CODE HERE ...

    /**
         * @param $from
         * @param $to
         * @param $subject
         * @param $message
         * @return string
         * @throws \ParagonIE\Halite\Alerts\InvalidSalt
         */
        public function save($from, $to, $subject, $message)
        {
            /*
             * create a new model object to hold the message
             */
            $messageModel = new MessageModel;

            /** @var EntityRepository $userRepository */
            $userRepository = $this->em->getRepository('Acme\Model\User');

            /*
             * search for the sender and recipient users
             */
            /** @var UserModel $fromUserModel */
            $fromUserModel = $userRepository->find($from);

            /** @var UserModel $toUserModel */
            $toUserModel = $userRepository->find($to);

            if (!$fromUserModel || !$toUserModel) {
                throw new \InvalidArgumentException('From or to user does not exist');
            }

            /*
             * create a placeholder for data, in order to generate a message id, used later to encrypt data.
             */
            $messageModel->setFromUser($fromUserModel)->setToUser($toUserModel);

            $this->em->persist($messageModel);
            $this->em->flush();

            /*
             * Retrieve the salts for both the sender and the recipient
             */
            $toUserSalt = $toUserModel->getSalt();

            /*
             * create encryption keys concatenating user's salt, a string representing the target field to be
             * encrypted, the message unique id, and a system wide salt.
             */
            $encryptionKeySubject = KeyFactory::deriveEncryptionKey(
                base64_decode($toUserSalt) . 'subject' . $messageModel->getId(),
                base64_decode(getenv('HALITE_ENCRYPTION_KEY_SALT')));
            $encryptionKeyMessage = KeyFactory::deriveEncryptionKey(
                base64_decode($toUserSalt) . 'message' . $messageModel->getId(),
                base64_decode(getenv('HALITE_ENCRYPTION_KEY_SALT')));

            /*
             * encrypt the subject and the message, each with their own encryption key
             */
            $cipherSubject = Crypto::encrypt($subject, $encryptionKeySubject, true);
            $cipherMessage = Crypto::encrypt($message, $encryptionKeyMessage, true);

            $messageModel->setSubject(base64_encode($cipherSubject))->setMessage(base64_encode($cipherMessage));

            $this->em->persist($messageModel);
            $this->em->flush();

            return $messageModel->getId();
        }
}

First, an object representing a record from the messages table is created to be used with Doctrine. This will make it easier to write data to the database. This object has attributes that are mapped 1:1 to the messages table.

Next, a Doctrine repository is created for the users table. The repository is used to easily retrieve data into model objects. The ease of retrieving data is shown with the statements $userRepository->find($from) and $userRepository->find($to) all without writing a single SQL command.

The resulting models are then assigned to our message model, and then persisted to the database using Doctrine’s entity manager via the $this->em->persist($messageModel); and $this->em->flush(); statements. This is done so the primary key created for this record can be retrieved and used later on to encrypt the data. For more information about the dual nature of persist/flush, see this post.

Each user in our database holds a unique random bytes string called ‘salt’. This salt, which is base64 encoded in order to be stored in the database as a series of printable characters, looks like this:

users table data

We need to retrieve both the sender’s and recipient’s salt to be used to encrypt the message. As you may have already guessed, this is done easily via the models loaded above with the $toUserSalt = $toUserModel->getSalt(); and $fromUserSalt = $fromUserModel->getSalt();.

Finally, the real fun begins. It is time to use the Halite library. One of the rules of cryptography dictates to never directly use a pregenerated key to encrypt data, as well as to not use the same key multiple times. We will use the \ParagonIE\Halite\KeyFactory class to generate encryption keys. Please look at the following statement carefully:

$encryptionKeySubject = KeyFactory::deriveEncryptionKey(base64_decode($toUserSalt) . 'subject' . $messageModel->getId(), base64_decode(getenv('HALITE_ENCRYPTION_KEY_SALT')));

The \ParagonIE\Halite\KeyFactory::deriveEncryptionKey method receives 2 parameters: the password to derive from, and a salt. Please notice that we are using the concatenation of the recipient’s salt (which has to be base64 decoded first), the string ‘subject’ and the message’s unique ID as the password parameter; this will ensure that the ‘password’ is unique for this recipient / this record / this field. The second parameter will hold an application wide salt set as an environment variable thanks to the dotenv library. This salt is stored in a .env file (along with the database credentials) encoded in base64. The .env file holds the salt as follows:

HALITE_ENCRYPTION_KEY_SALT="/t4xwLjjkG0RoRHGkmOQVw=="

This salt is a 16 byte (as per requirements of argon2 algorithm used in Halite’s key derivation function) random binary string. This string can be generated with base64_encode(random_bytes(16)). For this approach to be secure, the application server and the database server should be on separate hardware.

If both servers are on the same machine, and if an attacker gets access to it, they would have everything they need to decrypt the information. Another approach would be to encrypt the application’s salt using a key that would be provided at service boot time and/or persisted in memory. As long as this key is not intercepted, an attacker would have no way of decrypting anything even if both the web server and the database server are on the same hardware.

Now the actual encryption can take place

$cipherSubject = Crypto::encrypt($subject, $encryptionKeySubject, true);
$cipherMessage = Crypto::encrypt($message, $encryptionKeyMessage, true);

The first parameter of \ParagonIE\Halite\Symmetric\Crypto::encrypt is the plain text, the second parameter is the encryption key derived above. The third parameter will tell Halite to return the raw binary string. The default behavior is to return a hex encoded representation. This, of course, depends on your preference.

At the moment we have encrypted a message, so at first glance it won’t be known what the contents are. Halite already makes transparent the fact that it will add an authentication code to the cypher text in order to make sure that the contents of the message have not been tampered with. It will also use a different initialization vector to make sure that the same plain text encrypted with the same key will produce a different cypher text.

Now we are ready to assign the cypher text to our model (base64 encoding them first) $messageModel->setSubject(base64_encode($cipherSubject))->setMessage(base64_encode($cipherMessage)); to be able to persist it to our database.

Let’s see the full request and response

Request

POST /users/1/messages HTTP/1.1
Host: yourhost.dev
Cache-Control: no-cache

{
  "from": "2",
  "subject": "This is my subject",
  "message": "super secret information!!!"
}

Response

{
    "messageId": 1
}

If we query the database directly, the record will look like the following:

*************************** 1. row ***************************
        id: 1
  users_id: 1
fromUserId: 2
   subject: MUICAV13/brmlurlUIF6TOmhD6duztBI7vYzd4PGrJu8TinhAm69Kzv5QVFpNO55mssxsthPqmwb7l/py1iTCl0tSHUcB5Wsep0bcYDztIPhZ3g7VOcKKqu+YBTcuWprMvM22nvVdzcismGvCjkVW5hqCuNaCJPaUY+7VKRqCLg6FPY4WLMOdbY2yotQ4Q==
   message: MUICAaSGjcjwaDUORzctiK8rDla+w2Wm/6Z75EP7LJsRZbCk5zVHC/R6oxaJ6VrVbaB6WG1k9xtUcCt9fGN40r3zjFxp4M24QEdVu18t7A8N4wbiwd1W7LqbqjieziBchtKIJjE4oM68BlKxJMGO020GSnwNBuXIaz1b1MRVQDEsm1eT/oTx1oeLvwG1X94raKpHmK7D

If for any reason we were to send exactly the same request:

POST /users/1/messages HTTP/1.1
Host: yourhost.dev
Cache-Control: no-cache

{
  "from": "2",
  "subject": "This is my subject",
  "message": "super secret information!!!"
}
{
    "messageId": 2
}

And query the database again:

*************************** 1. row ***************************
        id: 1
  users_id: 1
fromUserId: 2
   subject: MUICAV13/brmlurlUIF6TOmhD6duztBI7vYzd4PGrJu8TinhAm69Kzv5QVFpNO55mssxsthPqmwb7l/py1iTCl0tSHUcB5Wsep0bcYDztIPhZ3g7VOcKKqu+YBTcuWprMvM22nvVdzcismGvCjkVW5hqCuNaCJPaUY+7VKRqCLg6FPY4WLMOdbY2yotQ4Q==
   message: MUICAaSGjcjwaDUORzctiK8rDla+w2Wm/6Z75EP7LJsRZbCk5zVHC/R6oxaJ6VrVbaB6WG1k9xtUcCt9fGN40r3zjFxp4M24QEdVu18t7A8N4wbiwd1W7LqbqjieziBchtKIJjE4oM68BlKxJMGO020GSnwNBuXIaz1b1MRVQDEsm1eT/oTx1oeLvwG1X94raKpHmK7D
*************************** 2. row ***************************
        id: 2
  users_id: 1
fromUserId: 2
   subject: MUICAeF/ci3fG7YWIhuLCU0pIxpbNDQ10KVWjSZF4G3K8lNVdV81LeveFyhAt/9PvJQy1ePLzl3EupJCROEQ+/L4nHUFRvCekEter2AQvnlyW03cxfjzZ6XwXBUkhzhZrT22JzxL7gSzpC6fPInAzsdDRIqhK/50wPhg/Wr29pH+PT/qwfuKr0rQWdyIfg==
   message: MUICAYOTzGO7DcpX7bVn4BMH4DKW+JCH1Eacj+lPx9I6eOHjNTA9jHT+u+VEz7cfdiitySU5UYYpBz+IBpV7b8hp/hwkqoLP7Durq/sPRJE/qMY9naKumaPXYW6XMhkysfwnwAIWQhjuiu/r9ARBwdTP3AgFNfVVc/jTlCuaPPZ8jw39xKCO5eNU3UyBrcXFhsGBnS6B
2 rows in set (0.00 sec)

Notice that while the plain text is actually the same in both messages, in the database they appear to be completely different, so even if an attacker had access to the database directly, without the encryption key they would not even be able to tell the messages are related, let alone identical, or find out their contents.

Now it is decryption time!

<?php
namespace Acme\Service;

use Acme\Model\Message as MessageModel;
use Acme\Model\Repository\Message as MessageRepository;
use Acme\Model\User as UserModel;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;
use ParagonIE\Halite\KeyFactory;
use ParagonIE\Halite\Symmetric\AuthenticationKey;
use ParagonIE\Halite\Symmetric\Crypto;
use ParagonIE\Halite\Symmetric\EncryptionKey;

/**
 * Class Message
 * @package Acme\Service
 */
class Message
{

// ... SOME CODE HERE ...

        /**
         * @param $userId
         * @param $messageId
         * @return array
         * @throws \InvalidArgumentException
         */
        public function get($userId, $messageId)
        {
            /** @var MessageRepository $repository */
            $repository = $this->em->getRepository('Acme\Model\Message');
            /** @var MessageModel $message */
            if (($message = $repository->find($messageId)) == true) {
                $toUser = $message->getToUser();

                /*
                 * Verify that the message belongs to the intended user
                 */
                if ($toUser->getId() == $userId) {
                    $toUserSalt = $toUser->getSalt();
                    $fromUser = $message->getFromUser();

                    $encryptionKeySubject = KeyFactory::deriveEncryptionKey(base64_decode($toUserSalt) . 'subject' . $message->getId(),
                        base64_decode(getenv('HALITE_ENCRYPTION_KEY_SALT')));

                    $encryptionKeyMessage = KeyFactory::deriveEncryptionKey(base64_decode($toUserSalt) . 'message' . $message->getId(),
                        base64_decode(getenv('HALITE_ENCRYPTION_KEY_SALT')));

                    $plainSubject = Crypto::decrypt(base64_decode($message->getSubject()), $encryptionKeySubject, true);
                    $plainMessage = Crypto::decrypt(base64_decode($message->getMessage()), $encryptionKeyMessage, true);

                    return [
                        'id' => $message->getId(),
                        'subject' => $plainSubject,
                        'message' => $plainMessage,
                        'name' => $fromUser->getName(),
                    ];

                }
            }

            throw new \InvalidArgumentException('Message not found');
        }
}

We get the message table repository with $repository = $this->em->getRepository('Acme\Model\Message');, and proceed to load the message with $message = $repository->find($messageId). To decrypt, we have to again derive the encryption key the same way we did when saving the message the first time, only this time we’ll use the \ParagonIE\Halite\Symmetric\Crypto::decrypt method:

$plainSubject = Crypto::decrypt(base64_decode($message->getSubject()), $encryptionKeySubject, true);
$plainMessage = Crypto::decrypt(base64_decode($message->getMessage()), $encryptionKeyMessage, true);

This method receives 3 parameters: the cypher text, the encryption key, and the flag to tell the method that it should expect raw binary bytes in the cypher text. (remember that we stored it base64 encoded in the database, which is why we need to base64 decode it). Now we are able to perform the request to retrieve a message by ID:

Request

GET /users/1/messages/1 HTTP/1.1
Host: yourhost.dev
Cache-Control: no-cache

Response

{
    "id": 1,
    "subject": "This is my subject",
    "message": "super secret information!!!",
    "name": "Jane Doe"
}

You can take a look at the full source code here.

Conclusion

Encrypting and decrypting is dead simple with Halite. This does not mean that the above example is super secure. There is still a lot of potential for improvement, and Halite has other interesting features which might be showcased in a later article. Remember not to take the above implementation as production ready – it’s only for educational purposes.

How do you do encryption / decryption in PHP? Have you used Halite? Let us know!

  • This would be an interesting feat if I wanted to be more 1st party about my personal/business emails and even develop my own HA email servers. I’m already developing a first part replacement for Mail Chimp. But cryptography has become a big topic for me lately as I’ve also started using ProtonMail. I know they’re not perfect either, but it’s a big start.

    • Miguel Ibarra

      The important thing is that since encryption with Halite can be implemented easily, you can use the library not only in email messaging-like applications, but to store any sensitive information in a database, file, etc…

      • Halite even has a separate API for file crypto so you can, for example, encrypt a 1.6 GB database dump on a system with 128 MB of RAM with your public key (using File::seal()). Our goal was security and usability. The only complaint I’ve received was “pass true to get raw binary”, but that’s what PHP developers have come to expect from PHP’s hash interfaces.

        • Miguel Ibarra

          Wow, that is amazing, I did not realize that such a tremendous amount of information could be processed with so few resources.

          • Yeah, it uses a streaming API for everything. The CMS I built Halite for, Airship, needs to be able to calculate/verify Ed25519 signatures of update files, which may be several MB in size. To make this less memory-intensive, we calculate a keyed 512-bit BLAKE2b hash of the file’s contents (using a streaming hash interface with 1MB chunks) and then sign the resultant hash.

            The security of this scheme remains at the 128-bit level (which is what Ed25519 offers); the hash itself offers 256 bits of security, so it’s a great performance gain on resource-constrained software without a security penalty.

            We published a blog post in this vein a while back, titled “Let’s Re-Think Security Trade-Offs”. Very relevant here. :)

            https://paragonie.com/blog/2015/12/let-s-re-think-security-trade-offs

  • Raghu Veer

    using Halite 1.0 (based on php 5.6) in one of the fintech application that we are working on :) more in terms of asymmetric encryption and signature perspectives

Recommended
Sponsors
Get the latest in PHP, once a week, for free.