As John notes, your example is incorrect:
user_email, sha256(raw_password+salt), salt
This is not a good way to store passwords. You should replace sha256 here with a Key Derivation Function (KDF) such as PBKDF2 or scrypt. Then it would be fine. A properly tuned KDF can get the hashing rate down to dozens a second or fewer, even on good hardware (there are various competing factors here because the attacker doesn't have the same language and hardware restrictions you likely do, but even in the worst cases this value can be kept very low in cryptographic terms).
But even if you used sha256 here, it would be dramatically stronger than an unsalted hash. It makes every hash different. This means that if multiple people have the same password (very common), then breaking one doesn't break all users having the same password. This protects against rainbow tables, and particularly protects people who have very common passwords (password, dragon, mustang, etc.)
But it also protects against other password-collision attacks. For example, say I want to know Alice's password, and I can see it has the same hash as Bob. I now know that tricking either of them into revealing their password through some means will reveal both of their passwords.
Doesn't that make it trivial for attackers to just recompute sha256(raw_password+salt) for all rows if the entire users table is compromised?
This is thinking about the problem backwards. If the attacker knew raw_password
, then yes, this would be trivial. But that's exactly the thing the attacker does not know (and if they did, they wouldn't need to do any hashing). So the attacker must make a full search of each row of the database, which even with just a single SHA-256 is quite slow.
There are approximately 96 characters you can easily type on most English keyboards. The complete space of those for an 8-character string is 96^8 or about 7x10^15. At 20M per second, that's about 360M seconds or roughly 11 CPU-years per row. That's not an impossible space to crack, but it's still not fast. (Obviously there are many thing pushing in both directions; users don't choose passwords randomly, but they also aren't limited to 8 characters. This computation is just for illustration.)
A key take-away is that knowing the salt gives you no information at all about sha256(salt+password)
if you don't know the password, too. That's a key feature of all cryptographic hashes (including the SHA series). If knowing part of the data gave you any information about the hash of the entire data, then that would tell us that the hash isn't secure.