1

I found this NPM package that looks like can be used to verify two factor authentication for websites, and also generating tokens for signing into other websites.

In the README there is a section about generating secrets.

For password security there is a huge emphasis on hashing and salting passwords and ensuring they remain secure and protected. Is there any such advice around storage of these secrets for the web server/platform that is providing the login service?

Obviously checking to make sure that there are no collisions between two different users secrets.

I haven’t seen very many tutorials or discussions around implementation tips and suggestions around two factor authentication.

2
  • 1
    If you're doing it right, checking for collisions is a waste of time. If you're doing it wrong, you've got much bigger issues. Commented Mar 21, 2019 at 21:56
  • @AndrolGenhald Makes sense. That is a good point since I’m assuming the chance of collisions is very slim in this case. Any other tips or pointers on anything else regarding it? Commented Mar 21, 2019 at 22:04

2 Answers 2

5

No clever storage method exists for HOTP and TOTP. You should do your best to limit which programs can look up shared secrets using ordinary access control methods. (Plus physically securing the computer and erasing hard dives before they need to be discarded.)

Encrypting the shared secrets with a key only the server knows helps, but only if the (encryption) key isn't hidden below the metaphorical doormat. If the key is stored on the same server (or same drive) as the shared-secrets database then you aren't likely to gain much.

In a normal password hashing authentication system the server can store the hash of a password instead of the password itself because

  • For any hash function, identical inputs result in identical outputs. (If P1 == P2, then H(P1) == H(P2). This is true because H is deterministic.)
  • For a cryptographic hash, hashes of two non-identical inputs will not match with near certainty.
    • Blindly guessing passwords fails because any one of the many possible outputs is equally probable. (A 256-bit hash has 2256 possible outputs. The probability that a blind guess produces a different output is 1 - 2-256.
    • By definition cryptographic secure hashes are preimage resistant. There is no feasible way to compute, for any chosen y, an input x such H(x) == y. (256-bit preimage resistance implies that there is no method more efficient than brute forcing around 2256 inputs.)

That means that in practice whether P1 equals P2 is the same as whether H(P1) equals H(P2).

If there is no other reason that the server needs to store plaintext, then we should store the hash instead. The advantage to storing (salted) hashes instead is that cryptographic hashes are one way. With a sufficiently strong password that leaves even the server unable to determine the user's original password once all copies of the plaintext password have been cleared.

This is why a hacker with read access to the system, a discarded hard drive, or a database dump cannot immediately determine a persons password. (But a person's password (not chosen by a secure RNG) can get cracked in the real world because humans make relatively predictable choices.)

TOTP and HOTP do not work this way. Instead one time passwords are derived from a shared secret. With that secret, both the client and the server can generate as many one time passwords as they want. However, someone who doesn't have access to that secret cannot generate new authentication codes.

The only method the server could use (without using something different from HOTP and TOTP) to give up their knowledge of the shared secret is to pre-generate, hash individual OTPs, and store the list of them somewhere. And that still doesn't prevent offline cracking because if the database is leaked then it is possible to bruteforce any of the OTPs from that list in less than 1010 hash function evaluations.

Fortunately there is no other value to a HOTP/TOTP shared secret except for defeating the authentication system itself. Generate a random shared secret for every new user, then the secret has no relation to any other information. Humans might reuse passwords across multiple services (which would make those services easier to hack if your security was compromised) and humans might do something like put their Social Security number in their password. For randomly generated secrets these would never be a concern.

So all you can do is use the same types of methods you might use to restrict access to things like credit card numbers, personal information, and other information you must have access to but must also prevent other people from accessing. (Examples are in the first and second paragraph. The only thing I can add is using HSMs for encryption, but everything else needs to be secure as well. Including server software, the computer itself, and the network it communicates over.)

2

One thing that might be possible is to encrypt the secret with the secondary hash of the user's password plus some sort of salt stored next to the secret on the database.

user_id password_hash secret_encrypted secret_encryption_salt
1 $2y$12$jkFEkBRKEecjbq/r/S6rE.lPedKRd3M1tJGKbyYIuPN9uWKK3Nw0O VI0FvQbwJVP4ofze/GVOG/u6VMjOcTxzxjd7uk8sBnY= generatedseed
2 ... ... ...
... ... ... ...

The sample above is for password 0123456789 and secret XXXXXXXXXXXXXXXX. Encryption is AES256 ECB using the SHA256 hash of password and salt.

This allows you to have the values secured. However, there are many cons to it:

  1. Can't use this method if you use any sort of third party login,
  2. You need to ask for the user's password each time even if you only need to verify OTP. (like when disabling it or verifying user with some critical functions)
  3. There is no clear advantage to it, because with a plain text or protected secret, when data is leaked, the bad actor still can't log into someone's account. And if the password is already leaked then having secret encrypted or not won't help anyway.

We just should remember that TOTP is not a substitute for the user's password.

2
  • This is an interesting idea, however, I encourage you to consider the method by which the key is derived to encrypt the secret. In the solution as it is presented, all that needs to happen is for an instance of the TOTP code and time of use to leak along with the database, and now the user's password (and OTP-secret) are exposed to an exhaustive search for a SHA2 hash. This could happen via an audit log? Even though the OTP secret is (should be) high-entropy, the user's password most likely won't be, which introduces the possibility of highly parallel dictionary searches.
    – brynk
    Commented Apr 28, 2021 at 20:24
  • @brynk Yeah, but we have the salt to add entropy. so a dictionary attack won't help; they actually need to do the search. In any case tho, this is just an example and you can bcrypt it 20 times or something and use the hash of that for encryption. I just used SHA256 since the output hash is 256bit and is ideal to be used as the AES256 key. Commented Apr 29, 2021 at 6:53

You must log in to answer this question.

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