I'm prototyping an app that uses client-side password hashing in order to hide the "real" account password from a server (the server hashes any recieved password anyway, so no plaintext records in the database). It seem to be a controversial thing to do but I didn't find any convincing arguments against it (I'm new to cryptography so please correct me if I'm wrong).
I don't think it's controversial so much as not much explored. There's a couple techniques that have been proposed for shifting the costly password hash over to the clients, for example:
These both work in a similar fashion. For example, to enroll a new password:
- The server picks a random
client_salt
and sends it to the client
- The client computes
client_hash = pwhash(client_salt, password)
and sends it to the server
- The server computes
server_hash = fasthash(client_hash)
, and stores (username, client_salt, server_hash)
in its password database
To log in:
- Client sends
username
to server
- Server looks it up in the password database and sends the corresponding
client_salt
to client
- Client computes
client_hash = pwhash(client_salt, password)
and sends it to server
- Server computes
server_hash = fasthash(client_hash)
and checks whether it matches its password entry
Downside compared to server-side hashing:
- Requires a roundtrip between client and server.
- What do you do if a client requests a username that doesn't exist? A naïve implementation allows an attacker to probe whether a username exists, which might be undesirable in some systems. One mitigation is to respond to requests for nonexistent users with a fake
client_salt
(which should probably be a pseudorandom function of the username), but even that might be defeated by timing attacks.
- The server discloses the
client_salt
value to any attacker who probes for that user ID. Which means that an attacker can start precomputing hashes for users of interest before they manage to steal the server_hash
stored on the server.
My understanding is: each salt value is better to be globally unique for each user so it seems like an email address could make an OK salt in a sense that it's unique, but not globally. Email + password would certainly lower the chances of a collision but some users might have the same passwords in many apps. I'm thinking about sha256(app name + email). Would it make a good salt?
The problem with these is that the salt ideally should be unique not just for each user as you presuppose, but rather for each password database entry state for the same user. What this means is that if a user changes their password, ideally a new salt should be generated for the new password entry, so that work cannot be reused for attacking both the old and the new password entry. None of your proposals achieves that.
Another problem is they allow an attacker who obtains or guesses email addresses and/or the app name string to precompute client-side hashes for users of interest before they manage to steal the server-side hashes. But note this problem is shared with the scheme sketched above—and more generally, it seems this cannot be solved unless you have a stateful client that generates and locally stores its own random client_salt
value that it keeps secret from attackers. This would seem to defeat the purpose of passwords (because you're then relying on the secrecy of a proper cryptographic key), but systems that do something like this exist—for example, 1Password has a "Secret Key" concept where their client generates a cryptographic key that they instruct you to print out and securely store, so that an attacker cannot crack the server-side passwords without stealing your device or the printed copy of the Secret Key.
Another related thing I'm wondering about is how easy is it to crack the hash and not the password itself? Let's say I use Argon2 to hash a password and use the same exact password as hash (argon2(pwd = "foobar", salt = sha256("foobar")). Would it help an attacker to reveal that password by any method except using a rainbow table?
The problem here is that in addition to being unique for each password entry state, the salt should ideally be independent of the password. This final example, where password = salt, is trivial to attack just by making a table of its result for very common passwords.