Basis Lab

Answers · Keeping it safe

Why isn't encrypting the password enough to keep it safe if the database leaks?

In shortBecause encryption can be undone by your own login code, so the key that lets you check a password usually sits near the database, and anyone who copies both can read every password.

Encrypting the password column does not make a stolen database harmless, because encryption is reversible and your login code must reverse it. The property you need is not secrecy but irrecoverability: somebody holding the stored value must not be able to produce the password. Encryption cannot give you that while it still lets your service check a login.

c=Ek(p),check(p′)=[Dk(c)=p′]c = E_k(p), \qquad \text{check}(p') = \big[D_k(c) = p'\big]

That distinction is easy to miss because both operations look like checking a password from the outside. At login, the server is handed a candidate and must answer yes or no. Encryption answers by going backwards from the stored ciphertext to the original and comparing. Hashing answers by going forwards from the candidate and comparing tags. The first puts the password back into memory every time, and anyone who can run the same code can do the same. The second never puts the original back, even for the legitimate service.

The key travels with the data

The natural guess is that an unreadable blob is safe so long as the key is secret. The failure is not that encryption is weak. It is that the key has to be available to the running service at the moment somebody logs in, so it lives in configuration, an environment variable, or a vault the service reaches with credentials that are themselves in configuration. Whoever copied the table was inside far enough to read a database; the step to the service configuration is short, and in many backup paths the key is in the same archive. With the table and the key, the attacker does not guess. They decrypt each row and read the password.

r=a⋅s=40,000×0.5=20,000r = a \cdot s = 40{,}000 \times 0.5 = 20{,}000

Take a table of 40,000 accounts. If the backup contains the encrypted column and the key, the attacker decrypts all 40,000 rows and has 40,000 passwords. The loss does not stop at your system. If about half of people reuse a password on another service, the number of accounts at risk elsewhere is 40,000 × 0.5 = 20,000. Those are accounts you have no relationship with, and their owners will not know where the leak came from.

Under encryption with the key, the attacker's work is not a guess multiplied by a cost; it is one decryption per row. Under a one-way value, the attacker's work becomes guesses multiplied by the cost of one computation. That is the exchange you are making: you give up the ability to recover a password so that the attacker has to search, and then you choose a function whose cost per search step is high enough. No arrangement makes the search impossible. It makes the search uneconomic for the value of the account.

What to store instead

Store a one-way value with a per-user salt and a cost parameter. At login you recompute the value from the typed password and the stored salt, then compare tags. You never need the original password back. An attacker holding the table has a tag and a salt, not a ciphertext and a key, so the only way back is to guess a candidate and compute the same function. That is still possible, which is why the function must be slow. A fast hash is better than encryption but not enough on its own: it turns one decryption per row into a guessing race, and a fast function makes each guess cheap. The salt matters for a second reason. To test one candidate against all 40,000 rows with a different salt per row, the attacker needs 1 × 40,000 = 40,000 computations. With no salt, one computation can be compared against every row, so one lucky guess can open every account that reused that password.

The narrow exception

There is a case where encryption is right: a credential you must recover later, such as a token for a third-party service you sign in to on the user's behalf. Then you have no choice, and the key belongs in hardware that will not release it. A password you only ever need to check is not that case. If the key were genuinely kept out of every backup and inside hardware the attacker cannot reach, a stolen table alone would be less damaging. But that is a narrower assumption than the one worth designing for, and the reversibility remains: anyone who can make your service check a login can also make it decrypt, if they have reached the service.

Worth keeping

  • Encryption is reversible by design, and the key needed to reverse it must be available to the login service, so a stolen database and a stolen key are often the same theft.
  • A password store should be able to confirm a candidate without being able to produce the original; a one-way value with a per-user salt and a cost parameter does that.
  • Reuse turns a local leak into damage on other systems: with 40,000 accounts and half reusing, 20,000 accounts elsewhere are exposed.

Where this goes next

This question is one step of How a Password Is Stored, a course that works through its subject from the beginning. The page that teaches this part of it is Never Store the Password, and it reads the whole way through whether or not you have an account.

What an account changes is the practice: problems standing under every idea, marked with a reason rather than a tick, in a copy of the course that is yours.

Read that pageThe whole course

Other questions people ask about this

  1. Why does storing a random salt with each hash stop rainbow tables?Because the salt is different for every account, a table built once from common passwords matches at most one row, so the work has to be redone account by account.
  2. How do I pick the iteration count or cost factor for password hashing?Pick a time budget for one login, measure what setting meets it on your own hardware under load, and store that setting in each stored record.
  3. How fast can an attacker guess passwords after stealing a hash database?About ten thousand million guesses a second on one rented graphics card against a fast hash, so most of a stolen table falls in under a second.
  4. How does text I saved in a comment end up running as JavaScript in someone else's browser?The comment stayed text in storage; a page that later pasted it into its own markup made the browser read it as code.
  5. Why does escaping the quotes in user input not stop SQL injection?Escaping quotes only cleans one context; SQL injection stops only when query text and values travel separately, so the value is never parsed as code.
  6. How do I stop one service from reading another service's password?Give every service its own identity, grant only the credentials it uses, and test that it cannot read another service's password.

Read alongside