How can I generate strong unique API keys with PHP?

12,307

Solution 1

This is the best solution i found.

Solution 2

As of PHP 7.0, you can use the random_bytes($length) method to generate a cryptographically-secure random string. This string is going to be in binary, so you'll want to encode it somehow. A straightforward way of doing this is with bin2hex($binaryString). This will give you a string $length * 2 bytes long, with $length * 8 bits of entropy to it.

You'll want $length to be high enough such that your key is effectively unguessable and that the chance of there being another key being generated with the same value is practically nil.

Putting this all together, you get this:

$key = bin2hex(random_bytes(32)); // 64 characters long

When you verify the API key, use only the first 32 characters to select the record from the database and then use hash_equals() to compare the API key as given by the user against what value you have stored. This helps protect against timing attacks. ParagonIE has an excellent write-up on this.

For an example of the checking logic:

$token = $request->bearerToken();

// Retrieve however works best for your situation,
// but it's critical that only the first 32 characters are used here.
$users = app('db')->table('users')->where('api_key', 'LIKE', substr($token, 0, 32) . '%')->get();

// $users should only have one record in it,
// but there is an extremely low chance that
// another record will share a prefix with it.
foreach ($users as $user) {
    // Performs a constant-time comparison of strings,
    // so you don't leak information about the token.
    if (hash_equals($user->api_token, $token)) {
        return $user;
    }
}

return null;

Bonus: Slightly More Advanced Use With Base64 Encoding

Using Base64 encoding is preferable to hexadecimal for space reasons, but is slightly more complicated because each character encodes 6 bits (instead of 4 for hexadecimal), which can leave the encoded value with padding at the end.

To keep this answer from dragging on, I'll just put some suggestions for handling Base64 without their supporting arguments. Pick a $length greater than 32 that is divisible by both 3 and 2. I like 42, so we'll use that for $length. Base64 encodings are of length 4 * ceil($length / 3), so our $key will be 56 characters long. You can use the first 28 characters for selection from your storage, leaving another 28 characters on the end that are protected from leaking by timing attacks with hash_equals.

Bonus 2: Secure Key Storage

Ideally, you should be treating the key much like a password. This means that instead of using hash_equals to compare the full string, you should hash the remainder of the key like a password, store that separately than the first half of your key (which is in plain-text), use the first half for selection from your database and verify the latter half with password_verify.

Share:
12,307
Ravirpandey - CS
Author by

Ravirpandey - CS

Updated on June 05, 2022

Comments

  • Ravirpandey - CS
    Ravirpandey - CS almost 2 years

    I need to generate a strong unique API key.

    Can anyone suggest the best solution for this? I don't want to use rand() function to generate random characters. Is there an alternative solution?