Server shouldn't know plain password
If the user types their password into a web page in a browser, the server that hosts that web page always has the opportunity to see what password was entered. There's no cryptographic mechanism that can prevent that (at this point in time).
To get around this limitation, you need to build a downloadable application instead (e.g. you could use NW.js and Cordova to package up HTML/JS/CSS into desktop/mobile apps). If you don't do this, there's essentially no point in trying to hide the password from the server. More about that in this article.
If you've done that (again, no point in continuing if you haven't), I would recommend against using a JS implementation of PBKDF2 or scrypt, if possible. A JS implementation of either will be slower and force you to use a lower iteration count (since users will only wait so long). Attackers will have access to a highly optimized C implementation, so this would give your attackers a significant advantage. Note that most languages have wrapper libraries for C implementations of PBKDF2 or scrypt (e.g. node-scrypt).
To derive the keys, I recommend taking the following approach on the client:
- Derive one 256-bit master key using scrypt (PBKDF2 is also acceptable). Use security parameters that are as high as practical, but keep in mind that mobile devices might have a rough time. I recommend using a unique salt for each user.
- Use HKDF-SHA256 to derive a login key. HKDF allows you to safely derive keys from a master key such that it is infeasible to determine the master key (or other keys) from the derived keys. In the
context
or info
parameter, use the string "server-login-key"
. 128 bits of output should be adequate.
- Use HKDF again to derive an encryption key. Be sure to derive the encryption key from the master key, not the login key. In the
context
/info
parameter, use the string "client-encryption-key"
. Generate a key long enough to match whatever encryption algorithm you plan to use (e.g. 256 bits for AES256).
When storing the login keys on the server, be sure to treat them like passwords (store only the salted hashes). You may use PBKDF2/bcrypt/scrypt here again, but it's not critical since the key has already been stretched. Additional stretching is a good thing, but it won't help if the server is actually malicious.
An important difference between this and your design is that it invokes PBKDF2/scrypt only once, instead of twice. This allows you to double the security parameter (i.e. one invocation with 2N iterations instead of two invocations with N iterations each).
If users don't have strong passwords, then the server will be able to crack the password based on the login key (dictionary attack), so give careful thought to this as well.
Note: The "Extract" step of HKDF is optional in this case because PBKDF2 and scrypt already output cryptographically strong keys, but it won't hurt.