TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -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 {}
|
||||
@@ -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 {}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user