From 893ee207091871aef0d79afdd5183ebb12080f55 Mon Sep 17 00:00:00 2001 From: Gerrit Mohrmann <mohrmann.t3@gmx.de> Date: Fri, 21 Feb 2020 20:22:32 +0100 Subject: [PATCH] [FEATURE] Add Argon2id to password hash algorithms This adds Argon2id to the password hash algorithms. It should be available since PHP 7.3. Resolves: #90262 Releases: master Change-Id: I3810ca11330b7c7079408cd5a7f504e514a3262e Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/63077 Tested-by: TYPO3com <noreply@typo3.com> Tested-by: Susanne Moog <look@susi.dev> Tested-by: Andreas Fernandez <a.fernandez@scripting-base.de> Reviewed-by: Susanne Moog <look@susi.dev> Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de> --- .../AbstractArgon2PasswordHash.php | 157 ++++++++++++ .../PasswordHashing/Argon2iPasswordHash.php | 130 +--------- .../PasswordHashing/Argon2idPasswordHash.php | 39 +++ .../Configuration/DefaultConfiguration.php | 1 + .../DefaultConfigurationDescription.yaml | 2 + ...62-AddArgon2idToPasswordHashAlgorithms.rst | 18 ++ .../Argon2idPasswordHashTest.php | 241 ++++++++++++++++++ .../PasswordHashing/Argon2idPreset.php | 57 +++++ .../PasswordHashingFeature.php | 1 + .../Controller/InstallerController.php | 4 +- .../SilentConfigurationUpgradeService.php | 2 + .../Presets/PasswordHashing/Argon2id.html | 31 +++ .../Presets/PasswordHashing/Bcrypt.html | 8 +- .../Presets/PasswordHashing/Pbkdf2.html | 8 +- .../Presets/PasswordHashing/Phpass.html | 2 +- .../SilentConfigurationUpgradeServiceTest.php | 9 +- 16 files changed, 574 insertions(+), 136 deletions(-) create mode 100644 typo3/sysext/core/Classes/Crypto/PasswordHashing/AbstractArgon2PasswordHash.php create mode 100644 typo3/sysext/core/Classes/Crypto/PasswordHashing/Argon2idPasswordHash.php create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Feature-90262-AddArgon2idToPasswordHashAlgorithms.rst create mode 100644 typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/Argon2idPasswordHashTest.php create mode 100644 typo3/sysext/install/Classes/Configuration/PasswordHashing/Argon2idPreset.php create mode 100644 typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Argon2id.html diff --git a/typo3/sysext/core/Classes/Crypto/PasswordHashing/AbstractArgon2PasswordHash.php b/typo3/sysext/core/Classes/Crypto/PasswordHashing/AbstractArgon2PasswordHash.php new file mode 100644 index 000000000000..3176f5e90149 --- /dev/null +++ b/typo3/sysext/core/Classes/Crypto/PasswordHashing/AbstractArgon2PasswordHash.php @@ -0,0 +1,157 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Core\Crypto\PasswordHashing; + +/* + * 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! + */ + +/** + * This abstract class implements the 'argon2' flavour of the php password api. + */ +abstract class AbstractArgon2PasswordHash implements PasswordHashInterface +{ + /** + * 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). + * + * @var array + */ + protected $options = [ + 'memory_cost' => 65536, + 'time_cost' => 16, + 'threads' => 2 + ]; + + /** + * Constructor sets options if given + * + * @param array $options + * @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 ((int)$options['threads'] < PASSWORD_ARGON2_DEFAULT_THREADS) { + throw new \InvalidArgumentException( + 'threads must not be lower than ' . PASSWORD_ARGON2_DEFAULT_THREADS, + 1533899614 + ); + } + $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(static::PASSWORD_ALGORITHM_NAME); + } + + /** + * 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. + * + * @return bool + */ + public function isAvailable(): bool + { + return defined(static::PASSWORD_ALGORITHM_NAME) && $this->getPasswordAlgorithm(); + } + + /** + * Creates a salted hash for a given plaintext password + * + * @param string $password Plaintext password to create a salted hash from + * @return string|null Salted hashed password + */ + public function getHashedPassword(string $password) + { + $hashedPassword = null; + if ($password !== '') { + $hashedPassword = password_hash($password, $this->getPasswordAlgorithm(), $this->options); + if (!is_string($hashedPassword) || 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 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 + { + $passwordInfo = password_get_info($saltedPW); + + return + isset($passwordInfo['algo']) + && $passwordInfo['algo'] === $this->getPasswordAlgorithm() + && strncmp($saltedPW, static::PREFIX, strlen(static::PREFIX)) === 0; + } +} diff --git a/typo3/sysext/core/Classes/Crypto/PasswordHashing/Argon2iPasswordHash.php b/typo3/sysext/core/Classes/Crypto/PasswordHashing/Argon2iPasswordHash.php index f13ef130978c..3038f3c1715e 100644 --- a/typo3/sysext/core/Classes/Crypto/PasswordHashing/Argon2iPasswordHash.php +++ b/typo3/sysext/core/Classes/Crypto/PasswordHashing/Argon2iPasswordHash.php @@ -25,135 +25,15 @@ namespace TYPO3\CMS\Core\Crypto\PasswordHashing; * * @see PASSWORD_ARGON2I in https://secure.php.net/manual/en/password.constants.php */ -class Argon2iPasswordHash implements PasswordHashInterface +class Argon2iPasswordHash extends AbstractArgon2PasswordHash { /** - * Prefix for the password hash. - */ - protected const PREFIX = '$argon2i$'; - - /** - * 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. - * - * @var array - */ - protected $options = [ - 'memory_cost' => 65536, - 'time_cost' => 16, - 'threads' => 2 - ]; - - /** - * Constructor sets options if given - * - * @param array $options - * @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 ((int)$options['threads'] < PASSWORD_ARGON2_DEFAULT_THREADS) { - throw new \InvalidArgumentException( - 'threads must not be lower than ' . PASSWORD_ARGON2_DEFAULT_THREADS, - 1533899614 - ); - } - $newOptions['threads'] = (int)$options['threads']; - } - $this->options = $newOptions; - } - - /** - * 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 + * The password algorithm constant name. */ - public function checkPassword(string $plainPW, string $saltedHashPW): bool - { - return password_verify($plainPW, $saltedHashPW); - } + protected const PASSWORD_ALGORITHM_NAME = 'PASSWORD_ARGON2I'; /** - * Returns true if PHP is compiled '--with-password-argon2' so - * the hash algorithm is available. - * - * @return bool - */ - public function isAvailable(): bool - { - return defined('PASSWORD_ARGON2I') && PASSWORD_ARGON2I; - } - - /** - * Creates a salted hash for a given plaintext password - * - * @param string $password Plaintext password to create a salted hash from - * @return string|null Salted hashed password - */ - public function getHashedPassword(string $password) - { - $hashedPassword = null; - if ($password !== '') { - $hashedPassword = password_hash($password, PASSWORD_ARGON2I, $this->options); - if (!is_string($hashedPassword) || 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, PASSWORD_ARGON2I, $this->options); - } - - /** - * 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 + * Prefix for the password hash. */ - public function isValidSaltedPW(string $saltedPW): bool - { - $result = false; - $passwordInfo = password_get_info($saltedPW); - if (isset($passwordInfo['algo']) - && $passwordInfo['algo'] === PASSWORD_ARGON2I - && strncmp($saltedPW, static::PREFIX, strlen(static::PREFIX)) === 0 - ) { - $result = true; - } - return $result; - } + protected const PREFIX = '$argon2i$'; } diff --git a/typo3/sysext/core/Classes/Crypto/PasswordHashing/Argon2idPasswordHash.php b/typo3/sysext/core/Classes/Crypto/PasswordHashing/Argon2idPasswordHash.php new file mode 100644 index 000000000000..85d9458a6412 --- /dev/null +++ b/typo3/sysext/core/Classes/Crypto/PasswordHashing/Argon2idPasswordHash.php @@ -0,0 +1,39 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Core\Crypto\PasswordHashing; + +/* + * 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! + */ + +/** + * 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 +{ + /** + * The password algorithm constant name. + */ + protected const PASSWORD_ALGORITHM_NAME = 'PASSWORD_ARGON2ID'; + + /** + * Prefix for the password hash. + */ + protected const PREFIX = '$argon2id$'; +} diff --git a/typo3/sysext/core/Configuration/DefaultConfiguration.php b/typo3/sysext/core/Configuration/DefaultConfiguration.php index ddd9b9b77983..7ace0f4d15ef 100644 --- a/typo3/sysext/core/Configuration/DefaultConfiguration.php +++ b/typo3/sysext/core/Configuration/DefaultConfiguration.php @@ -108,6 +108,7 @@ return [ 'reverseProxyPrefixSSL' => '', 'availablePasswordHashAlgorithms' => [ \TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash::class, + \TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2idPasswordHash::class, \TYPO3\CMS\Core\Crypto\PasswordHashing\BcryptPasswordHash::class, \TYPO3\CMS\Core\Crypto\PasswordHashing\Pbkdf2PasswordHash::class, \TYPO3\CMS\Core\Crypto\PasswordHashing\PhpassPasswordHash::class, diff --git a/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml b/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml index 98b779e7090f..c8328a716ee0 100644 --- a/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml +++ b/typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml @@ -383,6 +383,7 @@ BE: type: dropdown allowedValues: 'TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash': 'Good password hash mechanism. Used by default if available.' + 'TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2idPasswordHash': 'Good password hash mechanism.' 'TYPO3\CMS\Core\Crypto\PasswordHashing\BcryptPasswordHash': 'Good password hash mechanism.' 'TYPO3\CMS\Core\Crypto\PasswordHashing\Pbkdf2PasswordHash': 'Fallback hash mechanism if argon and bcrypt are not available.' 'TYPO3\CMS\Core\Crypto\PasswordHashing\PhpassPasswordHash': 'Fallback hash mechanism if none of the above are available.' @@ -528,6 +529,7 @@ FE: type: dropdown allowedValues: 'TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash': 'Good password hash mechanism. Used by default if available.' + 'TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2idPasswordHash': 'Good password hash mechanism.' 'TYPO3\CMS\Core\Crypto\PasswordHashing\BcryptPasswordHash': 'Good password hash mechanism.' 'TYPO3\CMS\Core\Crypto\PasswordHashing\Pbkdf2PasswordHash': 'Fallback hash mechanism if argon and bcrypt are not available.' 'TYPO3\CMS\Core\Crypto\PasswordHashing\PhpassPasswordHash': 'Fallback hash mechanism if none of the above are available.' diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-90262-AddArgon2idToPasswordHashAlgorithms.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-90262-AddArgon2idToPasswordHashAlgorithms.rst new file mode 100644 index 000000000000..e28e10e708e0 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-90262-AddArgon2idToPasswordHashAlgorithms.rst @@ -0,0 +1,18 @@ +.. include:: ../../Includes.txt + +=========================================================== +Feature: #90262 - Add Argon2id to password hash algorithms +=========================================================== + +See :issue:`90262` + +Description +=========== + +The hash algorithm `argon2id` is now available and can be selected in the +section `Configuration Presets` of the admin tools > settings module if +the PHP instance supports it. + +Argon2id is usually available on systems with PHP version 7.3 or higher. + +.. index:: Backend, Frontend, PHP-API, ext:install diff --git a/typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/Argon2idPasswordHashTest.php b/typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/Argon2idPasswordHashTest.php new file mode 100644 index 000000000000..cd85d663e571 --- /dev/null +++ b/typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/Argon2idPasswordHashTest.php @@ -0,0 +1,241 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Core\Tests\Unit\Crypto\PasswordHashing; + +/* + * 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! + */ + +use TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2idPasswordHash; +use TYPO3\TestingFramework\Core\Unit\UnitTestCase; + +/** + * Test case + */ +class Argon2idPasswordHashTest extends UnitTestCase +{ + /** + * @var Argon2idPasswordHash + */ + protected $subject; + + /** + * Sets up the subject for this test case. + * + * @requires PHP 7.3 + */ + protected function setUp(): void + { + parent::setUp(); + $options = [ + 'memory_cost' => 65536, + 'time_cost' => 4, + 'threads' => 2, + ]; + $this->subject = new Argon2idPasswordHash($options); + } + + /** + * @test + * @requires PHP 7.3 + */ + public function constructorThrowsExceptionIfMemoryCostIsTooLow() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(1533899612); + new Argon2idPasswordHash(['memory_cost' => 1]); + } + + /** + * @test + * @requires PHP 7.3 + */ + public function constructorThrowsExceptionIfTimeCostIsTooLow() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(1533899613); + new Argon2idPasswordHash(['time_cost' => 1]); + } + + /** + * @test + * @requires PHP 7.3 + */ + public function constructorThrowsExceptionIfThreadsIsTooLow() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionCode(1533899614); + new Argon2idPasswordHash(['threads' => 0]); + } + + /** + * @test + * @requires PHP 7.3 + */ + public function getHashedPasswordReturnsNullOnEmptyPassword() + { + self::assertNull($this->subject->getHashedPassword('')); + } + + /** + * @test + * @requires PHP 7.3 + */ + public function getHashedPasswordReturnsString() + { + $hash = $this->subject->getHashedPassword('password'); + self::assertNotNull($hash); + self::assertTrue(is_string($hash)); + } + + /** + * @test + * @requires PHP 7.3 + */ + public function isValidSaltedPwValidatesHastCreatedByGetHashedPassword() + { + $hash = $this->subject->getHashedPassword('password'); + self::assertTrue($this->subject->isValidSaltedPW($hash)); + } + + /** + * Tests authentication procedure with alphabet characters. + * + * @test + * @requires PHP 7.3 + */ + public function checkPasswordReturnsTrueForHashedPasswordWithValidAlphaCharClassPassword() + { + $password = 'aEjOtY'; + $hash = $this->subject->getHashedPassword($password); + self::assertTrue($this->subject->checkPassword($password, $hash)); + } + + /** + * Tests authentication procedure with numeric characters. + * + * @test + * @requires PHP 7.3 + */ + public function checkPasswordReturnsTrueForHashedPasswordWithValidNumericCharClassPassword() + { + $password = '01369'; + $hash = $this->subject->getHashedPassword($password); + self::assertTrue($this->subject->checkPassword($password, $hash)); + } + + /** + * Tests authentication procedure with US-ASCII special characters. + * + * @test + * @requires PHP 7.3 + */ + public function checkPasswordReturnsTrueForHashedPasswordWithValidAsciiSpecialCharClassPassword() + { + $password = ' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'; + $hash = $this->subject->getHashedPassword($password); + self::assertTrue($this->subject->checkPassword($password, $hash)); + } + + /** + * Tests authentication procedure with latin1 special characters. + * + * @test + * @requires PHP 7.3 + */ + public function checkPasswordReturnsTrueForHashedPasswordWithValidLatin1SpecialCharClassPassword() + { + $password = ''; + for ($i = 160; $i <= 191; $i++) { + $password .= chr($i); + } + $password .= chr(215) . chr(247); + $hash = $this->subject->getHashedPassword($password); + self::assertTrue($this->subject->checkPassword($password, $hash)); + } + + /** + * Tests authentication procedure with latin1 umlauts. + * + * @test + * @requires PHP 7.3 + */ + public function checkPasswordReturnsTrueForHashedPasswordWithValidLatin1UmlautCharClassPassword() + { + $password = ''; + for ($i = 192; $i <= 255; $i++) { + if ($i === 215 || $i === 247) { + // skip multiplication sign (×) and obelus (÷) + continue; + } + $password .= chr($i); + } + $hash = $this->subject->getHashedPassword($password); + self::assertTrue($this->subject->checkPassword($password, $hash)); + } + + /** + * @test + * @requires PHP 7.3 + */ + public function checkPasswordReturnsTrueForHashedPasswordWithNonValidPassword() + { + $password = 'password'; + $password1 = $password . 'INVALID'; + $hash = $this->subject->getHashedPassword($password); + self::assertFalse($this->subject->checkPassword($password1, $hash)); + } + + /** + * @test + * @requires PHP 7.3 + */ + public function isHashUpdateNeededReturnsFalseForJustGeneratedHash() + { + $password = 'password'; + $hash = $this->subject->getHashedPassword($password); + self::assertFalse($this->subject->isHashUpdateNeeded($hash)); + } + + /** + * @test + * @requires PHP 7.3 + */ + public function isHashUpdateNeededReturnsTrueForHashGeneratedWithOldOptions() + { + $originalOptions = [ + 'memory_cost' => 65536, + 'time_cost' => 4, + 'threads' => 2, + ]; + $subject = new Argon2idPasswordHash($originalOptions); + $hash = $subject->getHashedPassword('password'); + + // Change $memoryCost + $newOptions = $originalOptions; + $newOptions['memory_cost'] = $newOptions['memory_cost'] + 1; + $subject = new Argon2idPasswordHash($newOptions); + self::assertTrue($subject->isHashUpdateNeeded($hash)); + + // Change $timeCost + $newOptions = $originalOptions; + $newOptions['time_cost'] = $newOptions['time_cost'] + 1; + $subject = new Argon2idPasswordHash($newOptions); + self::assertTrue($subject->isHashUpdateNeeded($hash)); + + // Change $threads + $newOptions = $originalOptions; + $newOptions['threads'] = $newOptions['threads'] + 1; + $subject = new Argon2idPasswordHash($newOptions); + self::assertTrue($subject->isHashUpdateNeeded($hash)); + } +} diff --git a/typo3/sysext/install/Classes/Configuration/PasswordHashing/Argon2idPreset.php b/typo3/sysext/install/Classes/Configuration/PasswordHashing/Argon2idPreset.php new file mode 100644 index 000000000000..652a4694bfd7 --- /dev/null +++ b/typo3/sysext/install/Classes/Configuration/PasswordHashing/Argon2idPreset.php @@ -0,0 +1,57 @@ +<?php +declare(strict_types = 1); +namespace TYPO3\CMS\Install\Configuration\PasswordHashing; + +/* + * 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! + */ + +use TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2idPasswordHash; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Install\Configuration\AbstractPreset; + +/** + * Preset for password hashing method "argon2id" + * @internal only to be used within EXT:install + */ +class Argon2idPreset extends AbstractPreset +{ + /** + * @var string Name of preset + */ + protected $name = 'Argon2id'; + + /** + * @var int Priority of preset + */ + protected $priority = 80; + + /** + * @var array Configuration values handled by this preset + */ + protected $configurationValues = [ + 'BE/passwordHashing/className' => Argon2idPasswordHash::class, + 'BE/passwordHashing/options' => [], + 'FE/passwordHashing/className' => Argon2idPasswordHash::class, + 'FE/passwordHashing/options' => [], + ]; + + /** + * Find out if Argon2id is available on this system + * + * @return bool + */ + public function isAvailable(): bool + { + return GeneralUtility::makeInstance(Argon2idPasswordHash::class)->isAvailable(); + } +} diff --git a/typo3/sysext/install/Classes/Configuration/PasswordHashing/PasswordHashingFeature.php b/typo3/sysext/install/Classes/Configuration/PasswordHashing/PasswordHashingFeature.php index 920f26dc09c3..821a38963ac6 100644 --- a/typo3/sysext/install/Classes/Configuration/PasswordHashing/PasswordHashingFeature.php +++ b/typo3/sysext/install/Classes/Configuration/PasswordHashing/PasswordHashingFeature.php @@ -34,6 +34,7 @@ class PasswordHashingFeature extends AbstractFeature implements FeatureInterface */ protected $presetRegistry = [ Argon2iPreset::class, + Argon2idPreset::class, BcryptPreset::class, Pbkdf2Preset::class, PhpassPreset::class, diff --git a/typo3/sysext/install/Classes/Controller/InstallerController.php b/typo3/sysext/install/Classes/Controller/InstallerController.php index 2973c5043305..274d67b14f0b 100644 --- a/typo3/sysext/install/Classes/Controller/InstallerController.php +++ b/typo3/sysext/install/Classes/Controller/InstallerController.php @@ -24,6 +24,7 @@ use TYPO3\CMS\Core\Configuration\ConfigurationManager; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use TYPO3\CMS\Core\Configuration\SiteConfiguration; use TYPO3\CMS\Core\Core\Environment; +use TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2idPasswordHash; use TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash; use TYPO3\CMS\Core\Crypto\PasswordHashing\BcryptPasswordHash; use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashInterface; @@ -1244,7 +1245,7 @@ For each website you need a TypoScript template on the main page of your website * * This method is executed during installation *before* the preset did set up proper hash method * selection in LocalConfiguration. So PasswordHashFactory is not usable at this point. We thus loop through - * the four default hash mechanisms and select the first one that works. The preset calculation of step + * the default hash mechanisms and select the first one that works. The preset calculation of step * executeDefaultConfigurationAction() basically does the same later. * * @param string $password Plain text password @@ -1255,6 +1256,7 @@ For each website you need a TypoScript template on the main page of your website { $okHashMethods = [ Argon2iPasswordHash::class, + Argon2idPasswordHash::class, BcryptPasswordHash::class, Pbkdf2PasswordHash::class, PhpassPasswordHash::class, diff --git a/typo3/sysext/install/Classes/Service/SilentConfigurationUpgradeService.php b/typo3/sysext/install/Classes/Service/SilentConfigurationUpgradeService.php index 45cfde700c6c..70363f3bc158 100644 --- a/typo3/sysext/install/Classes/Service/SilentConfigurationUpgradeService.php +++ b/typo3/sysext/install/Classes/Service/SilentConfigurationUpgradeService.php @@ -15,6 +15,7 @@ namespace TYPO3\CMS\Install\Service; */ use TYPO3\CMS\Core\Configuration\ConfigurationManager; +use TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2idPasswordHash; use TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash; use TYPO3\CMS\Core\Crypto\PasswordHashing\BcryptPasswordHash; use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashInterface; @@ -1025,6 +1026,7 @@ class SilentConfigurationUpgradeService // Phpass is always available, so we have some last fallback if the others don't kick in $okHashMethods = [ Argon2iPasswordHash::class, + Argon2idPasswordHash::class, BcryptPasswordHash::class, Pbkdf2PasswordHash::class, PhpassPasswordHash::class, diff --git a/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Argon2id.html b/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Argon2id.html new file mode 100644 index 000000000000..c5eacd3db789 --- /dev/null +++ b/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Argon2id.html @@ -0,0 +1,31 @@ +<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true"> + +<f:be.infobox state="{f:if(condition:'{preset.isAvailable}', then:'0', else:'2')}" disableIcon="true"> + <input + type="radio" + class="t3-install-tool-configuration-radio" + id="t3-install-tool-configuration-passwordHashing-argon2id" + name="install[values][{feature.name}][enable]" + value="{preset.name}" + {f:if(condition:'{preset.isAvailable}', then:'', else:'disabled="disabled"')} + {f:if(condition: preset.isActive, then:'checked="checked"')} + /> + <label for="t3-install-tool-configuration-passwordHashing-argon2id" class="t3-install-tool-configuration-radio-label"> + <strong>Argon2id</strong> {f:if(condition: preset.isActive, then:' [Active]')} + </label> + <p> + <f:if condition="{preset.isAvailable}"> + <f:then> + <strong>Use Argon2id if you are using PHP >= 7.3 on all instances (local, test, production, ...)!</strong><br /> + Argon2id is a modern key derivation function. It provides better resistance + to some forms of attack compared to Argon2i. + </f:then> + <f:else> + Argon2id is not available on this system. If you want to use Argon2id make sure you are using PHP >= 7.3 + with Argon support. + </f:else> + </f:if> + </p> +</f:be.infobox> + +</html> diff --git a/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Bcrypt.html b/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Bcrypt.html index c1f00735edcb..4ff9eb12e699 100644 --- a/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Bcrypt.html +++ b/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Bcrypt.html @@ -17,13 +17,13 @@ <f:if condition="{preset.isAvailable}"> <f:then> bcrypt is a good password hashing algorithm. It however needs some additional quirks - for long passwords in PHP and should only be used if Argon2i is not available. + for long passwords in PHP and should only be used if Argon2id or Argon2i is not available. </f:then> <f:else> bcrypt is not available on this system. TYPO3 password storage not only requires bcrypt itself, - but also sha384 to be availble to use this algorithm. One of these or both are missing. bcrypt however - can be used as a fallback if Argon2i is not available, too. Ask reach out to your hoster to fix both - and prefer Argon2i. + but also sha384 to be available to use this algorithm. One of these or both are missing. + bcrypt is also used as a fallback if Argon2id or Argon2i are not available. + Reach out to your hoster to fix these issues and prefer installation of Argon2id or Argon2i. </f:else> </f:if> </p> diff --git a/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Pbkdf2.html b/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Pbkdf2.html index 1fe200e6b33a..6ba295b989c8 100644 --- a/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Pbkdf2.html +++ b/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Pbkdf2.html @@ -17,13 +17,13 @@ <f:if condition="{preset.isAvailable}"> <f:then> PBKDF2 is a key derivation function recommended by IETF in RFC 8018 as part of the PKCS series, even - though newer password hashing functions such as Argon2i are designed to address weaknesses of PBKDF2. + though newer password hashing functions such as Argon2id or Argon2i are designed to address weaknesses of PBKDF2. It could be a preferred password hash algorithm if storing passwords in a FIPS compliant way is necessary. - Usually, selecting Argon2i as hash algorithm is good to go. + Usually, selecting Argon2id or Argon2i if available as hash algorithm is ideal. </f:then> <f:else> - PBKDF2 is not available on this system. This is very uncommon. If Argon2i and bcrypt are also not available, - you should seriously question the quality of your current hoster and reach them out to fix this as soon as possible. + PBKDF2 is not available on this system. This is very uncommon. If Argon2id, Argon2i and bcrypt are also not available, + you should reach out to your hoster to fix this as soon as possible. </f:else> </f:if> </p> diff --git a/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Phpass.html b/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Phpass.html index 4989c378947c..5800cb782b5f 100644 --- a/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Phpass.html +++ b/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Phpass.html @@ -16,7 +16,7 @@ <p> <f:if condition="{preset.isAvailable}"> <f:then> - In almost all cases, a modern hash algorithm like Argon2i should be preferred and is good to go. + In almost all cases, a modern hash algorithm like Argon2id or Argon2i should be preferred. phpass is a portable public domain password hashing framework for use in PHP applications since 2005. The implementation should work on almost all PHP builds. It might be a suitable password storage hash method in seldom cases if third party systems must use the same password hash on a low database level diff --git a/typo3/sysext/install/Tests/Unit/Service/SilentConfigurationUpgradeServiceTest.php b/typo3/sysext/install/Tests/Unit/Service/SilentConfigurationUpgradeServiceTest.php index 8899f18f2a27..a9be8e69baa1 100644 --- a/typo3/sysext/install/Tests/Unit/Service/SilentConfigurationUpgradeServiceTest.php +++ b/typo3/sysext/install/Tests/Unit/Service/SilentConfigurationUpgradeServiceTest.php @@ -18,6 +18,7 @@ namespace TYPO3\CMS\Install\Tests\Unit\Service; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use TYPO3\CMS\Core\Configuration\ConfigurationManager; +use TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2idPasswordHash; use TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash; use TYPO3\CMS\Core\Crypto\PasswordHashing\BcryptPasswordHash; use TYPO3\CMS\Core\Package\PackageManager; @@ -924,17 +925,23 @@ class SilentConfigurationUpgradeServiceTest extends UnitTestCase /** * @test */ - public function migrateSaltedPasswordsSetsSpecificHashMethodIfArgon2iIsNotAvailable() + public function migrateSaltedPasswordsSetsSpecificHashMethodIfArgon2idAndArgon2iIsNotAvailable() { $configurationManagerProphecy = $this->prophesize(ConfigurationManager::class); $configurationManagerProphecy->getLocalConfigurationValueByPath('EXTENSIONS/saltedpasswords') ->shouldBeCalled()->willReturn(['thereIs' => 'something']); + $argon2idBeProphecy = $this->prophesize(Argon2idPasswordHash::class); + $argon2idBeProphecy->isAvailable()->shouldBeCalled()->willReturn(false); + GeneralUtility::addInstance(Argon2idPasswordHash::class, $argon2idBeProphecy->reveal()); $argonBeProphecy = $this->prophesize(Argon2iPasswordHash::class); $argonBeProphecy->isAvailable()->shouldBeCalled()->willReturn(false); GeneralUtility::addInstance(Argon2iPasswordHash::class, $argonBeProphecy->reveal()); $bcryptBeProphecy = $this->prophesize(BcryptPasswordHash::class); $bcryptBeProphecy->isAvailable()->shouldBeCalled()->willReturn(true); GeneralUtility::addInstance(BcryptPasswordHash::class, $bcryptBeProphecy->reveal()); + $argon2idFeProphecy = $this->prophesize(Argon2idPasswordHash::class); + $argon2idFeProphecy->isAvailable()->shouldBeCalled()->willReturn(false); + GeneralUtility::addInstance(Argon2idPasswordHash::class, $argon2idFeProphecy->reveal()); $argonFeProphecy = $this->prophesize(Argon2iPasswordHash::class); $argonFeProphecy->isAvailable()->shouldBeCalled()->willReturn(false); GeneralUtility::addInstance(Argon2iPasswordHash::class, $argonFeProphecy->reveal()); -- GitLab