tl;dr: I wouldn't generally recommend it, but it may be useful in specific circumstances
Advantages of SCRAM
- The password is never sent to the server. The only things sent on the wire are:
- The nonce
- The salt
- The iteration count
HMAC(PBKDF2(password), "Client Key") XOR HMAC(H(HMAC(PBKDF2(password), "Client Key")), AuthMessage)
(seriously, I'm not making this up!)
HMAC(HMAC(PBKDF2(password), "Server Key"), AuthMessage)
- The server never knows the password, and thus can't impersonate the user on other servers (as long as the salt on those servers is distinct from this server's salt)
Disadvantages of SCRAM
- Requires the use of PBKDF2. While PBKDF2 is significantly better than a fast hash, bcrypt is better (and nowadays you should consider Argon2 as well).
- Requires that all of the computational complexity be handled by the client
The first point is unfortunate, but the second point is the real killer. With smartphones and tablets everywhere now you simply can't assume that the client will be able to quickly compute PBKDF2 with enough iterations to be safe.
When would SCRAM be useful?
The use I see for SCRAM is where the client is trusted but the server is not. This is never true for a web application (as the "client" is the JavaScript sent by...the server), and is only sometimes true for local applications. The best examples I can think of are the ones Wikipedia gives: SMTP, IMAP, and XMPP, where you trust your email or chat client but are authenticating to a potentially malicious third party server.
If the client forces a high iteration count and unique salt it may mitigate the risk of DNS hijacking combined with a rogue CA, but you've already done that with certificate pinning. The only thing left to defend against is your server being malicious, which may be useful in some cases, but only if the gains outweigh the disadvantages of SCRAM.
SCRAM could useful to keep the server from obtaining the password if you know that a strong password will be used, but that the password will also be re-used elsewhere (if it's not reused why would you care if the server might know it?). Unfortunately this can't really be enforced, and the ideal would be to use a unique password anyway.
How does SCRAM work?
In SCRAM, the server stores:
- The salt
- The iteration count
ServerKey = HMAC(PBKDF2(password), "Server Key")
(where "Server Key"
is a known constant HMAC key)*
StoredKey = H(HMAC(PBKDF2(password), "Client Key"))
(ditto for "Client Key"
)
To authenticate:
- The client sends the username and a random nonce
- The server appends its own random nonce to the client's, and replies with the nonce, salt, and iteration count for the user
- The client computes the following:
ClientKey = HMAC(PBKDF2(password), "Client Key")
StoredKey = H(ClientKey)
ClientSignature = HMAC(StoredKey, AuthMessage)
(where AuthMessage
is the concatenation of all previously exchanged messages and the nonce, delimited by commas)
ClientProof = ClientKey XOR ClientSignature
- The client sends the nonce and
ClientProof
to the server
- The server computes the following:
ClientSignature = HMAC(StoredKey, AuthMessage)
ClientKey = ClientProof XOR ClientSignature
ServerSignature = HMAC(ServerKey, AuthMessage)
- The server verifies
H(ClientKey) == StoredKey
, proving that the client knows the password (or at least ClientKey
)
- The server replies with
ServerSignature
- The client computes
ServerSignature
and compares it to the value returned by the server, verifying that the server knows the ServerKey
This is rather simplified, as it excludes some things like hash selection, encoding, message format, and channel binding, but it should give a good idea of how it works.
* All uses of PBKDF2 obviously include the salt and iteration count as well, SCRAM sets dkLen
to the output length of the selected hash