You need something like the Secure Remote Password protocol (SRP). As pointed out by Earendul and André Ribeiro, simply moving the server hash to the client overrides all the security benefits - since an attacker who obtains a copy of the DB can simply use the stored hash to immediately log in as any user (since < strong> access credential becomes the hash, not the original password). You need a protocol whose security features hold even with the hash done on the client side.
And this protocol is the SRP. The original protocol has a weakness (weakness that remains present in the default implementation of SRP via SSL / TLS), which is the use of a simple SHA-256 as a hash function instead of a slow function like BCrypt. So you would have to implement yourself at the application layer and / or get a secure implementation in the same way.
It's a bit trickier than most protocols - because it involves multiple messages going back and forth between the client and the server. There are also some parameters to be set, see the referenced reference for more details.
To register a new user:
The client whose password is p
chooses a random salt s
and calculates the hash x = H(s, p)
; also calculates v = g^x
, where g
is a common parameter between the server and the clients.
The server stores v
and s
associated with that client's username . x
is discarded - so even if an attacker copies the BD, it will not know the result of the hash.
For an existing user to login:
The client chooses a random (and ephemeral) a
secret key and sends A = g^a
to the server (plus your username );
The server also chooses an ephemeral key b
, calculates B = kv + g^b
( k
is a parameter calculated independently by both parties) and sends B
and s
to client;
Both compute u = H(A, B)
;
The client calculates Sc = (B-kg^x)^(a + ux)
and K = H(Sc)
, making use again of its p
password to get x
;
Server calculates Ss = (Av^u)^b
and K = H(Ss)
.
Now both the client and the server have a shared (and ephemeral) secret key, derived in part from the user's password. It remains only each of them to prove to the other that they have reached the same result:
The client sends the server M1 = H(H(N) xor H(g) | H(I) | s | A | B | K)
, and the server checks using its K
value. |
means the concatenation of strings. N
is another common parameter between client and server, and I
is simply the username .
The server sends the client M2 = H(A | M1 | K)
, and the client checks using its K
value.
Source: Wikipedia
This is the original protocol, which uses SHA-256 as the hash. As you can see, it is used several times during the protocol, so it is unfeasible to replace it with a slow hash in all its uses - when all you want is to protect the password. One preferable option - such as pointed out by Tom Leek in security.SE - is to keep the protocol identical, only apply p = BCrypt(s, p)
in the password before using it (you can use the same salt s
, but if feasible it is preferable to use a s2
salt - if your implementation supports it of course). Thus you gain hash protection without increasing the load on the server.
An attacker who gains access to the DB will only see s
and v = g^x
, so he would have to compute x
to be able to log in to the server ("simulating" the offline protocol ). And since to get at x
it would have to redo the hash slow, protection is ensured.