Password reset links: random value or authenticated message?
Creating a random token containing absolutely no information and linking it in the database with a username and an expiration time is by far the best solution.
Encryption (and hashing) is used to store and transfer data that absolutely must be sent. If there is any way to do something without having to send that data, encrypted or not, that provides equal functionality and security, you should use that method. In other words, encryption should be a last resort for securing data.
These tokens are password equivalents, and as such must meet similar requirements. But by far, the biggest is that it must not be arbitrarily guessable by an adversary. It should therefore be indistinguishable from random data.
When you encode any data, obviously it needs to be encrypted. Now, this will likely involve some level of rolling your own, or at least putting the crypto primitives together yourself, which you'll likely do wrong (See: Don't be a Dave).
So, this means that your safest and simplest solution is to create a very random token, and store it hashed/salted in the database, as well as an id for that token. Join the id and token, and serialize it to hex, or Base32 and send it off to the user. When the token comes back in, use the id to grab the token/salt from the database, and hash the rest of the token to see if it matches.
You must ensure several extra things about tokens since they'll be in people's emails.
- Ensure they have a high enough entropy to resist guessing
- They must be short enough to encode into a URL comfortably
- They must expire after a reasonably short window of time as they will be in people's email inboxes
- They must one-use only
You could encode the expire time into a token and use an HMAC, but generally a password reset involves visiting a specific URL. Usually to make things easier for the user, one includes the token in the URL as a GET field.
Wanting to keep the URL relatively small and only made of printable characters, you'd probably encode the timestamp as an integer and use fixed-width settings. This has to specify the user, the expiration, and the MAC. I'd call it reliable, but it's not without some sort of coding effort.
Since you need to hit a database for passwords anyway and it doesn't take any thinking to encode a random fixed-width value, though, I'd just go with that. All else being equal (and I think it is rather equal), easy wins out. Added bonus: lower CPU usage.