8
$\begingroup$

I am redesigning a password reset email mechanism because the existing implementation scares the hell out of me. My goal is to generate reset codes that are:

  • Expired
  • Tamper Resistant
  • Single Use
  • Resistant to replay attacks

The format of the reset code I have come up with is:

B64( B64( iv ) || B64( E( uid || timestamp ) ) )

Where B64 is base64 encoding and E is for symmetric encryption. The iv is the initialization vector used for encryption (randomly generated), and is in the clear. The || indicates concatenation and I actually use the string "||" as the separator. The uid is an opaque identifier of the user so I can look them up in the database.

The timestamp allows for a code to be expired.

I am using AES 256 in Galois Counter Mode which should provide protection from modified ciphertext attacks.

The encryption key is stored on the server, and used to perform the encryption.

The iv has a dual use. When the reset code is generated the iv is stored with the user in the database. This is used to enforce single-use of a reset code (it gets cleared when a reset code is processed) as well as prevent use of reset codes generated previously. If the iv in the database does not match the iv used to decrypt the reset code, the request is refused. This may also help with security in the event that the encryption key on the server is somehow compromised, as the attacker has to inject the iv into the database as well to make it work. Of course if the attacker is on the host we are pretty much toast...

Are there any weaknesses in this design I am not seeing, or does it needs additional improvement?

$\endgroup$
6
  • 2
    $\begingroup$ Why are you only encrypting part of the message? Why encrypt, and not sign? Is the randomly generated iv generated using a source of real randomness or cryptographically-strong PRNG? $\endgroup$
    – Mike Samuel
    Jul 20, 2012 at 5:16
  • $\begingroup$ Can you store the (uid, timestamp, iv) relation in a db and check that it exists in the database (and is not stale) before allowing reset? $\endgroup$
    – Mike Samuel
    Jul 20, 2012 at 5:21
  • $\begingroup$ How are you delivering the reset code to the user? I also think this can be simplified. The reset code could just be a random string, and the timestamp and userID stored on the server with the code. $\endgroup$
    – Marcus Adams
    Jul 20, 2012 at 12:37
  • $\begingroup$ This is Java so SecureRandom is being used to generate the IV. Galois Counter Mode includes a MAC so the content can be authenticated. The IV is not encrypted because that is needed for decryption, and I can't look it up without decrypting the associated user first. The IV is matched in the database for the user. $\endgroup$
    – Peter Friend
    Jul 20, 2012 at 15:55
  • 1
    $\begingroup$ Reading Mike's comment again got me thinking. I think you are asking why am I encrypting the message at all, instead of just signing it. This would mean that the user identifier and timestamp would be passed in the clear, but a keyed MAC would be included for verification. This MAC could then be stored with the user server-side for verification. This would be a simpler implementation. I need to think about the security of this a bit more. $\endgroup$
    – Peter Friend
    Jul 20, 2012 at 19:08

3 Answers 3

6
$\begingroup$

What you describe looks secure to me, but excessively complicated. The following simpler procedure should suffice:

When a password reset token is requested:

  • Generate a secure random token and save it (or a cryptographic hash of it) in the database, along with a timestamp. Overwrite the previous token, if any.
  • Send the token to the user by e-mail (suitably encoded).

When the user submits a token:

  • Check that it matches the token in the database, and that the corresponding timestamp is not too old.

The only part of the process which needs to involve any cryptography at all is generating the random token, and optionally hashing it. You can usually offload the former task to the OS; on Unixish systems, for example, you can read the token from /dev/urandom. Alternatively, if you're using a decent crypto library, it should provide a routine for generating secure random numbers (which may call the OS random number generator internally).


Ps. The one situation where you might want to go with a more crypto-heavy solution would be if you didn't have access to a convenient database for storing the tokens. In that case, you could, for example, compute the token as MACK(userid || timestamp), where MACK is a message authentication code keyed by the secret key K, and have the user submit the timestamp along with the token. (You could also include a random number in the MAC input, but that's not really necessary — without the key K, an attacker won't be able to forge the MAC.) However, since you do have a database available, this doesn't really apply.

$\endgroup$
9
  • 2
    $\begingroup$ +1 for storing a hash of the token, which means an attacker can't reset passwords by merely getting a DB dump. As an extension, storing H(token,timestamp) and sticking the timestamp in the link works; the main disadvantage I can think of is that you don't know when they can be removed from the DB. $\endgroup$
    – tc.
    Jul 20, 2012 at 20:20
  • 1
    $\begingroup$ Yes, after some thought I have to agree. I was going to ditch the encryption and go with using CMAC of userid||timestamp with a randomly generated key, but a random token may suffice. The thing that was bothering me about storing a random token in the database was the possibility of collision and having to store additional indexes and constraints in the database for that token, and a randomly generated attack matching up with a reset request that had not been processed or expired yet. However as you say, I can simply MAC the token before storing it in the database. Thanks for your input. $\endgroup$
    – Peter Friend
    Jul 20, 2012 at 20:27
  • 2
    $\begingroup$ @PeterFriend: As long as the token submissions also include the user ID (which they should), you don't have to worry about indexing the tokens or ensuring their uniqueness. And as long as your tokens are long enough (say, 128 bits) brute force attacks will not be practical. $\endgroup$ Jul 20, 2012 at 20:40
  • $\begingroup$ Treat all password-reset tokens and auto-login tokens as passwords. They essentially are passwords. They need to be long and generated by a strong random number generator. When storing them in the database, hash them with a strong adaptive KDF such as bcrypt. $\endgroup$
    – Polynomial
    Jul 22, 2012 at 9:29
  • 2
    $\begingroup$ The better token would be MACk(userid || timestamp || pwdval), where pwdval is some password-related value that will change once the user resets password (for example, password hash or salt stored in the database). This method ensures that when a user changes the password, the token becomes invalid even if it hasn't expired (i.e. protects against replay attack). $\endgroup$
    – dchest
    Jul 22, 2012 at 22:09
0
$\begingroup$

As you might know good crypto is not easy, and sufficiently good crypto is even harder. So for to start, whatever you do it won't be tamper resistant, tamper resistance means that attacker cannot tamper with some values for his/her own agenda. The property that you are looking for is tamper evidentness, that is the ability to tell whether the supplied message is genuine.

Secondly as you probably noticed, to have a replay protection in such a beautiful protocol like http we need to track state (nonces just do not fit in well with single request http commands ;) ) so some database persistence will be needed. And if you already need to persist something I would really consider completely random code and store the code and the generation timestamp in database.

Your algorithm is really good if implemented correctly, for instance if you will display something like "Invalid iv for user 'Jane' or 123456" for tampered iv, then we do have an really big oops. Second thing is validation ;) I know that for you it's obvious but to often obvious things are the ones that get missed, so to validate the iv, and decipher the message, check validity of timestamp, and whether encrypted user id is matches the one in database for given iv. And there is a cryptographic storage issues of course (the Insecure Cryptographic storage is among owasp's Top Ten after all). If you will be able to properly address those issues I think you will be good.

The final word, if your page wont be visited by billions of users, or not a target to some overly eager attacker You will do fine (you are encrypting only known values after all, userid and timestamp are not considered to be secret), and as to further reading section i recomend, Owasp's Insecure cryptographic storage and Ross Andersons Security Engineering, also the Handbook Of applied Cryptography very educational :).

$\endgroup$
1
  • $\begingroup$ Hey thanks for the comments. Very good point on resistant vs. evident. Tamper evident is what I am after and why I used GCM. I have the resources you mention except te insecure storage one - I will check that out. All error messages are along the lines of invalid request or expired request. User info only shows up if the reset code is verified. $\endgroup$
    – Peter Friend
    Jul 20, 2012 at 16:03
0
$\begingroup$

The two main issues I see:

  • B64(iv || E(uid||timestamp)) is simpler and equivalent.
  • uid||timestamp looks like it might be vulnerable to "cryptographic splicing" (except you say that you recover the UID from the token, which should be fine).
  • If the IV is stored in the DB, it does not need to be in the token (and perhaps shouldn't be in the token).

Assuming uid/timestamp have no confidentiality requirements, the main advantage of using authenticated encryption is that you can't forget to check the MAC: D() just returns an error. There are still some language/implementation-dependent problems:

  • In most C APIs, if you ignore the error you end up reading whatever was in the output buffer. (If the decryption is in-place, then the output buffer is attacker-controlled!)
  • If a decryption error outputs the empty string and sets an error flag, you might end up with int('') == 0 and timestamp('') == timestamp().

The main advantage of storing an (unkeyed) hash is that you don't need to worry about key storage/compromise.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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