<?php

/**
 * Part of the Joomla Framework Authentication Package
 *
 * @copyright  Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
 * @license    GNU General Public License version 2 or later; see LICENSE
 */

namespace Joomla\Authentication\Password;

use Joomla\Authentication\Exception\UnsupportedPasswordHandlerException;

/**
 * Password handler for Argon2i hashed passwords
 *
 * @since  1.2.0
 */
class Argon2iHandler implements HandlerInterface
{
    /**
     * Generate a hash for a plaintext password
     *
     * @param   string  $plaintext  The plaintext password to validate
     * @param   array   $options    Options for the hashing operation
     *
     * @return  string
     *
     * @since   1.2.0
     * @throws  UnsupportedPasswordHandlerException if the password handler is not supported
     */
    public function hashPassword($plaintext, array $options = [])
    {
        // Use the password extension if able
        if (\defined('PASSWORD_ARGON2I')) {
            return password_hash($plaintext, \PASSWORD_ARGON2I, $options);
        }

        // Use the sodium extension (PHP 7.2 native or PECL 2.x) if able
        if (\function_exists('sodium_crypto_pwhash_str_verify')) {
            $hash = sodium_crypto_pwhash_str(
                $plaintext,
                \SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
                \SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
            );
            sodium_memzero($plaintext);

            return $hash;
        }

        // Use the libsodium extension (PECL 1.x) if able
        if (\extension_loaded('libsodium')) {
            $hash = \Sodium\crypto_pwhash_str(
                $plaintext,
                \Sodium\CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
                \Sodium\CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
            );
            \Sodium\memzero($plaintext);

            return $hash;
        }

        throw new UnsupportedPasswordHandlerException('Argon2i algorithm is not supported.');
    }

    /**
     * Check that the password handler is supported in this environment
     *
     * @return  boolean
     *
     * @since   1.2.0
     */
    public static function isSupported()
    {
        // Check for native PHP engine support in the password extension
        if (\defined('PASSWORD_ARGON2I')) {
            return true;
        }

        // Check if the sodium_compat polyfill is installed and look for compatibility through that
        if (class_exists('\\ParagonIE_Sodium_Compat') && method_exists('\\ParagonIE_Sodium_Compat', 'crypto_pwhash_is_available')) {
            return \ParagonIE_Sodium_Compat::crypto_pwhash_is_available();
        }

        // Check for support from the (lib)sodium extension
        return \function_exists('sodium_crypto_pwhash_str') || \extension_loaded('libsodium');
    }

    /**
     * Validate a password
     *
     * @param   string  $plaintext  The plain text password to validate
     * @param   string  $hashed     The password hash to validate against
     *
     * @return  boolean
     *
     * @since   1.2.0
     * @throws  UnsupportedPasswordHandlerException if the password handler is not supported
     */
    public function validatePassword($plaintext, $hashed)
    {
        // Use the password extension if able
        if (\defined('PASSWORD_ARGON2I')) {
            return password_verify($plaintext, $hashed);
        }

        // Use the sodium extension (PHP 7.2 native or PECL 2.x) if able
        if (\function_exists('sodium_crypto_pwhash_str_verify')) {
            $valid = sodium_crypto_pwhash_str_verify($hashed, $plaintext);
            sodium_memzero($plaintext);

            return $valid;
        }

        // Use the libsodium extension (PECL 1.x) if able
        if (\extension_loaded('libsodium')) {
            $valid = \Sodium\crypto_pwhash_str_verify($hashed, $plaintext);
            \Sodium\memzero($plaintext);

            return $valid;
        }

        throw new UnsupportedPasswordHandlerException('Argon2i algorithm is not supported.');
    }
}
