For a new website (HTTPS with HSTS+HPKP), we would like to restrict login access only on authorized user's devices. For that, there is a WebCrypto ECDSA public/private keys generated on each new device. The server store the public key of the new device and return a device ID. The browser save in an IndexedDB named « device » the private key (not extractable) and the device ID.

When the user register his account or want to authorize a new device, we ask him a password for this device, but we don't want to save his password in server database.

  • We could use SRP protocol and send the salt & « verifier » to the server, but rather using the user password, we use a derivated password (WebCrypto PBKDF2 or Argon2 library)

  • Or we could use WebCrypto again to create new ECDSA keys, specific for this user on this device, and send the public key on the server. On the browser, we store in an IndexedDB named « base64(SHA-256(login) » this user private key but encrypted (wrapKey function) with AES-GCM algorithm and for the key we use the user password derivation (WebCrypto PBKDF2 or Argon2 external library). Then to login, the server send a challenge, a simple random string, the client return the signature of this challenge with this new user specific private key (so he need to known the good password to unwrap the key).

If our DataBase is leaked:

  • With SRP, the verifier can't permit to guess easily the user password, but I suppose we need to ask a new password to all our users.

  • With WebCrypto, it's only a public key. Users don't need to change their password. EDIT: A downside is it's possible to do multiple passwords check on the client side, for example if I known my victim I could try may be 1000 possible passwords on the JS Console to unwrap the user private key, without the need to contact the server.

Of course, if the IndexedDBs are deleted on the browser, this require for the user to start a process to recover his account (private question, OTP by email, whatever... it's not the subject), but this is not specific to the WebCrypto option, it's also happend for the SRP option because we need to detect the device ID and check their signature to be sure it's a device authorized by the user.

Just for information, we also add U2F after this first authentification challenge.

In my specific case, do you recommend to use SRP challenge or this WebCrypto challenge please?

  • does the server record previously sent challenges to prevent replays, or does it naively just make sure the combo is valid?
    – dandavis
    Commented Oct 25, 2017 at 20:51
  • @dandavis No record saved. I've read SRP is immune against replay attacks. And for the WebCrypto challenge, I suppose if I make each time a challenge like this : SHA256(new uuid() + getRandomValues(1024)) this should be ok to prevent replay attack (because UUID is based on current time)
    – lakano
    Commented Oct 25, 2017 at 22:47

1 Answer 1


I propose this solution to answer to my question:

WebCrypto ECDSA keys are really useful, but if we use it as explained in my question (wrapped encrypted with AES-GCM), it's possible for an JS Coder to go on the victim's browser and try thousand of passwords in the JS console to known if it's the good password or not, without any connection to the server. So, it's surely better to not permit to try the password without server interaction. It's why my solution is to only keep the device ECDSA keys, with not extractable private key, not encrypted and global to all users on this device.

SRP is widely-tested and widely-deployed, however in case of DB leak, even if it's difficult to retrieve the user password from brute-force, it's possible because the attacker have the SRP salt and the verifier. So, the best is to not use the user password but a random OTP, and renew it at each successful connection.

Firstly, if the device doesn't have a stored private key:

  • Generate new WebCrypto ECDSA keys
  • Send this device public key to the server
  • The server return a deviceUUID and a sort of JWT for next requests

When user sign up or autorize a new device:

  • Generate an OTP of 512 random bits
  • Generate a salt of 128 random bits for the derived OTP
  • Derive the OTP with Argon2 512 bits, salt=derivedOTPSalt
  • Generate a salt of 128 random bits for the SRP
  • Create the SRP verifier for this derivedOTP key
  • If user have a U2F device, register it for this device
  • Send to the server the deviceUUID, SRP salt/verifier of the derivedOTP key and the U2F PublicKey. The request is signed with the ECDSA device private key.
  • If the user registration is accepted by the server, then, request a password from the user for this device
  • Generate a salt of 128 random bits for the password derivation
  • Derive the user password with Argon2 512 bits, salt=derivedPasswordSalt
  • Create a masked key: OTP ^ derivedPassword (XOR)
  • We save in a user dedied DB IndexedDB/sha256(login) : maskedKey, derivedOTPSalt, derivedPasswordSalt, u2f=(true/false)

Then, when user want to sign in:

  • Send login and deviceUUID to the server and request SRP/U2F challenges (request signed by the ECDSA private key, like all requests to the server)
  • If the signature of the device is ok, the server return: challengeUUID, SRP Salt/B challenge and a random text challenge to sign with U2F device (even if we known that the user does not have one)
  • If the user have a U2F device, sign the challenge or generate random bits (to fool any MITM)
  • Request the user password for this device and derive it (Argon2, salt=derivedPasswordSalt)
  • Get the result of stored maskedKey ^ derivedPasswordAttempt (XOR)
  • From this result (in theory, we get the original OTP), then we derive it with Argon2 / salt=derivedOTPSalt, and create from this the A/M1 SRP challenge answer
  • We also prepare the next OTP following the sign up method
  • Send to the server: challengeUUID, U2F challenge, SRP A/M1 challenge and the next SRP verifier & salt for the next OTP.

The server can confirm it's the good OTP, the good ECDSA device signature, and if user have a U2F device the good U2F signature. If all is ok, the server replace the old OTP verifier/salt with the next one, and return an new authorization JWT.

With this solution, even if the DB have been leaked, it's impossible to retrieve the real user password, it's only possible to retrieve the derived OTP, and it's not enough for the attacker to spoof the user identity because he also need the private key of the device (and an possible U2F device).

For a MITM, he can't read user password or OTP, but can read each OTP verifier/salt. Otherwise, it's not enough because he also need the private device key and may be the user U2F device. We also use HTTPS with HSTS+HPKP to prevent MITM attacks.

For an attacker with victim's device access, he can use the private key of the device, and can read salts and the masked key, but this shouldn't help to guess the password, even with brute-force attack on client side, because he need to check if the OTP is ok with a server request.

This solution also reset each time a new OTP after each sucessful authentification. In case of DB leak, we can send a PUSH notification to all users to just ask them to connect to the service, for reseting their OTP.

So, this solution use respected protocols (SRP, ECDSA device keys, U2F verification, OTP, Argon2/PBKDF2) and are mixed to increase the security.

If some peer could review this design to point any point of failure, this could be really appreciated :)

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .