Earlier this week, we published an article titled How to Safely Store Your Users' Passwords in 2016, which ended up on the front page of Hacker News for a full day. The feedback we received from the community was overwhelmingly constructive (and mostly positive), but many first-time readers had questions that, upon closer investigation, boiled down to a difference in philosophy rather than a simple communication error.
It occurred to me that we never officially formalized our philosophy anywhere, partly because I assumed nobody would be interested in reading about it, but also because we don't have a mono-culture; just because these are the principles that we hold doesn't mean anyone who joins our company as we grow needs to agree with our statements along with all of their nuances and immediate logical consequences. It's also not a mindset that we would ever seek to force upon others (especially clients).
However, I've decided to publish this today as a blog post for two reasons:
- Should we ever need to explain ourselves, we've already written it out.
- The philosophy we share may prove useful for others, e.g. for the word choice in a particular argument.
(Unlike most of our blog posts, there is little-to-no technical information ahead.)
In a Nutshell
Software should be secure by default. Tools should be simply, yet comprehensively, secure. Cryptography should be boring (PDF).
Software Should be Secure by Default
Without any configuration changes, every piece of software we provide should aim to be secure. For web applications, this might mean:
- Using HTTPS everywhere.
- When interacting with a database, only using prepared statements.
- When displaying data on a web page, escaping output to prevent XSS attacks.
- Locking down forms with CSRF tokens.
- Not allowing user input to influence file system operations.
- Using carefully selected cryptography libraries and primitives (e.g. always using an AEAD mode or following an Encrypt-then-MAC construction).
- Sending security headers, such as
Content-Security-Policy
, to prime the browser's exploit mitigation mechanisms for action. - Minimizing the reliance on JavaScript, never relying on: Java, Flash, Silverlight, or HTML5 EME.
That last point (avoiding EME) is also crucial: we also believe in software freedom, which includes the freedom to intentionally disable security features.
Consider, for example, the discussion last year around what PHP 7's CSPRNG functions should do if the operating system didn't offer a suitable CSPRNG. There were three competing ideas:
- Raise an
E_WARNING
error, returnFALSE
. Before PHP 7.0.0-RC3, this was the implemented behavior. - Raise an
E_ERROR
error. - Throw an
Exception
.
Option 1 failed open. If you set your error_reporting
directive to ignore E_WARNING
errors, it was a silent failure. Meanwhile, random_str()
would always return aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
. This is undesirable for security.
Option 2 failed closed, which is desirable for security, but it unavoidably crashed your application, which is bad for usability and would have encouraged users to swap random_int()
out for mt_rand()
because "it just works". Developers would have become gun-shy of the new CSPRNG features, citing stability concerns. As Avi Douglen puts it: Security at the expense of usability comes at the expense of security.
Option 3 was the sweet spot: It failed closed by default, but you could add a try
block to gracefully handle the exception (or fail open, or whatever you wanted).
try {
$bytes = random_bytes(32);
} catch (Exception $ex) {
// Graceful handling rather than an ugly error message that discloses your full
// path and/or a stack trace
$this->log('critical', $ex->getMessage());
$this->view->render(
'errors/500.twig',
[
'error' => 'A CSPRNG failure occurred. Sorry for the inconvenience.'
]
);
exit;
}
This was the optimal decision: it maximizes freedom and usability for the developer that uses these features, and if they neglect to add their own code to gracefully handle failures, their users won't be negatively impacted.
Tools Should Be Simply, yet Comprehensively, Secure
This principle is best demonstrated by example.
How to securely encrypt and then decrypt an encrypted message with Halite (once you have a key):
<?php
use \ParagonIE\Halite\Symmetric\Crypto as SymmetricCrypto;
$message = "This code rocks";
$ciphertext = SymmetricCrypto::encrypt($message, $key);
$plaintext = SymmetricCrypto::decrypt($ciphertext, $key);
How to securely decrypt an encrypted message using PHP's bundled OpenSSL and Hash extensions (demo):
<?php
/**
* From https://github.com/defuse/php-encryption
*
* Use HKDF to derive multiple keys from one.
* http://tools.ietf.org/html/rfc5869
*
* @param string $hash Hash Function
* @param string $ikm Initial Keying Material
* @param int $length How many bytes?
* @param string $info What sort of key are we deriving?
* @param string $salt
* @return string
* @throws Exception
*/
function hash_hkdf(
string $algo,
string $ikm,
int $length,
string $info = '',
$salt = null
): string {
$digest_length = mb_strlen(hash_hmac($algo, '', '', true), '8bit');
// Sanity-check the desired output length.
if (empty($length) || $length < 0 || $length > 255 * $digest_length) {
throw new Exception("Bad output length requested of HKDF.");
}
// "if [salt] not provided, is set to a string of HashLen zeroes."
if ($salt === null) {
$salt = str_repeat("\x00", $digest_length);
}
// HKDF-Extract:
// PRK = HMAC-Hash(salt, IKM)
// The salt is the HMAC key.
$prk = hash_hmac($algo, $ikm, $salt, true);
// HKDF-Expand:
// This check is useless, but it serves as a reminder to the spec.
if (mb_strlen($prk, '8bit') < $digest_length) {
throw new Exception('HKDF-Expand failed');
}
// T(0) = ''
$t = '';
$last_block = '';
for ($block_index = 1; mb_strlen($t, '8bit') < $length; ++$block_index) {
// T(i) = HMAC-Hash(PRK, T(i-1) | info | 0x??)
$last_block = hash_hmac(
$algo,
$last_block . $info . chr($block_index),
$prk,
true
);
// T = T(1) | T(2) | T(3) | ... | T(N)
$t .= $last_block;
}
// ORM = first L octets of T
$orm = mb_substr($t, 0, $length, '8bit');
if ($orm === false) {
throw new Exception('Unexpected error');
}
return $orm;
}
/**
* Encrypt a message with AES-256-CTR + HMAC-SHA256
*
* This is example code. Use defuse/php-encryption instead.
*
* @param string $message
* @param string $key
* @return string
*/
function aes256ctr_hmacsha256_encrypt(string $message, string $key): string
{
$iv = random_bytes(16);
$salt = random_bytes(16);
$eKey = hash_hkdf('sha256', $key, 32, 'Encryption Key', $salt);
$aKey = hash_hkdf('sha256', $key, 32, 'Authentication Key', $salt);
$ciphertext = $iv . $salt . openssl_encrypt(
$message,
'aes-256-ctr',
$eKey,
OPENSSL_RAW_DATA,
$iv
);
$mac = hash_hmac('sha256', $ciphertext, $aKey, true);
return base64_encode($mac . $ciphertext);
}
/**
* Decrypt a message with AES-256-CTR + HMAC-SHA256
*
* This is example code. Use defuse/php-encryption instead.
*
* @param string $message
* @param string $key
* @return string
* @throws Exception
*/
function aes256ctr_hmacsha256_decrypt(string $ciphertext, string $key): string
{
$decode = base64_decode($ciphertext);
if ($decode === false) {
throw new Exception("Encoding error");
}
$mac = mb_substr($decode, 0, 32, '8bit');
$iv = mb_substr($decode, 32, 16, '8bit');
$salt = mb_substr($decode, 48, 16, '8bit');
$ciphertext = mb_substr($decode, 64, null, '8bit');
$aKey = hash_hkdf('sha256', $key, 32, 'Authentication Key', $salt);
$calcMac = hash_hmac('sha256', $iv . $salt . $ciphertext, $aKey, true);
if (!hash_equals($calcMac, $mac)) {
throw new Exception("Invalid message");
}
$eKey = hash_hkdf('sha256', $key, 32, 'Encryption Key', $salt);
return openssl_decrypt(
$ciphertext,
'aes-256-ctr',
$eKey,
OPENSSL_RAW_DATA,
$iv
);
}
$message = "This code rocks";
$ciphertext = aes256ctr_hmacsha256_encrypt($message, $key);
$plaintext = aes256ctr_hmacsha256_decrypt($ciphertext, $key);
Both snippets achieve approximately the same level of security (although Halite probably has better side-channel resistance by virtue of using libsodium internally). Which snippet would you rather type into your code?
The frameworks, libraries, and tools that you use to develop your applications should be comprehensively secure while offering simple and usable interfaces. Most password hashing interfaces we recommend follow this format:
# hashing:
ascii_safe_hash = pw.hash(password)
# verifying:
if (pw.verify(password, ascii_safe_hash)):
# Successful
else:
# Incorrect password
This is how 99.99% of programmers should experience password validation. The rich world of cryptography engineering is a concern that the maintainers of the interface should care about, but the users shouldn't even need to be aware of in order to use it safely.
Cryptography Should Be Boring
Right now, there is an ongoing discussion about the (in)security of Vim's cryptmethod, which uses unauthenticated Blowfish-CFB encryption. One response that sticks out from that discussion is:
Please don't throw words like "broken" around, you clearly have a distorted world view. If I encrypt a file with Vim my sister can't read it and has no way to make it readable. Therefore it works just fine.
Ignoring the obvious problems with the reasoning of the vim maintainers, CFB mode is less well-studied than CBC mode (mostly because it's rarely used), which makes it a very interesting target for cryptography researchers. A person or team looking to make a name for themselves could succeed by breaking CFB mode, and its use in vim provides a real-world incentive to do so.
RSA encryption in general is interesting. If an advancement in integer factoring surfaces tomorrow, will 2048-bit RSA still be secure?
RSA encryption with PKCS1v1.5 padding is hilariously broken.
X25519 key exchange + Xsalsa20 encryption with a Poly1305 authentication tag is boring, and it will almost certainly remain boring until a practical quantum computer surfaces.