The server certainly needs to store a verifier for every password entry. Our verifiers will be (user_salt, server_salt, server_hash) triples, where:
user_saltandserver_saltare random 16 byte arrays.server_hash = keyedhash(server_salt, pwhash(user_salt, password)), where:pwhashdenotes a "slow" password hashing function like PBKDF2, bcrypt, scrypt or Argon2;keyedhashdenotes a keyed hash function like HMAC-SHA2.
Now we can use an authentication protocol like this:
- Client: Sends
usernameto server; - Server: Responds with the corresponding
user_salt; - Client: Computes
client_hash = pwhash(user_salt, password); - Client: Sends
client_hashto server; - Server: Computes
server_hash = keyedhash(server_salt, client_hash); - Compares
server_hashto the value stored in the user's password entry.
The enrollment/password change protocol is similar:
- Client: authenticates with the server;
- Client: sends a password change request;
- Server: responds with a random
new_client_salt; - Client: computes
new_client_hash = pwhash(new_client_salt, new_password); - Client: sends
new_client_hash; - Server: selects a random
new_server_salt; - Server: computes
new_server_hash = keyedhash(new_server_salt, new_client_hash); - Server: stores
(new_client_salt, new_server_salt, new_server_hash)asusername's new password entry.
Both protocols should be executed over an authenticated, confidential channel (e.g., an SSL connection). An eavesdropper who sees client_hash would be able to impersonate the user.
One of the advantages of a protocol like this is that, in addition to never sending the password over the channel, it computes the slow password hashing function on the client side, which means you can crank up the function's work factor to make password cracking attacks harder.
See also these links for similar proposals: