TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Crypto\PasswordHashing;
|
||||
|
||||
/**
|
||||
* This abstract class implements the 'argon2' flavour of the php password api.
|
||||
*/
|
||||
abstract class AbstractArgon2PasswordHash implements PasswordHashInterface, Argon2PasswordHashInterface
|
||||
{
|
||||
/**
|
||||
* The PHP defaults are rather low ('memory_cost' => 65536, 'time_cost' => 4, 'threads' => 1)
|
||||
* We raise that significantly by default. At the time of this writing, with the options
|
||||
* below, password_verify() needs about 130ms on an I7 6820 on 2 CPU's (argon2i).
|
||||
*
|
||||
* We are not raising the amount of threads used, as that might lead to problems on various
|
||||
* systems - see #90612
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $options = [
|
||||
'memory_cost' => 65536,
|
||||
'time_cost' => 16,
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor sets options if given
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$newOptions = $this->options;
|
||||
if (isset($options['memory_cost'])) {
|
||||
if ((int)$options['memory_cost'] < PASSWORD_ARGON2_DEFAULT_MEMORY_COST) {
|
||||
throw new \InvalidArgumentException(
|
||||
'memory_cost must not be lower than ' . PASSWORD_ARGON2_DEFAULT_MEMORY_COST,
|
||||
1533899612
|
||||
);
|
||||
}
|
||||
$newOptions['memory_cost'] = (int)$options['memory_cost'];
|
||||
}
|
||||
if (isset($options['time_cost'])) {
|
||||
if ((int)$options['time_cost'] < PASSWORD_ARGON2_DEFAULT_TIME_COST) {
|
||||
throw new \InvalidArgumentException(
|
||||
'time_cost must not be lower than ' . PASSWORD_ARGON2_DEFAULT_TIME_COST,
|
||||
1533899613
|
||||
);
|
||||
}
|
||||
$newOptions['time_cost'] = (int)$options['time_cost'];
|
||||
}
|
||||
if (isset($options['threads'])) {
|
||||
if (extension_loaded('sodium')) {
|
||||
// Libsodium does not support threads, so ignore the
|
||||
// options and force single-thread.
|
||||
$newOptions['threads'] = 1;
|
||||
} elseif ((int)$options['threads'] < PASSWORD_ARGON2_DEFAULT_THREADS) {
|
||||
throw new \InvalidArgumentException(
|
||||
'threads must not be lower than ' . PASSWORD_ARGON2_DEFAULT_THREADS,
|
||||
1533899614
|
||||
);
|
||||
} else {
|
||||
$newOptions['threads'] = (int)$options['threads'];
|
||||
}
|
||||
}
|
||||
$this->options = $newOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns password algorithm constant from name
|
||||
*
|
||||
* Since PHP 7.4 Password hashing algorithm identifiers
|
||||
* are nullable strings rather than integers.
|
||||
*
|
||||
* @return int|string|null
|
||||
*/
|
||||
protected function getPasswordAlgorithm()
|
||||
{
|
||||
return constant($this->getPasswordAlgorithmName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given plaintext password is correct by comparing it with
|
||||
* a given salted hashed password.
|
||||
*
|
||||
* @param string $plainPW plain text password to compare with salted hash
|
||||
* @param string $saltedHashPW Salted hash to compare plain-text password with
|
||||
* @return bool TRUE, if plaintext password is correct, otherwise FALSE
|
||||
*/
|
||||
public function checkPassword(string $plainPW, string $saltedHashPW): bool
|
||||
{
|
||||
return password_verify($plainPW, $saltedHashPW);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if PHP is compiled '--with-password-argon2' so
|
||||
* the hash algorithm is available.
|
||||
*/
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return defined($this->getPasswordAlgorithmName()) && $this->getPasswordAlgorithm();
|
||||
}
|
||||
|
||||
public function getHashedPassword(string $password): ?string
|
||||
{
|
||||
$hashedPassword = null;
|
||||
if ($password !== '') {
|
||||
$hashedPassword = password_hash($password, $this->getPasswordAlgorithm(), $this->options);
|
||||
if (empty($hashedPassword)) {
|
||||
throw new InvalidPasswordHashException('Cannot generate password, probably invalid options', 1526052118);
|
||||
}
|
||||
}
|
||||
return $hashedPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a user's hashed password needs to be replaced with a new hash,
|
||||
* for instance if options changed.
|
||||
*
|
||||
* @param string $passString Salted hash to check if it needs an update
|
||||
* @return bool TRUE if salted hash needs an update, otherwise FALSE
|
||||
*/
|
||||
public function isHashUpdateNeeded(string $passString): bool
|
||||
{
|
||||
return password_needs_rehash($passString, $this->getPasswordAlgorithm(), $this->options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a given string is a valid password hash.
|
||||
*
|
||||
* @param string $saltedPW String to check
|
||||
* @return bool TRUE if it's valid salted hashed password, otherwise FALSE
|
||||
*/
|
||||
public function isValidSaltedPW(string $saltedPW): bool
|
||||
{
|
||||
$passwordInfo = password_get_info($saltedPW);
|
||||
|
||||
return
|
||||
isset($passwordInfo['algo'])
|
||||
&& $passwordInfo['algo'] === $this->getPasswordAlgorithm()
|
||||
&& strncmp($saltedPW, $this->getPasswordHashPrefix(), strlen($this->getPasswordHashPrefix())) === 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Crypto\PasswordHashing;
|
||||
|
||||
interface Argon2PasswordHashInterface
|
||||
{
|
||||
public function getPasswordAlgorithmName(): string;
|
||||
public function getPasswordHashPrefix(): string;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Crypto\PasswordHashing;
|
||||
|
||||
/**
|
||||
* This class implements the 'argon2i' flavour of the php password api.
|
||||
*
|
||||
* Hashes are identified by the prefix '$argon2i$'.
|
||||
*
|
||||
* The length of an argon2i password hash (in the form it is received from
|
||||
* PHP) depends on the environment.
|
||||
*
|
||||
* @see PASSWORD_ARGON2I in https://secure.php.net/manual/en/password.constants.php
|
||||
*/
|
||||
class Argon2iPasswordHash extends AbstractArgon2PasswordHash
|
||||
{
|
||||
public function getPasswordAlgorithmName(): string
|
||||
{
|
||||
return 'PASSWORD_ARGON2I';
|
||||
}
|
||||
|
||||
public function getPasswordHashPrefix(): string
|
||||
{
|
||||
return '$argon2i$';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Crypto\PasswordHashing;
|
||||
|
||||
/**
|
||||
* This class implements the 'argon2id' flavour of the php password api.
|
||||
*
|
||||
* Hashes are identified by the prefix '$argon2id$'.
|
||||
*
|
||||
* The length of an argon2id password hash (in the form it is received from
|
||||
* PHP) depends on the environment.
|
||||
*
|
||||
* @see PASSWORD_ARGON2ID in https://secure.php.net/manual/en/password.constants.php
|
||||
*/
|
||||
class Argon2idPasswordHash extends AbstractArgon2PasswordHash
|
||||
{
|
||||
public function getPasswordAlgorithmName(): string
|
||||
{
|
||||
return 'PASSWORD_ARGON2ID';
|
||||
}
|
||||
|
||||
public function getPasswordHashPrefix(): string
|
||||
{
|
||||
return '$argon2id$';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Crypto\PasswordHashing;
|
||||
|
||||
/**
|
||||
* This class implements the 'bcrypt' flavour of the php password api.
|
||||
*
|
||||
* Hashes are identified by the prefix '$2y$'.
|
||||
*
|
||||
* To work around the limitations of bcrypt (accepts not more than 72
|
||||
* chars and truncates on NUL bytes), the plain password is pre-hashed
|
||||
* before the actual password-hash is generated/verified.
|
||||
*
|
||||
* @see PASSWORD_BCRYPT in https://secure.php.net/manual/en/password.constants.php
|
||||
*/
|
||||
class BcryptPasswordHash implements PasswordHashInterface
|
||||
{
|
||||
/**
|
||||
* Prefix for the password hash
|
||||
*/
|
||||
protected const PREFIX = '$2y$';
|
||||
|
||||
/**
|
||||
* Set default PHP cost: Default is 10 with PHP <8.4, 12 since PHP 8.4. At the time
|
||||
* of this writing, this leads to 150-200ms computing time on a casual I7 CPU.
|
||||
*/
|
||||
protected array $options = [
|
||||
'cost' => 12,
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor sets options if given
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$newOptions = $this->options;
|
||||
// Check options for validity
|
||||
if (isset($options['cost'])) {
|
||||
if (!$this->isValidBcryptCost((int)$options['cost'])) {
|
||||
throw new \InvalidArgumentException(
|
||||
'cost must not be lower than 10 or higher than 31',
|
||||
1533902002
|
||||
);
|
||||
}
|
||||
$newOptions['cost'] = (int)$options['cost'];
|
||||
}
|
||||
$this->options = $newOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* bcrypt is always available in PHP core hash functions.
|
||||
*/
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given plaintext password is correct by comparing it with
|
||||
* a given salted hashed password.
|
||||
*
|
||||
* @param string $plainPW plain text password to compare with salted hash
|
||||
* @param string $saltedHashPW Salted hash to compare plain-text password with
|
||||
*/
|
||||
public function checkPassword(string $plainPW, string $saltedHashPW): bool
|
||||
{
|
||||
return password_verify($this->processPlainPassword($plainPW), $saltedHashPW);
|
||||
}
|
||||
|
||||
public function getHashedPassword(string $password): ?string
|
||||
{
|
||||
$hashedPassword = null;
|
||||
if ($password !== '') {
|
||||
$password = $this->processPlainPassword($password);
|
||||
$hashedPassword = password_hash($password, PASSWORD_BCRYPT, $this->options);
|
||||
if (empty($hashedPassword)) {
|
||||
throw new InvalidPasswordHashException('Cannot generate password, probably invalid options', 1517174114);
|
||||
}
|
||||
}
|
||||
return $hashedPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a given string is a valid salted hashed password.
|
||||
*
|
||||
* @param string $saltedPW String to check
|
||||
* @return bool TRUE if it's valid salted hashed password, otherwise FALSE
|
||||
*/
|
||||
public function isValidSaltedPW(string $saltedPW): bool
|
||||
{
|
||||
$result = false;
|
||||
$passwordInfo = password_get_info($saltedPW);
|
||||
// Validate the cost value, password_get_info() does not check it
|
||||
$cost = (int)substr($saltedPW, 4, 2);
|
||||
if (isset($passwordInfo['algo'])
|
||||
&& $passwordInfo['algo'] === PASSWORD_BCRYPT
|
||||
&& strncmp($saltedPW, static::PREFIX, strlen(static::PREFIX)) === 0
|
||||
&& $this->isValidBcryptCost($cost)
|
||||
) {
|
||||
$result = true;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
/**
|
||||
* Checks whether a user's hashed password needs to be replaced with a new hash.
|
||||
*
|
||||
* @param string $passString Salted hash to check if it needs an update
|
||||
* @return bool TRUE if salted hash needs an update, otherwise FALSE
|
||||
*/
|
||||
public function isHashUpdateNeeded(string $passString): bool
|
||||
{
|
||||
return password_needs_rehash($passString, PASSWORD_BCRYPT, $this->options);
|
||||
}
|
||||
|
||||
/**
|
||||
* The plain password is processed through sha384 and then base64
|
||||
* encoded. This will produce a 64 characters input to use with
|
||||
* password_* functions, which has some advantages:
|
||||
* 1. It is close to the (bcrypt-) maximum of 72 character keyspace
|
||||
* 2. base64 will never produce NUL bytes (bcrypt truncates on NUL bytes)
|
||||
* 3. sha384 is resistant to length extension attacks
|
||||
*/
|
||||
protected function processPlainPassword(string $password): string
|
||||
{
|
||||
return base64_encode(hash('sha384', $password, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://github.com/php/php-src/blob/php-7.2.0/ext/standard/password.c#L441-L444
|
||||
*/
|
||||
protected function isValidBcryptCost(int $cost): bool
|
||||
{
|
||||
return $cost >= 10 && $cost <= 31;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Crypto\PasswordHashing;
|
||||
|
||||
use TYPO3\CMS\Core\Crypto\Random;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Class that implements Blowfish salted hashing based on PHP's
|
||||
* crypt() function.
|
||||
*
|
||||
* Warning: Blowfish salted hashing with PHP's crypt() is not available
|
||||
* on every system.
|
||||
*/
|
||||
class BlowfishPasswordHash implements PasswordHashInterface
|
||||
{
|
||||
/**
|
||||
* Prefix for the password hash.
|
||||
*/
|
||||
protected const PREFIX = '$2a$';
|
||||
|
||||
/**
|
||||
* @var array The default log2 number of iterations for password stretching.
|
||||
*/
|
||||
protected $options = [
|
||||
'hash_count' => 7,
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor sets options if given
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$newOptions = $this->options;
|
||||
if (isset($options['hash_count'])) {
|
||||
if ((int)$options['hash_count'] < 4 || (int)$options['hash_count'] > 17) {
|
||||
throw new \InvalidArgumentException(
|
||||
'hash_count must not be lower than 4 or bigger than 17',
|
||||
1533903545
|
||||
);
|
||||
}
|
||||
$newOptions['hash_count'] = (int)$options['hash_count'];
|
||||
}
|
||||
$this->options = $newOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method checks if a given plaintext password is correct by comparing it with
|
||||
* a given salted hashed password.
|
||||
*
|
||||
* @param string $plainPW plain-text password to compare with salted hash
|
||||
* @param string $saltedHashPW salted hash to compare plain-text password with
|
||||
* @return bool TRUE, if plain-text password matches the salted hash, otherwise FALSE
|
||||
*/
|
||||
public function checkPassword(string $plainPW, string $saltedHashPW): bool
|
||||
{
|
||||
$isCorrect = false;
|
||||
if ($this->isValidSalt($saltedHashPW)) {
|
||||
$isCorrect = password_verify($plainPW, $saltedHashPW);
|
||||
}
|
||||
return $isCorrect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether all prerequisites for the hashing methods are matched
|
||||
*
|
||||
* @return bool Method available
|
||||
*/
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return (bool)CRYPT_BLOWFISH;
|
||||
}
|
||||
|
||||
public function getHashedPassword(string $password): ?string
|
||||
{
|
||||
$saltedPW = null;
|
||||
if (!empty($password)) {
|
||||
$salt = $this->getGeneratedSalt();
|
||||
$saltedPW = crypt($password, $this->applySettingsToSalt($salt));
|
||||
}
|
||||
return $saltedPW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a user's hashed password needs to be replaced with a new hash.
|
||||
*
|
||||
* This is typically called during the login process when the plain text
|
||||
* password is available. A new hash is needed when the desired iteration
|
||||
* count has changed through a change in the variable $hashCount or
|
||||
* HASH_COUNT.
|
||||
*
|
||||
* @param string $saltedPW Salted hash to check if it needs an update
|
||||
* @return bool TRUE if salted hash needs an update, otherwise FALSE
|
||||
*/
|
||||
public function isHashUpdateNeeded(string $saltedPW): bool
|
||||
{
|
||||
// Check whether the iteration count used differs from the standard number.
|
||||
$countLog2 = $this->getCountLog2($saltedPW);
|
||||
return $countLog2 !== null && $countLog2 < $this->options['hash_count'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Method determines if a given string is a valid salted hashed password.
|
||||
*
|
||||
* @param string $saltedPW String to check
|
||||
* @return bool TRUE if it's valid salted hashed password, otherwise FALSE
|
||||
*/
|
||||
public function isValidSaltedPW(string $saltedPW): bool
|
||||
{
|
||||
$isValid = !strncmp(self::PREFIX, $saltedPW, strlen(self::PREFIX));
|
||||
if ($isValid) {
|
||||
$isValid = $this->isValidSalt($saltedPW);
|
||||
}
|
||||
return $isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random base 64-encoded salt prefixed and suffixed with settings for the hash.
|
||||
*
|
||||
* Proper use of salts may defeat a number of attacks, including:
|
||||
* - The ability to try candidate passwords against multiple hashes at once.
|
||||
* - The ability to use pre-hashed lists of candidate passwords.
|
||||
* - The ability to determine whether two users have the same (or different)
|
||||
* password without actually having to guess one of the passwords.
|
||||
*
|
||||
* @return string A character string containing settings and a random salt
|
||||
*/
|
||||
protected function getGeneratedSalt(): string
|
||||
{
|
||||
$randomBytes = GeneralUtility::makeInstance(Random::class)->generateRandomBytes(16);
|
||||
return $this->base64Encode($randomBytes, 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method applies settings (prefix, hash count) to a salt.
|
||||
*
|
||||
* @param string $salt A salt to apply setting to
|
||||
* @return string Salt with setting
|
||||
*/
|
||||
protected function applySettingsToSalt(string $salt): string
|
||||
{
|
||||
$saltWithSettings = $salt;
|
||||
$reqLenBase64 = $this->getLengthBase64FromBytes(16);
|
||||
// salt without setting
|
||||
if (strlen($salt) == $reqLenBase64) {
|
||||
$saltWithSettings = self::PREFIX . sprintf('%02u', $this->options['hash_count']) . '$' . $salt;
|
||||
}
|
||||
return $saltWithSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the log2 iteration count from a stored hash or setting string.
|
||||
*
|
||||
* @param string $setting Complete hash or a hash's setting string or to get log2 iteration count from
|
||||
* @return int|null Used hashcount for given hash string
|
||||
*/
|
||||
protected function getCountLog2(string $setting): ?int
|
||||
{
|
||||
$countLog2 = null;
|
||||
$setting = substr($setting, strlen(self::PREFIX));
|
||||
$firstSplitPos = strpos($setting, '$');
|
||||
// Hashcount existing
|
||||
if ($firstSplitPos !== false && $firstSplitPos <= 2 && is_numeric(substr($setting, 0, $firstSplitPos))) {
|
||||
$countLog2 = (int)substr($setting, 0, $firstSplitPos);
|
||||
}
|
||||
return $countLog2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string for mapping an int to the corresponding base 64 character.
|
||||
*
|
||||
* @return string String for mapping an int to the corresponding base 64 character
|
||||
*/
|
||||
protected function getItoa64(): string
|
||||
{
|
||||
return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
}
|
||||
|
||||
/**
|
||||
* Method determines if a given string is a valid salt.
|
||||
*
|
||||
* @param string $salt String to check
|
||||
* @return bool TRUE if it's valid salt, otherwise FALSE
|
||||
*/
|
||||
protected function isValidSalt(string $salt): bool
|
||||
{
|
||||
$isValid = ($skip = false);
|
||||
$reqLenBase64 = $this->getLengthBase64FromBytes(16);
|
||||
if (strlen($salt) >= $reqLenBase64) {
|
||||
// Salt with prefixed setting
|
||||
if (!strncmp('$', $salt, 1)) {
|
||||
if (!strncmp(self::PREFIX, $salt, strlen(self::PREFIX))) {
|
||||
$isValid = true;
|
||||
$salt = substr($salt, (int)strrpos($salt, '$') + 1);
|
||||
} else {
|
||||
$skip = true;
|
||||
}
|
||||
}
|
||||
// Checking base64 characters
|
||||
if (!$skip && strlen($salt) >= $reqLenBase64) {
|
||||
if (preg_match('/^[' . preg_quote($this->getItoa64(), '/') . ']{' . $reqLenBase64 . ',' . $reqLenBase64 . '}$/', substr($salt, 0, $reqLenBase64))) {
|
||||
$isValid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes bytes into printable base 64 using the *nix standard from crypt().
|
||||
*
|
||||
* @param string $input The string containing bytes to encode.
|
||||
* @param int $count The number of characters (bytes) to encode.
|
||||
* @return string Encoded string
|
||||
*/
|
||||
protected function base64Encode(string $input, int $count): string
|
||||
{
|
||||
$output = '';
|
||||
$i = 0;
|
||||
$itoa64 = $this->getItoa64();
|
||||
do {
|
||||
$value = ord($input[$i++]);
|
||||
$output .= $itoa64[$value & 63];
|
||||
if ($i < $count) {
|
||||
$value |= ord($input[$i]) << 8;
|
||||
}
|
||||
$output .= $itoa64[$value >> 6 & 63];
|
||||
if ($i++ >= $count) {
|
||||
break;
|
||||
}
|
||||
if ($i < $count) {
|
||||
$value |= ord($input[$i]) << 16;
|
||||
}
|
||||
$output .= $itoa64[$value >> 12 & 63];
|
||||
if ($i++ >= $count) {
|
||||
break;
|
||||
}
|
||||
$output .= $itoa64[$value >> 18 & 63];
|
||||
} while ($i < $count);
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method determines required length of base64 characters for a given
|
||||
* length of a byte string.
|
||||
*
|
||||
* @param int $byteLength Length of bytes to calculate in base64 chars
|
||||
* @return int Required length of base64 characters
|
||||
*/
|
||||
protected function getLengthBase64FromBytes(int $byteLength): int
|
||||
{
|
||||
// Calculates bytes in bits in base64
|
||||
return (int)ceil($byteLength * 8 / 6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Crypto\PasswordHashing;
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* InvalidPasswordHashException thrown if salting went wrong.
|
||||
*/
|
||||
class InvalidPasswordHashException extends Exception {}
|
||||
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Crypto\PasswordHashing;
|
||||
|
||||
use TYPO3\CMS\Core\Crypto\Random;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Class that implements MD5 salted hashing based on PHP's
|
||||
* crypt() function.
|
||||
*
|
||||
* MD5 salted hashing with PHP's crypt() should be available
|
||||
* on most of the systems.
|
||||
*/
|
||||
class Md5PasswordHash implements PasswordHashInterface
|
||||
{
|
||||
/**
|
||||
* Prefix for the password hash.
|
||||
*/
|
||||
protected const PREFIX = '$1$';
|
||||
|
||||
/**
|
||||
* Method checks if a given plaintext password is correct by comparing it with
|
||||
* a given salted hashed password.
|
||||
*
|
||||
* @param string $plainPW plain-text password to compare with salted hash
|
||||
* @param string $saltedHashPW salted hash to compare plain-text password with
|
||||
* @return bool TRUE, if plain-text password matches the salted hash, otherwise FALSE
|
||||
*/
|
||||
public function checkPassword(string $plainPW, string $saltedHashPW): bool
|
||||
{
|
||||
$isCorrect = false;
|
||||
if ($this->isValidSalt($saltedHashPW)) {
|
||||
$isCorrect = password_verify($plainPW, $saltedHashPW);
|
||||
}
|
||||
return $isCorrect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether all prerequisites for the hashing methods are matched
|
||||
*
|
||||
* @return bool Method available
|
||||
*/
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return (bool)CRYPT_MD5;
|
||||
}
|
||||
|
||||
public function getHashedPassword(string $password): ?string
|
||||
{
|
||||
$saltedPW = null;
|
||||
if (!empty($password)) {
|
||||
$salt = $this->getGeneratedSalt();
|
||||
$saltedPW = crypt($password, $this->applySettingsToSalt($salt));
|
||||
}
|
||||
return $saltedPW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a user's hashed password needs to be replaced with a new hash.
|
||||
*
|
||||
* This is typically called during the login process when the plain text
|
||||
* password is available. A new hash is needed when the desired iteration
|
||||
* count has changed through a change in the variable $hashCount or HASH_COUNT.
|
||||
*
|
||||
* @param string $passString Salted hash to check if it needs an update
|
||||
* @return bool TRUE if salted hash needs an update, otherwise FALSE
|
||||
*/
|
||||
public function isHashUpdateNeeded(string $passString): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method determines if a given string is a valid salted hashed password.
|
||||
*
|
||||
* @param string $saltedPW String to check
|
||||
* @return bool TRUE if it's valid salted hashed password, otherwise FALSE
|
||||
*/
|
||||
public function isValidSaltedPW(string $saltedPW): bool
|
||||
{
|
||||
$isValid = !strncmp(self::PREFIX, $saltedPW, strlen(self::PREFIX));
|
||||
if ($isValid) {
|
||||
$isValid = $this->isValidSalt($saltedPW);
|
||||
}
|
||||
return $isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random base 64-encoded salt prefixed and suffixed with settings for the hash.
|
||||
*
|
||||
* Proper use of salts may defeat a number of attacks, including:
|
||||
* - The ability to try candidate passwords against multiple hashes at once.
|
||||
* - The ability to use pre-hashed lists of candidate passwords.
|
||||
* - The ability to determine whether two users have the same (or different)
|
||||
* password without actually having to guess one of the passwords.
|
||||
*
|
||||
* @return string A character string containing settings and a random salt
|
||||
*/
|
||||
protected function getGeneratedSalt(): string
|
||||
{
|
||||
$randomBytes = GeneralUtility::makeInstance(Random::class)->generateRandomBytes(6);
|
||||
return $this->base64Encode($randomBytes, 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method applies settings (prefix, suffix) to a salt.
|
||||
*
|
||||
* @param string $salt A salt to apply setting to
|
||||
* @return string Salt with setting
|
||||
*/
|
||||
protected function applySettingsToSalt(string $salt): string
|
||||
{
|
||||
$saltWithSettings = $salt;
|
||||
$reqLenBase64 = $this->getLengthBase64FromBytes(6);
|
||||
// Salt without setting
|
||||
if (strlen($salt) == $reqLenBase64) {
|
||||
$saltWithSettings = self::PREFIX . $salt . '$';
|
||||
}
|
||||
return $saltWithSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string for mapping an int to the corresponding base 64 character.
|
||||
*
|
||||
* @return string String for mapping an int to the corresponding base 64 character
|
||||
*/
|
||||
protected function getItoa64(): string
|
||||
{
|
||||
return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
}
|
||||
|
||||
/**
|
||||
* Method determines if a given string is a valid salt
|
||||
*
|
||||
* @param string $salt String to check
|
||||
* @return bool TRUE if it's valid salt, otherwise FALSE
|
||||
*/
|
||||
protected function isValidSalt(string $salt): bool
|
||||
{
|
||||
$isValid = ($skip = false);
|
||||
$reqLenBase64 = $this->getLengthBase64FromBytes(6);
|
||||
if (strlen($salt) >= $reqLenBase64) {
|
||||
// Salt with prefixed setting
|
||||
if (!strncmp('$', $salt, 1)) {
|
||||
if (!strncmp(self::PREFIX, $salt, strlen(self::PREFIX))) {
|
||||
$isValid = true;
|
||||
$salt = substr($salt, strlen(self::PREFIX));
|
||||
} else {
|
||||
$skip = true;
|
||||
}
|
||||
}
|
||||
// Checking base64 characters
|
||||
if (!$skip && strlen($salt) >= $reqLenBase64) {
|
||||
if (preg_match('/^[' . preg_quote($this->getItoa64(), '/') . ']{' . $reqLenBase64 . ',' . $reqLenBase64 . '}$/', substr($salt, 0, $reqLenBase64))) {
|
||||
$isValid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes bytes into printable base 64 using the *nix standard from crypt().
|
||||
*
|
||||
* @param string $input The string containing bytes to encode.
|
||||
* @param int $count The number of characters (bytes) to encode.
|
||||
* @return string Encoded string
|
||||
*/
|
||||
protected function base64Encode(string $input, int $count): string
|
||||
{
|
||||
$output = '';
|
||||
$i = 0;
|
||||
$itoa64 = $this->getItoa64();
|
||||
do {
|
||||
$value = ord($input[$i++]);
|
||||
$output .= $itoa64[$value & 63];
|
||||
if ($i < $count) {
|
||||
$value |= ord($input[$i]) << 8;
|
||||
}
|
||||
$output .= $itoa64[$value >> 6 & 63];
|
||||
if ($i++ >= $count) {
|
||||
break;
|
||||
}
|
||||
if ($i < $count) {
|
||||
$value |= ord($input[$i]) << 16;
|
||||
}
|
||||
$output .= $itoa64[$value >> 12 & 63];
|
||||
if ($i++ >= $count) {
|
||||
break;
|
||||
}
|
||||
$output .= $itoa64[$value >> 18 & 63];
|
||||
} while ($i < $count);
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method determines required length of base64 characters for a given
|
||||
* length of a byte string.
|
||||
*
|
||||
* @param int $byteLength Length of bytes to calculate in base64 chars
|
||||
* @return int Required length of base64 characters
|
||||
*/
|
||||
protected function getLengthBase64FromBytes(int $byteLength): int
|
||||
{
|
||||
// Calculates bytes in bits in base64
|
||||
return (int)ceil($byteLength * 8 / 6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Crypto\PasswordHashing;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Factory class to find and return hash instances of given hashed passwords
|
||||
* and to find and return default hash instances to hash new passwords.
|
||||
*/
|
||||
readonly class PasswordHashFactory
|
||||
{
|
||||
/**
|
||||
* Find a hash class that handles given hash and return an instance of it.
|
||||
*
|
||||
* @param string $hash Given hash to find instance for
|
||||
* @param string $mode 'FE' for frontend users, 'BE' for backend users
|
||||
* @return PasswordHashInterface Object that can handle given hash
|
||||
* @throws \LogicException
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws InvalidPasswordHashException If no class was found that handles given hash
|
||||
*/
|
||||
public function get(string $hash, string $mode): PasswordHashInterface
|
||||
{
|
||||
if ($mode !== 'FE' && $mode !== 'BE') {
|
||||
throw new \InvalidArgumentException('Mode must be either \'FE\' or \'BE\', ' . $mode . ' given.', 1533948312);
|
||||
}
|
||||
|
||||
$registeredHashClasses = static::getRegisteredSaltedHashingMethods();
|
||||
|
||||
if (empty($GLOBALS['TYPO3_CONF_VARS'][$mode]['passwordHashing']['className'])
|
||||
|| !isset($GLOBALS['TYPO3_CONF_VARS'][$mode]['passwordHashing']['options'])
|
||||
|| !is_array($GLOBALS['TYPO3_CONF_VARS'][$mode]['passwordHashing']['options'])
|
||||
) {
|
||||
throw new \LogicException(
|
||||
'passwordHashing configuration of ' . $mode . ' broken',
|
||||
1533949053
|
||||
);
|
||||
}
|
||||
$defaultHashClassName = $GLOBALS['TYPO3_CONF_VARS'][$mode]['passwordHashing']['className'];
|
||||
$defaultHashOptions = (array)$GLOBALS['TYPO3_CONF_VARS'][$mode]['passwordHashing']['options'];
|
||||
|
||||
foreach ($registeredHashClasses as $className) {
|
||||
if ($className === $defaultHashClassName) {
|
||||
$hashInstance = GeneralUtility::makeInstance($className, $defaultHashOptions);
|
||||
} else {
|
||||
$hashInstance = GeneralUtility::makeInstance($className);
|
||||
}
|
||||
if (!$hashInstance instanceof PasswordHashInterface) {
|
||||
throw new \LogicException('Class ' . $className . ' does not implement PasswordHashInterface', 1533818569);
|
||||
}
|
||||
if ($hashInstance->isAvailable() && $hashInstance->isValidSaltedPW($hash)) {
|
||||
return $hashInstance;
|
||||
}
|
||||
}
|
||||
// Do not add the hash to the exception to prevent information disclosure
|
||||
throw new InvalidPasswordHashException(
|
||||
'No implementation found to handle given hash. This happens if the stored hash uses a'
|
||||
. ' mechanism not supported by current server. Follow the documentation link to fix this issue.',
|
||||
1533818591
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine configured default hash method and return an instance of the class representing it.
|
||||
*
|
||||
* @param string $mode 'FE' for frontend users, 'BE' for backend users
|
||||
* @return PasswordHashInterface Class instance that is configured as default hash method
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \LogicException
|
||||
* @throws InvalidPasswordHashException If configuration is broken
|
||||
*/
|
||||
public function getDefaultHashInstance(string $mode): PasswordHashInterface
|
||||
{
|
||||
if ($mode !== 'FE' && $mode !== 'BE') {
|
||||
throw new \InvalidArgumentException('Mode must be either \'FE\' or \'BE\', ' . $mode . ' given.', 1533820041);
|
||||
}
|
||||
|
||||
if (empty($GLOBALS['TYPO3_CONF_VARS'][$mode]['passwordHashing']['className'])
|
||||
|| !isset($GLOBALS['TYPO3_CONF_VARS'][$mode]['passwordHashing']['options'])
|
||||
|| !is_array($GLOBALS['TYPO3_CONF_VARS'][$mode]['passwordHashing']['options'])
|
||||
) {
|
||||
throw new \LogicException(
|
||||
'passwordHashing configuration of ' . $mode . ' broken',
|
||||
1533950622
|
||||
);
|
||||
}
|
||||
|
||||
$defaultHashClassName = $GLOBALS['TYPO3_CONF_VARS'][$mode]['passwordHashing']['className'];
|
||||
$defaultHashOptions = $GLOBALS['TYPO3_CONF_VARS'][$mode]['passwordHashing']['options'];
|
||||
$availableHashClasses = static::getRegisteredSaltedHashingMethods();
|
||||
|
||||
if (!in_array($defaultHashClassName, $availableHashClasses, true)) {
|
||||
throw new InvalidPasswordHashException(
|
||||
'Configured default hash method ' . $defaultHashClassName . ' is not registered',
|
||||
1533820194
|
||||
);
|
||||
}
|
||||
$hashInstance = GeneralUtility::makeInstance($defaultHashClassName, $defaultHashOptions);
|
||||
if (!$hashInstance instanceof PasswordHashInterface) {
|
||||
throw new \LogicException(
|
||||
'Configured default hash method ' . $defaultHashClassName . ' is not an instance of PasswordHashInterface',
|
||||
1533820281
|
||||
);
|
||||
}
|
||||
if (!$hashInstance->isAvailable()) {
|
||||
throw new InvalidPasswordHashException(
|
||||
'Configured default hash method ' . $defaultHashClassName . ' is not available. If'
|
||||
. ' the instance has just been upgraded, please log in to the standalone install tool'
|
||||
. ' at ?__typo3_install to fix this. Follow the documentation link for more details.',
|
||||
1533822084
|
||||
);
|
||||
}
|
||||
return $hashInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of all registered hashing methods. Used eg. in
|
||||
* extension configuration to select the default hashing method.
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public static function getRegisteredSaltedHashingMethods(): array
|
||||
{
|
||||
$saltMethods = $GLOBALS['TYPO3_CONF_VARS']['SYS']['availablePasswordHashAlgorithms'];
|
||||
if (!is_array($saltMethods) || empty($saltMethods)) {
|
||||
throw new \RuntimeException('No password hash methods configured', 1533948733);
|
||||
}
|
||||
return $saltMethods;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Crypto\PasswordHashing;
|
||||
|
||||
/**
|
||||
* Interface with public methods needed to be implemented
|
||||
* in a salting hashing class.
|
||||
*/
|
||||
interface PasswordHashInterface
|
||||
{
|
||||
/**
|
||||
* Method checks if a given plaintext password is correct by comparing it with
|
||||
* a given salted hashed password.
|
||||
*
|
||||
* @param string $plainPW plain-text password to compare with salted hash
|
||||
* @param string $saltedHashPW Salted hash to compare plain-text password with
|
||||
* @return bool TRUE, if plaintext password is correct, otherwise FALSE
|
||||
*/
|
||||
public function checkPassword(string $plainPW, string $saltedHashPW): bool;
|
||||
|
||||
/**
|
||||
* Returns whether all prerequisites for the hashing methods are matched
|
||||
*
|
||||
* @return bool Method available
|
||||
*/
|
||||
public function isAvailable(): bool;
|
||||
|
||||
/**
|
||||
* Method creates a hash for a given plaintext password
|
||||
*
|
||||
* @param string $password Plaintext password to create a hash from
|
||||
* @return string|null Hashed password or null on empty password
|
||||
*/
|
||||
public function getHashedPassword(string $password);
|
||||
|
||||
/**
|
||||
* Checks whether a user's hashed password needs to be replaced with a new hash.
|
||||
*
|
||||
* This is typically called during the login process when the plain text
|
||||
* password is available. A new hash is needed when the desired iteration
|
||||
* count has changed through a change in the variable $hashCount or HASH_COUNT.
|
||||
*
|
||||
* @param string $passString Salted hash to check if it needs an update
|
||||
* @return bool TRUE if salted hash needs an update, otherwise FALSE
|
||||
*/
|
||||
public function isHashUpdateNeeded(string $passString): bool;
|
||||
|
||||
/**
|
||||
* Method determines if a given string is a valid salted hashed password.
|
||||
*
|
||||
* @param string $saltedPW String to check
|
||||
* @return bool TRUE if it's valid salted hashed password, otherwise FALSE
|
||||
*/
|
||||
public function isValidSaltedPW(string $saltedPW): bool;
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Crypto\PasswordHashing;
|
||||
|
||||
use TYPO3\CMS\Core\Crypto\Random;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Class that implements PBKDF2 salted hashing based on PHP's
|
||||
* hash_pbkdf2() function.
|
||||
*/
|
||||
class Pbkdf2PasswordHash implements PasswordHashInterface
|
||||
{
|
||||
/**
|
||||
* Prefix for the password hash.
|
||||
*/
|
||||
protected const PREFIX = '$pbkdf2-sha256$';
|
||||
|
||||
/**
|
||||
* @var array The default log2 number of iterations for password stretching.
|
||||
*/
|
||||
protected $options = [
|
||||
'hash_count' => 25000,
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor sets options if given
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$newOptions = $this->options;
|
||||
if (isset($options['hash_count'])) {
|
||||
if ((int)$options['hash_count'] < 1000 || (int)$options['hash_count'] > 10000000) {
|
||||
throw new \InvalidArgumentException(
|
||||
'hash_count must not be lower than 1000 or bigger than 10000000',
|
||||
1533903544
|
||||
);
|
||||
}
|
||||
$newOptions['hash_count'] = (int)$options['hash_count'];
|
||||
}
|
||||
$this->options = $newOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method checks if a given plaintext password is correct by comparing it with
|
||||
* a given salted hashed password.
|
||||
*
|
||||
* @param string $plainPW plain-text password to compare with salted hash
|
||||
* @param string $saltedHashPW salted hash to compare plain-text password with
|
||||
* @return bool TRUE, if plain-text password matches the salted hash, otherwise FALSE
|
||||
*/
|
||||
public function checkPassword(string $plainPW, string $saltedHashPW): bool
|
||||
{
|
||||
return $this->isValidSalt($saltedHashPW) && hash_equals((string)$this->getHashedPasswordInternal($plainPW, $saltedHashPW), $saltedHashPW);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether all prerequisites for the hashing methods are matched
|
||||
*
|
||||
* @return bool Method available
|
||||
*/
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getHashedPassword(string $password): ?string
|
||||
{
|
||||
return $this->getHashedPasswordInternal($password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method determines if a given string is a valid salted hashed password.
|
||||
*
|
||||
* @param string $saltedPW String to check
|
||||
* @return bool TRUE if it's valid salted hashed password, otherwise FALSE
|
||||
*/
|
||||
public function isValidSaltedPW(string $saltedPW): bool
|
||||
{
|
||||
$isValid = !strncmp(self::PREFIX, $saltedPW, strlen(self::PREFIX));
|
||||
if ($isValid) {
|
||||
$isValid = $this->isValidSalt($saltedPW);
|
||||
}
|
||||
return $isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a user's hashed password needs to be replaced with a new hash.
|
||||
*
|
||||
* This is typically called during the login process when the plain text
|
||||
* password is available. A new hash is needed when the desired iteration
|
||||
* count has changed through a change in the variable $this->options['hashCount'].
|
||||
*
|
||||
* @param string $saltedPW Salted hash to check if it needs an update
|
||||
* @return bool TRUE if salted hash needs an update, otherwise FALSE
|
||||
*/
|
||||
public function isHashUpdateNeeded(string $saltedPW): bool
|
||||
{
|
||||
// Check whether this was an updated password.
|
||||
if (strncmp($saltedPW, self::PREFIX, strlen(self::PREFIX)) || !$this->isValidSalt($saltedPW)) {
|
||||
return true;
|
||||
}
|
||||
// Check whether the iteration count used differs from the standard number.
|
||||
$iterationCount = $this->getIterationCount($saltedPW);
|
||||
return $iterationCount !== null && $iterationCount < $this->options['hash_count'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the log2 iteration count from a stored hash or setting string.
|
||||
*
|
||||
* @param string $setting Complete hash or a hash's setting string or to get log2 iteration count from
|
||||
* @return int|null Used hashcount for given hash string
|
||||
*/
|
||||
protected function getIterationCount(string $setting)
|
||||
{
|
||||
$iterationCount = null;
|
||||
$setting = substr($setting, strlen(self::PREFIX));
|
||||
$firstSplitPos = strpos($setting, '$');
|
||||
// Hashcount existing
|
||||
if ($firstSplitPos !== false
|
||||
&& $firstSplitPos <= strlen((string)10000000)
|
||||
&& is_numeric(substr($setting, 0, $firstSplitPos))
|
||||
) {
|
||||
$iterationCount = (int)substr($setting, 0, $firstSplitPos);
|
||||
}
|
||||
return $iterationCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method creates a salted hash for a given plaintext password
|
||||
*
|
||||
* @param string $password plaintext password to create a salted hash from
|
||||
* @param string $salt Optional custom salt with setting to use
|
||||
* @return string|null Salted hashed password
|
||||
*/
|
||||
protected function getHashedPasswordInternal(string $password, ?string $salt = null)
|
||||
{
|
||||
$saltedPW = null;
|
||||
if ($password !== '') {
|
||||
$hashCount = $this->options['hash_count'];
|
||||
if (empty($salt) || !$this->isValidSalt($salt)) {
|
||||
$salt = $this->getGeneratedSalt();
|
||||
} else {
|
||||
$hashCount = $this->getIterationCount($salt);
|
||||
$salt = $this->getStoredSalt($salt);
|
||||
}
|
||||
$hash = hash_pbkdf2('sha256', $password, $salt, $hashCount, 0, true);
|
||||
$saltWithSettings = $salt;
|
||||
// salt without setting
|
||||
if (strlen($salt) === 16) {
|
||||
$saltWithSettings = self::PREFIX . sprintf('%02u', $hashCount) . '$' . $this->base64Encode($salt, 16);
|
||||
}
|
||||
$saltedPW = $saltWithSettings . '$' . $this->base64Encode($hash, strlen($hash));
|
||||
}
|
||||
return $saltedPW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random base 64-encoded salt prefixed and suffixed with settings for the hash.
|
||||
*
|
||||
* Proper use of salts may defeat a number of attacks, including:
|
||||
* - The ability to try candidate passwords against multiple hashes at once.
|
||||
* - The ability to use pre-hashed lists of candidate passwords.
|
||||
* - The ability to determine whether two users have the same (or different)
|
||||
* password without actually having to guess one of the passwords.
|
||||
*
|
||||
* @return string A character string containing settings and a random salt
|
||||
*/
|
||||
protected function getGeneratedSalt(): string
|
||||
{
|
||||
return GeneralUtility::makeInstance(Random::class)->generateRandomBytes(16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the salt out of a salt string including settings. If the salt does not include settings
|
||||
* it is returned unmodified.
|
||||
*/
|
||||
protected function getStoredSalt(string $salt): string
|
||||
{
|
||||
if (!strncmp('$', $salt, 1)) {
|
||||
if (!strncmp(self::PREFIX, $salt, strlen(self::PREFIX))) {
|
||||
$saltParts = GeneralUtility::trimExplode('$', $salt, true);
|
||||
$salt = $saltParts[2];
|
||||
}
|
||||
}
|
||||
return $this->base64Decode($salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string for mapping an int to the corresponding base 64 character.
|
||||
*
|
||||
* @return string String for mapping an int to the corresponding base 64 character
|
||||
*/
|
||||
protected function getItoa64(): string
|
||||
{
|
||||
return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
}
|
||||
|
||||
/**
|
||||
* Method determines if a given string is a valid salt.
|
||||
*
|
||||
* @param string $salt String to check
|
||||
* @return bool TRUE if it's valid salt, otherwise FALSE
|
||||
*/
|
||||
protected function isValidSalt(string $salt): bool
|
||||
{
|
||||
$isValid = ($skip = false);
|
||||
$reqLenBase64 = $this->getLengthBase64FromBytes(16);
|
||||
if (strlen($salt) >= $reqLenBase64) {
|
||||
// Salt with prefixed setting
|
||||
if (!strncmp('$', $salt, 1)) {
|
||||
if (!strncmp(self::PREFIX, $salt, strlen(self::PREFIX))) {
|
||||
$isValid = true;
|
||||
$salt = substr($salt, (int)strrpos($salt, '$') + 1);
|
||||
} else {
|
||||
$skip = true;
|
||||
}
|
||||
}
|
||||
// Checking base64 characters
|
||||
if (!$skip && strlen($salt) >= $reqLenBase64) {
|
||||
if (preg_match('/^[' . preg_quote($this->getItoa64(), '/') . ']{' . $reqLenBase64 . ',' . $reqLenBase64 . '}$/', substr($salt, 0, $reqLenBase64))) {
|
||||
$isValid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method determines required length of base64 characters for a given
|
||||
* length of a byte string.
|
||||
*
|
||||
* @param int $byteLength Length of bytes to calculate in base64 chars
|
||||
* @return int Required length of base64 characters
|
||||
*/
|
||||
protected function getLengthBase64FromBytes(int $byteLength): int
|
||||
{
|
||||
// Calculates bytes in bits in base64
|
||||
return (int)ceil($byteLength * 8 / 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapted version of base64_encoding for compatibility with python passlib. The output of this function is
|
||||
* is identical to base64_encode, except that it uses . instead of +, and omits trailing padding = and whitespace.
|
||||
*
|
||||
* @param string $input The string containing bytes to encode.
|
||||
* @param int $count The number of characters (bytes) to encode.
|
||||
* @return string Encoded string
|
||||
*/
|
||||
protected function base64Encode(string $input, int $count): string
|
||||
{
|
||||
$input = substr($input, 0, $count);
|
||||
return rtrim(str_replace('+', '.', base64_encode($input)), " =\r\n\t\0\x0B");
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapted version of base64_encoding for compatibility with python passlib. The output of this function is
|
||||
* is identical to base64_encode, except that it uses . instead of +, and omits trailing padding = and whitespace.
|
||||
*/
|
||||
protected function base64Decode(string $value): string
|
||||
{
|
||||
return base64_decode(str_replace('.', '+', $value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Crypto\PasswordHashing;
|
||||
|
||||
use TYPO3\CMS\Core\Crypto\Random;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Class that implements PHPass salted hashing based on Drupal's
|
||||
* modified Openwall implementation.
|
||||
*
|
||||
* Derived from Drupal CMS
|
||||
* original license: GNU General Public License (GPL)
|
||||
*
|
||||
* PHPass should work on every system.
|
||||
* @see http://drupal.org/node/29706/
|
||||
* @see http://www.openwall.com/phpass/
|
||||
*/
|
||||
class PhpassPasswordHash implements PasswordHashInterface
|
||||
{
|
||||
/**
|
||||
* Prefix for the password hash.
|
||||
*/
|
||||
protected const PREFIX = '$P$';
|
||||
|
||||
/**
|
||||
* @var array The default log2 number of iterations for password stretching.
|
||||
*/
|
||||
protected $options = [
|
||||
'hash_count' => 14,
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor sets options if given
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$newOptions = $this->options;
|
||||
if (isset($options['hash_count'])) {
|
||||
if ((int)$options['hash_count'] < 7 || (int)$options['hash_count'] > 24) {
|
||||
throw new \InvalidArgumentException(
|
||||
'hash_count must not be lower than 7 or bigger than 24',
|
||||
1533940454
|
||||
);
|
||||
}
|
||||
$newOptions['hash_count'] = (int)$options['hash_count'];
|
||||
}
|
||||
$this->options = $newOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method checks if a given plaintext password is correct by comparing it with
|
||||
* a given salted hashed password.
|
||||
*
|
||||
* @param string $plainPW Plain-text password to compare with salted hash
|
||||
* @param string $saltedHashPW Salted hash to compare plain-text password with
|
||||
* @return bool TRUE, if plain-text password matches the salted hash, otherwise FALSE
|
||||
*/
|
||||
public function checkPassword(string $plainPW, string $saltedHashPW): bool
|
||||
{
|
||||
$hash = $this->cryptPassword($plainPW, $saltedHashPW);
|
||||
return $hash && hash_equals($hash, $saltedHashPW);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether all prerequisites for the hashing methods are matched
|
||||
*
|
||||
* @return bool Method available
|
||||
*/
|
||||
public function isAvailable(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getHashedPassword(string $password): ?string
|
||||
{
|
||||
$saltedPW = null;
|
||||
if (!empty($password)) {
|
||||
$salt = $this->getGeneratedSalt();
|
||||
$saltedPW = $this->cryptPassword($password, $this->applySettingsToSalt($salt));
|
||||
}
|
||||
return $saltedPW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a user's hashed password needs to be replaced with a new hash.
|
||||
*
|
||||
* This is typically called during the login process when the plain text
|
||||
* password is available. A new hash is needed when the desired iteration
|
||||
* count has changed through a change in the variable $hashCount or HASH_COUNT.
|
||||
*
|
||||
* @param string $passString Salted hash to check if it needs an update
|
||||
* @return bool TRUE if salted hash needs an update, otherwise FALSE
|
||||
*/
|
||||
public function isHashUpdateNeeded(string $passString): bool
|
||||
{
|
||||
// Check whether this was an updated password.
|
||||
if (strncmp($passString, '$P$', 3) || strlen($passString) != 34) {
|
||||
return true;
|
||||
}
|
||||
// Check whether the iteration count used differs from the standard number.
|
||||
return $this->getCountLog2($passString) < $this->options['hash_count'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Method determines if a given string is a valid salted hashed password.
|
||||
*
|
||||
* @param string $saltedPW String to check
|
||||
* @return bool TRUE if it's valid salted hashed password, otherwise FALSE
|
||||
*/
|
||||
public function isValidSaltedPW(string $saltedPW): bool
|
||||
{
|
||||
$isValid = !strncmp(self::PREFIX, $saltedPW, strlen(self::PREFIX));
|
||||
if ($isValid) {
|
||||
$isValid = $this->isValidSalt($saltedPW);
|
||||
}
|
||||
return $isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method applies settings (prefix, hash count) to a salt.
|
||||
*
|
||||
* @param string $salt A salt to apply setting to
|
||||
* @return string Salt with setting
|
||||
*/
|
||||
protected function applySettingsToSalt(string $salt): string
|
||||
{
|
||||
$saltWithSettings = $salt;
|
||||
$reqLenBase64 = $this->getLengthBase64FromBytes(6);
|
||||
// Salt without setting
|
||||
if (strlen($salt) == $reqLenBase64) {
|
||||
// We encode the final log2 iteration count in base 64.
|
||||
$itoa64 = $this->getItoa64();
|
||||
$saltWithSettings = self::PREFIX . $itoa64[$this->options['hash_count']];
|
||||
$saltWithSettings .= $salt;
|
||||
}
|
||||
return $saltWithSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashes a password using a secure stretched hash.
|
||||
*
|
||||
* By using a salt and repeated hashing the password is "stretched". Its
|
||||
* security is increased because it becomes much more computationally costly
|
||||
* for an attacker to try to break the hash by brute-force computation of the
|
||||
* hashes of a large number of plain-text words or strings to find a match.
|
||||
*
|
||||
* @param string $password Plain-text password to hash
|
||||
* @param string $setting An existing hash or the output of getGeneratedSalt()
|
||||
* @return mixed A string containing the hashed password (and salt)
|
||||
*/
|
||||
protected function cryptPassword(string $password, string $setting)
|
||||
{
|
||||
$saltedPW = null;
|
||||
$reqLenBase64 = $this->getLengthBase64FromBytes(6);
|
||||
// Retrieving settings with salt
|
||||
$setting = substr($setting, 0, strlen(self::PREFIX) + 1 + $reqLenBase64);
|
||||
$count_log2 = $this->getCountLog2($setting);
|
||||
// Hashes may be imported from elsewhere, so we allow != HASH_COUNT
|
||||
if ($count_log2 >= 7 && $count_log2 <= 24) {
|
||||
$salt = substr($setting, strlen(self::PREFIX) + 1, $reqLenBase64);
|
||||
// We must use md5() or sha1() here since they are the only cryptographic
|
||||
// primitives always available in PHP 5. To implement our own low-level
|
||||
// cryptographic function 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).
|
||||
$count = 1 << $count_log2;
|
||||
$hash = md5($salt . $password, true);
|
||||
do {
|
||||
$hash = md5($hash . $password, true);
|
||||
} while (--$count);
|
||||
$saltedPW = $setting . $this->base64Encode($hash, 16);
|
||||
// base64Encode() of a 16 byte MD5 will always be 22 characters.
|
||||
return strlen($saltedPW) == 34 ? $saltedPW : false;
|
||||
}
|
||||
return $saltedPW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the log2 iteration count from a stored hash or setting string.
|
||||
*
|
||||
* @param string $setting Complete hash or a hash's setting string or to get log2 iteration count from
|
||||
* @return int Used hashcount for given hash string
|
||||
*/
|
||||
protected function getCountLog2(string $setting): int
|
||||
{
|
||||
return strpos($this->getItoa64(), $setting[strlen(self::PREFIX)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random base 64-encoded salt prefixed and suffixed with settings for the hash.
|
||||
*
|
||||
* Proper use of salts may defeat a number of attacks, including:
|
||||
* - The ability to try candidate passwords against multiple hashes at once.
|
||||
* - The ability to use pre-hashed lists of candidate passwords.
|
||||
* - The ability to determine whether two users have the same (or different)
|
||||
* password without actually having to guess one of the passwords.
|
||||
*
|
||||
* @return string A character string containing settings and a random salt
|
||||
*/
|
||||
protected function getGeneratedSalt(): string
|
||||
{
|
||||
$randomBytes = GeneralUtility::makeInstance(Random::class)->generateRandomBytes(6);
|
||||
return $this->base64Encode($randomBytes, 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string for mapping an int to the corresponding base 64 character.
|
||||
*
|
||||
* @return string String for mapping an int to the corresponding base 64 character
|
||||
*/
|
||||
protected function getItoa64(): string
|
||||
{
|
||||
return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
}
|
||||
|
||||
/**
|
||||
* Method determines if a given string is a valid salt.
|
||||
*
|
||||
* @param string $salt String to check
|
||||
* @return bool TRUE if it's valid salt, otherwise FALSE
|
||||
*/
|
||||
protected function isValidSalt(string $salt): bool
|
||||
{
|
||||
$isValid = ($skip = false);
|
||||
$reqLenBase64 = $this->getLengthBase64FromBytes(6);
|
||||
if (strlen($salt) >= $reqLenBase64) {
|
||||
// Salt with prefixed setting
|
||||
if (!strncmp('$', $salt, 1)) {
|
||||
if (!strncmp(self::PREFIX, $salt, strlen(self::PREFIX))) {
|
||||
$isValid = true;
|
||||
$salt = substr($salt, (int)strrpos($salt, '$') + 2);
|
||||
} else {
|
||||
$skip = true;
|
||||
}
|
||||
}
|
||||
// Checking base64 characters
|
||||
if (!$skip && strlen($salt) >= $reqLenBase64) {
|
||||
if (preg_match('/^[' . preg_quote($this->getItoa64(), '/') . ']{' . $reqLenBase64 . ',' . $reqLenBase64 . '}$/', substr($salt, 0, $reqLenBase64))) {
|
||||
$isValid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes bytes into printable base 64 using the *nix standard from crypt().
|
||||
*
|
||||
* @param string $input The string containing bytes to encode.
|
||||
* @param int $count The number of characters (bytes) to encode.
|
||||
* @return string Encoded string
|
||||
*/
|
||||
protected function base64Encode(string $input, int $count): string
|
||||
{
|
||||
$output = '';
|
||||
$i = 0;
|
||||
$itoa64 = $this->getItoa64();
|
||||
do {
|
||||
$value = ord($input[$i++]);
|
||||
$output .= $itoa64[$value & 63];
|
||||
if ($i < $count) {
|
||||
$value |= ord($input[$i]) << 8;
|
||||
}
|
||||
$output .= $itoa64[$value >> 6 & 63];
|
||||
if ($i++ >= $count) {
|
||||
break;
|
||||
}
|
||||
if ($i < $count) {
|
||||
$value |= ord($input[$i]) << 16;
|
||||
}
|
||||
$output .= $itoa64[$value >> 12 & 63];
|
||||
if ($i++ >= $count) {
|
||||
break;
|
||||
}
|
||||
$output .= $itoa64[$value >> 18 & 63];
|
||||
} while ($i < $count);
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method determines required length of base64 characters for a given
|
||||
* length of a byte string.
|
||||
*
|
||||
* @param int $byteLength Length of bytes to calculate in base64 chars
|
||||
* @return int Required length of base64 characters
|
||||
*/
|
||||
protected function getLengthBase64FromBytes(int $byteLength): int
|
||||
{
|
||||
// Calculates bytes in bits in base64
|
||||
return (int)ceil($byteLength * 8 / 6);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user