Last year, we began developing Halite, a FOSS high-level wrapper for the PHP bindings to libsodium. We use Halite extensively in our own projects (including our upcoming CMS which has quite a few of its own innovative cryptography features baked-in).
As of version 2.1.0
, we are confident that Halite solves all of the application-layer cryptography problems that most PHP developers face; and it does so in three easy steps. (For transport-layer cryptography, you should still use TLS, of course.)
The Three Steps to Simply Secure Cryptography Development
Step One: Managing Cryptography Keys
Managing keys in Halite is drop-dead simple. You don't need to stress over the details. This is best demonstrated by example:
<?php
use \ParagonIE\Halite\KeyFactory;
// Generate a new random encryption key:
$encryptionKey = KeyFactory::generateEncryptionKey();
Once you have a cryptography key, you can save it to a file and then reload it like so:
// Saving a key to a file:
KeyFactory::save($encryptionKey, '/path/to/encryption.key');
// Loading the key from a file:
$key = KeyFactory::loadEncryptionKey('/path/to/encryption.key');
That's all there is to managing keys.
- Generate a new key, which will be secure, then save it somewhere.
- Load it every time you need to use it.
Alternatively, if you wish to derive the key from a password, you can just do this:
<?php
use \ParagonIE\Halite\KeyFactory;
// Do this ONCE per key:
$salt = random_bytes(16);
// Generate a new random encryption key:
$encryptionKey = KeyFactory::deriveEncryptionKey(
$password,
$salt // Generated by random_bytes(16), never rand() or mt_rand()
KeyFactory::MODERATE // (This third parameter is totally optional.)
// also allows KeyFactory::INTERACTIVE or KeyFactory::SENSITIVE
// defaults to KeyFactory::INTERACTIVE
);
There are six types of keys Halite uses, which correspond to distinct cryptography features:
- Symmetric Cryptography (both parties know the same key)
-
AuthenticationKey
- used for authenticating messages -
EncryptionKey
- used for encrypting messages
-
- Asymmetric Cryptography (both parties have their own secret key, and exchange their public keys)
-
EncryptionPublicKey
-
EncryptionSecretKey
-
SignaturePublicKey
-
SignatureSecretKey
-
To make keys easier to organize, you can just generate a KeyPair
object for asymmetric keys. This isn't necessary; if you have the *SecretKey
you can produce the corresponding *PublicKey
:
$signSecretKey = KeyFactory::generateSignatureSecretKey();
$signPublicKey = $signSecretKey->derivePublicKey();
// $signPublicKey is a SignaturePublicKey object
Step Two: Encrypting or Authenticating with Halite
Once you have your keys set up, using Halite is a walk in the park.
Symmetric-Key Encryption
This is all you have to do:
<?php
use \ParagonIE\Halite\{
Symmetric\Crypto as Symmetric,
KeyFactory
};
// Loading the key from a file:
$key = KeyFactory::loadEncryptionKey('/path/to/encryption.key');
// And now, to encrypt a message:
$encrypted = Symmetric::encrypt('this is your plaintext', $key);
You don't need to worry about accidentally passing a weak key to encrypt()
; it requires an EncryptionKey
object. You don't need to know what a nonce is; Halite generates it for you. You don't need to worry about chosen-ciphertext attacks; Halite strictly uses authenticated encryption behind-the-scenes.
See below for how to decrypt these messages.
Symmetric-Key Message Authentication
If you only need to authenticate a message (e.g. for API communication) and encryption isn't necessary, Halite has you covered too.
<?php
use \ParagonIE\Halite\{
Symmetric\Crypto as Symmetric,
KeyFactory
};
// Loading the key from a file:
$key = KeyFactory::loadAuthenticationKey('/path/to/authentication.key');
// And now, to authenticate a message:
$authCode = Symmetric::auth('this is your plaintext', $key);
See below for how to verify these messages.
Asymmetric-Key Encryption
There are two flavors of asymmetric (Public-Key) encryption offered by Halite:
-
Authenticated Public-Key Encryption, which uses your
EncryptionSecretKey
and theirEncryptionPublicKey
. Both the sender and receiver can decrypt these messages. -
Anonymous Public-Key Encryption, which only uses your recipient's
EncryptionPublicKey
. Only the recipient can decrypt these messages.
The authenticated case uses encrypt()
and decrypt()
, like so:
<?php
use \ParagonIE\Halite\{
Asymmetric\Crypto as Asymmetric,
KeyFactory
};
$aliceSecretKey = KeyFactory::loadEncryptionSecretKey('/path/to/enc.secret.key');
$BobPublicKey = KeyFactory::loadEncryptionPublicKey('/path/to/bob.enc.public.key');
// And now, to encrypt a message:
$encrypted = Asymmetric::encrypt(
'this is your plaintext',
$aliceSecretKey,
$bobPublicKey
);
The anonymous case uses seal()
and unseal()
, like so:
<?php
use \ParagonIE\Halite\{
Asymmetric\Crypto as Asymmetric,
KeyFactory
};
$bobPublicKey = KeyFactory::loadEncryptionPublicKey('/path/to/bob.enc.public.key');
// And now, to encrypt a message:
$encrypted = Asymmetric::seal(
'this is your plaintext',
$bobPublicKey
);
See below for how to decrypt these messages.
Asymmetric-Key Message Authentication (Digital Signatures)
This is as straightforward as you might expect:
<?php
use \ParagonIE\Halite\{
Asymmetric\Crypto as Asymmetric,
KeyFactory
};
// Loading the key from a file:
$secretKey = KeyFactory::loadSignatureSecretKey('/path/to/sign.secret.key');
// And now, to authenticate a message:
$signature = Asymmetric::sign('this is your message', $secretKey);
See below for how to verify digital signatures.
Step Three: Decrypt or Verify
Symmetric-Key Decryption
Following from the symmetric-key encryption example, decryption is straightforward:
$plaintext = Symmetric::decrypt($encrypted, $key);
If the contents of $encrypted
were tampered with, instead of getting a result, it will throw an \ParagonIE\Halite\Alerts\InvalidMessage
exception.
You may choose to catch the exception (i.e. to handle the failure gracefully for the sake of consistent user experience). If you do not catch it, it will terminate script execution rather than fail open. (If you somehow forget to turn display_errors
off, your keys will not be exposed by a stack trace.)
This will never return FALSE
; there is no need to check the return value.
Symmetric-Key Message Verification
Following from the symmetric-key message authentication example, verification is straightforward:
if (Symmetric::verify('this is your plaintext', $key, $authCode)) {
// Success
} else {
// Invalid authentication code for this message
}
Asymmetric-Key Decryption
To decrypt a message that was encrypted using encrypt()
:
<?php
use \ParagonIE\Halite\{
Asymmetric\Crypto as Asymmetric,
KeyFactory
};
$bobSecretKey = KeyFactory::loadEncryptionSecretKey('/path/to/enc.secret.key');
$alicePublicKey = KeyFactory::loadEncryptionPublicKey('/path/to/alice.enc.public.key');
// And now, to decrypt a message:
$plaintext = Asymmetric::decrypt(
$encrypted,
$bobSecretKey,
$alicePublicKey
);
The unseal()
API is as simple as seal()
:
<?php
use \ParagonIE\Halite\{
Asymmetric\Crypto as Asymmetric,
KeyFactory
};
$bobSecretKey = KeyFactory::loadEncryptionSecretKey('/path/to/enc.secret.key');
// And now, to encrypt a message:
$plaintext = Asymmetric::unseal(
$encrypted,
$bobSecretKey
);
As with symmetric-key decryption, if the encrypted message is tampered with in-transit, these methods will throw an InvalidMessage
exception rather than return FALSE
.
Asymmetric-Key Digital Signature Verification
Verifying digital signatures is as straightfoward as creating digital signatures:
<?php
use \ParagonIE\Halite\{
Asymmetric\Crypto as Asymmetric,
KeyFactory
};
// Loading the key from a file:
$publicKey = KeyFactory::loadSignaturePublicKey('/path/to/sign.public.key');
if (Asymmetric::verify($message, $publicKey, $signature)) {
// Valid signature for this message
} else {
// Incorrect signature
}
There is No Step Four
That's it. You've just solved 90% of the cryptography use-cases in three easy steps:
- Generate/derive keys (and store them for later use).
- Encrypt/authenticate your messages.
- Decrypt/verify your messages.
It could hardly be any simpler while still being secure.
What about the other 10% of use-cases?
Halite has most of them covered as well, and they also only require up to two steps to implement once your keys exist. Read on to learn more about any specific solution to a problem you encounter.
The Other Problems Solved By Halite
Encrypted Password Hashes
As mentioned in our previous blog post about the cryptography powering CMS Airship, Halite's Password
class offers encrypted password hashes, which allows you to add a level of security above the already impressive threshold of Argon2i in hardware-separated environments. (An attacker cannot even begin to attempt to crack the Argon2i hashes, since they're encrypted, and the attacker cannot obtain the key from a database server compromise.)
<?php
use \ParagonIE\Halite\{
KeyFactory,
Password
};
$key = KeyFactory::loadEncryptionKey('/path/to/encryption.key');
$hash = Password::hash($password, $key);
As with key derivation, you may pass an optional KeyFactory::MODERATE
or KeyFactory::SENSITIVE
to the third parameter to increase the Argon2i cost factors. This probably isn't necessary (and may expose your servers to DoS attacks without proper rate-limiting).
Verifying passwords (and checking if a re-hash is needed):
if (Password::verify($password, $hash, $key)) {
// Login successful, but first, check that our hash is still good (i.e. in case Halite updates):
if (Password::needsRehash($hash, $key, KeyFactory::INTERACTIVE)) {
$replaceStoredHash = Password::hash($password, $key);
}
} else {
// Incorrect username or password
}
File Cryptography
Halite has a File
class to facilitate whole-file cryptography. File
shines in situations where, for example, you need to encrypt/sign/seal a 10 GB file on a server with very little RAM (say, 128 MB).
Its public API is rather simple and mirrors the features outlined in the three steps outlined above.
-
File::checksum()
- Calculates the Blake2b-512 checksum of a file. -
File::encrypt()
- Symmetric-key Encryption -
File::decrypt()
- Symmetric-key Decryption -
File::seal()
- Asymmetric-key Anonymous Encryption -
File::unseal()
- Asymmetric-key Anonymous Decryption -
File::sign()
- Calculate a digital signature of (the keyed checksum of) a file- BLAKE2b-512 with the
SignaturePublicKey
used as the BLAKE2b key
- BLAKE2b-512 with the
-
File::verify()
- Verify the digital signature of (the keyed checksum of) a file
For example, encrypting a file with a public key (using the seal()
API) looks like this:
<?php
use \ParagonIE\Halite\{
File,
KeyFactory
};
$bobPublicKey = KeyFactory::loadEncryptionPublicKey('/path/to/bob.enc.public.key');
// And now, to encrypt a message:
File::seal(
'/path/to/input.file',
'/path/to/output.file',
$bobPublicKey
);
The recipient only needs to run:
File::unseal(
'/path/to/encrypted.file',
'/path/to/decrypted.file',
$bobSecretKey
);
This feature is fully documented here.
BLAKE2 Merkle Trees
You can use Halite to build a BLAKE2 Merkle Tree, like so:
<?php
use \ParagonIE\Halite\Structure\{
MerkleTree,
Node
};
$tree = new MerkleTree(
new Node('GENESIS BLOCK'),
new Node('Next piece of data'),
new Node('Yet another piece of data')
);
var_dump($tree->getRoot());
// string(64) "15343b62af2602fc9d0c93b440a4b0f6c745537a094e890c6bef08bebf71a72b"
A Merkle Tree is a building block for more powerful cryptography features (e.g. Keyggdrasil). It effectively provides an append-only data structure that can be independently audited (sans any practical hash collisions).
As of Halite 2.1.0, you can adjust the hash size of your Merkle tree as well as personalize all of the BLAKE2b hashes with a string unique to your application.
$tree->setPersonalizationString('some constant');
var_dump($tree->getRoot());
// string(64) "087d6d429ae665915bce2652c6036acc9d4584834179884887005aa84074744d"
$tree->setHashSize(16); // Raw bytes, not hexits
var_dump($tree->getRoot());
// string(32) "6a77dec5b6229be067aa7cf678ce32cf"
Merkle Trees are immutable. If you wish to append another node to the tree, use the getExpandedTree()
method, which creates a new MerkleTree
object.
$otherTree = $tree->getExpandedTree(
new Node('Extra Data'),
new Node('Something else goes here')
);
$otherTree->setHashSize(16); // Raw bytes, not hexits
var_dump($otherTree->getRoot());
// string(32) "ed3fbf1c01399fba7fe0c43086b95113"
Expanded trees will inherit the parent tree's personalization string and hash size parameters.
$newTree = new MerkleTree(
new Node('GENESIS BLOCK'),
new Node('Next piece of data'),
new Node('Yet another piece of data'),
new Node('Extra Data'),
new Node('Something else goes here')
);
var_dump($newTree->getRoot());
// string(64) "9228af238afa1b94c30cc763a176dc3397f72fd6fca773146af40a03fc3f01f2"
Secure Encrypted Cookies
Many PHP developers over the years decided to offload session storage into the client's cookie. This has a lot of problems (they're strictly irrevocable if the client doesn't want to play nice, but mostly bad cryptography), which Halite's Cookie
class intends to solve.
<?php
use \ParagonIE\Halite\{
KeyFactory,
Cookie
};
$key = KeyFactory::loadEncryptionKey('/path/to/encryption.key');
$cookieStorage = new Cookie($key);
// Store a value in an encrypted cookie:
$cookieStorage->store('name', 'value');
// Retrieve a value from an encrypted cookie:
$value = $cookieStorage->fetch('name');
That's Too Easy! What's the Catch?
Halite offers a lot of utility for PHP developers above and beyond what many PHP cryptography libraries try to offer. We're able to get away with covering this much ground, without introducing really dumb security vulnerabilities, because our underlying cryptography primitives are provided by libsodium.
We designed Halite to be as simple as possible so PHP developers who aren't also cryptography experts can successfully use it to its full potential. Even its internal code is, reportedly, easy for people who don't program in PHP to read.
It's time to ditch mcrypt, abolish hacky home-grown unauthenticated encryption protocols, and embrace the simple and the secure. Did I mention Halite was Free Software?