<?php
/**
 * Implements the Password class for the MediaWiki software.
 *
 * @license GPL-2.0-or-later
 * @file
 */

declare( strict_types = 1 );

namespace MediaWiki\Password;

use InvalidArgumentException;
use RuntimeException;

/**
 * Represents a password hash for use in authentication
 *
 * Note: All password types are transparently prefixed with :<TYPE>:, where <TYPE>
 * is the registered type of the hash. This prefix is stripped in the constructor
 * and is added back in the toString() function.
 *
 * When inheriting this class, there are a couple of expectations
 * to be fulfilled:
 *  * If Password::toString() is called on an object, and the result is passed back in
 *    to PasswordFactory::newFromCiphertext(), the result will be identical to the original.
 * With these two points in mind, when creating a new Password sub-class, there are some functions
 * you have to override (because they are abstract) and others that you may want to override.
 *
 * The abstract functions that must be overridden are:
 *  * Password::crypt(), which takes a plaintext password and hashes it into a string hash suitable
 *    for being passed to the constructor of that class, and then stores that hash (and whatever
 *     other data) into the internal state of the object.
 * The functions that can optionally be overridden are:
 *  * Password::parseHash(), which can be useful to override if you need to extract values from or
 *    otherwise parse a password hash when it's passed to the constructor.
 *  * Password::needsUpdate(), which can be useful if a specific password hash has different
 *    logic for when the hash needs to be updated.
 *  * Password::toString(), which can be useful if the hash was changed in the constructor and
 *    needs to be re-assembled before being returned as a string. This function is expected to add
 *    the type back on to the hash, so make sure to do that if you override the function.
 *  * Password::verify() - This function checks if $this->hash was generated with the given
 *    password. The default is to just hash the password and do a timing-safe string comparison with
 *    $this->hash.
 *
 * After creating a new password hash type, it can be registered using the static
 * Password::register() method. The default type is set using the Password::setDefaultType() type.
 * Types must be registered before they can be set as the default.
 *
 * @since 1.24
 */
abstract class Password {
	/**
	 * @var PasswordFactory Factory that created the object
	 */
	protected $factory;

	/**
	 * String representation of the hash without the type
	 * @var string|null
	 */
	protected $hash;

	/**
	 * Array of configuration variables injected from the constructor
	 * @var array
	 */
	protected $config;

	/**
	 * Hash must fit in user_password, which is a tinyblob
	 */
	private const MAX_HASH_SIZE = 255;

	/**
	 * Construct the Password object using a string hash
	 *
	 * It is strongly recommended not to call this function directly unless you
	 * have a reason to. Use the PasswordFactory class instead.
	 *
	 * @param PasswordFactory $factory Factory object that created the password
	 * @param array $config Array of engine configuration options for hashing
	 * @param string|null $hash The raw hash, including the type
	 */
	final public function __construct( PasswordFactory $factory, array $config, ?string $hash = null ) {
		if ( !$this->isSupported() ) {
			throw new RuntimeException( 'PHP support not found for ' . get_class( $this ) );
		}
		if ( !isset( $config['type'] ) ) {
			throw new InvalidArgumentException( 'Password configuration must contain a type name.' );
		}
		$this->config = $config;
		$this->factory = $factory;

		if ( $hash !== null && strlen( $hash ) >= 3 ) {
			// Strip the type from the hash for parsing
			$hash = substr( $hash, strpos( $hash, ':', 1 ) + 1 );
		}

		$this->hash = $hash;
		$this->parseHash( $hash );
	}

	/**
	 * Get the type name of the password
	 *
	 * @return string Password type
	 */
	final public function getType(): string {
		return $this->config['type'];
	}

	/**
	 * Whether current password type is supported on this system.
	 */
	protected function isSupported(): bool {
		return true;
	}

	/**
	 * Perform any parsing necessary on the hash to see if the hash is valid
	 * and/or to perform logic for seeing if the hash needs updating.
	 *
	 * @param string|null $hash The hash, with the :<TYPE>: prefix stripped
	 * @throws PasswordError If there is an error in parsing the hash
	 */
	protected function parseHash( ?string $hash ): void {
	}

	/**
	 * Determine if the hash needs to be updated
	 *
	 * @return bool True if needs update, false otherwise
	 */
	abstract public function needsUpdate(): bool;

	/**
	 * Checks whether the given password matches the hash stored in this object.
	 *
	 * @param string $password Password to check
	 * @return bool
	 */
	public function verify( string $password ): bool {
		// No need to use the factory because we're definitely making
		// an object of the same type.
		$obj = clone $this;
		$obj->crypt( $password );

		return hash_equals( $this->toString(), $obj->toString() );
	}

	/**
	 * Convert this hash to a string that can be stored in the database
	 *
	 * The resulting string should be considered the serialized representation
	 * of this hash, i.e., if the return value were recycled back into
	 * PasswordFactory::newFromCiphertext, the returned object would be equivalent to
	 * this; also, if two objects return the same value from this function, they
	 * are considered equivalent.
	 *
	 * @return string
	 * @throws PasswordError if password cannot be serialized to fit a tinyblob.
	 */
	public function toString(): string {
		$result = ':' . $this->config['type'] . ':' . $this->hash;
		$this->assertIsSafeSize( $result );
		return $result;
	}

	/**
	 * Assert that hash will fit in a tinyblob field.
	 *
	 * This prevents MW from inserting it into the DB
	 * and having MySQL silently truncating it, locking
	 * the user out of their account.
	 *
	 * @param string $hash The hash in question.
	 * @throws PasswordError If hash does not fit in DB.
	 */
	final protected function assertIsSafeSize( string $hash ): void {
		if ( strlen( $hash ) > self::MAX_HASH_SIZE ) {
			throw new PasswordError( "Password hash is too big" );
		}
	}

	/**
	 * Hash a password and store the result in this object
	 *
	 * The result of the password hash should be put into the internal
	 * state of the hash object.
	 *
	 * @param string $password Password to hash
	 * @throws PasswordError If an internal error occurs in hashing
	 */
	abstract public function crypt( string $password ): void;
}

/** @deprecated since 1.43 use MediaWiki\\Password\\PasswordFactory */
class_alias( Password::class, 'Password' );
