you are viewing a single comment's thread.

view the rest of the comments →

[–]L_X_A 2 insightful - 2 fun2 insightful - 1 fun3 insightful - 2 fun -  (0 children)

If you want to prevent the attack I described and still not have to store the passwords/salt-values in your server, you could go the authentication through encryption route.

Namely, you'd ditch the email and hash cookies for a single cookie containing the user's encrypted email address.

  • When the user registers with user_email, you send them an URL that will result in them receiving the cookie user-cookie = Encrypt(user_email, server_secret).
    I'd recommend a symmetric, strong (enough) cipher such as AES-256-GCM or ChaCha20Poly1305. You did choose MD5 as hashing algo earlier, and I'm assuming you did so for performance reasons. So it's up to you to judge which cipher would still accommodate your performance needs.

  • Don't forget to set the HttpOnly, Secure and SameSite flags.

  • On every request, the server would decrypt the ciphertext in the user-cookie's value using Decrypt(ciphertext, server_secret). If it matches the email of a user account, the authentication succeeded. Here's where you should watch out for your performance needs. This needs to be done on every request.

This solves the following problems:

  • You are no longer storing (plaintext) user information in the cookie, thus compliant with GDPR (see: https://www.gdpreu.org/the-regulation/key-concepts/personal-data/)

  • If someone steals the cookie, they won't be able to know what's in there.

  • If you chose a decent cypher, a plaintext collision attack as I described earlier becomes unfeasible.

This method still has the problem that every user's email is encrypted with the same key, though. So should someone be able to crack server_secret (very difficult depending on the cipher you choose, but still), they would be able to access every account they know the email of.

To circumvent this, you could extend this pattern with a Diffie-Hellman-based KDF functionality:

  • On your server, instead of the symmetric key as stated above, you generate and store a secret prime number which will be used in a "deferred" Diffie-Hellman key agreement. That is: server_secretPrime

  • When the user registers, you generate an ephemeral secret prime for the user, and calculate the user's public prime: user_publicPrime.
    You then store the following cookies:
    ** The encrypted email address: user-access = Encrypt(user_email, DH_KDF(user_publicPrime, server_secretPrime))
    ** The user-specific public prime used for the Diffie-Hellman key agreement: user-prime = user_publicPrime

  • When you receive a request from the user, you use the values stored on the cookie to authenticate them: Decrypt(user_email, DH_KDF(user_publicPrime, server_secretPrime))

This will ensure that a new key is used for every user, and it will not require you to store user-specific passwords in your DB.

It still leaves singular users vulnerable to someone stealing their cookie and getting access to their account in perpetuity (user-access + user-prime will always produce a valid ciphertext).

If you want to prevent this from happening, I see no other alternative than storing user_publicPrime in the DB and associating it with the user_email. Whereby you'd invalidate the cookie by generating a new value of user_publicPrime and storing it in the DB.

If you do this, you could of course forgo the DH_KDF pattern altogether by simply saving user-specific server_secrets. Then again, you'd be storing cryptographic material in a DB. Not exactly something you want to do. With the DH_KDF pattern, you can keep 1 server_secretPrime stored somewhere secure, while user_publicPrime can be stored in the DB without concern.