4

I want to make a one time token that users use to reset their password. The token should be invalidated once it is used, is it safe to use their currently hashed password as the aes key to encrypt the token so that once the password changes, the token can no longer be decrypted? This token will only be encrypted/decrypted at the server.

What are the downside/upsides to this approach?

  • What is the token? Is it a random value, structured data, known data? – AndrolGenhald Dec 18 '18 at 19:50
  • Do you have a reason to not simply generate a random string to serve a the token and delete it from the database once it's used ? Bonus: Store it alongside a datetime object so you can invalidate the token if it has been generated a while ago and never been used. – Theophany Dec 18 '18 at 21:24
  • @Theophany we're using an LDAP database and I want to avoid 'bloating' the database? Would also need to set up custom schemas to store the token in the database – JF6IX Dec 18 '18 at 21:46
  • @AndrolGenhald The token will contain data. I was planning to create the token by encrypting the data. – JF6IX Dec 18 '18 at 21:48
2

The problem you run into is that if the password hash is all that's needed to generate a valid token, an attacker with a database dump can generate reset tokens for everyone (which sort of defeats the purpose of hashing passwords in the first place). This problem can be solved, but it ends up being a lot more complicated than using random values for reset tokens and storing their hashes in a table.


Important note: I wouldn't implement this myself without more examination, I just came up with this mostly off the top of my head for the fun of it. Give others a chance to point out what I got wrong. Seriously, I already screwed it up once.


What properties does a password reset token need?

  1. Identifies a specific user and is valid for no one else
  2. No longer valid after password is changed
  3. Expires after a time
  4. Secret and very hard to guess
  5. Database dump doesn't give an attacker any existing tokens or allow them to generate tokens at will

Since you want to have reset tokens without having to store them in a database, I would suggest this psuedocode for generation:

token = {
    "user": [username],
    "expiry": [unix timestamp]
}

// Very important!!!
// Password hash should be bcrypt or argon2 with good cost
// Secret application "pepper" is used to salt the key with HKDF, so a database dump won't allow generating reset tokens
key = hkdfsha256(user_password_hash, pepper)

return base64encode(token + hmacsha256(token, key))

And to verify:

data = base64decode(data)
token = data[:-32]
submitted_hmac = data[-32:] // last 32 bytes are hmacsha256
if (token['expiry'] > now()) {
    return false
}
// [retrieve password hash here]
key = hkdfsha256(user_password_hash, pepper)
return hmacsha256(token, key) == submitted_hmac

Examination

  1. The username identifies the user the token is for
  2. Deriving the HMAC key from the password hash means once the password is changed the token is no longer valid
  3. The expiry check prevents an unused token from remaining valid for too long
  4. The password hash and pepper make the HMAC output (and thus the full token) unguessable
  5. Using a pepper to derive the HMAC key prevents an attacker with a database dump from generating tokens for everyone

Pros

  • No database storage
  • Validity of token is intrinsically tied to current password
  • All currently valid tokens can be easily invalidated by changing the pepper

Cons

  • Somewhat large token size (108 base64 characters given a 15 character username), but it should still be ok for a URL in an email.
  • Token isn't entirely opaque, users could see the user and expiry info. This doesn't affect the security, but if you want to avoid annoying questions from people that think it's a problem you could encrypt it as well, although this means adding an IV (if you do this you should encrypt before HMAC).
  • If an attacker gets a database dump and the pepper they can generate tokens at will.
  • No way to know how many valid tokens exist at a given time, or for which users.

Some thoughts

  • Using the entire password hash means the salt will be part of the HMAC key, so it should contain a large amount of entropy already even if the password is terrible and the pepper gets exposed. To attack this scheme both the pepper and database would need to be exposed.
  • If you really want to make the token smaller, you could truncate the HMAC output to 16 bytes, null terminate the username, and do username + 5 byte unix timestamp + 16 byte HMAC. This avoids the 2038 problem with the timestamp, and with a 15 character username is down to 52 base64 characters. I wouldn't recommend doing the 5 byte timestamp though, the 3 extra bytes don't seem worth it.
  • Since the amount of space you're saving is strictly smaller than the users table you're already storing, I don't think all this is worth it anyway. Maybe if you weren't able to alter the database schema and it didn't already have a way to store a reset token.
