TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
@@ -0,0 +1,20 @@
<?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\Cipher;
class CipherDecryptionFailedException extends CipherException {}
+22
View File
@@ -0,0 +1,22 @@
<?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\Cipher;
use TYPO3\CMS\Core\Exception;
class CipherException extends Exception {}
+65
View File
@@ -0,0 +1,65 @@
<?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\Cipher;
/**
* Provides encryption and decryption based on XChaCha20-Poly1305.
*/
final readonly class CipherService
{
/**
* Encrypts the provided plain text using a shared key and additional authenticated data.
*
* @param string $plainText The plain text to be encrypted.
* @param SharedKey $key The shared key used for encryption.
* @param string $additionalData Optional additional authenticated data that will be included in the encryption.
*/
public function encrypt(string $plainText, SharedKey $key, string $additionalData = ''): CipherValue
{
$nonce = random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
$cipher = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt(
$plainText,
$additionalData,
$nonce,
$key->value
);
return new CipherValue($nonce, $cipher);
}
/**
* Decrypts the provided cipher value using a shared key and optional additional authenticated data.
*
* @param CipherValue $cipherValue The cipher value containing encrypted data and nonce.
* @param SharedKey $key The shared key used for decryption.
* @param string $additionalData Optional additional authenticated data that was included during encryption.
* @throws CipherDecryptionFailedException If decryption fails or the integrity check is invalid.
*/
public function decrypt(CipherValue $cipherValue, SharedKey $key, string $additionalData = ''): string
{
$result = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt(
$cipherValue->cipher,
$additionalData,
$cipherValue->nonce,
$key->value
);
if ($result === false) {
throw new CipherDecryptionFailedException('Cipher could not be decrypted', 1762465681);
}
return $result;
}
}
+59
View File
@@ -0,0 +1,59 @@
<?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\Cipher;
use TYPO3\CMS\Core\Utility\StringUtility;
final readonly class CipherValue implements \Stringable
{
public static function fromSerialized(string $value): self
{
$data = json_decode(StringUtility::base64urlDecode($value) ?: '', true);
$nonce = StringUtility::base64urlDecode($data['nonce'] ?? '');
$cipher = StringUtility::base64urlDecode($data['cipher'] ?? '');
if (empty($nonce) || empty($cipher)) {
throw new CipherException('Incorrect encoded message format', 1762450821);
}
return new self($nonce, $cipher);
}
public function __construct(public string $nonce, public string $cipher)
{
if (strlen($nonce) !== SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES) {
throw new CipherException('Incorrect nonce byte length', 1762450477);
}
}
public function __toString(): string
{
return $this->encode();
}
public function encode(): string
{
$data = [
'nonce' => StringUtility::base64urlEncode($this->nonce),
'cipher' => StringUtility::base64urlEncode($this->cipher),
];
try {
return StringUtility::base64urlEncode(json_encode($data, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR));
} catch (\JsonException) {
throw new CipherException('Failed to encode cipher value', 1763068727);
}
}
}
+87
View File
@@ -0,0 +1,87 @@
<?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\Cipher;
final readonly class KeyFactory
{
/**
* Derives a 32-byte key, based on the existing encryptionKey.
* The key is supposed to be used in a symmetric XChaCha20-Poly1305 ciphering.
*
* @param string $seed (non-secret) value, used to build an 8-byte context (e.g. classname)
* @param int $subKeyId variation to the resulting derived key (value from 0 to PHP_INT_MAX)
* @throws CipherException
* @throws \SodiumException
*/
public function deriveSharedKeyFromEncryptionKey(string $seed, int $subKeyId = 1): SharedKey
{
$key = $this->adjustKeyLength($this->resolveEncryptionKey());
// context must be exactly 8 bytes
$context = hash('xxh64', $seed, true);
return new SharedKey(
sodium_crypto_kdf_derive_from_key(
SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES,
$subKeyId,
$context,
$key
)
);
}
/**
* Creates a SharedKey instance from a given key.
*
* @throws CipherException
*/
public function createSharedKeyFromString(#[\SensitiveParameter] string $key): SharedKey
{
return new SharedKey($this->adjustKeyLength($key));
}
/**
* Generates a SharedKey instance from a random key.
*
* @throws CipherException
* @throws \Random\RandomException
*/
public function generateSharedKey(): SharedKey
{
return new SharedKey(random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES));
}
/**
* Ensures to use a 32-byte key for XChaCha20-Poly1305 encryption
* (having a length of `SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES`).
*/
private function adjustKeyLength(#[\SensitiveParameter] $key): string
{
if (strlen($key) === SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES) {
return $key;
}
return hash('sha3-256', $key, true);
}
private function resolveEncryptionKey(): string
{
$key = $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] ?? null;
if (!is_string($key) || $key === '') {
throw new CipherException('No encryption key configured', 1762897148);
}
return $key;
}
}
+38
View File
@@ -0,0 +1,38 @@
<?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\Cipher;
/**
* Holds the secret key to be used with XChaCha20-Poly1305
* (and basically other Sodium-based algorithms), having 32 bytes.
*/
final readonly class SharedKey
{
public function __construct(#[\SensitiveParameter] public string $value)
{
if (strlen($this->value) !== SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES) {
throw new CipherException(
sprintf(
'Length of key value must be %d bytes',
SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES
),
1762508248
);
}
}
}
+77
View File
@@ -0,0 +1,77 @@
<?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;
/**
* Enum to be used for hashing functions.
*/
enum HashAlgo: string
{
// SHA1 is still acceptable for HMAC, but not recommended
case SHA1 = 'sha1';
case SHA256 = 'sha256';
case SHA384 = 'sha384';
case SHA512 = 'sha512';
// preferring Keccak SHA3 over SHA2
case SHA3_256 = 'sha3-256';
case SHA3_384 = 'sha3-384';
case SHA3_512 = 'sha3-512';
private const ALLOWED_HMAC_ALGOS = [
self::SHA1,
self::SHA256,
self::SHA384,
self::SHA512,
self::SHA3_256,
self::SHA3_384,
self::SHA3_512,
];
private const BINARY_LENGTHS = [
self::SHA1->value => 20,
self::SHA256->value => 32,
self::SHA384->value => 48,
self::SHA512->value => 64,
self::SHA3_256->value => 32,
self::SHA3_384->value => 48,
self::SHA3_512->value => 64,
];
public function isAllowedForHmac(): bool
{
return in_array($this, self::ALLOWED_HMAC_ALGOS, true);
}
/**
* @param bool $binary whether to return binary or hex length
*/
public function length(bool $binary = false): int
{
return self::BINARY_LENGTHS[$this->value] * ($binary ? 1 : 2);
}
public function equals(string $other): bool
{
return strtolower($this->value) === strtolower($other);
}
public function hash(string $data, bool $binary = false): string
{
return hash($this->value, $data, $binary);
}
}
+96
View File
@@ -0,0 +1,96 @@
<?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;
use TYPO3\CMS\Core\Exception\Crypto\InvalidHashStringException;
/**
* A hash service to generate and validate SHA-1 hashes.
*/
final readonly class HashService
{
/**
* Returns a proper HMAC with a length of 40 (HMAC-SHA-1) on a given input string, additional secret
* and the secret TYPO3 encryption key.
*
* @param non-empty-string $additionalSecret
*
* @return non-empty-string
*/
public function hmac(string $input, string $additionalSecret, HashAlgo $algo = HashAlgo::SHA1): string
{
if ($additionalSecret === '') {
throw new \LogicException('The ' . __METHOD__ . ' function requires a non-empty additional secret.', 1704453167);
}
if (!$algo->isAllowedForHmac()) {
throw new \LogicException('The ' . __METHOD__ . ' function does not allow "' . $algo->value . '".', 1763812644);
}
$secret = $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] . $additionalSecret;
return hash_hmac($algo->value, $input, $secret);
}
/**
* Appends a hash (HMAC) to a given string and additional secret and returns the result
*
* @param non-empty-string $additionalSecret
*
* @return non-empty-string
*/
public function appendHmac(string $string, string $additionalSecret, HashAlgo $algo = HashAlgo::SHA1): string
{
return $string . $this->hmac($string, $additionalSecret, $algo);
}
/**
* Returns, if a string $string and $additionalSecret matches the HMAC given by $hash.
*
* @param non-empty-string $additionalSecret
*/
public function validateHmac(string $string, string $additionalSecret, string $hmac, HashAlgo $algo = HashAlgo::SHA1): bool
{
return hash_equals($this->hmac($string, $additionalSecret, $algo), $hmac);
}
/**
* Tests if the last 40 characters of a given string $string and $additionalSecret matches the HMAC of
* the rest of the string and, if true, returns the string without the HMAC. In case of an invalid HMAC string
* an exception is thrown.
*
* @param non-empty-string $string
* @param non-empty-string $additionalSecret
*/
public function validateAndStripHmac(string $string, string $additionalSecret, HashAlgo $algo = HashAlgo::SHA1): string
{
$hashLength = $algo->length();
if (strlen($string) < $hashLength) {
throw new InvalidHashStringException(
sprintf(
'A hashed string must contain at least %d characters, the given string was only %d characters long.',
$hashLength,
strlen($string)
),
1704454152
);
}
$stringWithoutHmac = substr($string, 0, -$hashLength);
if ($this->validateHmac($stringWithoutHmac, $additionalSecret, substr($string, -$hashLength), $algo) !== true) {
throw new InvalidHashStringException('The given string was not appended with a valid HMAC.', 1704454157);
}
return $stringWithoutHmac;
}
}
@@ -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);
}
}
+132
View File
@@ -0,0 +1,132 @@
<?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;
use Random\Randomizer;
use TYPO3\CMS\Core\Exception\InvalidPasswordRulesException;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Crypto safe pseudo-random value generation
*/
readonly class Random
{
private const int DEFAULT_PASSWORD_LENGTH = 16;
private const string LOWERCASE_CHARACTERS = 'abcdefghijklmnopqrstuvwxyz';
private const string UPPERCASE_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
private const string SPECIAL_CHARACTERS = '!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~';
private const string DIGIT_CHARACTERS = '1234567890';
/**
* Generates cryptographic secure pseudo-random bytes
*/
public function generateRandomBytes(int $length): string
{
return random_bytes($length);
}
/**
* Generates cryptographic secure pseudo-random integers
*/
public function generateRandomInteger(int $min, int $max): int
{
return random_int($min, $max);
}
/**
* Generates cryptographic secure pseudo-random hex string
*/
public function generateRandomHexString(int $length): string
{
return substr(bin2hex($this->generateRandomBytes((int)(($length + 1) / 2))), 0, $length);
}
/**
* Generates cryptographic secure pseudo-random base64 string
*/
public function generateRandomBase64String(int $length): string
{
return substr(StringUtility::base64urlEncode($this->generateRandomBytes((int)ceil(($length / 4) * 3))), 0, $length);
}
/**
* Generates cryptographic secure pseudo-random password based on given password rules
*
* @internal Only to be used within TYPO3. Might change in the future.
*/
public function generateRandomPassword(array $passwordRules): string
{
$passwordLength = (int)($passwordRules['length'] ?? self::DEFAULT_PASSWORD_LENGTH);
if ($passwordLength < 8) {
throw new InvalidPasswordRulesException(
'Password rules are invalid. Length must be at least 8.',
1667557900
);
}
$password = '';
if ($passwordRules['random'] ?? false) {
$password = match ((string)$passwordRules['random']) {
'hex' => $this->generateRandomHexString($passwordLength),
'base64' => $this->generateRandomBase64String($passwordLength),
default => throw new InvalidPasswordRulesException('Invalid value for special password rule \'random\'. Valid options are: \'hex\' and \'base64\'', 1667557901),
};
} else {
$characters = [];
$characterSets = [];
if (filter_var($passwordRules['lowerCaseCharacters'] ?? true, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)) {
$characters = array_merge($characters, str_split(self::LOWERCASE_CHARACTERS));
$characterSets[] = self::LOWERCASE_CHARACTERS;
}
if (filter_var($passwordRules['upperCaseCharacters'] ?? true, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)) {
$characters = array_merge($characters, str_split(self::UPPERCASE_CHARACTERS));
$characterSets[] = self::UPPERCASE_CHARACTERS;
}
if (filter_var($passwordRules['digitCharacters'] ?? true, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)) {
$characters = array_merge($characters, str_split(self::DIGIT_CHARACTERS));
$characterSets[] = self::DIGIT_CHARACTERS;
}
if (filter_var($passwordRules['specialCharacters'] ?? false, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)) {
$characters = array_merge($characters, str_split(self::SPECIAL_CHARACTERS));
$characterSets[] = self::SPECIAL_CHARACTERS;
}
if ($characterSets === []) {
throw new InvalidPasswordRulesException(
'Password rules are invalid. At least one character set must be allowed.',
1667557902
);
}
// enforces that at least one character matches the requirements
foreach ($characterSets as $characterSet) {
$password .= $characterSet[random_int(0, strlen($characterSet) - 1)];
}
$charactersCount = count($characters);
for ($i = 0; $i < $passwordLength - count($characterSets); $i++) {
$password .= $characters[random_int(0, $charactersCount - 1)];
}
$password = (new Randomizer())->shuffleBytes($password);
}
return $password;
}
}