Using Halite for Privacy and Two-Way Encryption of Emails

Share this article

Using Halite for Privacy and Two-Way Encryption of Emails

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!

Frequently Asked Questions (FAQs) about Halite and Two-Way Encryption of Emails

What is Halite and how does it enhance email privacy?

Halite is a high-level cryptography interface that uses the Libsodium library to ensure the highest level of security. It simplifies the process of encrypting and decrypting data, making it more accessible to developers. When used for emails, Halite can encrypt the content of the message, ensuring that only the intended recipient can decrypt and read it. This significantly enhances email privacy as it prevents unauthorized access to the content of the emails.

How does Halite compare to other encryption methods?

Halite stands out due to its simplicity and high level of security. While other encryption methods may require extensive knowledge of cryptography, Halite provides a user-friendly interface that simplifies the process. Furthermore, it uses the Libsodium library, which is known for its robust security features. This makes Halite a reliable choice for developers looking to implement encryption in their applications.

Can Halite be used for two-way encryption?

Yes, Halite supports two-way encryption, also known as symmetric encryption. This means that the same key is used for both encryption and decryption. This is particularly useful for scenarios where data needs to be encrypted and then decrypted later, such as in the case of storing passwords or encrypting email content.

How secure is Halite for email encryption?

Halite provides a high level of security for email encryption. It uses the Libsodium library, which is known for its strong security features. Furthermore, Halite uses authenticated encryption, which not only ensures the confidentiality of the data but also verifies its integrity. This means that if the encrypted data is tampered with, the decryption process will fail, alerting the user to the tampering.

What is the difference between encryption and hashing?

Encryption and hashing are both techniques used to secure data, but they serve different purposes. Encryption is a two-way process, meaning data can be encrypted and then decrypted back into its original form. This is useful for protecting data that needs to be accessed and read. Hashing, on the other hand, is a one-way process. Once data is hashed, it cannot be converted back into its original form. This is often used for storing passwords, as it allows the system to verify the password without actually knowing what it is.

How does Halite handle key management?

Halite simplifies key management by providing a user-friendly interface for generating, storing, and using encryption keys. It supports both symmetric and asymmetric keys, allowing developers to choose the most appropriate type of key for their specific use case.

Can Halite be used for other types of data besides emails?

Yes, Halite can be used to encrypt any type of data. While this article focuses on email encryption, Halite is versatile and can be used to secure a wide range of data types, from text files to database entries.

Is Halite suitable for beginners in cryptography?

Halite is designed to be user-friendly and accessible, even for those who are new to cryptography. It provides a high-level interface that simplifies the process of encrypting and decrypting data, making it a good choice for beginners.

What are the prerequisites for using Halite?

To use Halite, you need to have PHP 7 or later installed on your system. You also need to install the Libsodium library and the PHP extension for Libsodium.

Where can I find more resources to learn about Halite?

The official Halite documentation is a great place to start. It provides a comprehensive overview of the library and its features, along with examples and tutorials to help you get started. You can also find more information on the Libsodium library and its PHP extension on their respective official websites.

Miguel Ibarra RomeroMiguel Ibarra Romero
View Author

Web application developer, database administrator, project lead in a variety of enterprise apps and now article author. Interested in database design and optimization. Amazed and involved in distributed systems development. Cryptography and information security fan.

BrunoScryptocryptographydoctrineencryptionhaliteOOPHPPHPsecuritysilex
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week