Commit 893ee207 authored by Gerrit Mohrmann's avatar Gerrit Mohrmann Committed by Andreas Fernandez
Browse files

[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: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Susanne Moog's avatarSusanne Moog <look@susi.dev>
Tested-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Susanne Moog's avatarSusanne Moog <look@susi.dev>
Reviewed-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
parent d1d3a178
<?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;
}
}
......@@ -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$';
}
<?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$';
}
......@@ -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,
......
......@@ -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.'
......
.. 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
<?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';