<?php
/**
 * @author Todd Burry <todd@vanillaforums.com>
 * @copyright 2005 phpBB Group (Original source code)
 * @copyright 2009-2014 Vanilla Forums Inc. (Source code changes)
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
 *
 * This class adapts functions from phpBB 3 (/includes/functions.php). Any source code that was taken from the
 * phpBB project is copyright 2005 phpBB Group. Any source code changes are copyright 2009-2014 Vanilla Forums Inc.
 *
 */

namespace Garden\Password;

/**
 * Implements phpBB's password hashing algorithm.
 */
class PhpbbPassword implements PasswordInterface {
    private static $itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';

    /**
     * Check for a correct password.
     *
     * @param string $password The password in plain text.
     * @param string $hash The stored password hash.
     * @return bool Returns true if the password is correct, false if not.
     */
    public function verify($password, $hash) {
        if (strlen($hash) == 34) {
            return ($this->cryptPrivate($password, $hash) === $hash) ? true : false;
        }

        return (md5($password) === $hash) ? true : false;
    }

    /**
     * The crypt function/replacement.
     *
     * @param string $password The password to encrypt.
     * @param string $setting The hash prefix setting. It should start with $H$.
     * @return string The encrypted password.
     */
    private function cryptPrivate($password, $setting) {
        $output = '*';

        // Check for correct hash
        if (substr($setting, 0, 3) != '$H$') {
            return $output;
        }

        $count_log2 = strpos(self::$itoa64, $setting[3]);

        if ($count_log2 < 7 || $count_log2 > 30) {
            return $output;
        }

        $count = 1 << $count_log2;
        $salt = substr($setting, 4, 8);

        if (strlen($salt) != 8) {
            return $output;
        }

        /**
         * We're kind of forced to use MD5 here since it's the only
         * cryptographic primitive available in all versions of PHP
         * currently in use.  To implement our own low-level crypto
         * in PHP would result in much worse performance and
         * consequently in lower iteration counts and hashes that are
         * quicker to crack (by non-PHP code).
         */
        if (PHP_VERSION >= 5) {
            $hash = md5($salt.$password, true);
            do {
                $hash = md5($hash.$password, true);
            } while (--$count);
        } else {
            $hash = pack('H*', md5($salt.$password));
            do {
                $hash = pack('H*', md5($hash.$password));
            } while (--$count);
        }

        $output = substr($setting, 0, 12);
        $output .= $this->encode64($hash, 16);

        return $output;
    }

    /**
     * Encode hash.
     *
     * @param string $input The input to encode.
     * @param int $count The number of characters to encode.
     * @return string The encoded string.
     */
    private function encode64($input, $count) {
        $output = '';
        $i = 0;

        do {
            $value = ord($input[$i++]);
            $output .= self::$itoa64[$value & 0x3f];

            if ($i < $count) {
                $value |= ord($input[$i]) << 8;
            }

            $output .= self::$itoa64[($value >> 6) & 0x3f];

            if ($i++ >= $count) {
                break;
            }

            if ($i < $count) {
                $value |= ord($input[$i]) << 16;
            }

            $output .= self::$itoa64[($value >> 12) & 0x3f];

            if ($i++ >= $count) {
                break;
            }

            $output .= self::$itoa64[($value >> 18) & 0x3f];
        } while ($i < $count);

        return $output;
    }

    /**
     * Hashes a plaintext password.
     *
     * @param string $password The password to hash.
     * @return string Returns the hashed password.
     */
    public function hash($password) {
        $random = openssl_random_pseudo_bytes(6);

        $hash = $this->cryptPrivate($password, $this->gensaltPrivate($random));
        if (strlen($hash) == 34) {
            return $hash;
        }

        return '';
    }

    /**
     * Generate a password salt based on the given input string.
     *
     * @param string $input The input string to generate the salt from.
     * @return string Returns the password salt prefixed with `$P$`.
     */
    private function gensaltPrivate($input) {
        $output = '$H$';
        $output .= self::$itoa64[min(8 + ((PHP_VERSION >= '5') ? 5 : 3), 30)];
        $output .= $this->encode64($input, 6);

        return $output;
    }

    /**
     * Checks if a given password hash needs to be re-hashed to to a stronger algorithm.
     *
     * @param string $hash The hash to check.
     * @return bool Returns `true`
     */
    public function needsRehash($hash) {
        return $hash === '*';
    }
}
