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:
11 Getting Started with Zend Framework 237 11.1 Installation.37.
Some of the cryptographic terms used in this article can be defined as follow:
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.
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 UbuntuInstall in CentOS
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:
The most relevant fields are:
Let’s begin by initializing a
composer.json file, and then declaring the dependency of our project on the Halite library using Composer.
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.
After successfully downloading the libraries, the
composer.json file should be modified to look like the following:
The Acme namespace will hold our custom classes.
The RESTful services in our sample app have the following structure:
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):
When we want to sent a message to John, from Jane, then we should send the following request:
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
AcmeServiceMessage::save method.
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:
![]()
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
ParagonIEHaliteKeyFactory class to generate encryption keys. Please look at the following statement carefully:
The
ParagonIEHaliteKeyFactory::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:
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
The first parameter of
ParagonIEHaliteSymmetricCrypto::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
Response
If we query the database directly, the record will look like the following:
If for any reason we were to send exactly the same request:
And query the database again:
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!
We get the message table repository with
$repository = $this->em->getRepository('AcmeModelMessage'); , 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 ParagonIEHaliteSymmetricCrypto::decrypt method:
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
Response
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!
![]() Comments are closed.
|
AuthorWrite something about yourself. No need to be fancy, just an overview. Archives
March 2023
Categories |