<?php

/**
 * This file contains an example helper classes for the php-scrypt extension.
 *
 * As with all cryptographic code; it is recommended that you use a tried and
 * tested library which uses this library; rather than rolling your own.
 *
 * PHP version 5
 *
 * @category Security
 * @package  Scrypt
 * @author   Dominic Black <thephenix@gmail.com>
 * @license  http://www.opensource.org/licenses/BSD-2-Clause BSD 2-Clause License
 * @link     http://github.com/DomBlack/php-scrypt
 */

/**
 * This class abstracts away from scrypt module, allowing for easy use.
 *
 * You can create a new hash for a password by calling Password::hash($password)
 *
 * You can check a password by calling Password::check($password, $hash)
 *
 * @category Security
 * @package  Scrypt
 * @author   Dominic Black <thephenix@gmail.com>
 * @license  http://www.opensource.org/licenses/BSD-2-Clause BSD 2-Clause License
 * @link     http://github.com/DomBlack/php-scrypt
 */
abstract class Password
{

    /**
     *
     * @var int The key length
     */
    private static $_keyLength = 32;

    /**
     * Get the byte-length of the given string
     *
     * @param string $str Input string
     *
     * @return int
     */
    protected static function strlen( $str ) {
        static $isShadowed = null;

        if ($isShadowed === null) {
            $isShadowed = extension_loaded('mbstring') &&
                ini_get('mbstring.func_overload') & 2;
        }

        if ($isShadowed) {
            return mb_strlen($str, '8bit');
        } else {
            return strlen($str);
        }
    }

    /**
     * Generates a random salt
     *
     * @param int $length The length of the salt
     *
     * @return string The salt
     */
    public static function generateSalt($length = 8)
    {
        $buffer = '';
        $buffer_valid = false;
        if (function_exists('random_bytes')) {
            try {
                $buffer = random_bytes($length);
                $buffer_valid = true;
            } catch (Exception $ignored) { }
        }

        if (!$buffer_valid && function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
            $buffer = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
            if ($buffer) {
                $buffer_valid = true;
            }
        }

        if (!$buffer_valid && is_readable('/dev/urandom')) {
            $f = fopen('/dev/urandom', 'r');
            $read = static::strlen($buffer);
            while ($read < $length) {
                $buffer .= fread($f, $length - $read);
                $read = static::strlen($buffer);
            }
            fclose($f);
            if ($read >= $length) {
                $buffer_valid = true;
            }
        }

        if (!$buffer_valid) {
            throw new Exception("No suitable random number generator available");
        }

        $salt = str_replace(array('+', '$'), array('.', ''), base64_encode($buffer));

        return $salt;
    }

    /**
     * Create a password hash
     *
     * @param string $password The clear text password
     * @param string $salt     The salt to use, or null to generate a random one
     * @param int    $N        The CPU difficultly (must be a power of 2, > 1)
     * @param int    $r        The memory difficultly
     * @param int    $p        The parallel difficultly
     *
     * @return string The hashed password
     */
    public static function hash($password, $salt = false, $N = 16384, $r = 8, $p = 1)
    {
        if ($N == 0 || ($N & ($N - 1)) != 0) {
            throw new \InvalidArgumentException("N must be > 0 and a power of 2");
        }

        if ($N > PHP_INT_MAX / 128 / $r) {
            throw new \InvalidArgumentException("Parameter N is too large");
        }

        if ($r > PHP_INT_MAX / 128 / $p) {
            throw new \InvalidArgumentException("Parameter r is too large");
        }

        if ($salt === false) {
            $salt = self::generateSalt();
        } else {
            // Remove dollar signs from the salt, as we use that as a separator.
            $salt = str_replace(array('+', '$'), array('.', ''), base64_encode($salt));
        }

        $hash = scrypt($password, $salt, $N, $r, $p, self::$_keyLength);

        return $N . '$' . $r . '$' . $p . '$' . $salt . '$' . $hash;
    }

    /**
     * Check a clear text password against a hash
     *
     * @param string $password The clear text password
     * @param string $hash     The hashed password
     *
     * @return boolean If the clear text matches
     */
    public static function check($password, $hash)
    {
        // Is there actually a hash?
        if (!$hash) {
            return false;
        }

        list ($N, $r, $p, $salt, $hash) = explode('$', $hash);

        // No empty fields?
        if (empty($N) or empty($r) or empty($p) or empty($salt) or empty($hash)) {
            return false;
        }

        // Are numeric values numeric?
        if (!is_numeric($N) or !is_numeric($r) or !is_numeric($p)) {
            return false;
        }

        $calculated = scrypt($password, $salt, $N, $r, $p, self::$_keyLength);

        // Use compareStrings to avoid timeing attacks
        return self::compareStrings($hash, $calculated);
    }

    /**
     * Zend Framework (http://framework.zend.com/)
     *
     * @link      http://github.com/zendframework/zf2 for the canonical source repository
     * @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
     * @license   http://framework.zend.com/license/new-bsd New BSD License
     *
     * Compare two strings to avoid timing attacks
     *
     * C function memcmp() internally used by PHP, exits as soon as a difference
     * is found in the two buffers. That makes possible of leaking
     * timing information useful to an attacker attempting to iteratively guess
     * the unknown string (e.g. password).
     *
     * @param string $expected
     * @param string $actual
     *
     * @return boolean If the two strings match.
     */
    public static function compareStrings($expected, $actual)
    {
        $expected    = (string) $expected;
        $actual      = (string) $actual;
        $lenExpected = static::strlen($expected);
        $lenActual   = static::strlen($actual);
        $len         = min($lenExpected, $lenActual);

        $result = 0;
        for ($i = 0; $i < $len; $i ++) {
            $result |= ord($expected[$i]) ^ ord($actual[$i]);
        }
        $result |= $lenExpected ^ $lenActual;

        return ($result === 0);
    }
}
