Search code examples
phppassword-hash

How does PHP's password_hash generate the salt?


Hello as you may know PHP recently introduced password_hash built-in in latest versions. The documentation says:

If omitted, a random salt will be created and the default cost will be used.

The question is what kind of method does it use to add the salt?

I'm interested because I'd like to know if the salt is created randomly so that when I store my hashed passwords they are always unique.


Solution

  • The salt is created randomly. They should be statistically unique.

    To see how, check out the C source code.

    On Windows, it will attempt to use php_win32_get_random_bytes() to generate the salt:

    BYTE *iv_b = (BYTE *) buffer;
    if (php_win32_get_random_bytes(iv_b, raw_length) == SUCCESS) {
        buffer_valid = 1;
    }
    

    On Linux, it will attempt to read /dev/urandom to generate the salt:

    int fd, n;
    size_t read_bytes = 0;
    fd = open("/dev/urandom", O_RDONLY);
    if (fd >= 0) {
        while (read_bytes < raw_length) {
            n = read(fd, buffer + read_bytes, raw_length - read_bytes);
            if (n < 0) {
                break;
            }
            read_bytes += (size_t) n;
        }
        close(fd);
    }
    if (read_bytes >= raw_length) {
        buffer_valid = 1;
    }
    

    Then, after those two, if the buffer is not valid (not full, it could be partial), it uses rand() to fill it out. Note that in practice this should never happen, it's just a fallback:

    if (!buffer_valid) {
        for (i = 0; i < raw_length; i++) {
            buffer[i] ^= (char) (255.0 * php_rand(TSRMLS_C) / RAND_MAX);
        }
    }
    

    Now, if C isn't your cup of tea, the same logic and algorithms are implemented in PHP in my compat library:

    $buffer = '';
    $raw_length = (int) ($required_salt_len * 3 / 4 + 1);
    $buffer_valid = false;
    if (function_exists('mcrypt_create_iv')) {
        $buffer = mcrypt_create_iv($raw_length, MCRYPT_DEV_URANDOM);
        if ($buffer) {
            $buffer_valid = true;
        }
    }
    if (!$buffer_valid && function_exists('openssl_random_pseudo_bytes')) {
        $buffer = openssl_random_pseudo_bytes($raw_length);
        if ($buffer) {
            $buffer_valid = true;
        }
    }
    if (!$buffer_valid && is_readable('/dev/urandom')) {
        $f = fopen('/dev/urandom', 'r');
        $read = strlen($buffer);
        while ($read < $raw_length) {
            $buffer .= fread($f, $raw_length - $read);
            $read = strlen($buffer);
        }
        fclose($f);
        if ($read >= $raw_length) {
            $buffer_valid = true;
        }
    }
    if (!$buffer_valid || strlen($buffer) < $raw_length) {
        $bl = strlen($buffer);
        for ($i = 0; $i < $raw_length; $i++) {
            if ($i < $bl) {
                $buffer[$i] = $buffer[$i] ^ chr(mt_rand(0, 255));
            } else {
                $buffer .= chr(mt_rand(0, 255));
            }
        }
    }
    

    The only difference is that the PHP version will use mcrypt or openssl if either is installed...