While working on [sodium_compat, our pure-PHP implementation of libsodium](https://github.com/paragonie/sodium_compat), it has come to our attention that a lot of the engineering decisions we've made to minimize the risk of side-channels aren't well-known outside of our development team. In the interest of transparency, as well as promoting a better understanding among the PHP developer community, we'd like to take this opportunity to document our methods. > Important: Because sodium_compat [has not been audited yet](https://github.com/paragonie/sodium_compat/issues/8), this should not be considered exhaustive. There may be additional steps that are necessary for producing secure cryptography code in PHP. This page should be considered supplementary to the existing [rules for programming cryptography code in C](https://cryptocoding.net/index.php/Coding_rules). ## The Zeroth Rule of PHP Cryptography > If you can avoid writing cryptography code in PHP, then ***don't write it in PHP***. Use a reputable PHP extension (such as [libsodium](http://pecl.php.net/package/libsodium)) instead. For example, sodium_compat implements the 0th rule of PHP Cryptography by offloading to libsodium (via core if PHP 7.2+, PECL otherwise) if it's available. ## Easy Wins for PHP Cryptography Code
try {
$data = random_bytes(32);
} catch (Error $ex) {
// You made a mistake.
} catch (Exception $ex) {
// Cannot access the OS's CSPRNG. Handle this however you deem appropriate.
}
It may be tempting to cry foul here, since we wrote random_compat and are recommending it in a blog post.
However, random_compat is one of the most widely used PHP libraries with [about 20 million installs on Packagist](https://packagist.org/packages/paragonie/random_compat) as of this writing (which does not count WordPress 4.4+ websites). It's used by virtually every serious PHP framework (Symfony, Laravel, Zend) that still supports PHP 5.
You will be hard-pressed to find a security expert familiar with PHP 5 that doesn't recommend it.
**DO NOT USE** these PHP functions for cryptography or security purposes:
* `rand()`
* `mt_rand()`
* `uniqid()`
* `lcg_value()`
* `gmp_random_*()`
* Messy hacks like `hash($algo, /* ...stuff... */ microtime() /* ...stuff... */)`
## PHP Cryptography: The Hard Parts
So far, everything covered is generally universalizable: You need a sane API, strong randomness, and constant-time string comparison to do secure cryptography in every programming language. This is where things get a bit tricky and PHP-specific.
<?php
/**
* @param string $knownString
* @param string $userString
* @return bool
*/
function unsafe_hash_equals($knownString, $userString)
{
$kLen = strlen($knownString);
$uLen = strlen($userString);
if ($kLen !== $uLen) {
return false;
}
$result = 0;
for ($i = 0; $i < $kLen; $i++) {
$result |= (ord($knownString[$i]) ^ ord($userString[$i]));
}
// They are only identical strings if $result is exactly 0...
return 0 === $result;
}
// 8 chars but 32 bytes
$hashA = "\xF0\x9D\x92\xB3" . "\xF0\x9D\xA5\xB3" .
"\xF0\x9D\x92\xB3" . "\xF0\x9D\xA5\xB3" .
"\xF0\x9D\x92\xB3" . "\xF0\x9D\xA5\xB3" .
"\xF0\x9D\x92\xB3" . "\xF0\x9D\xA5\xB3";
$hashB = "\xF0\x9D\x92\xB3" . "\xF0\x9D\xA5\xB3" .
"\xF0\xAD\x9F\xC0" . "\xF0\xAD\x9F\xC0" .
"\xF0\xAD\x9F\xC0" . "\xF0\xAD\x9F\xC0" .
"\xF0\xAD\x9F\xC0" . "\xF0\xAD\x9F\xC0";
var_dump(unsafe_hash_equals($hashA, $hashB));
If you run this code with `mbstring.func_overload` set to 0, it returns `FALSE` as you'd expect. Clearly, those two strings are not the same value. However, if you set `mbstring.func_overload` to 2, 3, or 7, it suddenly returns `TRUE`.
To guarantee that PHP is treating strings as raw binary strings (where 1 byte = 1 character), use the multi-byte functions (`mb_strlen()`, `mb_substr()`) instead, taking care to specify `8bit` as the charset.
- $kLen = strlen($knownString);
- $uLen = strlen($userString);
+ $kLen = mb_strlen($knownString, '8bit');
+ $uLen = mb_strlen($userString, '8bit');
To be totally clear: there's a little more to it than that if you're writing software that may run on machines that don't have the mbstring extension installed. Fortunately, the `mbstring.func_overload` option only does anything if mbstring is installed, so if it's not, you can be sure that the original functions behave as expected.
if (CG(one_char_string)[c]) {
ZVAL_INTERNED_STR(return_value, CG(one_char_string)[c]);
}
It helps to know that `one_char_string` is populated [here](https://github.com/php/php-src/blob/04f150679c6ba7c137b8774cb29e8333a1e4c24f/ext/opcache/ZendAccelerator.c#L522-L527), and maps the integers 0 - 255 to their ASCII equivalent.
Why does this matter? Because if the data you're encoding or decoding is secret (either the plaintext or a secret key), using `chr()` means that you are [using table look-ups indexed by secret data](https://cryptocoding.net/index.php/Coding_rules#Avoid_table_look-ups_indexed_by_secret_data), which creates a textbook example of a [cache-timing side-channel](https://eprint.iacr.org/2016/002).
To side-step this, we can use `pack('C', $int);` to safely convert an integer into an ASCII character, since [`pack()` doesn't use any sort of table look-up](https://github.com/php/php-src/blob/c8aa6f3a9a3d2c114d0c5e0c9fdd0a465dbb54a5/ext/standard/pack.c#L523).
/**
* Multiply two integers in constant-time
*
* @param int $a
* @param int $b
* @return int
*/
public static function mul($a, $b)
{
# Important detail: If you know your platform is secure, you can opt
# out of our implementation in favor of just multiplying integers the
# old fashioned way by setting a static variable to true at run-time.
#
# It defaults to FALSE and we advise no one set it to TRUE unless you
# have irrefutable proof that this is safe for your operating environment.
if (ParagonIE_Sodium_Compat::$fastMult) {
return (int) ($a * $b);
}
# Optimization: Cache this value after the first run
static $size = null;
if (!$size) {
# On 64-bit platforms, this will be 63.
# On 32-bit platforms, this will be 31.
$size = (PHP_INT_SIZE << 3) - 1;
}
$c = 0;
#
# Equivalent to: $mask = ($b < 0) ? -1 : 0;
#
# -1 in binary is all 1 bits; 0 in binary is all 0 bits.
$mask = -(($b >> $size) & 1);
#
# We're stripping off the negative sign so we can safely
# use bitshift operations and eventually get 0.
$b = ($b & ~$mask) | ($mask & -$b);
#
# This is the actual multiplication loop.
# It always runs ($size + 1) times, and consists of
# addition and bitwise operators.
for ($i = $size; $i >= 0; --$i) {
# Equivalent to: if ($b & 1) $c += $a;
$c += (int) ($a & -($b & 1));
# Double $a
$a <<= 1;
# Halve $b
$b >>= 1;
}
# Equivalent to: if ($mask < 0) { $c *= -1; }
#
# This just reintroduces the negative sign again if $b had
# a negative value.
$c = ($c & ~$mask) | ($mask & -$c);
return (int) $c;
}
Thus, you can eliminate *timing side-channels introduced by integer multiplication* from your PHP code.
## The Impossible
Some cryptography best practices are simply not possible. To wit: PHP doesn't allow you to perform direct memory management, so zeroing out memory buffers is not possible.
Furthermore, if a vulnerability is introduced somewhere else in the PHP interpreter (for example, via OpCache), there's very little (if anything) you can do to mitigate it from a PHP script.
## The Ambitious
Are you thinking about writing your own cryptography code in PHP? Have you inherited a legacy application where someone decided to be inventive with their strategy for handling sensitive data? Or are you just interested in seeing if an existing PHP cryptography library you want to use is secure?
We consult, we develop, we pen-test, and we also audit cryptography code.