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