Adam Innes · Blog

Adobe's Breach: Encrypted Passwords Are Not Hashed Passwords

· 7 min · security, passwords, php, cryptography

On Thursday, October 3, Adobe’s Chief Security Officer Brad Arkin posted an Important Customer Security Announcement. Adobe’s security team had found attacks on its network involving customer information and source code for numerous Adobe products. One word in it caught my eye: Adobe says its investigation indicates the attackers accessed customer IDs and encrypted passwords.

I don’t know how Adobe stored its passwords beyond what Adobe has said, and I’m not going to guess. But “encrypted” is worth slowing down on, because a lot of us say it when we mean “hashed”, and for passwords that difference decides how bad a stolen database is. So this post is mostly about your systems, not Adobe’s.

What Adobe has said so far

Beyond those IDs and passwords, Adobe believes the attackers removed information relating to 2.9 million customers, including names, encrypted credit or debit card numbers, expiration dates and other order information, and says it doesn’t believe decrypted card numbers were removed.

On the password side, Adobe says it’s resetting relevant customer passwords as a precaution, and that people whose ID and password were involved will get an email explaining how to change it. It also recommends changing your password on any other site where you used the same ID and password. Adobe’s customer security alert page, as it read last week, adds that Adobe ID is separate from the logins for EchoSign, Behance, TypeKit, Marketing Cloud and Connect Pro, so reused passwords there should change too. To avoid phishing, it suggests going directly to Adobe.com to change your password instead of clicking links in an email.

Encryption is built to be undone

Encryption is a two way street. You take plaintext and a key, you get ciphertext, and anyone holding the key can turn the ciphertext back into the original plaintext. That’s what you want for a card number you have to send to a payment processor later.

It’s not what you want for a password. Your login code never needs to know what a user’s password is. It only needs to answer one question: does what they just typed match what they set up? If passwords are stored encrypted, the protection of every account is only as good as the secrecy of the key. The key has to live somewhere your application can reach, so an attacker who gets far enough in to copy the users table may well find it too, and then every password falls out at once.

One way password hashing flips this around. A hash function turns a password into a fixed value, and there’s no key that turns that value back into the password. To log someone in, you hash what they typed the same way and compare. An attacker holding the hashes has to guess passwords, hash each guess and look for a match. How painful that is depends on how you hash.

Why MD5 and SHA aren’t the answer

Swapping encrypt() for md5() or sha1() helps less than you’d hope. General purpose hashes are designed to be fast, which is great for checksums and terrible for passwords, because an attacker’s guessing speed is set by how quickly they can compute your hash.

Niels Provos and David Mazières made this argument back in 1999 in A Future-Adaptable Password Scheme, the USENIX paper that introduced bcrypt. Passwords don’t get longer or more random over time, while attackers’ hardware keeps getting faster. When the traditional Unix crypt was deployed in 1976 it could hash fewer than 4 passwords per second, but by the time of the paper a fast workstation could run over 200,000 crypt operations per second. Even MD5 crypt, which was much slower than the original, had a fixed cost and so couldn’t keep up with faster machines. Their conclusion was that a password function needs a tunable cost you can raise as hardware improves.

Salt, then slow

The first fix is a salt: a random value generated per password and stored alongside the hash. Without one, the same password always produces the same hash, so an attacker can precompute a table of common passwords once and use it against every database. The Provos and Mazières paper describes how a large random salt makes lookup tables useless, leaving the attacker to compute the function for each guess against each account. RFC 2898, which republishes RSA Laboratories’ PKCS #5 v2.0, makes the same point about precomputation and recommends a random salt of at least eight octets.

Salt alone doesn’t slow down guessing against a single account, though. That’s the job of the second fix, key stretching, where the function is made deliberately expensive. There are three well known options.

PBKDF2 comes from that same RFC 2898. It runs a pseudorandom function (the RFC’s example is HMAC-SHA-1) over and over, and the iteration count sets the cost. The RFC recommends a minimum of 1000 iterations, but that figure was written in 2000, so treat it as a floor and not a target.