1

One significant downside is that you can’t time limit it.

Imagine the user requests a reset but never uses it as they remember the password. At this point there is a token with a lifetime until the password changes.

It would be better to generate a unique token per reset attempt using any secure means. Put this in a separate database table with an expiry.

Bonus points for monitoring if old reset tokens are used, as this could indicate suspicious activity (if combined with other signals).

  • Thanks for the response, could I not include an expiration timestamp in the token? – JF6IX Dec 18 '18 at 0:05
1

What you describe does not contain an intrinsic expiry date - so will continue to be viable until the password (and/or salt) is changed. Of course you could embed an expiry date in the clear text you are encrypting along with other interesting facts about the reset request.

While it is very unlikely that the both the aes encryption and the password hashing will be broken, I would need to spend some time thinking about whether this very small risk was outweighed by the benefit of using this as the key over the cost of generating a key in some other way (e.g. a random key) and of storing/indexing it, and of course, flagging it as used/superceded. My initial gut reaction was no, but the more I think about it, the more elegant it seems e.g. there shouldn't be any risk of a padding Oracle attack if you are using half decent password hashes.

Do make sure you use the password hash and not the other data stored in the attribute. You need a response from someone who knows a lot more about cryptography than I do as to whether there is a need for an initialization vector, but if the payload is less than or equal to the encryption block size, I don't think there's any benefit. Do bear in mind that, if you expect the user to type the full cyphertext (you can't truncate it with variable plaintext) you are still looking at 20+ chars of gibberish (without an IV).

  • If the token is just 16 random bytes I'm pretty sure that's basically the 1 case where ECB is acceptable, otherwise I'd probably go with AES-GCM. I'm trying to think if a MAC is necessary, but better safe than sorry. – AndrolGenhald Dec 18 '18 at 20:00
  • The clear text should be completely opaque, hence there's no benefit to using a random value. OTOH there is a benefit to adding something from which an expiry time can be derived. Also a benefit to requiring the username to be presented inside or along with the encrypted data. – symcbean Dec 18 '18 at 22:30
1

This is something a system like PBKDF2 or RFC2898 was invented for, in combination with key wrapping. They are designed to take user input and return strong hash-based strings you can use as-is for password checking but also for further operations as key material. It is not the only option and more (and different) systems to do this are being put in use for this scenario.

Look into password based key derivation and key wrapping to learn more. And as always with crypto: do not roll your own but use a well tested library instead.

For your specific case (use a token to reset an account): is there a case against generating a random token instead? The reason to use something like AES would be reversible encryption using the same key used to encrypt it, which does not immediatly seem like a usecase here.

  • My assumption was that the token was random, but I see OP didn't really specify. Also, given that OP is (presumably) already using a decent password hash (bcrypt or argon2), there's no need to run it through a slow KDF again. – AndrolGenhald Dec 18 '18 at 19:54
  • Yeah, I assumed it at first as well, then un-assumed it because it wasn't specified, and now I'm not sure what to think ¯_(ツ)_/¯ – John Keates Dec 18 '18 at 19:56
  • The token will contain data. I was planning to create the token by encrypting the data in json format. Passwords are hashed using SSHA512 (5000 iterations) – JF6IX Dec 18 '18 at 20:21
  • @JF6IX Are you using SHA512 to store user password in your database for the purpose of authentication, or only as the AES key of your encrypted token ? – Theophany Dec 18 '18 at 21:11
  • @Theopahany for the purpose of authentication – JF6IX Dec 18 '18 at 21:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.