14

I'm generating a token to be used when clicking on the link in a verification e-mail. I plan on using uniqid() but the output will be predictable allowing an attacker to bypass the confirmation e-mails. My solution is to hash it. But that's still not enough because if the hash I'm using is discovered then it will still be predictable. The solution is to salt it. That I'm not sure how to do because if I use a function to give variable salt (e.g. in pseudocode hash(uniqid()+time())) then isn't the uniqueness of the hash no longer guaranteed? Should I use a constant hash and that would be good enough (e.g. hash(uniqid()+asd741))

I think all answers miss an important point. It needs to be unique. What if openssl_random_pseudo_bytes() procduces the same number twice? Then one user wouldn't be able to activate his account. Is people's counter argument that it's unlikely for it to produce the same number twice? That's why I was considering uniqid() because it's output is unique.

I guess I could use both and append them together.

Celeritas
  • 10,039
  • 22
  • 77
  • 144
  • Well no hard feelings but they didn't answer the question. It's been too long to remove the down votes. – Celeritas Aug 08 '13 at 21:52
  • @Celeritas Quoting from Tom's answer: “making collisions so utterly improbable that you don't need to worry about them”. The token has to be generated in such a way that it won't collide with a value picked by the attacker, and the attacker may be using `openssl_random_pseudo_bytes` as well, to the token will not collide with another token that you generate (except with less-likely-than-a-meteorite-strike probability). – Gilles 'SO- stop being evil' Aug 08 '13 at 21:56
  • @Celeritas **All** of the answers have directly answered your question. The issue is with _your own_ lack of knowledge. Honestly, there's nothing wrong with not knowing stuff, I certainly had no idea about crypto 6 months ago. `openssl_random_pseudo_bytes` gets its random bytes from sources that the OS has put a lot effort in generating. It gathers _a lot_ of entropy to come up with them. So collisions are very unlikely, they're at least as unlikely as collisions from `uniqid()` if not even more. – Adi Aug 08 '13 at 21:58
  • @TildalWave That's obvious to someone who's been immersed in security or crypto for a while. Please consider that it is not obvious to everyone. The relationship between randomness and uniqueness is actually fairly subtle. – Gilles 'SO- stop being evil' Aug 08 '13 at 22:11
  • In practice using "truly random" bytes is a better way to ensure uniqueness (keeping in mind the [birthday problem](https://en.wikipedia.org/wiki/Birthday_attack)) than any other source of information. Other methods (like time or UUIDs) require more caution, increases the likelihood of repetitions, or needs more organization. And those methods are, of course, more predictable than random data. – Future Security Nov 14 '18 at 17:45

4 Answers4

25

You want unguessable randomness. Then, use unguessable randomness. Use openssl_random_pseudo_bytes() which will plug into the local cryptographically strong PRNG. Don't use rand() or mt_rand(), since these are predictable (mt_rand() is statistically good but it does not hold against sentient attackers). Don't compromise, use a good PRNG. Don't try to make something yourself by throwing in hash function and the like; only sorrow lies at the end of this path.

Generate 16 bytes, then encode them into a string if you need a string. bin2hex() encodes in hexadecimal; 16 bytes become 32 characters. base64_encode() encodes in Base64; 16 bytes become 24 characters (the last two of which being '=' signs).

16 bytes is 128 bits, that's the "safe value" making collisions so utterly improbable that you don't need to worry about them. Don't go below that unless you have a good reason (and, even then, don't go below 16 anyway).

Tom Leek
  • 168,808
  • 28
  • 337
  • 475
19

Make sure you have OpenSSL support, and you'll never go wrong with this one-liner

$token = bin2hex(openssl_random_pseudo_bytes(16));
Adi
  • 43,808
  • 16
  • 135
  • 167
  • 1
    12 bytes are a bit low to my taste; better make it 16. Why stop at 96 bits when you can boast about "128-bit security" ? Powers of 2 are supreme. – Tom Leek Aug 08 '13 at 21:16
  • @TomLeek While I don't particularly disagree, I still feel that for this usage (short-lived confirmation emails) 96 bits is very decent. But hey, OP or others, knock yourselves out. Why stop at 128? Go for 256 or even 512. One thing I definitely agree on, 128-bit sure sounds better. – Adi Aug 08 '13 at 21:23
  • @Svetlana I feel unnaturally inclined toward upvoting this answer purely because of the profile picture. – tylerl Aug 09 '13 at 07:09
  • _Security_ is about "guessing" the token, hence 96-bit security for a 96-bit token (and yes, that's good enough), but there is also a _usability_ issue related to collisions, in case you use the token as an indexing key. With 96-bit tokens, first collisions will be encountered _on average_ after 2^48 tokens -- and _that_ is the number which is "a bit low to my taste". 128-bit keys, 128-bit tokens (e.g. UUID) are a nice "default" value so that's what I recommend for something as general as "you'll never go wrong". – Tom Leek Aug 09 '13 at 11:22
1

In PHP 7, you can just use random_bytes:

Generates an arbitrary length string of cryptographic random bytes that are suitable for cryptographic use, such as when generating salts, keys or initialization vectors.

So to generate a 16 byte hex token you would do:

$token = bin2hex(random_bytes(16));

Formally, there is no guarantee that all numbers will be unique. But a collission is so unlikely to happend that you'll be better off worrying about being hit by a meterorite.

Anders
  • 64,406
  • 24
  • 178
  • 215
  • This is the best solution now. Better than OpenSSL The best way to get uniqueness within the context of a single process is to increment a counter. (Atomically and without being reset even if power goes out.) The best way to get uniqueness globally is to take a sufficiently large sample from a TRNG. 16 bytes is definitely more than enough for confirmation emails. In other contexts 24-32 bytes is better. – Future Security Nov 14 '18 at 17:25
-1

This is the function I use to generate a random token in PHP :

<?php
function generateToken($length = 24) {
        if(function_exists('openssl_random_pseudo_bytes')) {
            $token = base64_encode(openssl_random_pseudo_bytes($length, $strong));
            if($strong == TRUE)
                return strtr(substr($token, 0, $length), '+/=', '-_,'); //base64 is about 33% longer, so we need to truncate the result
        }

        //fallback to mt_rand if php < 5.3 or no openssl available
        $characters = '0123456789';
        $characters .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz/+'; 
        $charactersLength = strlen($characters)-1;
        $token = '';

        //select some random characters
        for ($i = 0; $i < $length; $i++) {
            $token .= $characters[mt_rand(0, $charactersLength)];
        }        

        return $token;
}
?>
null
  • 1,193
  • 6
  • 16
  • please explain how it works – Celeritas Aug 08 '13 at 21:05
  • I am not sure what you want me to explain, this function generates a random token that can be embedded in an email. The token is then stored in a database. – null Aug 08 '13 at 21:10
  • 3
    Is `mt_rand` unpredictable? If it isn't, you shouldn't fall back to it, you should error out instead. – Gilles 'SO- stop being evil' Aug 08 '13 at 21:20
  • 1
    I am going to quote @TomLeek "mt_rand() is statistically good but it does not hold against sentient attackers". Anyway if you cannot do otherwise it still better than nothing. – null Aug 08 '13 at 22:27
  • This is not safe. Falling back on `mt_rand` will make your numbers predictable. That is not an acceptable solution to the problem. – Anders Nov 14 '18 at 15:39