bcrypt is the scheme from the Provos and Mazières paper. It’s built on eksblowfish, a variant of the Blowfish cipher with a purposely expensive key setup. It takes a 128-bit salt and a cost parameter, and the expensive part of the setup runs 2 to the power of cost times, so every step up in cost doubles the work. The output bundles the cost and salt together with the hash.

scrypt is the newest of the three. Colin Percival originally developed it for the Tarsnap backup service, and his scrypt page points to the paper he presented at BSDCan in 2009, Stronger Key Derivation via Sequential Memory-Hard Functions. His concern was attackers with custom parallel hardware, which can run huge numbers of guesses side by side. scrypt is designed to need a lot of memory as well as time, and a bigger circuit per guess means fewer copies fit on the same silicon.

Any of the three beats a plain hash. Pick one, use a real library for it and set the cost as high as your login traffic can stand.

Doing it in PHP 5.5

If you’re on PHP, the easy path arrived this summer. The PHP 5.5.0 release announcement lists a simplified password hashing API among the new features, and 5.5.0 came out on June 20. The password_hash manual page describes it as creating a hash with a strong one way algorithm. With PASSWORD_DEFAULT, that’s bcrypt as of PHP 5.5.0, a random salt is generated for each password if you don’t supply one (which is the intended way to use it), and the cost defaults to 10. The manual also recommends testing on your own servers and adjusting the cost, and the version current this month suggests a cost where one hash takes roughly 0.1 to 0.5 seconds.

Here’s the whole flow for signup and login:

<?php
// Signing up or changing a password: store $hash in a VARCHAR(255) column.
$hash = password_hash($password, PASSWORD_DEFAULT);
if ($hash === false) {
    throw new RuntimeException('Password hashing failed');
}

// Logging in: load that account's $hash, then check the attempt against it.
if (password_verify($attempt, $hash)) {
    if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
        $hash = password_hash($attempt, PASSWORD_DEFAULT);
        // Save the new $hash for this account.
    }
    // The password is correct, so log the user in.
}

There’s no separate salt column. password_verify works because the string from password_hash() already contains the algorithm, cost and salt. With bcrypt that’s a 60 character string starting with $2y$, then the two digit cost and the salt, then the hash. The manual suggests a column that can grow past 60 characters, with 255 as a good choice, because PASSWORD_DEFAULT is meant to change as stronger algorithms are added. That’s also why the password_needs_rehash() check is there: when you raise the cost or the default changes, users quietly get upgraded the next time they log in, since that’s the only moment you have their plaintext password.

If you already store encrypted passwords, you don’t have to wait for logins to fix it: decrypt them once, hash each with password_hash(), and then destroy the key.

Don’t leave a trail around the password

Hashing the password does no good if the same table holds something that gives it away. The obvious one is a password hint stored in plain text. A hint is written to remind someone of their password, so it’s easy for one to give away most of it, or all of it. If your account recovery needs a hint, I’d drop the feature and send a reset link to the email address on file instead. The same thinking goes for security answers stored as plain text, and for logs that record full request bodies from your login form.

Plan the reset before you need it

The other lesson in Adobe’s response is the reset itself. If you ever have reason to believe your password data was copied, even well hashed data, forcing a reset is the responsible move. Slow hashes buy time, and weak passwords still fall to guessing given enough of it.

Build that now, while nothing is on fire: a way to invalidate the stored hashes for some or all accounts, expire sessions and remember-me tokens, and send a notice. Adobe’s alert touched on two details worth copying: tell people to change the same password anywhere else they used it, and tell them to go to your site directly instead of clicking a link in an email, since a reset notice is exactly the kind of message a phisher would like to imitate.

The takeaway

Encryption protects data you need to read again, and passwords aren’t that. Store something you can check but can’t read back, with a random salt and a deliberately slow function like bcrypt, scrypt or PBKDF2, through a library. On PHP 5.5 that’s password_hash() and password_verify(). Keep hints out of the database and have a reset plan ready, so a stolen users table is a pile of expensive guesses and not a list of passwords.

← all posts