<?php
/**
 * @package cryptography
 */
/**
 * PBKDF2 is a cryptography class for hashing and comparing messages
 * using the PBKDF2-Algorithm with salting.
 * This is the most advanced hashing algorithm Symphony provides.
 *
 * @since Symphony 2.3.1
 * @see toolkit.Cryptography
 */
class PBKDF2 extends Cryptography
{
    /**
     * Salt length
     */
    #const SALT_LENGTH = 20;
    const SALT_LENGTH = 24;

    /**
     * Key length
     */
    #const KEY_LENGTH = 40;
    const KEY_LENGTH = 64;

    /**
     * Key length
     */
    #const ITERATIONS = 10000;
    const ITERATIONS = 310000;

    /**
     * Algorithm to be used
     */
    #const ALGORITHM = 'sha256';
    const ALGORITHM = 'sha512';

    /**
     * Prefix to identify the algorithm used
     */
    #const PREFIX = 'PBKDF2v1';
    const PREFIX = 'PBKDF2v2';

    /**
     * Uses `PBKDF2` and random salt generation to create a hash based on some input.
     * Original implementation was under public domain, taken from
     * http://www.itnewb.com/tutorial/Encrypting-Passwords-with-PHP-for-Storage-Using-the-RSA-PBKDF2-Standard
     *
     * @param string $input
     * the string to be hashed
     * @param string $salt
     * an optional salt
     * @param integer $iterations
     * an optional number of iterations to be used
     * @param string $keylength
     * an optional length the key will be cropped to fit
     * @return string
     * the hashed string
     */
    public static function hash($input, $salt = null, $iterations = null, $keylength = null)
    {
        if ($salt === null) {
            $salt = self::generateSalt(self::SALT_LENGTH);
        }

        if ($iterations === null) {
            $iterations = self::ITERATIONS;
        }

        if ($keylength === null) {
            $keylength = self::KEY_LENGTH;
        }

        $hashlength = strlen(hash(self::ALGORITHM, null, true));
        $blocks = ceil(self::KEY_LENGTH / $hashlength);
        $key = '';

        for ($block = 1; $block <= $blocks; $block++) {
            $ib = $b = hash_hmac(self::ALGORITHM, $salt . pack('N', $block), $input, true);

            for ($i = 1; $i < $iterations; $i++) {
                $ib ^= ($b = hash_hmac(self::ALGORITHM, $b, $input, true));
            }

            $key .= $ib;
        }

        return self::PREFIX . "|" . $iterations . "|" . $salt . "|" . base64_encode(substr($key, 0, $keylength));
    }

    public static function hashV2(string $password): string {

        $salt = self::generateSalt(self::SALT_LENGTH);
        $iterations = self::ITERATIONS;
        $keylength = self::KEY_LENGTH;
        $algo = self::ALGORITHM;
        $prefix = self::PREFIX;

        $key = hash_pbkdf2($algo, $password, $salt, $iterations, $keylength, true);

        return "$prefix|$algo|$iterations|$salt|" . base64_encode($key);
    }

    /**
     * Compares a given hash with a cleantext password. Also extracts the salt
     * from the hash.
     *
     * @param string $input
     *  the cleartext password
     * @param string $hash
     *  the hash the password should be checked against
     * @param boolean $isHash
     * @return boolean
     *  the result of the comparison
     */
    public static function compare($input, $hash, $isHash = false)
    {
        if (str_starts_with($hash, 'PBKDF2v1')) {
            return self::compareV1($input, $hash);
        }
        if (str_starts_with($hash, 'PBKDF2v2')) {
            return self::compareV2($input, $hash);
        }

        $salt = self::extractSalt($hash);
        $iterations = self::extractIterations($hash);
        $keylength = strlen(base64_decode(self::extractHash($hash)));

        return $hash === self::hash($input, $salt, $iterations, $keylength);
    }


    private static function compareV1(string $password, string $hash): bool {
        $parts = explode('|', $hash, 4);

        if (count($parts) !== 4) {
            return false; // Invalid format
        }

        list($prefix, $iterations, $salt, $stored) = $parts;

        $keylength = strlen(base64_decode($stored)); // korrekt, ergibt 40 bei alten Hashes

        $derived = hash_pbkdf2(
            'sha256',  // alter Algorithmus
            $password,
            $salt,
            (int)$iterations,
            $keylength,
            true      // binär, weil du base64_decode verwendest
        );

        return hash_equals($derived, base64_decode($stored));
    }

/*
    private static function compareV1(string $password, string $hash): bool {
        list($prefix, $salt, $stored) = explode('|', $hash, 4);

        $derived = hash_pbkdf2(
            'sha256',  // old Algorithm
            $password,
            $salt,
            10000,     // old Iterations
            40,        // old Key-Length
            false
        );

        return hash_equals($stored, $derived);
    }
*/
    /**
     * PBKDF2v2 (introduced in 2025)
     * Uses sha512, 310k iterations, 64-byte derived key
     * See: OWASP Password Storage Cheat Sheet (2024 edition)
     */
    private static function compareV2(string $password, string $hash): bool {
        list($prefix, $algo, $iterations, $salt, $stored) = explode('|', $hash, 5);

        $keylength1 = strlen(base64_decode($stored));
        $keylength2 = strlen(base64_decode($stored));
        Symphony::Log()->pushToLog('compareV2 erreicht. KEY_LENGTH:' . $keylength1 . ':' . $keylength2 , E_NOTICE, true);

        $derived = hash_pbkdf2(
            $algo,
            $password,
            $salt,
            (int)$iterations,
            static::KEY_LENGTH,
            true
        );

        Symphony::Log()->pushToLog("Stored:   >" . $stored . "<", E_NOTICE, true);
        Symphony::Log()->pushToLog("Derived:  >" . base64_encode($derived) . "<", E_NOTICE, true);

       #Symphony::Log()->pushToLog($content, E_NOTICE, true);

       return hash_equals($derived, base64_decode($stored));
    }

    /**
     * Extracts the hash from a hash/salt-combination
     *
     * @param string $input
     * the hashed string
     * @return string
     * the hash
     */
    public static function extractHash($input)
    {
        $data = explode("|", $input, 4);

        return $data[3];
    }

    /**
     * Extracts the salt from a hash/salt-combination
     *
     * @param string $input
     * the hashed string
     * @return string
     * the salt
     */
    public static function extractSalt($input)
    {
        $data = explode("|", $input, 4);

        return $data[2];
    }

    /**
     * Extracts the saltlength from a hash/salt-combination
     *
     * @param string $input
     * the hashed string
     * @return integer
     * the saltlength
     */
    public static function extractSaltlength($input)
    {
        return strlen(self::extractSalt($input));
    }

    /**
     * Extracts the number of iterations from a hash/salt-combination
     *
     * @param string $input
     * the hashed string
     * @return integer
     * the number of iterations
     */
    public static function extractIterations($input)
    {
        $data = explode("|", $input, 4);

        return (int) $data[1];
    }

    /**
     * Checks if provided hash has been computed by most recent algorithm
     * returns true if otherwise
     *
     * @param string $hash
     * the hash to be checked
     * @return boolean
     * whether the hash should be re-computed
     */
    public static function requiresMigration($hash)
    {
        $length = self::extractSaltlength($hash);
        $iterations = self::extractIterations($hash);
        $keylength = strlen(base64_decode(self::extractHash($hash)));

        if ($length !== self::SALT_LENGTH || $iterations !== self::ITERATIONS || $keylength !== self::KEY_LENGTH) {
            return true;
        } else {
            return false;
        }
    }
}
