3
\$\begingroup\$

I want a cryptographically secure version of array_rand(). Is this it?

/**
 * retrieve a random key from an array, using a cryptographically secure rng.
 * - it does the same as array_rand(), except that this one use a cryptographically secure rng.
 * - relatively speaking, it should be significantly slower and more memory hungry than array_rand(), because it is creating a copy of all the keys of the array..
 *
 * @param array $arr
 * @throws ValueError if array is empty
 * @return mixed
 */
function array_rand_cryptographically_secure(array $array) {
    if (count ( $array ) < 1) {
        throw new ValueError ( 'Argument #1 ($array) cannot be empty' );
    }
    if (PHP_MAJOR_VERSION >= 8 && array_is_list ( $array )) {
        // optimization, this avoids creating a copy of all the keys
        return random_int ( 0, count ( $array ) - 1 );
    }
    $keys = array_keys ( $array );
    return $keys [random_int ( 0, count ( $keys ) - 1 )];
}

Significant edit: I figured I can use array_is_list() to avoid copying the keys when given a list.

\$\endgroup\$

2 Answers 2

3
\$\begingroup\$
  1. array_is_list() is available from PHP8.1. You can check the PHP version major&minor version with version_compare() (version_compare(PHP_VERSION, '8.1', '>=')) or function_exists() -- the latter is more concise and explicit than the former.
  2. The $max key position is shared by both branches of the conditional logic, so you can count() just once. In other words, regardless of if you call array_keys(), the array's size will not change.
  3. With $max already declared, $keys becomes a single-use variable and is therefore omittable.
  4. I prefer to type hint whenever possible. Since array keys can only either be integers or strings, the return value can be typed. With PHP8, union types allow the explicit dual-typing of the return value as a string or an integer. Of course, if you are trying to make a function that works under PHP8, then disregard this point.

Code: (Demo)

function array_rand_cryptographically_secure(array $array): int|string {
    $max = count($array) - 1;
    if ($max < 0) {
        throw new ValueError('Argument #1 ($array) cannot be empty');
    }
    if (function_exists('array_is_list') && array_is_list($array)) {
        // optimization, this avoids creating a copy of all the keys
        return random_int(0, $max);
    }
    return array_keys($array)[random_int(0, $max)];
}

$tests = [
    [5, 6, 7],
    ['a' => 1, 'b' => 2, 'c' => 3],
    ['zero', 4 => 'four', 9 => 'nine']
];
foreach ($tests as $test) {
    echo array_rand_cryptographically_secure($test) . "\n";
}

Alternatively, you can slice and preserve the original keys to avoid doubling the memory cost. (Demo)

function array_rand_cryptographically_secure(array $array): int|string {
    $max = count($array) - 1;
    if ($max < 0) {
        throw new ValueError('Argument #1 ($array) cannot be empty');
    }
    return key(array_slice($array, random_int(0, $max), 1, true));
}

And finally, the most compact and perhaps hardest to read because of all of the nested function calls: (Demo)

function array_rand_cryptographically_secure(array $array): int|string {
    if (!$array) {
        throw new ValueError ('Argument #1 ($array) cannot be empty');
    }
    return key(array_slice($array, random_int(0, count($array) - 1), 1, true));
}
\$\endgroup\$
0
\$\begingroup\$
  1. This is not API compatible with array_rand, as it doesn't have the ability to return more than one key (but array_rand does). This may or may not matter for your application, but this is not a generic "cryptographically secure version of array_rand()". Specifically, it is attempting to be a cryptographically secure version of array_rand for just the single key case.

  2. The array_is_list function is not appropriate for use in cryptographic security, as it does not guarantee a constant time answer. So if you want to use that, you should write your own function (e.g. constant_time_array_is_list) or note that your function is not safe from timing attacks (i.e. better define what you mean when you say it is cryptographically secure). Just as hash_equals is the timing attack safe version of strcmp. But strcmp is fine if both strings are publicly known.

\$\endgroup\$

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.