[TASK] Merge EXT:saltedpasswords into EXT:core 85/57885/13
authorChristian Kuhn <lolli@schwarzbu.ch>
Mon, 13 Aug 2018 11:56:35 +0000 (13:56 +0200)
committerGeorg Ringer <georg.ringer@gmail.com>
Tue, 14 Aug 2018 04:18:11 +0000 (06:18 +0200)
Move all classes and other resources from EXT:saltedpasswords to
EXT:core.

Classes live in TYPO3\CMS\Core\Crypto\PasswordHashing. This namespace
will be clean in v10 when the classes that are currently only kept for
backwards compatibility are removed.

The documentation has been integrated into the "Core API" docs at
https://docs.typo3.org/typo3cms/CoreApiReference/stable/ApiOverview/PasswordHashing/

Resolves: #85833
Resolves: #85026
Releases: master
Change-Id: Ie6ac7fbf215fe61711f0acdd6dc5a318bce1ad35
Reviewed-on: https://review.typo3.org/57885
Reviewed-by: Stephan Großberndt <stephan.grossberndt@typo3.org>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
121 files changed:
composer.json
composer.lock
typo3/sysext/core/Classes/Authentication/AuthenticationService.php
typo3/sysext/core/Classes/Authentication/CommandLineUserAuthentication.php
typo3/sysext/core/Classes/Crypto/PasswordHashing/AbstractComposedSalt.php [new file with mode: 0644]
typo3/sysext/core/Classes/Crypto/PasswordHashing/Argon2iPasswordHash.php [new file with mode: 0644]
typo3/sysext/core/Classes/Crypto/PasswordHashing/BcryptPasswordHash.php [new file with mode: 0644]
typo3/sysext/core/Classes/Crypto/PasswordHashing/BlowfishPasswordHash.php [new file with mode: 0644]
typo3/sysext/core/Classes/Crypto/PasswordHashing/ComposedPasswordHashInterface.php [new file with mode: 0644]
typo3/sysext/core/Classes/Crypto/PasswordHashing/ExtensionManagerConfigurationUtility.php [new file with mode: 0644]
typo3/sysext/core/Classes/Crypto/PasswordHashing/InvalidPasswordHashException.php [new file with mode: 0644]
typo3/sysext/core/Classes/Crypto/PasswordHashing/Md5PasswordHash.php [new file with mode: 0644]
typo3/sysext/core/Classes/Crypto/PasswordHashing/PasswordHashFactory.php [new file with mode: 0644]
typo3/sysext/core/Classes/Crypto/PasswordHashing/PasswordHashInterface.php [new file with mode: 0644]
typo3/sysext/core/Classes/Crypto/PasswordHashing/Pbkdf2PasswordHash.php [new file with mode: 0644]
typo3/sysext/core/Classes/Crypto/PasswordHashing/PhpassPasswordHash.php [new file with mode: 0644]
typo3/sysext/core/Classes/Crypto/PasswordHashing/SaltedPasswordService.php [new file with mode: 0644]
typo3/sysext/core/Classes/Crypto/PasswordHashing/SaltedPasswordsUtility.php [new file with mode: 0644]
typo3/sysext/core/Classes/DataHandling/DataHandler.php
typo3/sysext/core/Classes/Localization/LocalizationFactory.php
typo3/sysext/core/Configuration/DefaultConfiguration.php
typo3/sysext/core/Documentation/Changelog/master/Deprecation-85833-ExtensionSaltedpasswordsMergedIntoCoreExtension.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Important-85833-SaltedpasswordsExtensionMergedIntoCoreExtension.rst [new file with mode: 0644]
typo3/sysext/core/Migrations/Code/ClassAliasMap.php
typo3/sysext/core/Migrations/Code/LegacyClassesForIde.php
typo3/sysext/core/Resources/Private/Language/locallang_deprecated_saltedpasswords.xlf [new file with mode: 0644]
typo3/sysext/core/Resources/Private/Language/locallang_deprecated_saltedpasswords_em.xlf [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/Crypto/PasswordHashing/Fixtures/be_users.xml [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/Crypto/PasswordHashing/Fixtures/fe_users.xml [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/Crypto/PasswordHashing/SaltedPasswordServiceTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/Crypto/PasswordHashing/SaltedPasswordsUtilityTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/Argon2iPasswordHashTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/BcryptPasswordHashTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/BlowfishPasswordHashTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/Fixtures/TestPasswordHash.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/Md5PasswordHashTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/PasswordHashFactoryTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/Pbkdf2PaswordHashTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/PhpassPasswordHashTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/UnitDeprecated/Crypto/PasswordHashing/Argon2iPasswordHashTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/UnitDeprecated/Crypto/PasswordHashing/BcryptPasswordHashTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/UnitDeprecated/Crypto/PasswordHashing/BlowfishPasswordHashTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/UnitDeprecated/Crypto/PasswordHashing/Md5PasswordHashTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/UnitDeprecated/Crypto/PasswordHashing/PasswordHashFactoryTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/UnitDeprecated/Crypto/PasswordHashing/Pbkdf2PasswordHashTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/UnitDeprecated/Crypto/PasswordHashing/PhpassPasswordHashTest.php [new file with mode: 0644]
typo3/sysext/core/composer.json
typo3/sysext/felogin/Classes/Controller/FrontendLoginController.php
typo3/sysext/install/Classes/Authentication/AuthenticationService.php
typo3/sysext/install/Classes/Configuration/PasswordHashing/Argon2iPreset.php
typo3/sysext/install/Classes/Configuration/PasswordHashing/BcryptPreset.php
typo3/sysext/install/Classes/Configuration/PasswordHashing/CustomPreset.php
typo3/sysext/install/Classes/Configuration/PasswordHashing/Pbkdf2Preset.php
typo3/sysext/install/Classes/Configuration/PasswordHashing/PhpassPreset.php
typo3/sysext/install/Classes/Controller/InstallerController.php
typo3/sysext/install/Classes/Controller/MaintenanceController.php
typo3/sysext/install/Classes/Controller/SettingsController.php
typo3/sysext/install/Classes/Http/RequestHandler.php
typo3/sysext/install/Classes/Report/SecurityStatusReport.php
typo3/sysext/install/Classes/Service/SilentConfigurationUpgradeService.php
typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php
typo3/sysext/install/Tests/Unit/Service/SilentConfigurationUpgradeServiceTest.php
typo3/sysext/reports/Classes/Report/Status/SecurityStatus.php
typo3/sysext/reports/Resources/Private/Language/locallang_reports.xlf
typo3/sysext/saltedpasswords/.gitattributes [deleted file]
typo3/sysext/saltedpasswords/Classes/Exception/InvalidSaltException.php [deleted file]
typo3/sysext/saltedpasswords/Classes/Salt/AbstractComposedSalt.php [deleted file]
typo3/sysext/saltedpasswords/Classes/Salt/Argon2iSalt.php [deleted file]
typo3/sysext/saltedpasswords/Classes/Salt/BcryptSalt.php [deleted file]
typo3/sysext/saltedpasswords/Classes/Salt/BlowfishSalt.php [deleted file]
typo3/sysext/saltedpasswords/Classes/Salt/ComposedSaltInterface.php [deleted file]
typo3/sysext/saltedpasswords/Classes/Salt/Md5Salt.php [deleted file]
typo3/sysext/saltedpasswords/Classes/Salt/Pbkdf2Salt.php [deleted file]
typo3/sysext/saltedpasswords/Classes/Salt/PhpassSalt.php [deleted file]
typo3/sysext/saltedpasswords/Classes/Salt/SaltFactory.php [deleted file]
typo3/sysext/saltedpasswords/Classes/Salt/SaltInterface.php [deleted file]
typo3/sysext/saltedpasswords/Classes/SaltedPasswordService.php [deleted file]
typo3/sysext/saltedpasswords/Classes/Utility/ExtensionManagerConfigurationUtility.php [deleted file]
typo3/sysext/saltedpasswords/Classes/Utility/SaltedPasswordsUtility.php [deleted file]
typo3/sysext/saltedpasswords/Documentation/.gitignore [deleted file]
typo3/sysext/saltedpasswords/Documentation/Configuration/Index.rst [deleted file]
typo3/sysext/saltedpasswords/Documentation/Credits/Index.rst [deleted file]
typo3/sysext/saltedpasswords/Documentation/DevelopersGuide/Index.rst [deleted file]
typo3/sysext/saltedpasswords/Documentation/Images/ExtensionConfigurationCheck.png [deleted file]
typo3/sysext/saltedpasswords/Documentation/Images/SaltedHashInDatabase.png [deleted file]
typo3/sysext/saltedpasswords/Documentation/Includes.txt [deleted file]
typo3/sysext/saltedpasswords/Documentation/Index.rst [deleted file]
typo3/sysext/saltedpasswords/Documentation/Installation/Index.rst [deleted file]
typo3/sysext/saltedpasswords/Documentation/Introduction/Index.rst [deleted file]
typo3/sysext/saltedpasswords/Documentation/Licenses/Index.rst [deleted file]
typo3/sysext/saltedpasswords/Documentation/Overview/Index.rst [deleted file]
typo3/sysext/saltedpasswords/Documentation/Settings.cfg [deleted file]
typo3/sysext/saltedpasswords/Documentation/Targets.rst [deleted file]
typo3/sysext/saltedpasswords/LICENSE.txt [deleted file]
typo3/sysext/saltedpasswords/Migrations/Code/ClassAliasMap.php [deleted file]
typo3/sysext/saltedpasswords/Resources/Private/Language/locallang.xlf [deleted file]
typo3/sysext/saltedpasswords/Resources/Private/Language/locallang_em.xlf [deleted file]
typo3/sysext/saltedpasswords/Resources/Public/Icons/Extension.png [deleted file]
typo3/sysext/saltedpasswords/Tests/Functional/Fixtures/be_users.xml [deleted file]
typo3/sysext/saltedpasswords/Tests/Functional/Fixtures/fe_users.xml [deleted file]
typo3/sysext/saltedpasswords/Tests/Functional/SaltedPasswordServiceTest.php [deleted file]
typo3/sysext/saltedpasswords/Tests/Functional/Utility/SaltedPasswordsUtilityTest.php [deleted file]
typo3/sysext/saltedpasswords/Tests/Unit/Salt/Argon2iSaltTest.php [deleted file]
typo3/sysext/saltedpasswords/Tests/Unit/Salt/BcryptSaltTest.php [deleted file]
typo3/sysext/saltedpasswords/Tests/Unit/Salt/BlowfishSaltTest.php [deleted file]
typo3/sysext/saltedpasswords/Tests/Unit/Salt/Fixtures/TestSalt.php [deleted file]
typo3/sysext/saltedpasswords/Tests/Unit/Salt/Md5SaltTest.php [deleted file]
typo3/sysext/saltedpasswords/Tests/Unit/Salt/Pbkdf2SaltTest.php [deleted file]
typo3/sysext/saltedpasswords/Tests/Unit/Salt/PhpassSaltTest.php [deleted file]
typo3/sysext/saltedpasswords/Tests/Unit/Salt/SaltFactoryTest.php [deleted file]
typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/Argon2iSaltTest.php [deleted file]
typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/BcryptSaltTest.php [deleted file]
typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/BlowfishSaltTest.php [deleted file]
typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/Md5SaltTest.php [deleted file]
typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/Pbkdf2SaltTest.php [deleted file]
typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/PhpassSaltTest.php [deleted file]
typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/SaltFactoryTest.php [deleted file]
typo3/sysext/saltedpasswords/composer.json [deleted file]
typo3/sysext/saltedpasswords/ext_emconf.php [deleted file]
typo3/sysext/saltedpasswords/ext_localconf.php [deleted file]
typo3/sysext/setup/Classes/Controller/SetupModuleController.php

index 7632b74..fea281a 100644 (file)
@@ -95,7 +95,6 @@
                                "typo3/sysext/lowlevel/Migrations/Code/ClassAliasMap.php",
                                "typo3/sysext/recordlist/Migrations/Code/ClassAliasMap.php",
                                "typo3/sysext/reports/Migrations/Code/ClassAliasMap.php",
-                               "typo3/sysext/saltedpasswords/Migrations/Code/ClassAliasMap.php",
                                "typo3/sysext/t3editor/Migrations/Code/ClassAliasMap.php",
                                "typo3/sysext/workspaces/Migrations/Code/ClassAliasMap.php"
                        ]
                "typo3/cms-reports": "self.version",
                "typo3/cms-rsaauth": "self.version",
                "typo3/cms-rte-ckeditor": "self.version",
-               "typo3/cms-saltedpasswords": "self.version",
                "typo3/cms-scheduler": "self.version",
                "typo3/cms-seo": "self.version",
                "typo3/cms-setup": "self.version",
                        "TYPO3\\CMS\\Reports\\": "typo3/sysext/reports/Classes/",
                        "TYPO3\\CMS\\Rsaauth\\": "typo3/sysext/rsaauth/Classes/",
                        "TYPO3\\CMS\\RteCKEditor\\": "typo3/sysext/rte_ckeditor/Classes/",
-                       "TYPO3\\CMS\\Saltedpasswords\\": "typo3/sysext/saltedpasswords/Classes/",
                        "TYPO3\\CMS\\Scheduler\\": "typo3/sysext/scheduler/Classes/",
                        "TYPO3\\CMS\\Seo\\": "typo3/sysext/seo/Classes/",
                        "TYPO3\\CMS\\Setup\\": "typo3/sysext/setup/Classes/",
                        "TYPO3\\CMS\\Recordlist\\Tests\\": "typo3/sysext/recordlist/Tests/",
                        "TYPO3\\CMS\\Reports\\Tests\\": "typo3/sysext/reports/Tests/",
                        "TYPO3\\CMS\\Rsaauth\\Tests\\": "typo3/sysext/rsaauth/Tests/",
-                       "TYPO3\\CMS\\Saltedpasswords\\Tests\\": "typo3/sysext/saltedpasswords/Tests/",
                        "TYPO3\\CMS\\Scheduler\\Tests\\": "typo3/sysext/scheduler/Tests/",
                        "TYPO3\\CMS\\Seo\\Tests\\": "typo3/sysext/seo/Tests/",
                        "TYPO3\\CMS\\Setup\\Tests\\": "typo3/sysext/setup/Tests/",
index 54c7bb6..8dc5d4f 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "63588d48aa7557054c5c4a21e8acff6a",
+    "content-hash": "2d4286e7d7266515cb472a37beeeffe7",
     "packages": [
         {
             "name": "cogpowered/finediff",
index c92f75d..8ff05b4 100644 (file)
@@ -14,14 +14,14 @@ namespace TYPO3\CMS\Core\Authentication;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashInterface;
 use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Saltedpasswords\Exception\InvalidSaltException;
-use TYPO3\CMS\Saltedpasswords\Salt\SaltFactory;
-use TYPO3\CMS\Saltedpasswords\Salt\SaltInterface;
 
 /**
  * Authentication services class
@@ -118,20 +118,20 @@ class AuthenticationService extends AbstractAuthenticationService
         $isReHashNeeded = false;
         $isDomainLockMet = false;
 
-        $saltFactory = GeneralUtility::makeInstance(SaltFactory::class);
+        $saltFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
 
         // Get a hashed password instance for the hash stored in db of this user
         try {
             $hashInstance = $saltFactory->get($passwordHashInDatabase, TYPO3_MODE);
-        } catch (InvalidSaltException $e) {
+        } catch (InvalidPasswordHashException $e) {
             // This can be refactored if the 'else' part below is gone in v10: Log and return 100 here
             $hashInstance = null;
         }
         // An instance of the currently configured salted password mechanism
-        // Don't catch InvalidSaltException here: Only install tool should handle those configuration failures
+        // Don't catch InvalidPasswordHashException here: Only install tool should handle those configuration failures
         $defaultHashInstance = $saltFactory->getDefaultHashInstance(TYPO3_MODE);
 
-        if ($hashInstance instanceof SaltInterface) {
+        if ($hashInstance instanceof PasswordHashInterface) {
             // We found a hash class that can handle this type of hash
             $isSaltedPassword = true;
             $isValidPassword = $hashInstance->checkPassword($submittedPassword, $passwordHashInDatabase);
@@ -156,7 +156,7 @@ class AuthenticationService extends AbstractAuthenticationService
             if (substr($user['password'], 0, 2) === 'M$') {
                 // If the stored db password starts with M$, it may be a md5 password that has been
                 // upgraded to a salted md5 using the old salted passwords scheduler task.
-                // See if a salt instance is returned if we cut off the M, so Md5Salt kicks in
+                // See if a salt instance is returned if we cut off the M, so Md5PasswordHash kicks in
                 try {
                     $hashInstance = $saltFactory->get(substr($passwordHashInDatabase, 1), TYPO3_MODE);
                     $isSaltedPassword = true;
@@ -172,7 +172,7 @@ class AuthenticationService extends AbstractAuthenticationService
                             $isDomainLockMet = true;
                         }
                     }
-                } catch (InvalidSaltException $e) {
+                } catch (InvalidPasswordHashException $e) {
                     // Still no instance found: $isSaltedPasswords is NOT set to true, logging and return done below
                 }
             }
index 94ca947..442a37e 100644 (file)
@@ -15,11 +15,11 @@ namespace TYPO3\CMS\Core\Authentication;
  */
 
 use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
 use TYPO3\CMS\Core\Crypto\Random;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Saltedpasswords\Salt\SaltFactory;
 
 /**
  * TYPO3 backend user authentication on a CLI level
@@ -148,7 +148,7 @@ class CommandLineUserAuthentication extends BackendUserAuthentication
     {
         $cryptoService = GeneralUtility::makeInstance(Random::class);
         $password = $cryptoService->generateRandomBytes(20);
-        $hashInstance = GeneralUtility::makeInstance(SaltFactory::class)->getDefaultHashInstance('BE');
+        $hashInstance = GeneralUtility::makeInstance(PasswordHashFactory::class)->getDefaultHashInstance('BE');
         return $hashInstance->getHashedPassword($password);
     }
 }
diff --git a/typo3/sysext/core/Classes/Crypto/PasswordHashing/AbstractComposedSalt.php b/typo3/sysext/core/Classes/Crypto/PasswordHashing/AbstractComposedSalt.php
new file mode 100644 (file)
index 0000000..e51978a
--- /dev/null
@@ -0,0 +1,83 @@
+<?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!
+ */
+
+/**
+ * Abstract class with methods needed to be extended
+ * in a salted hashing class that composes an own salted password hash.
+ *
+ * @deprecated and will be removed in TYPO3 v10.0.
+ */
+abstract class AbstractComposedSalt
+{
+    /**
+     * Returns a string for mapping an int to the corresponding base 64 character.
+     *
+     * @return string String for mapping an int to the corresponding base 64 character
+     */
+    abstract protected function getItoa64(): string;
+
+    /**
+     * Encodes bytes into printable base 64 using the *nix standard from crypt().
+     *
+     * @param string $input The string containing bytes to encode.
+     * @param int $count The number of characters (bytes) to encode.
+     * @return string Encoded string
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function base64Encode(string $input, int $count): string
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        $output = '';
+        $i = 0;
+        $itoa64 = $this->getItoa64();
+        do {
+            $value = ord($input[$i++]);
+            $output .= $itoa64[$value & 63];
+            if ($i < $count) {
+                $value |= ord($input[$i]) << 8;
+            }
+            $output .= $itoa64[$value >> 6 & 63];
+            if ($i++ >= $count) {
+                break;
+            }
+            if ($i < $count) {
+                $value |= ord($input[$i]) << 16;
+            }
+            $output .= $itoa64[$value >> 12 & 63];
+            if ($i++ >= $count) {
+                break;
+            }
+            $output .= $itoa64[$value >> 18 & 63];
+        } while ($i < $count);
+        return $output;
+    }
+
+    /**
+     * Method determines required length of base64 characters for a given
+     * length of a byte string.
+     *
+     * @param int $byteLength Length of bytes to calculate in base64 chars
+     * @return int Required length of base64 characters
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    protected function getLengthBase64FromBytes(int $byteLength): int
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        // Calculates bytes in bits in base64
+        return (int)ceil($byteLength * 8 / 6);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Crypto/PasswordHashing/Argon2iPasswordHash.php b/typo3/sysext/core/Classes/Crypto/PasswordHashing/Argon2iPasswordHash.php
new file mode 100644 (file)
index 0000000..0e75a62
--- /dev/null
@@ -0,0 +1,226 @@
+<?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 'argon2i' flavour of the php password api.
+ *
+ * Hashes are identified by the prefix '$argon2i$'.
+ *
+ * The length of a argon2i password hash (in the form it is received from
+ * PHP) depends on the environment.
+ *
+ * @see PASSWORD_ARGON2I in https://secure.php.net/manual/en/password.constants.php
+ */
+class Argon2iPasswordHash implements PasswordHashInterface
+{
+    /**
+     * Prefix for the password hash.
+     */
+    protected const PREFIX = '$argon2i$';
+
+    /**
+     * The PHP defaults are rather low ('memory_cost' => 1024, 'time_cost' => 2, 'threads' => 2)
+     * 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.
+     *
+     * Note the default values are set again in 'setOptions' below if needed.
+     *
+     * @var array
+     */
+    protected $options = [
+        'memory_cost' => 16384,
+        '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
+     */
+    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('PASSWORD_ARGON2I') && PASSWORD_ARGON2I;
+    }
+
+    /**
+     * Creates a salted hash for a given plaintext password
+     *
+     * @param string $password Plaintext password to create a salted hash from
+     * @param string $salt Deprecated optional custom salt to use
+     * @return string|null Salted hashed password
+     */
+    public function getHashedPassword(string $password, string $salt = null)
+    {
+        if ($salt !== null) {
+            trigger_error(static::class . ': using a custom salt is deprecated in PHP password api and ignored', E_USER_DEPRECATED);
+        }
+        $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
+     */
+    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;
+    }
+
+    /**
+     * @return array
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function getOptions(): array
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return $this->options;
+    }
+
+    /**
+     * Set new memory_cost, time_cost, and thread values.
+     *
+     * @param array $options
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function setOptions(array $options): void
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        $newOptions = [];
+
+        // Check options for validity, else use hard coded defaults
+        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,
+                    1526042080
+                );
+            }
+            $newOptions['memory_cost'] = (int)$options['memory_cost'];
+        } else {
+            $newOptions['memory_cost'] = 16384;
+        }
+
+        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,
+                    1526042081
+                );
+            }
+            $newOptions['time_cost'] = (int)$options['time_cost'];
+        } else {
+            $newOptions['time_cost'] = 16;
+        }
+
+        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,
+                    1526042082
+                );
+            }
+            $newOptions['threads'] = (int)$options['threads'];
+        } else {
+            $newOptions['threads'] = 2;
+        }
+
+        $this->options = $newOptions;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Crypto/PasswordHashing/BcryptPasswordHash.php b/typo3/sysext/core/Classes/Crypto/PasswordHashing/BcryptPasswordHash.php
new file mode 100644 (file)
index 0000000..b65a1e1
--- /dev/null
@@ -0,0 +1,214 @@
+<?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 'bcrypt' flavour of the php password api.
+ *
+ * Hashes are identified by the prefix '$2y$'.
+ *
+ * To workaround the limitations of bcrypt (accepts not more than 72
+ * chars and truncates on NUL bytes), the plain password is pre-hashed
+ * before the actual password-hash is generated/verified.
+ *
+ * @see PASSWORD_BCRYPT in https://secure.php.net/manual/en/password.constants.php
+ */
+class BcryptPasswordHash implements PasswordHashInterface
+{
+    /**
+     * Prefix for the password hash
+     */
+    protected const PREFIX = '$2y$';
+
+    /**
+     * Raise default PHP cost (10). At the time of this writing, this leads to
+     * 150-200ms computing time on a casual I7 CPU.
+     *
+     * Note the default values are set again in 'setOptions' below if needed.
+     *
+     * @var array
+     */
+    protected $options = [
+        'cost' => 12,
+    ];
+
+    /**
+     * Constructor sets options if given
+     *
+     * @param array $options
+     * @throws \InvalidArgumentException
+     */
+    public function __construct(array $options = [])
+    {
+        $newOptions = $this->options;
+        // Check options for validity
+        if (isset($options['cost'])) {
+            if (!$this->isValidBcryptCost((int)$options['cost'])) {
+                throw new \InvalidArgumentException(
+                    'cost must not be lower than ' . PASSWORD_BCRYPT_DEFAULT_COST . ' or higher than 31',
+                    1533902002
+                );
+            }
+            $newOptions['cost'] = (int)$options['cost'];
+        }
+        $this->options = $newOptions;
+    }
+
+    /**
+     * Returns true if sha384 for pre-hashing and bcrypt itself is available.
+     *
+     * @return bool
+     */
+    public function isAvailable(): bool
+    {
+        return defined('PASSWORD_BCRYPT')
+            && PASSWORD_BCRYPT
+            && function_exists('hash')
+            && function_exists('hash_algos')
+            && in_array('sha384', hash_algos());
+    }
+
+    /**
+     * 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
+     */
+    public function checkPassword(string $plainPW, string $saltedHashPW): bool
+    {
+        return password_verify($this->processPlainPassword($plainPW), $saltedHashPW);
+    }
+
+    /**
+     * Extend parent method to workaround bcrypt limitations.
+     *
+     * @param string $password Plaintext password to create a salted hash from
+     * @param string $salt Deprecated optional custom salt to use
+     * @return string Salted hashed password
+     */
+    public function getHashedPassword(string $password, string $salt = null)
+    {
+        if ($salt !== null) {
+            trigger_error(static::class . ': using a custom salt is deprecated in PHP password api and thus ignored', E_USER_DEPRECATED);
+        }
+        $hashedPassword = null;
+        if ($password !== '') {
+            $password = $this->processPlainPassword($password);
+            $hashedPassword = password_hash($password, PASSWORD_BCRYPT, $this->options);
+            if (!is_string($hashedPassword) || empty($hashedPassword)) {
+                throw new InvalidPasswordHashException('Cannot generate password, probably invalid options', 1517174114);
+            }
+        }
+        return $hashedPassword;
+    }
+
+    /**
+     * Determines if a given string is a valid salted hashed password.
+     *
+     * @param string $saltedPW String to check
+     * @return bool TRUE if it's valid salted hashed password, otherwise FALSE
+     */
+    public function isValidSaltedPW(string $saltedPW): bool
+    {
+        $result = false;
+        $passwordInfo = password_get_info($saltedPW);
+        // Validate the cost value, password_get_info() does not check it
+        $cost = (int)substr($saltedPW, 4, 2);
+        if (isset($passwordInfo['algo'])
+            && $passwordInfo['algo'] === PASSWORD_BCRYPT
+            && strncmp($saltedPW, static::PREFIX, strlen(static::PREFIX)) === 0
+            && $this->isValidBcryptCost($cost)
+        ) {
+            $result = true;
+        }
+        return $result;
+    }
+    /**
+     * Checks whether a user's hashed password needs to be replaced with a new hash.
+     *
+     * @param string $passString Salted hash to check if it needs an update
+     * @return bool TRUE if salted hash needs an update, otherwise FALSE
+     */
+    public function isHashUpdateNeeded(string $passString): bool
+    {
+        return password_needs_rehash($passString, PASSWORD_BCRYPT, $this->options);
+    }
+
+    /**
+     * The plain password is processed through sha384 and then base64
+     * encoded. This will produce a 64 characters input to use with
+     * password_* functions, which has some advantages:
+     * 1. It is close to the (bcrypt-) maximum of 72 character keyspace
+     * 2. base64 will never produce NUL bytes (bcrypt truncates on NUL bytes)
+     * 3. sha384 is resistant to length extension attacks
+     *
+     * @param string $password
+     * @return string
+     */
+    protected function processPlainPassword(string $password): string
+    {
+        return base64_encode(hash('sha384', $password, true));
+    }
+
+    /**
+     * @see https://github.com/php/php-src/blob/php-7.2.0/ext/standard/password.c#L441-L444
+     * @param int $cost
+     * @return bool
+     */
+    protected function isValidBcryptCost(int $cost): bool
+    {
+        return $cost >= PASSWORD_BCRYPT_DEFAULT_COST && $cost <= 31;
+    }
+
+    /**
+     * @return array
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function getOptions(): array
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return $this->options;
+    }
+
+    /**
+     * Set new memory_cost, time_cost, and thread values.
+     *
+     * @param array $options
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function setOptions(array $options): void
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        $newOptions = [];
+
+        // Check options for validity, else use hard coded defaults
+        if (isset($options['cost'])) {
+            if (!$this->isValidBcryptCost((int)$options['cost'])) {
+                throw new \InvalidArgumentException(
+                    'cost must not be lower than ' . PASSWORD_BCRYPT_DEFAULT_COST . ' or higher than 31',
+                    1526042084
+                );
+            }
+            $newOptions['cost'] = (int)$options['cost'];
+        } else {
+            $newOptions['cost'] = 12;
+        }
+
+        $this->options = $newOptions;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Crypto/PasswordHashing/BlowfishPasswordHash.php b/typo3/sysext/core/Classes/Crypto/PasswordHashing/BlowfishPasswordHash.php
new file mode 100644 (file)
index 0000000..7d28c64
--- /dev/null
@@ -0,0 +1,423 @@
+<?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!
+ */
+
+use TYPO3\CMS\Core\Compatibility\PublicMethodDeprecationTrait;
+use TYPO3\CMS\Core\Crypto\Random;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Class that implements Blowfish salted hashing based on PHP's
+ * crypt() function.
+ *
+ * Warning: Blowfish salted hashing with PHP's crypt() is not available
+ * on every system.
+ */
+class BlowfishPasswordHash implements PasswordHashInterface
+{
+    use PublicMethodDeprecationTrait;
+
+    /**
+     * @var array
+     */
+    private $deprecatedPublicMethods = [
+        'isValidSalt' => 'Using BlowfishPasswordHash::isValidSalt() is deprecated and will not be possible anymore in TYPO3 v10.',
+        'base64Encode' => 'Using BlowfishPasswordHash::base64Encode() is deprecated and will not be possible anymore in TYPO3 v10.',
+    ];
+
+    /**
+     * Prefix for the password hash.
+     */
+    protected const PREFIX = '$2a$';
+
+    /**
+     * @var array The default log2 number of iterations for password stretching.
+     */
+    protected $options = [
+        'hash_count' => 7
+    ];
+
+    /**
+     * Keeps a string for mapping an int to the corresponding
+     * base 64 character.
+     *
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    const ITOA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+
+    /**
+     * The default log2 number of iterations for password stretching.
+     *
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    const HASH_COUNT = 7;
+
+    /**
+     * The default maximum allowed log2 number of iterations for
+     * password stretching.
+     *
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    const MAX_HASH_COUNT = 17;
+
+    /**
+     * The default minimum allowed log2 number of iterations for
+     * password stretching.
+     *
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    const MIN_HASH_COUNT = 4;
+
+    /**
+     * Constructor sets options if given
+     *
+     * @param array $options
+     * @throws \InvalidArgumentException
+     */
+    public function __construct(array $options = [])
+    {
+        $newOptions = $this->options;
+        if (isset($options['hash_count'])) {
+            if ((int)$options['hash_count'] < 4 || (int)$options['hash_count'] > 17) {
+                throw new \InvalidArgumentException(
+                    'hash_count must not be lower than 4 or bigger than 17',
+                    1533903545
+                );
+            }
+            $newOptions['hash_count'] = (int)$options['hash_count'];
+        }
+        $this->options = $newOptions;
+    }
+
+    /**
+     * Method checks if a given plaintext password is correct by comparing it with
+     * a given salted hashed password.
+     *
+     * @param string $plainPW plain-text password to compare with salted hash
+     * @param string $saltedHashPW salted hash to compare plain-text password with
+     * @return bool TRUE, if plain-text password matches the salted hash, otherwise FALSE
+     */
+    public function checkPassword(string $plainPW, string $saltedHashPW): bool
+    {
+        $isCorrect = false;
+        if ($this->isValidSalt($saltedHashPW)) {
+            $isCorrect = \password_verify($plainPW, $saltedHashPW);
+        }
+        return $isCorrect;
+    }
+
+    /**
+     * Returns whether all prerequisites for the hashing methods are matched
+     *
+     * @return bool Method available
+     */
+    public function isAvailable(): bool
+    {
+        return (bool)CRYPT_BLOWFISH;
+    }
+
+    /**
+     * Method creates a salted hash for a given plaintext password
+     *
+     * @param string $password plaintext password to create a salted hash from
+     * @param string $salt Deprecated optional custom salt with setting to use
+     * @return string Salted hashed password
+     */
+    public function getHashedPassword(string $password, string $salt = null)
+    {
+        if ($salt !== null) {
+            trigger_error(static::class . ': using a custom salt is deprecated.', E_USER_DEPRECATED);
+        }
+        $saltedPW = null;
+        if (!empty($password)) {
+            if (empty($salt) || !$this->isValidSalt($salt)) {
+                $salt = $this->getGeneratedSalt();
+            }
+            $saltedPW = crypt($password, $this->applySettingsToSalt($salt));
+        }
+        return $saltedPW;
+    }
+
+    /**
+     * Checks whether a user's hashed password needs to be replaced with a new hash.
+     *
+     * This is typically called during the login process when the plain text
+     * password is available.  A new hash is needed when the desired iteration
+     * count has changed through a change in the variable $hashCount or
+     * HASH_COUNT.
+     *
+     * @param string $saltedPW Salted hash to check if it needs an update
+     * @return bool TRUE if salted hash needs an update, otherwise FALSE
+     */
+    public function isHashUpdateNeeded(string $saltedPW): bool
+    {
+        // Check whether the iteration count used differs from the standard number.
+        $countLog2 = $this->getCountLog2($saltedPW);
+        return $countLog2 !== null && $countLog2 < $this->options['hash_count'];
+    }
+
+    /**
+     * Method determines if a given string is a valid salted hashed password.
+     *
+     * @param string $saltedPW String to check
+     * @return bool TRUE if it's valid salted hashed password, otherwise FALSE
+     */
+    public function isValidSaltedPW(string $saltedPW): bool
+    {
+        $isValid = !strncmp(self::PREFIX, $saltedPW, strlen(self::PREFIX));
+        if ($isValid) {
+            $isValid = $this->isValidSalt($saltedPW);
+        }
+        return $isValid;
+    }
+
+    /**
+     * Generates a random base 64-encoded salt prefixed and suffixed with settings for the hash.
+     *
+     * Proper use of salts may defeat a number of attacks, including:
+     * - The ability to try candidate passwords against multiple hashes at once.
+     * - The ability to use pre-hashed lists of candidate passwords.
+     * - The ability to determine whether two users have the same (or different)
+     * password without actually having to guess one of the passwords.
+     *
+     * @return string A character string containing settings and a random salt
+     */
+    protected function getGeneratedSalt(): string
+    {
+        $randomBytes = GeneralUtility::makeInstance(Random::class)->generateRandomBytes(16);
+        return $this->base64Encode($randomBytes, 16);
+    }
+
+    /**
+     * Method applies settings (prefix, hash count) to a salt.
+     *
+     * @param string $salt A salt to apply setting to
+     * @return string Salt with setting
+     */
+    protected function applySettingsToSalt(string $salt): string
+    {
+        $saltWithSettings = $salt;
+        $reqLenBase64 = $this->getLengthBase64FromBytes(16);
+        // salt without setting
+        if (strlen($salt) == $reqLenBase64) {
+            $saltWithSettings = self::PREFIX . sprintf('%02u', $this->options['hash_count']) . '$' . $salt;
+        }
+        return $saltWithSettings;
+    }
+
+    /**
+     * Parses the log2 iteration count from a stored hash or setting string.
+     *
+     * @param string $setting Complete hash or a hash's setting string or to get log2 iteration count from
+     * @return int Used hashcount for given hash string
+     */
+    protected function getCountLog2(string $setting): int
+    {
+        $countLog2 = null;
+        $setting = substr($setting, strlen(self::PREFIX));
+        $firstSplitPos = strpos($setting, '$');
+        // Hashcount existing
+        if ($firstSplitPos !== false && $firstSplitPos <= 2 && is_numeric(substr($setting, 0, $firstSplitPos))) {
+            $countLog2 = (int)substr($setting, 0, $firstSplitPos);
+        }
+        return $countLog2;
+    }
+
+    /**
+     * Returns a string for mapping an int to the corresponding base 64 character.
+     *
+     * @return string String for mapping an int to the corresponding base 64 character
+     */
+    protected function getItoa64(): string
+    {
+        return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+    }
+
+    /**
+     * Method determines if a given string is a valid salt.
+     *
+     * @param string $salt String to check
+     * @return bool TRUE if it's valid salt, otherwise FALSE
+     */
+    protected function isValidSalt(string $salt): bool
+    {
+        $isValid = ($skip = false);
+        $reqLenBase64 = $this->getLengthBase64FromBytes(16);
+        if (strlen($salt) >= $reqLenBase64) {
+            // Salt with prefixed setting
+            if (!strncmp('$', $salt, 1)) {
+                if (!strncmp(self::PREFIX, $salt, strlen(self::PREFIX))) {
+                    $isValid = true;
+                    $salt = substr($salt, strrpos($salt, '$') + 1);
+                } else {
+                    $skip = true;
+                }
+            }
+            // Checking base64 characters
+            if (!$skip && strlen($salt) >= $reqLenBase64) {
+                if (preg_match('/^[' . preg_quote($this->getItoa64(), '/') . ']{' . $reqLenBase64 . ',' . $reqLenBase64 . '}$/', substr($salt, 0, $reqLenBase64))) {
+                    $isValid = true;
+                }
+            }
+        }
+        return $isValid;
+    }
+
+    /**
+     * Encodes bytes into printable base 64 using the *nix standard from crypt().
+     *
+     * @param string $input The string containing bytes to encode.
+     * @param int $count The number of characters (bytes) to encode.
+     * @return string Encoded string
+     */
+    protected function base64Encode(string $input, int $count): string
+    {
+        $output = '';
+        $i = 0;
+        $itoa64 = $this->getItoa64();
+        do {
+            $value = ord($input[$i++]);
+            $output .= $itoa64[$value & 63];
+            if ($i < $count) {
+                $value |= ord($input[$i]) << 8;
+            }
+            $output .= $itoa64[$value >> 6 & 63];
+            if ($i++ >= $count) {
+                break;
+            }
+            if ($i < $count) {
+                $value |= ord($input[$i]) << 16;
+            }
+            $output .= $itoa64[$value >> 12 & 63];
+            if ($i++ >= $count) {
+                break;
+            }
+            $output .= $itoa64[$value >> 18 & 63];
+        } while ($i < $count);
+        return $output;
+    }
+
+    /**
+     * Method determines required length of base64 characters for a given
+     * length of a byte string.
+     *
+     * @param int $byteLength Length of bytes to calculate in base64 chars
+     * @return int Required length of base64 characters
+     */
+    protected function getLengthBase64FromBytes(int $byteLength): int
+    {
+        // Calculates bytes in bits in base64
+        return (int)ceil($byteLength * 8 / 6);
+    }
+
+    /**
+     * Method returns log2 number of iterations for password stretching.
+     *
+     * @return int log2 number of iterations for password stretching
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function getHashCount(): int
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return $this->options['hash_count'];
+    }
+
+    /**
+     * Method returns maximum allowed log2 number of iterations for password stretching.
+     *
+     * @return int Maximum allowed log2 number of iterations for password stretching
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function getMaxHashCount(): int
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return 17;
+    }
+
+    /**
+     * Method returns minimum allowed log2 number of iterations for password stretching.
+     *
+     * @return int Minimum allowed log2 number of iterations for password stretching
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function getMinHashCount(): int
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return 4;
+    }
+
+    /**
+     * Returns length of a Blowfish salt in bytes.
+     *
+     * @return int Length of a Blowfish salt in bytes
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function getSaltLength(): int
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return 16;
+    }
+
+    /**
+     * Returns setting string of Blowfish salted hashes.
+     *
+     * @return string Setting string of Blowfish salted hashes
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function getSetting(): string
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return self::PREFIX;
+    }
+
+    /**
+     * Method sets log2 number of iterations for password stretching.
+     *
+     * @param int $hashCount log2 number of iterations for password stretching to set
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function setHashCount(int $hashCount = null)
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        if ($hashCount >= 4 && $hashCount <= 17) {
+            $this->options['hash_count'] = $hashCount;
+        }
+    }
+
+    /**
+     * Method sets maximum allowed log2 number of iterations for password stretching.
+     *
+     * @param int $maxHashCount Maximum allowed log2 number of iterations for password stretching to set
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function setMaxHashCount(int $maxHashCount = null)
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        // Empty, max hash count is hard coded to 17
+    }
+
+    /**
+     * Method sets minimum allowed log2 number of iterations for password stretching.
+     *
+     * @param int $minHashCount Minimum allowed log2 number of iterations for password stretching to set
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function setMinHashCount(int $minHashCount = null)
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        // Empty, min hash count is hard coded to 4
+    }
+}
diff --git a/typo3/sysext/core/Classes/Crypto/PasswordHashing/ComposedPasswordHashInterface.php b/typo3/sysext/core/Classes/Crypto/PasswordHashing/ComposedPasswordHashInterface.php
new file mode 100644 (file)
index 0000000..1ed050e
--- /dev/null
@@ -0,0 +1,40 @@
+<?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!
+ */
+
+/**
+ * Interface for implementing salts that compose the password-hash string
+ * themselves.
+ *
+ * @deprecated and will be removed in TYPO3 v10.0.
+ */
+interface ComposedPasswordHashInterface extends PasswordHashInterface
+{
+    /**
+     * Returns length of required salt.
+     *
+     * @return int Length of required salt
+     */
+    public function getSaltLength(): int;
+
+    /**
+     * Method determines if a given string is a valid salt
+     *
+     * @param string $salt String to check
+     * @return bool TRUE if it's valid salt, otherwise FALSE
+     */
+    public function isValidSalt(string $salt): bool;
+}
diff --git a/typo3/sysext/core/Classes/Crypto/PasswordHashing/ExtensionManagerConfigurationUtility.php b/typo3/sysext/core/Classes/Crypto/PasswordHashing/ExtensionManagerConfigurationUtility.php
new file mode 100644 (file)
index 0000000..c313ee6
--- /dev/null
@@ -0,0 +1,134 @@
+<?php
+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!
+ */
+
+use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * class providing configuration checks for saltedpasswords.
+ *
+ * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
+ */
+class ExtensionManagerConfigurationUtility
+{
+    /**
+     * @var array
+     */
+    protected $extConf = [];
+
+    /**
+     * Deprecate this class
+     */
+    public function __construct()
+    {
+        trigger_error(self::class . ' is obsolete and will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+    }
+
+    /**
+     * Initializes this object.
+     */
+    private function init()
+    {
+        $requestSetup = $this->processPostData((array)$_REQUEST['data']);
+        $extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('saltedpasswords');
+        $this->extConf['BE'] = array_merge((array)$extConf['BE'], (array)$requestSetup['BE']);
+        $this->extConf['FE'] = array_merge((array)$extConf['FE'], (array)$requestSetup['FE']);
+        $this->getLanguageService()->includeLLFile('EXT:saltedpasswords/Resources/Private/Language/locallang.xlf');
+    }
+
+    /**
+     * Renders a selector element that allows to select the hash method to be used.
+     *
+     * @param array $params Field information to be rendered
+     * @param string $disposal The configuration disposal ('FE' or 'BE')
+     * @return string The HTML selector
+     */
+    protected function buildHashMethodSelector(array $params, $disposal)
+    {
+        $this->init();
+        $propertyName = $params['propertyName'];
+        $unknownVariablePleaseRenameMe = '\'' . substr(md5($propertyName), 0, 10) . '\'';
+        $pField = '';
+        $registeredMethods = \TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory::getRegisteredSaltedHashingMethods();
+        foreach ($registeredMethods as $class => $reference) {
+            $classInstance = GeneralUtility::makeInstance($reference);
+            if ($classInstance instanceof \TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashInterface && $classInstance->isAvailable()) {
+                $sel = $this->extConf[$disposal]['saltedPWHashingMethod'] == $class ? ' selected="selected" ' : '';
+                $label = 'ext.saltedpasswords.title.' . strtolower(end(explode('\\', $class)));
+                $pField .= '<option value="' . htmlspecialchars($class) . '"' . $sel . '>' . $GLOBALS['LANG']->getLL($label) . '</option>';
+            }
+        }
+        $pField = '<select class="form-control" id="' . $propertyName . '" name="' . $params['fieldName'] .
+            '" onChange="uFormUrl(' . $unknownVariablePleaseRenameMe . ')">' . $pField . '</select>';
+        return $pField;
+    }
+
+    /**
+     * Renders a selector element that allows to select the hash method to be
+     * used (frontend disposal).
+     *
+     * @param array $params Field information to be rendered
+     * @return string The HTML selector
+     */
+    public function buildHashMethodSelectorFE(array $params)
+    {
+        return $this->buildHashMethodSelector($params, 'FE');
+    }
+
+    /**
+     * Renders a selector element that allows to select the hash method to
+     * be used (backend disposal)
+     *
+     * @param array $params Field information to be rendered
+     * @return string The HTML selector
+     */
+    public function buildHashMethodSelectorBE(array $params)
+    {
+        return $this->buildHashMethodSelector($params, 'BE');
+    }
+
+    /**
+     * Processes the information submitted by the user using a POST request and
+     * transforms it to a TypoScript node notation.
+     *
+     * @param array $postArray Incoming POST information
+     * @return array Processed and transformed POST information
+     */
+    protected function processPostData(array $postArray = [])
+    {
+        foreach ($postArray as $key => $value) {
+            // @todo Explain
+            $parts = explode('.', $key, 2);
+            if (count($parts) == 2) {
+                // @todo Explain
+                $value = $this->processPostData([$parts[1] => $value]);
+                $postArray[$parts[0] . '.'] = array_merge((array)$postArray[$parts[0] . '.'], $value);
+            } else {
+                // @todo Explain
+                $postArray[$parts[0]] = $value;
+            }
+        }
+        return $postArray;
+    }
+
+    /**
+     * @return \TYPO3\CMS\Core\Localization\LanguageService
+     */
+    protected function getLanguageService()
+    {
+        return $GLOBALS['LANG'];
+    }
+}
diff --git a/typo3/sysext/core/Classes/Crypto/PasswordHashing/InvalidPasswordHashException.php b/typo3/sysext/core/Classes/Crypto/PasswordHashing/InvalidPasswordHashException.php
new file mode 100644 (file)
index 0000000..5cd809e
--- /dev/null
@@ -0,0 +1,23 @@
+<?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!
+ */
+
+/**
+ * InvalidPasswordHashException thrown if salting went wrong.
+ */
+class InvalidPasswordHashException extends \TYPO3\CMS\Core\Exception
+{
+}
diff --git a/typo3/sysext/core/Classes/Crypto/PasswordHashing/Md5PasswordHash.php b/typo3/sysext/core/Classes/Crypto/PasswordHashing/Md5PasswordHash.php
new file mode 100644 (file)
index 0000000..6bf5f08
--- /dev/null
@@ -0,0 +1,277 @@
+<?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!
+ */
+
+use TYPO3\CMS\Core\Compatibility\PublicMethodDeprecationTrait;
+use TYPO3\CMS\Core\Crypto\Random;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Class that implements MD5 salted hashing based on PHP's
+ * crypt() function.
+ *
+ * MD5 salted hashing with PHP's crypt() should be available
+ * on most of the systems.
+ */
+class Md5PasswordHash implements PasswordHashInterface
+{
+    use PublicMethodDeprecationTrait;
+
+    /**
+     * @var array
+     */
+    private $deprecatedPublicMethods = [
+        'isValidSalt' => 'Using Md5PasswordHash::isValidSalt() is deprecated and will not be possible anymore in TYPO3 v10.',
+        'base64Encode' => 'Using Md5PasswordHash::base64Encode() is deprecated and will not be possible anymore in TYPO3 v10.',
+    ];
+
+    /**
+     * Prefix for the password hash.
+     */
+    protected const PREFIX = '$1$';
+
+    /**
+     * Keeps a string for mapping an int to the corresponding
+     * base 64 character.
+     *
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    const ITOA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+
+    /**
+     * Method checks if a given plaintext password is correct by comparing it with
+     * a given salted hashed password.
+     *
+     * @param string $plainPW plain-text password to compare with salted hash
+     * @param string $saltedHashPW salted hash to compare plain-text password with
+     * @return bool TRUE, if plain-text password matches the salted hash, otherwise FALSE
+     */
+    public function checkPassword(string $plainPW, string $saltedHashPW): bool
+    {
+        $isCorrect = false;
+        if ($this->isValidSalt($saltedHashPW)) {
+            $isCorrect = \password_verify($plainPW, $saltedHashPW);
+        }
+        return $isCorrect;
+    }
+
+    /**
+     * Returns whether all prerequisites for the hashing methods are matched
+     *
+     * @return bool Method available
+     */
+    public function isAvailable(): bool
+    {
+        return (bool)CRYPT_MD5;
+    }
+
+    /**
+     * Method creates a salted hash for a given plaintext password
+     *
+     * @param string $password plaintext password to create a salted hash from
+     * @param string $salt Deprecated optional custom salt with setting to use
+     * @return string Salted hashed password
+     */
+    public function getHashedPassword(string $password, string $salt = null)
+    {
+        if ($salt !== null) {
+            trigger_error(static::class . ': using a custom salt is deprecated.', E_USER_DEPRECATED);
+        }
+        $saltedPW = null;
+        if (!empty($password)) {
+            if (empty($salt) || !$this->isValidSalt($salt)) {
+                $salt = $this->getGeneratedSalt();
+            }
+            $saltedPW = crypt($password, $this->applySettingsToSalt($salt));
+        }
+        return $saltedPW;
+    }
+
+    /**
+     * Checks whether a user's hashed password needs to be replaced with a new hash.
+     *
+     * This is typically called during the login process when the plain text
+     * password is available.  A new hash is needed when the desired iteration
+     * count has changed through a change in the variable $hashCount or HASH_COUNT.
+     *
+     * @param string $passString Salted hash to check if it needs an update
+     * @return bool TRUE if salted hash needs an update, otherwise FALSE
+     */
+    public function isHashUpdateNeeded(string $passString): bool
+    {
+        return false;
+    }
+
+    /**
+     * Method determines if a given string is a valid salted hashed password.
+     *
+     * @param string $saltedPW String to check
+     * @return bool TRUE if it's valid salted hashed password, otherwise FALSE
+     */
+    public function isValidSaltedPW(string $saltedPW): bool
+    {
+        $isValid = !strncmp(self::PREFIX, $saltedPW, strlen(self::PREFIX));
+        if ($isValid) {
+            $isValid = $this->isValidSalt($saltedPW);
+        }
+        return $isValid;
+    }
+
+    /**
+     * Generates a random base 64-encoded salt prefixed and suffixed with settings for the hash.
+     *
+     * Proper use of salts may defeat a number of attacks, including:
+     * - The ability to try candidate passwords against multiple hashes at once.
+     * - The ability to use pre-hashed lists of candidate passwords.
+     * - The ability to determine whether two users have the same (or different)
+     * password without actually having to guess one of the passwords.
+     *
+     * @return string A character string containing settings and a random salt
+     */
+    protected function getGeneratedSalt(): string
+    {
+        $randomBytes = GeneralUtility::makeInstance(Random::class)->generateRandomBytes(6);
+        return $this->base64Encode($randomBytes, 6);
+    }
+
+    /**
+     * Method applies settings (prefix, suffix) to a salt.
+     *
+     * @param string $salt A salt to apply setting to
+     * @return string Salt with setting
+     */
+    protected function applySettingsToSalt(string $salt): string
+    {
+        $saltWithSettings = $salt;
+        $reqLenBase64 = $this->getLengthBase64FromBytes(6);
+        // Salt without setting
+        if (strlen($salt) == $reqLenBase64) {
+            $saltWithSettings = self::PREFIX . $salt . '$';
+        }
+        return $saltWithSettings;
+    }
+
+    /**
+     * Returns a string for mapping an int to the corresponding base 64 character.
+     *
+     * @return string String for mapping an int to the corresponding base 64 character
+     */
+    protected function getItoa64(): string
+    {
+        return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+    }
+
+    /**
+     * Method determines if a given string is a valid salt
+     *
+     * @param string $salt String to check
+     * @return bool TRUE if it's valid salt, otherwise FALSE
+     */
+    protected function isValidSalt(string $salt): bool
+    {
+        $isValid = ($skip = false);
+        $reqLenBase64 = $this->getLengthBase64FromBytes(6);
+        if (strlen($salt) >= $reqLenBase64) {
+            // Salt with prefixed setting
+            if (!strncmp('$', $salt, 1)) {
+                if (!strncmp(self::PREFIX, $salt, strlen(self::PREFIX))) {
+                    $isValid = true;
+                    $salt = substr($salt, strlen(self::PREFIX));
+                } else {
+                    $skip = true;
+                }
+            }
+            // Checking base64 characters
+            if (!$skip && strlen($salt) >= $reqLenBase64) {
+                if (preg_match('/^[' . preg_quote($this->getItoa64(), '/') . ']{' . $reqLenBase64 . ',' . $reqLenBase64 . '}$/', substr($salt, 0, $reqLenBase64))) {
+                    $isValid = true;
+                }
+            }
+        }
+        return $isValid;
+    }
+
+    /**
+     * Encodes bytes into printable base 64 using the *nix standard from crypt().
+     *
+     * @param string $input The string containing bytes to encode.
+     * @param int $count The number of characters (bytes) to encode.
+     * @return string Encoded string
+     */
+    protected function base64Encode(string $input, int $count): string
+    {
+        $output = '';
+        $i = 0;
+        $itoa64 = $this->getItoa64();
+        do {
+            $value = ord($input[$i++]);
+            $output .= $itoa64[$value & 63];
+            if ($i < $count) {
+                $value |= ord($input[$i]) << 8;
+            }
+            $output .= $itoa64[$value >> 6 & 63];
+            if ($i++ >= $count) {
+                break;
+            }
+            if ($i < $count) {
+                $value |= ord($input[$i]) << 16;
+            }
+            $output .= $itoa64[$value >> 12 & 63];
+            if ($i++ >= $count) {
+                break;
+            }
+            $output .= $itoa64[$value >> 18 & 63];
+        } while ($i < $count);
+        return $output;
+    }
+
+    /**
+     * Method determines required length of base64 characters for a given
+     * length of a byte string.
+     *
+     * @param int $byteLength Length of bytes to calculate in base64 chars
+     * @return int Required length of base64 characters
+     */
+    protected function getLengthBase64FromBytes(int $byteLength): int
+    {
+        // Calculates bytes in bits in base64
+        return (int)ceil($byteLength * 8 / 6);
+    }
+
+    /**
+     * Returns setting string of MD5 salted hashes.
+     *
+     * @return string Setting string of MD5 salted hashes
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function getSetting(): string
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return self::PREFIX;
+    }
+
+    /**
+     * Returns length of a MD5 salt in bytes.
+     *
+     * @return int Length of a MD5 salt in bytes
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function getSaltLength(): int
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return 6;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Crypto/PasswordHashing/PasswordHashFactory.php b/typo3/sysext/core/Classes/Crypto/PasswordHashing/PasswordHashFactory.php
new file mode 100644 (file)
index 0000000..52fb94b
--- /dev/null
@@ -0,0 +1,250 @@
+<?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!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Factory class to find and return hash instances of given hashed passwords
+ * and to find and return default hash instances to hash new passwords.
+ */
+class PasswordHashFactory
+{
+    /**
+     * An instance of the salted hashing method.
+     * This member is set in the getSaltingInstance() function.
+     *
+     * @var PasswordHashInterface
+     * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
+     */
+    protected static $instance;
+
+    /**
+     * Find a hash class that handles given hash and return an instance of it.
+     *
+     * @param string $hash Given hash to find instance for
+     * @param string $mode 'FE' for frontend users, 'BE' for backend users
+     * @return PasswordHashInterface Object that can handle given hash
+     * @throws \LogicException
+     * @throws \InvalidArgumentException
+     * @throws InvalidPasswordHashException If no class was found that handles given hash
+     */
+    public function get(string $hash, string $mode): PasswordHashInterface
+    {
+        if ($mode !== 'FE' && $mode !== 'BE') {
+            throw new \InvalidArgumentException('Mode must be either \'FE\' or \'BE\', ' . $mode . ' given.', 1533948312);
+        }
+
+        $registeredHashClasses = static::getRegisteredSaltedHashingMethods();
+
+        if (empty($GLOBALS['TYPO3_CONF_VARS'][$mode]['passwordHashing']['className'])
+            || !isset($GLOBALS['TYPO3_CONF_VARS'][$mode]['passwordHashing']['options'])
+            || !is_array($GLOBALS['TYPO3_CONF_VARS'][$mode]['passwordHashing']['options'])
+        ) {
+            throw new \LogicException(
+                'passwordHashing configuration of ' . $mode . ' broken',
+                1533949053
+            );
+        }
+        $defaultHashClassName = $GLOBALS['TYPO3_CONF_VARS'][$mode]['passwordHashing']['className'];
+        $defaultHashOptions = (array)$GLOBALS['TYPO3_CONF_VARS'][$mode]['passwordHashing']['options'];
+
+        foreach ($registeredHashClasses as $className) {
+            if ($className === $defaultHashClassName) {
+                $hashInstance = GeneralUtility::makeInstance($className, $defaultHashOptions);
+            } else {
+                $hashInstance = GeneralUtility::makeInstance($className);
+            }
+            if (!$hashInstance instanceof PasswordHashInterface) {
+                throw new \LogicException('Class ' . $className . ' does not implement PasswordHashInterface', 1533818569);
+            }
+            if ($hashInstance->isAvailable() && $hashInstance->isValidSaltedPW($hash)) {
+                return $hashInstance;
+            }
+        }
+        // Do not add the hash to the exception to prevent information disclosure
+        throw new InvalidPasswordHashException('No implementation found that handles given hash.', 1533818591);
+    }
+
+    /**
+     * Determine configured default hash method and return an instance of the class representing it.
+     *
+     * @param string $mode 'FE' for frontend users, 'BE' for backend users
+     * @return PasswordHashInterface Class instance that is configured as default hash method
+     * @throws \InvalidArgumentException
+     * @throws \LogicException
+     * @throws InvalidPasswordHashException If configuration is broken
+     */
+    public function getDefaultHashInstance(string $mode): PasswordHashInterface
+    {
+        if ($mode !== 'FE' && $mode !== 'BE') {
+            throw new \InvalidArgumentException('Mode must be either \'FE\' or \'BE\', ' . $mode . ' given.', 1533820041);
+        }
+
+        if (empty($GLOBALS['TYPO3_CONF_VARS'][$mode]['passwordHashing']['className'])
+            || !isset($GLOBALS['TYPO3_CONF_VARS'][$mode]['passwordHashing']['options'])
+            || !is_array($GLOBALS['TYPO3_CONF_VARS'][$mode]['passwordHashing']['options'])
+        ) {
+            throw new \LogicException(
+                'passwordHashing configuration of ' . $mode . ' broken',
+                1533950622
+            );
+        }
+
+        $defaultHashClassName = $GLOBALS['TYPO3_CONF_VARS'][$mode]['passwordHashing']['className'];
+        $defaultHashOptions = $GLOBALS['TYPO3_CONF_VARS'][$mode]['passwordHashing']['options'];
+        $availableHashClasses = static::getRegisteredSaltedHashingMethods();
+
+        if (!in_array($defaultHashClassName, $availableHashClasses, true)) {
+            throw new InvalidPasswordHashException(
+                'Configured default hash method ' . $defaultHashClassName . ' is not registered',
+                1533820194
+            );
+        }
+        $hashInstance =  GeneralUtility::makeInstance($defaultHashClassName, $defaultHashOptions);
+        if (!$hashInstance instanceof PasswordHashInterface) {
+            throw new \LogicException(
+                'Configured default hash method ' . $defaultHashClassName . ' is not an instance of PasswordHashInterface',
+                1533820281
+            );
+        }
+        if (!$hashInstance->isAvailable()) {
+            throw new InvalidPasswordHashException(
+                'Configured default hash method ' . $defaultHashClassName . ' is not available, missing php requirement?',
+                1533822084
+            );
+        }
+        return $hashInstance;
+    }
+
+    /**
+     * Returns list of all registered hashing methods. Used eg. in
+     * extension configuration to select the default hashing method.
+     *
+     * @return array
+     * @throws \RuntimeException
+     */
+    public static function getRegisteredSaltedHashingMethods(): array
+    {
+        $saltMethods = $GLOBALS['TYPO3_CONF_VARS']['SYS']['availablePasswordHashAlgorithms'];
+        if (!is_array($saltMethods) || empty($saltMethods)) {
+            throw new \RuntimeException('No password hash methods configured', 1533948733);
+        }
+        if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/saltedpasswords']['saltMethods'])) {
+            trigger_error(
+                'Registering additional hash algorithms in $GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'ext/saltedpasswords\'][\'saltMethods\']'
+                . ' has been deprecated. Extend $GLOBALS[\'TYPO3_CONF_VARS\'][\'SYS\'][\'availablePasswordHashAlgorithms\'] instead',
+                E_USER_DEPRECATED
+            );
+            $configuredMethods = (array)$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/saltedpasswords']['saltMethods'];
+            if (!empty($configuredMethods)) {
+                $saltMethods = array_merge($saltMethods, $configuredMethods);
+            }
+        }
+        return $saltMethods;
+    }
+
+    /**
+     * Obtains a salting hashing method instance.
+     *
+     * This function will return an instance of a class that implements
+     * \TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashInterface
+     *
+     * Use parameter NULL to reset the factory!
+     *
+     * @param string|null $saltedHash Salted hashed password to determine the type of used method from or NULL to reset to the default type
+     * @param string $mode The TYPO3 mode (FE or BE) saltedpasswords shall be used for
+     * @return PasswordHashInterface|null An instance of salting hash method class or null if given hash is not supported
+     * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
+     */
+    public static function getSaltingInstance($saltedHash = '', $mode = TYPO3_MODE)
+    {
+        trigger_error(
+            'This method is obsolete and will be removed in TYPO3 v10. Use get() and getDefaultHashInstance() instead.',
+            E_USER_DEPRECATED
+        );
+        // Creating new instance when
+        // * no instance existing
+        // * a salted hash given to determine salted hashing method from
+        // * a NULL parameter given to reset instance back to default method
+        if (!is_object(self::$instance) || !empty($saltedHash) || $saltedHash === null) {
+            // Determine method by checking the given hash
+            if (!empty($saltedHash)) {
+                $result = self::determineSaltingHashingMethod($saltedHash, $mode);
+                if (!$result) {
+                    self::$instance = null;
+                }
+            } else {
+                $classNameToUse = SaltedPasswordsUtility::getDefaultSaltingHashingMethod($mode);
+                self::$instance = GeneralUtility::makeInstance($classNameToUse);
+            }
+        }
+        return self::$instance;
+    }
+
+    /**
+     * Method tries to determine the salting hashing method used for given salt.
+     *
+     * Method implicitly sets the instance of the found method object in the class property when found.
+     *
+     * @param string $saltedHash
+     * @param string $mode (optional) The TYPO3 mode (FE or BE) saltedpasswords shall be used for
+     * @return bool TRUE, if salting hashing method has been found, otherwise FALSE
+     * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
+     */
+    public static function determineSaltingHashingMethod(string $saltedHash, $mode = TYPO3_MODE): bool
+    {
+        trigger_error(
+            'This method is obsolete and will be removed in TYPO3 v10.',
+            E_USER_DEPRECATED
+        );
+        $registeredMethods = static::getRegisteredSaltedHashingMethods();
+        $defaultClassName = SaltedPasswordsUtility::getDefaultSaltingHashingMethod($mode);
+        unset($registeredMethods[$defaultClassName]);
+        // place the default method first in the order
+        $registeredMethods = [$defaultClassName => $defaultClassName] + $registeredMethods;
+        $methodFound = false;
+        foreach ($registeredMethods as $method) {
+            $objectInstance = GeneralUtility::makeInstance($method);
+            if ($objectInstance instanceof PasswordHashInterface && $objectInstance->isAvailable()) {
+                $methodFound = $objectInstance->isValidSaltedPW($saltedHash);
+                if ($methodFound) {
+                    self::$instance = $objectInstance;
+                    break;
+                }
+            }
+        }
+        return $methodFound;
+    }
+
+    /**
+     * Method sets a custom salting hashing method class.
+     *
+     * @param string $resource Object resource to use (e.g. \TYPO3\CMS\Core\Crypto\PasswordHashing\BlowfishPasswordHash::class)
+     * @return PasswordHashInterface|null An instance of salting hashing method object or null
+     * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
+     */
+    public static function setPreferredHashingMethod(string $resource)
+    {
+        trigger_error('This method is obsolete and will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        self::$instance = null;
+        $objectInstance = GeneralUtility::makeInstance($resource);
+        if ($objectInstance instanceof PasswordHashInterface) {
+            self::$instance = $objectInstance;
+        }
+        return self::$instance;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Crypto/PasswordHashing/PasswordHashInterface.php b/typo3/sysext/core/Classes/Crypto/PasswordHashing/PasswordHashInterface.php
new file mode 100644 (file)
index 0000000..14e6eeb
--- /dev/null
@@ -0,0 +1,68 @@
+<?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!
+ */
+
+/**
+ * Interface with public methods needed to be implemented
+ * in a salting hashing class.
+ */
+interface PasswordHashInterface
+{
+    /**
+     * Method checks if a given plaintext password is correct by comparing it with
+     * a given salted hashed password.
+     *
+     * @param string $plainPW plain-text password to compare with salted hash
+     * @param string $saltedHashPW Salted hash to compare plain-text password with
+     * @return bool TRUE, if plaintext password is correct, otherwise FALSE
+     */
+    public function checkPassword(string $plainPW, string $saltedHashPW): bool;
+
+    /**
+     * Returns whether all prequesites for the hashing methods are matched
+     *
+     * @return bool Method available
+     */
+    public function isAvailable(): bool;
+
+    /**
+     * Method creates a salted hash for a given plaintext password
+     *
+     * @param string $password Plaintext password to create a salted hash from
+     * @return string Salted hashed password
+     */
+    public function getHashedPassword(string $password);
+
+    /**
+     * Checks whether a user's hashed password needs to be replaced with a new hash.
+     *
+     * This is typically called during the login process when the plain text
+     * password is available.  A new hash is needed when the desired iteration
+     * count has changed through a change in the variable $hashCount or HASH_COUNT.
+     *
+     * @param string $passString Salted hash to check if it needs an update
+     * @return bool TRUE if salted hash needs an update, otherwise FALSE
+     */
+    public function isHashUpdateNeeded(string $passString): bool;
+
+    /**
+     * Method determines if a given string is a valid salted hashed password.
+     *
+     * @param string $saltedPW String to check
+     * @return bool TRUE if it's valid salted hashed password, otherwise FALSE
+     */
+    public function isValidSaltedPW(string $saltedPW): bool;
+}
diff --git a/typo3/sysext/core/Classes/Crypto/PasswordHashing/Pbkdf2PasswordHash.php b/typo3/sysext/core/Classes/Crypto/PasswordHashing/Pbkdf2PasswordHash.php
new file mode 100644 (file)
index 0000000..4f97252
--- /dev/null
@@ -0,0 +1,434 @@
+<?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!
+ */
+
+use TYPO3\CMS\Core\Compatibility\PublicMethodDeprecationTrait;
+use TYPO3\CMS\Core\Crypto\Random;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Class that implements PBKDF2 salted hashing based on PHP's
+ * hash_pbkdf2() function.
+ */
+class Pbkdf2PasswordHash implements PasswordHashInterface
+{
+    use PublicMethodDeprecationTrait;
+
+    /**
+     * @var array
+     */
+    private $deprecatedPublicMethods = [
+        'isValidSalt' => 'Using Pbkdf2PasswordHash::isValidSalt() is deprecated and will not be possible anymore in TYPO3 v10.',
+        'base64Encode' => 'Using Pbkdf2PasswordHash::base64Encode() is deprecated and will not be possible anymore in TYPO3 v10.',
+        'base64Decode' => 'Using Pbkdf2PasswordHash::base64Decode() is deprecated and will not be possible anymore in TYPO3 v10.',
+    ];
+
+    /**
+     * Prefix for the password hash.
+     */
+    protected const PREFIX = '$pbkdf2-sha256$';
+
+    /**
+     * @var array The default log2 number of iterations for password stretching.
+     */
+    protected $options = [
+        'hash_count' => 25000
+    ];
+
+    /**
+     * Keeps a string for mapping an int to the corresponding
+     * base 64 character.
+     *
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    const ITOA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+
+    /**
+     * The default number of iterations for password stretching.
+     *
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    const HASH_COUNT = 25000;
+
+    /**
+     * The default maximum allowed number of iterations for password stretching.
+     *
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    const MAX_HASH_COUNT = 10000000;
+
+    /**
+     * The default minimum allowed number of iterations for password stretching.
+     *
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    const MIN_HASH_COUNT = 1000;
+
+    /**
+     * Constructor sets options if given
+     *
+     * @param array $options
+     */
+    public function __construct(array $options = [])
+    {
+        $newOptions = $this->options;
+        if (isset($options['hash_count'])) {
+            if ((int)$options['hash_count'] < 1000 || (int)$options['hash_count'] > 10000000) {
+                throw new \InvalidArgumentException(
+                    'hash_count must not be lower than 1000 or bigger than 10000000',
+                    1533903544
+                );
+            }
+            $newOptions['hash_count'] = (int)$options['hash_count'];
+        }
+        $this->options = $newOptions;
+    }
+
+    /**
+     * Method checks if a given plaintext password is correct by comparing it with
+     * a given salted hashed password.
+     *
+     * @param string $plainPW plain-text password to compare with salted hash
+     * @param string $saltedHashPW salted hash to compare plain-text password with
+     * @return bool TRUE, if plain-text password matches the salted hash, otherwise FALSE
+     */
+    public function checkPassword(string $plainPW, string $saltedHashPW): bool
+    {
+        return $this->isValidSalt($saltedHashPW) && hash_equals($this->getHashedPasswordInternal($plainPW, $saltedHashPW), $saltedHashPW);
+    }
+
+    /**
+     * Returns whether all prerequisites for the hashing methods are matched
+     *
+     * @return bool Method available
+     */
+    public function isAvailable(): bool
+    {
+        return function_exists('hash_pbkdf2');
+    }
+
+    /**
+     * Method creates a salted hash for a given plaintext password
+     *
+     * @param string $password plaintext password to create a salted hash from
+     * @param string $salt Deprecated optional custom salt with setting to use
+     * @return string|null Salted hashed password
+     */
+    public function getHashedPassword(string $password, string $salt = null)
+    {
+        if ($salt !== null) {
+            trigger_error(static::class . ': using a custom salt is deprecated.', E_USER_DEPRECATED);
+        }
+        return $this->getHashedPasswordInternal($password, $salt);
+    }
+
+    /**
+     * Method determines if a given string is a valid salted hashed password.
+     *
+     * @param string $saltedPW String to check
+     * @return bool TRUE if it's valid salted hashed password, otherwise FALSE
+     */
+    public function isValidSaltedPW(string $saltedPW): bool
+    {
+        $isValid = !strncmp(self::PREFIX, $saltedPW, strlen(self::PREFIX));
+        if ($isValid) {
+            $isValid = $this->isValidSalt($saltedPW);
+        }
+        return $isValid;
+    }
+
+    /**
+     * Checks whether a user's hashed password needs to be replaced with a new hash.
+     *
+     * This is typically called during the login process when the plain text
+     * password is available.  A new hash is needed when the desired iteration
+     * count has changed through a change in the variable $this->options['hashCount'].
+     *
+     * @param string $saltedPW Salted hash to check if it needs an update
+     * @return bool TRUE if salted hash needs an update, otherwise FALSE
+     */
+    public function isHashUpdateNeeded(string $saltedPW): bool
+    {
+        // Check whether this was an updated password.
+        if (strncmp($saltedPW, self::PREFIX, strlen(self::PREFIX)) || !$this->isValidSalt($saltedPW)) {
+            return true;
+        }
+        // Check whether the iteration count used differs from the standard number.
+        $iterationCount = $this->getIterationCount($saltedPW);
+        return $iterationCount !== null && $iterationCount < $this->options['hash_count'];
+    }
+
+    /**
+     * Parses the log2 iteration count from a stored hash or setting string.
+     *
+     * @param string $setting Complete hash or a hash's setting string or to get log2 iteration count from
+     * @return int|null Used hashcount for given hash string
+     */
+    protected function getIterationCount(string $setting)
+    {
+        $iterationCount = null;
+        $setting = substr($setting, strlen(self::PREFIX));
+        $firstSplitPos = strpos($setting, '$');
+        // Hashcount existing
+        if ($firstSplitPos !== false
+            && $firstSplitPos <= strlen((string)10000000)
+            && is_numeric(substr($setting, 0, $firstSplitPos))
+        ) {
+            $iterationCount = (int)substr($setting, 0, $firstSplitPos);
+        }
+        return $iterationCount;
+    }
+
+    /**
+     * Method creates a salted hash for a given plaintext password
+     *
+     * @param string $password plaintext password to create a salted hash from
+     * @param string $salt Optional custom salt with setting to use
+     * @return string|null Salted hashed password
+     */
+    protected function getHashedPasswordInternal(string $password, string $salt = null)
+    {
+        $saltedPW = null;
+        if ($password !== '') {
+            $hashCount = $this->options['hash_count'];
+            if (empty($salt) || !$this->isValidSalt($salt)) {
+                $salt = $this->getGeneratedSalt();
+            } else {
+                $hashCount = $this->getIterationCount($salt);
+                $salt = $this->getStoredSalt($salt);
+            }
+            $hash = hash_pbkdf2('sha256', $password, $salt, $hashCount, 0, true);
+            $saltWithSettings = $salt;
+            // salt without setting
+            if (strlen($salt) === 16) {
+                $saltWithSettings = self::PREFIX . sprintf('%02u', $hashCount) . '$' . $this->base64Encode($salt, 16);
+            }
+            $saltedPW = $saltWithSettings . '$' . $this->base64Encode($hash, strlen($hash));
+        }
+        return $saltedPW;
+    }
+
+    /**
+     * Generates a random base 64-encoded salt prefixed and suffixed with settings for the hash.
+     *
+     * Proper use of salts may defeat a number of attacks, including:
+     * - The ability to try candidate passwords against multiple hashes at once.
+     * - The ability to use pre-hashed lists of candidate passwords.
+     * - The ability to determine whether two users have the same (or different)
+     * password without actually having to guess one of the passwords.
+     *
+     * @return string A character string containing settings and a random salt
+     */
+    protected function getGeneratedSalt(): string
+    {
+        return GeneralUtility::makeInstance(Random::class)->generateRandomBytes(16);
+    }
+
+    /**
+     * Parses the salt out of a salt string including settings. If the salt does not include settings
+     * it is returned unmodified.
+     *
+     * @param string $salt
+     * @return string
+     */
+    protected function getStoredSalt(string $salt): string
+    {
+        if (!strncmp('$', $salt, 1)) {
+            if (!strncmp(self::PREFIX, $salt, strlen(self::PREFIX))) {
+                $saltParts = GeneralUtility::trimExplode('$', $salt, 4);
+                $salt = $saltParts[2];
+            }
+        }
+        return $this->base64Decode($salt);
+    }
+
+    /**
+     * Returns a string for mapping an int to the corresponding base 64 character.
+     *
+     * @return string String for mapping an int to the corresponding base 64 character
+     */
+    protected function getItoa64(): string
+    {
+        return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+    }
+
+    /**
+     * Method determines if a given string is a valid salt.
+     *
+     * @param string $salt String to check
+     * @return bool TRUE if it's valid salt, otherwise FALSE
+     */
+    protected function isValidSalt(string $salt): bool
+    {
+        $isValid = ($skip = false);
+        $reqLenBase64 = $this->getLengthBase64FromBytes(16);
+        if (strlen($salt) >= $reqLenBase64) {
+            // Salt with prefixed setting
+            if (!strncmp('$', $salt, 1)) {
+                if (!strncmp(self::PREFIX, $salt, strlen(self::PREFIX))) {
+                    $isValid = true;
+                    $salt = substr($salt, strrpos($salt, '$') + 1);
+                } else {
+                    $skip = true;
+                }
+            }
+            // Checking base64 characters
+            if (!$skip && strlen($salt) >= $reqLenBase64) {
+                if (preg_match('/^[' . preg_quote($this->getItoa64(), '/') . ']{' . $reqLenBase64 . ',' . $reqLenBase64 . '}$/', substr($salt, 0, $reqLenBase64))) {
+                    $isValid = true;
+                }
+            }
+        }
+        return $isValid;
+    }
+
+    /**
+     * Method determines required length of base64 characters for a given
+     * length of a byte string.
+     *
+     * @param int $byteLength Length of bytes to calculate in base64 chars
+     * @return int Required length of base64 characters
+     */
+    protected function getLengthBase64FromBytes(int $byteLength): int
+    {
+        // Calculates bytes in bits in base64
+        return (int)ceil($byteLength * 8 / 6);
+    }
+
+    /**
+     * Adapted version of base64_encoding for compatibility with python passlib. The output of this function is
+     * is identical to base64_encode, except that it uses . instead of +, and omits trailing padding = and whitepsace.
+     *
+     * @param string $input The string containing bytes to encode.
+     * @param int $count The number of characters (bytes) to encode.
+     * @return string Encoded string
+     */
+    protected function base64Encode(string $input, int $count): string
+    {
+        $input = substr($input, 0, $count);
+        return rtrim(str_replace('+', '.', base64_encode($input)), " =\r\n\t\0\x0B");
+    }
+
+    /**
+     * Adapted version of base64_encoding for compatibility with python passlib. The output of this function is
+     * is identical to base64_encode, except that it uses . instead of +, and omits trailing padding = and whitepsace.
+     *
+     * @param string $value
+     * @return string
+     */
+    protected function base64Decode(string $value): string
+    {
+        return base64_decode(str_replace('.', '+', $value));
+    }
+
+    /**
+     * Method returns number of iterations for password stretching.
+     *
+     * @return int number of iterations for password stretching
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function getHashCount(): int
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return $this->options['hash_count'];
+    }
+
+    /**
+     * Method returns maximum allowed number of iterations for password stretching.
+     *
+     * @return int Maximum allowed number of iterations for password stretching
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function getMaxHashCount(): int
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return 10000000;
+    }
+
+    /**
+     * Method returns minimum allowed number of iterations for password stretching.
+     *
+     * @return int Minimum allowed number of iterations for password stretching
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function getMinHashCount(): int
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return 1000;
+    }
+
+    /**
+     * Returns length of a PBKDF2 salt in bytes.
+     *
+     * @return int Length of a PBKDF2 salt in bytes
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function getSaltLength(): int
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return 16;
+    }
+
+    /**
+     * Returns setting string of PBKDF2 salted hashes.
+     *
+     * @return string Setting string of PBKDF2 salted hashes
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function getSetting(): string
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return self::PREFIX;
+    }
+
+    /**
+     * Method sets number of iterations for password stretching.
+     *
+     * @param int $hashCount number of iterations for password stretching to set
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function setHashCount(int $hashCount = null)
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        if ($hashCount >= 1000 && $hashCount <= 10000000) {
+            $this->options['hash_count'] = $hashCount;
+        }
+    }
+
+    /**
+     * Method sets maximum allowed number of iterations for password stretching.
+     *
+     * @param int $maxHashCount Maximum allowed number of iterations for password stretching to set
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function setMaxHashCount(int $maxHashCount = null)
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        // Empty, max hash count is hard coded to 10000000
+    }
+
+    /**
+     * Method sets minimum allowed number of iterations for password stretching.
+     *
+     * @param int $minHashCount Minimum allowed number of iterations for password stretching to set
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function setMinHashCount(int $minHashCount = null)
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        // Empty, max hash count is hard coded to 1000
+    }
+}
diff --git a/typo3/sysext/core/Classes/Crypto/PasswordHashing/PhpassPasswordHash.php b/typo3/sysext/core/Classes/Crypto/PasswordHashing/PhpassPasswordHash.php
new file mode 100644 (file)
index 0000000..3f4e1e4
--- /dev/null
@@ -0,0 +1,460 @@
+<?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!
+ */
+
+use TYPO3\CMS\Core\Compatibility\PublicMethodDeprecationTrait;
+use TYPO3\CMS\Core\Crypto\Random;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Class that implements PHPass salted hashing based on Drupal's
+ * modified Openwall implementation.
+ *
+ * Derived from Drupal CMS
+ * original license: GNU General Public License (GPL)
+ *
+ * PHPass should work on every system.
+ * @see http://drupal.org/node/29706/
+ * @see http://www.openwall.com/phpass/
+ */
+class PhpassPasswordHash implements PasswordHashInterface
+{
+    use PublicMethodDeprecationTrait;
+
+    /**
+     * @var array
+     */
+    private $deprecatedPublicMethods = [
+        'isValidSalt' => 'Using PhpassPasswordHash::isValidSalt() is deprecated and will not be possible anymore in TYPO3 v10.',
+        'base64Encode' => 'Using PhpassPasswordHash::base64Encode() is deprecated and will not be possible anymore in TYPO3 v10.',
+    ];
+
+    /**
+     * Prefix for the password hash.
+     */
+    protected const PREFIX = '$P$';
+
+    /**
+     * @var array The default log2 number of iterations for password stretching.
+     */
+    protected $options = [
+        'hash_count' => 14
+    ];
+
+    /**
+     * Keeps a string for mapping an int to the corresponding
+     * base 64 character.
+     *
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    const ITOA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+
+    /**
+     * The default log2 number of iterations for password stretching.
+     *
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    const HASH_COUNT = 14;
+
+    /**
+     * The default maximum allowed log2 number of iterations for
+     * password stretching.
+     *
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    const MAX_HASH_COUNT = 24;
+
+    /**
+     * The default minimum allowed log2 number of iterations for
+     * password stretching.
+     *
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    const MIN_HASH_COUNT = 7;
+
+    /**
+     * Constructor sets options if given
+     *
+     * @param array $options
+     */
+    public function __construct(array $options = [])
+    {
+        $newOptions = $this->options;
+        if (isset($options['hash_count'])) {
+            if ((int)$options['hash_count'] < 7 || (int)$options['hash_count'] > 24) {
+                throw new \InvalidArgumentException(
+                    'hash_count must not be lower than 7 or bigger than 24',
+                    1533940454
+                );
+            }
+            $newOptions['hash_count'] = (int)$options['hash_count'];
+        }
+        $this->options = $newOptions;
+    }
+
+    /**
+     * Method checks if a given plaintext password is correct by comparing it with
+     * a given salted hashed password.
+     *
+     * @param string $plainPW Plain-text password to compare with salted hash
+     * @param string $saltedHashPW Salted hash to compare plain-text password with
+     * @return bool TRUE, if plain-text password matches the salted hash, otherwise FALSE
+     */
+    public function checkPassword(string $plainPW, string $saltedHashPW): bool
+    {
+        $hash = $this->cryptPassword($plainPW, $saltedHashPW);
+        return $hash && hash_equals($hash, $saltedHashPW);
+    }
+
+    /**
+     * Returns whether all prerequisites for the hashing methods are matched
+     *
+     * @return bool Method available
+     */
+    public function isAvailable(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Method creates a salted hash for a given plaintext password
+     *
+     * @param string $password Plaintext password to create a salted hash from
+     * @param string $salt Deprecated optional custom salt with setting to use
+     * @return string|null salted hashed password
+     */
+    public function getHashedPassword(string $password, string $salt = null)
+    {
+        if ($salt !== null) {
+            trigger_error(static::class . ': using a custom salt is deprecated.', E_USER_DEPRECATED);
+        }
+        $saltedPW = null;
+        if (!empty($password)) {
+            if (empty($salt) || !$this->isValidSalt($salt)) {
+                $salt = $this->getGeneratedSalt();
+            }
+            $saltedPW = $this->cryptPassword($password, $this->applySettingsToSalt($salt));
+        }
+        return $saltedPW;
+    }
+
+    /**
+     * Checks whether a user's hashed password needs to be replaced with a new hash.
+     *
+     * This is typically called during the login process when the plain text
+     * password is available. A new hash is needed when the desired iteration
+     * count has changed through a change in the variable $hashCount or HASH_COUNT.
+     *
+     * @param string $passString Salted hash to check if it needs an update
+     * @return bool TRUE if salted hash needs an update, otherwise FALSE
+     */
+    public function isHashUpdateNeeded(string $passString): bool
+    {
+        // Check whether this was an updated password.
+        if (strncmp($passString, '$P$', 3) || strlen($passString) != 34) {
+            return true;
+        }
+        // Check whether the iteration count used differs from the standard number.
+        return $this->getCountLog2($passString) < $this->options['hash_count'];
+    }
+
+    /**
+     * Method determines if a given string is a valid salted hashed password.
+     *
+     * @param string $saltedPW String to check
+     * @return bool TRUE if it's valid salted hashed password, otherwise FALSE
+     */
+    public function isValidSaltedPW(string $saltedPW): bool
+    {
+        $isValid = !strncmp(self::PREFIX, $saltedPW, strlen(self::PREFIX));
+        if ($isValid) {
+            $isValid = $this->isValidSalt($saltedPW);
+        }
+        return $isValid;
+    }
+
+    /**
+     * Method applies settings (prefix, hash count) to a salt.
+     *
+     * @param string $salt A salt to apply setting to
+     * @return string Salt with setting
+     */
+    protected function applySettingsToSalt(string $salt): string
+    {
+        $saltWithSettings = $salt;
+        $reqLenBase64 = $this->getLengthBase64FromBytes(6);
+        // Salt without setting
+        if (strlen($salt) == $reqLenBase64) {
+            // We encode the final log2 iteration count in base 64.
+            $itoa64 = $this->getItoa64();
+            $saltWithSettings = self::PREFIX . $itoa64[$this->options['hash_count']];
+            $saltWithSettings .= $salt;
+        }
+        return $saltWithSettings;
+    }
+
+    /**
+     * Hashes a password using a secure stretched hash.
+     *
+     * By using a salt and repeated hashing the password is "stretched". Its
+     * security is increased because it becomes much more computationally costly
+     * for an attacker to try to break the hash by brute-force computation of the
+     * hashes of a large number of plain-text words or strings to find a match.
+     *
+     * @param string $password Plain-text password to hash
+     * @param string $setting An existing hash or the output of getGeneratedSalt()
+     * @return mixed A string containing the hashed password (and salt)
+     */
+    protected function cryptPassword(string $password, string $setting)
+    {
+        $saltedPW = null;
+        $reqLenBase64 = $this->getLengthBase64FromBytes(6);
+        // Retrieving settings with salt
+        $setting = substr($setting, 0, strlen(self::PREFIX) + 1 + $reqLenBase64);
+        $count_log2 = $this->getCountLog2($setting);
+        // Hashes may be imported from elsewhere, so we allow != HASH_COUNT
+        if ($count_log2 >= 7 && $count_log2 <= 24) {
+            $salt = substr($setting, strlen(self::PREFIX) + 1, $reqLenBase64);
+            // We must use md5() or sha1() here since they are the only cryptographic
+            // primitives always available in PHP 5. To implement our own low-level
+            // cryptographic function in PHP would result in much worse performance and
+            // consequently in lower iteration counts and hashes that are quicker to crack
+            // (by non-PHP code).
+            $count = 1 << $count_log2;
+            $hash = md5($salt . $password, true);
+            do {
+                $hash = md5($hash . $password, true);
+            } while (--$count);
+            $saltedPW = $setting . $this->base64Encode($hash, 16);
+            // base64Encode() of a 16 byte MD5 will always be 22 characters.
+            return strlen($saltedPW) == 34 ? $saltedPW : false;
+        }
+        return $saltedPW;
+    }
+
+    /**
+     * Parses the log2 iteration count from a stored hash or setting string.
+     *
+     * @param string $setting Complete hash or a hash's setting string or to get log2 iteration count from
+     * @return int Used hashcount for given hash string
+     */
+    protected function getCountLog2(string $setting): int
+    {
+        return strpos($this->getItoa64(), $setting[strlen(self::PREFIX)]);
+    }
+
+    /**
+     * Generates a random base 64-encoded salt prefixed and suffixed with settings for the hash.
+     *
+     * Proper use of salts may defeat a number of attacks, including:
+     * - The ability to try candidate passwords against multiple hashes at once.
+     * - The ability to use pre-hashed lists of candidate passwords.
+     * - The ability to determine whether two users have the same (or different)
+     * password without actually having to guess one of the passwords.
+     *
+     * @return string A character string containing settings and a random salt
+     */
+    protected function getGeneratedSalt(): string
+    {
+        $randomBytes = GeneralUtility::makeInstance(Random::class)->generateRandomBytes(6);
+        return $this->base64Encode($randomBytes, 6);
+    }
+
+    /**
+     * Returns a string for mapping an int to the corresponding base 64 character.
+     *
+     * @return string String for mapping an int to the corresponding base 64 character
+     */
+    protected function getItoa64(): string
+    {
+        return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
+    }
+
+    /**
+     * Method determines if a given string is a valid salt.
+     *
+     * @param string $salt String to check
+     * @return bool TRUE if it's valid salt, otherwise FALSE
+     */
+    protected function isValidSalt(string $salt): bool
+    {
+        $isValid = ($skip = false);
+        $reqLenBase64 = $this->getLengthBase64FromBytes(6);
+        if (strlen($salt) >= $reqLenBase64) {
+            // Salt with prefixed setting
+            if (!strncmp('$', $salt, 1)) {
+                if (!strncmp(self::PREFIX, $salt, strlen(self::PREFIX))) {
+                    $isValid = true;
+                    $salt = substr($salt, strrpos($salt, '$') + 2);
+                } else {
+                    $skip = true;
+                }
+            }
+            // Checking base64 characters
+            if (!$skip && strlen($salt) >= $reqLenBase64) {
+                if (preg_match('/^[' . preg_quote($this->getItoa64(), '/') . ']{' . $reqLenBase64 . ',' . $reqLenBase64 . '}$/', substr($salt, 0, $reqLenBase64))) {
+                    $isValid = true;
+                }
+            }
+        }
+        return $isValid;
+    }
+
+    /**
+     * Encodes bytes into printable base 64 using the *nix standard from crypt().
+     *
+     * @param string $input The string containing bytes to encode.
+     * @param int $count The number of characters (bytes) to encode.
+     * @return string Encoded string
+     */
+    protected function base64Encode(string $input, int $count): string
+    {
+        $output = '';
+        $i = 0;
+        $itoa64 = $this->getItoa64();
+        do {
+            $value = ord($input[$i++]);
+            $output .= $itoa64[$value & 63];
+            if ($i < $count) {
+                $value |= ord($input[$i]) << 8;
+            }
+            $output .= $itoa64[$value >> 6 & 63];
+            if ($i++ >= $count) {
+                break;
+            }
+            if ($i < $count) {
+                $value |= ord($input[$i]) << 16;
+            }
+            $output .= $itoa64[$value >> 12 & 63];
+            if ($i++ >= $count) {
+                break;
+            }
+            $output .= $itoa64[$value >> 18 & 63];
+        } while ($i < $count);
+        return $output;
+    }
+
+    /**
+     * Method determines required length of base64 characters for a given
+     * length of a byte string.
+     *
+     * @param int $byteLength Length of bytes to calculate in base64 chars
+     * @return int Required length of base64 characters
+     */
+    protected function getLengthBase64FromBytes(int $byteLength): int
+    {
+        // Calculates bytes in bits in base64
+        return (int)ceil($byteLength * 8 / 6);
+    }
+
+    /**
+     * Method returns log2 number of iterations for password stretching.
+     *
+     * @return int log2 number of iterations for password stretching
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function getHashCount(): int
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return $this->options['hash_count'];
+    }
+
+    /**
+     * Method returns maximum allowed log2 number of iterations for password stretching.
+     *
+     * @return int Maximum allowed log2 number of iterations for password stretching
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function getMaxHashCount(): int
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return 24;
+    }
+
+    /**
+     * Method returns minimum allowed log2 number of iterations for password stretching.
+     *
+     * @return int Minimum allowed log2 number of iterations for password stretching
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function getMinHashCount(): int
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return 7;
+    }
+
+    /**
+     * Returns length of a Blowfish salt in bytes.
+     *
+     * @return int Length of a Blowfish salt in bytes
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function getSaltLength(): int
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return 6;
+    }
+
+    /**
+     * Returns setting string of PHPass salted hashes.
+     *
+     * @return string Setting string of PHPass salted hashes
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function getSetting(): string
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return self::PREFIX;
+    }
+
+    /**
+     * Method sets log2 number of iterations for password stretching.
+     *
+     * @param int $hashCount log2 number of iterations for password stretching to set
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function setHashCount(int $hashCount = null)
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        if ($hashCount >= 7 && $hashCount <= 24) {
+            $this->options['hash_count'] = $hashCount;
+        }
+    }
+
+    /**
+     * Method sets maximum allowed log2 number of iterations for password stretching.
+     *
+     * @param int $maxHashCount Maximum allowed log2 number of iterations for password stretching to set
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function setMaxHashCount(int $maxHashCount = null)
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        // Empty, max hash count is hard coded to 24
+    }
+
+    /**
+     * Method sets minimum allowed log2 number of iterations for password stretching.
+     *
+     * @param int $minHashCount Minimum allowed log2 number of iterations for password stretching to set
+     * @deprecated and will be removed in TYPO3 v10.0.
+     */
+    public function setMinHashCount(int $minHashCount = null)
+    {
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        // Empty, max hash count is hard coded to 7
+    }
+}
diff --git a/typo3/sysext/core/Classes/Crypto/PasswordHashing/SaltedPasswordService.php b/typo3/sysext/core/Classes/Crypto/PasswordHashing/SaltedPasswordService.php
new file mode 100644 (file)
index 0000000..03d361c
--- /dev/null
@@ -0,0 +1,247 @@
+<?php
+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!
+ */
+
+use TYPO3\CMS\Core\Authentication\AbstractAuthenticationService;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\TimeTracker\TimeTracker;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Class implements salted-password hashes authentication service.
+ * Contains authentication service class for salted hashed passwords.
+ *
+ * @deprecated since v9, will be removed in v10
+ */
+class SaltedPasswordService extends AbstractAuthenticationService
+{
+    /**
+     * Keeps class name.
+     *
+     * @var string
+     */
+    public $prefixId = 'tx_saltedpasswords_sv1';
+
+    /**
+     * Keeps extension key.
+     *
+     * @var string
+     */
+    public $extKey = 'saltedpasswords';
+
+    /**
+     * Keeps extension configuration.
+     *
+     * @var mixed
+     */
+    protected $extConf;
+
+    /**
+     * An instance of the salted hashing method.
+     * This member is set in the getSaltingInstance() function.
+     *
+     * @var \TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashInterface
+     */
+    protected $objInstanceSaltedPW;
+
+    /**
+     * Indicates whether the salted password authentication has failed.
+     *
+     * Prevents authentication bypass. See vulnerability report:
+     * { @link http://forge.typo3.org/issues/22030 }
+     *
+     * @var bool
+     */
+    protected $authenticationFailed = false;
+
+    /**
+     * Constructor deprecates this class.
+     */
+    public function __construct()
+    {
+        trigger_error('Class SaltedPasswordService has been deprecated since v9 and will be removed in v10', E_USER_DEPRECATED);
+    }
+
+    /**
+     * Set salted passwords extension configuration to $this->extConf
+     *
+     * @return bool TRUE
+     */
+    public function init()
+    {
+        $this->extConf = TYPO3\CMS\Core\Crypto\PasswordHashing\SaltedPasswordsUtility::returnExtConf();
+        parent::init();
+        return true;
+    }
+
+    /**
+     * Checks the login data with the user record data for builtin login method.
+     *
+     * @param array $user User data array
+     * @param array $loginData Login data array
+     * @param string $passwordCompareStrategy Password compare strategy
+     * @return bool TRUE if login data matched
+     */
+    public function compareUident(array $user, array $loginData, $passwordCompareStrategy = '')
+    {
+        $validPasswd = false;
+        $password = $loginData['uident_text'];
+        // Determine method used for given salted hashed password
+        // This calls deprecated getSaltingInstance(). This is "ok" since this SaltedPasswordsService in itself is deprecated.
+        $this->objInstanceSaltedPW = PasswordHashFactory::getSaltingInstance($user['password']);
+        // Existing record is in format of Salted Hash password
+        if (is_object($this->objInstanceSaltedPW)) {
+            $validPasswd = $this->objInstanceSaltedPW->checkPassword($password, $user['password']);
+            // Record is in format of Salted Hash password but authentication failed
+            // skip further authentication methods
+            if (!$validPasswd) {
+                $this->authenticationFailed = true;
+            }
+            $defaultHashingClassName = TYPO3\CMS\Core\Crypto\PasswordHashing\SaltedPasswordsUtility::getDefaultSaltingHashingMethod();
+            $skip = false;
+            // Test for wrong salted hashing method (only if current method is not related to default method)
+            if ($validPasswd && get_class($this->objInstanceSaltedPW) !== $defaultHashingClassName && !is_subclass_of($this->objInstanceSaltedPW, $defaultHashingClassName)) {
+                // Instantiate default method class
+                // This calls deprecated getSaltingInstance(). This is "ok" since this SaltedPasswordsService in itself is deprecated.
+                $this->objInstanceSaltedPW = PasswordHashFactory::getSaltingInstance(null);
+                $this->updatePassword((int)$user['uid'], ['password' => $this->objInstanceSaltedPW->getHashedPassword($password)]);
+            }
+            if ($validPasswd && !$skip && $this->objInstanceSaltedPW->isHashUpdateNeeded($user['password'])) {
+                $this->updatePassword((int)$user['uid'], ['password' => $this->objInstanceSaltedPW->getHashedPassword($password)]);
+            }
+        } else {
+            // Stored password is in deprecated salted hashing method
+            $hashingMethod = substr($user['password'], 0, 2);
+            if ($hashingMethod === 'M$') {
+                // Instantiate default method class
+                // This calls deprecated getSaltingInstance(). This is "ok" since this SaltedPasswordsService in itself is deprecated.
+                $this->objInstanceSaltedPW = PasswordHashFactory::getSaltingInstance(substr($user['password'], 1));
+                // md5 passwords that have been upgraded to salted passwords using old scheduler task
+                // @todo: The entire 'else' should be dropped in v10, admins had to upgrade users to salted passwords with v8 latest since the
+                // @todo: scheduler task has been dropped with v9, users should have had logged in in v9 era, this fallback is obsolete with v10.
+                $validPasswd = $this->objInstanceSaltedPW->checkPassword(md5($password), substr($user['password'], 1));
+
+                // Skip further authentication methods
+                if (!$validPasswd) {
+                    $this->authenticationFailed = true;
+                }
+            }
+            // Upgrade to a sane salt mechanism if password was correct
+            if ($validPasswd) {
+                // Instantiate default method class
+                // This calls deprecated getSaltingInstance(). This is "ok" since this SaltedPasswordsService in itself is deprecated.
+                $this->objInstanceSaltedPW = PasswordHashFactory::getSaltingInstance(null);
+                $this->updatePassword((int)$user['uid'], ['password' => $this->objInstanceSaltedPW->getHashedPassword($password)]);
+            }
+        }
+        return $validPasswd;
+    }
+
+    /**
+     * Method adds a further authUser method.
+     *
+     * Will return one of following authentication status codes:
+     * - 0 - authentication failure
+     * - 100 - just go on. User is not authenticated but there is still no reason to stop
+     * - 200 - the service was able to authenticate the user
+     *
+     * @param array $user Array containing FE user data of the logged user.
+     * @return int Authentication statuscode, one of 0,100 and 200
+     */
+    public function authUser(array $user)
+    {
+        $OK = 100;
+        // The salted password service can only work correctly, if a non empty username along with a non empty password is provided.
+        // Otherwise a different service is allowed to check for other login credentials
+        if ((string)$this->login['uident_text'] !== '' && (string)$this->login['uname'] !== '') {
+            $validPasswd = $this->compareUident($user, $this->login);
+            if (!$validPasswd) {
+                // Failed login attempt (wrong password)
+                $errorMessage = 'Login-attempt from ###IP###, username \'%s\', password not accepted!';
+                // No delegation to further services
+                if ($this->authenticationFailed) {
+                    $this->writeLogMessage(TYPO3_MODE . ' Authentication failed - wrong password for username \'%s\'', $this->login['uname']);
+                    $OK = 0;
+                } else {
+                    $this->writeLogMessage($errorMessage, $this->login['uname']);
+                }
+                $this->writelog(255, 3, 3, 1, $errorMessage, [
+                    $this->login['uname']
+                ]);
+                $this->logger->info(sprintf($errorMessage, $this->login['uname']));
+            } elseif ($validPasswd && $user['lockToDomain'] && strcasecmp($user['lockToDomain'], $this->authInfo['HTTP_HOST'])) {
+                // Lock domain didn't match, so error:
+                $errorMessage = 'Login-attempt from ###IP###, username \'%s\', locked domain \'%s\' did not match \'%s\'!';
+                $this->writeLogMessage($errorMessage, $user[$this->db_user['username_column']], $user['lockToDomain'], $this->authInfo['HTTP_HOST']);
+                $this->writelog(255, 3, 3, 1, $errorMessage, [
+                    $user[$this->db_user['username_column']],
+                    $user['lockToDomain'],
+                    $this->authInfo['HTTP_HOST']
+                ]);
+                $this->logger->info(sprintf($errorMessage, $user[$this->db_user['username_column']], $user['lockToDomain'], $this->authInfo['HTTP_HOST']));
+                $OK = 0;
+            } elseif ($validPasswd) {
+                $this->writeLogMessage(TYPO3_MODE . ' Authentication successful for username \'%s\'', $this->login['uname']);
+                $OK = 200;
+            }
+        }
+        return $OK;
+    }
+
+    /**
+     * Method updates a FE/BE user record - in this case a new password string will be set.
+     *
+     * @param int $uid uid of user record that will be updated
+     * @param mixed $updateFields Field values as key=>value pairs to be updated in database
+     */
+    protected function updatePassword($uid, $updateFields)
+    {
+        $connection = GeneralUtility::makeInstance(ConnectionPool::class)
+            ->getConnectionForTable($this->pObj->user_table);
+
+        $connection->update(
+            $this->pObj->user_table,
+            $updateFields,
+            ['uid' => (int)$uid]
+        );
+
+        $this->logger->notice('Automatic password update for user record in ' . $this->pObj->user_table . ' with uid ' . $uid);
+    }
+
+    /**
+     * Writes log message. Destination log depends on the current system mode.
+     * For FE the function writes to the admin panel log. For BE messages are
+     * sent to the system log. If developer log is enabled, messages are also
+     * sent there.
+     *
+     * This function accepts variable number of arguments and can format
+     * parameters. The syntax is the same as for sprintf()
+     *
+     * @param string $message Message to output
+     * @param array<int, mixed> $params
+     */
+    public function writeLogMessage($message, ...$params)
+    {
+        if (!empty($params)) {
+            $message = vsprintf($message, $params);
+        }
+        if (TYPO3_MODE === 'FE') {
+            /** @var TimeTracker $timeTracker */
+            $timeTracker = GeneralUtility::makeInstance(TimeTracker::class);
+            $timeTracker->setTSlogMessage($message);
+        }
+        $this->logger->notice($message);
+    }
+}
diff --git a/typo3/sysext/core/Classes/Crypto/PasswordHashing/SaltedPasswordsUtility.php b/typo3/sysext/core/Classes/Crypto/PasswordHashing/SaltedPasswordsUtility.php
new file mode 100644 (file)
index 0000000..261d4ce
--- /dev/null
@@ -0,0 +1,134 @@
+<?php
+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!
+ */
+
+use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * General library class.
+ *
+ * @deprecated since v9, will be removed in v10
+ */
+class SaltedPasswordsUtility
+{
+    /**
+     * Keeps this extension's key.
+     *
+     * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
+     */
+    const EXTKEY = 'saltedpasswords';
+
+    /**
+     * Calculates number of backend users, who have no saltedpasswords protection.
+     *
+     * @return int
+     * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
+     */
+    public static function getNumberOfBackendUsersWithInsecurePassword()
+    {
+        trigger_error('This method is obsolete and will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
+        $queryBuilder->getRestrictions()->removeAll();
+
+        $userCount = $queryBuilder
+            ->count('*')
+            ->from('be_users')
+            ->where(
+                $queryBuilder->expr()->neq('password', $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)),
+                $queryBuilder->expr()->notLike('password', $queryBuilder->createNamedParameter('$%', \PDO::PARAM_STR)),
+                $queryBuilder->expr()->notLike('password', $queryBuilder->createNamedParameter('M$%', \PDO::PARAM_STR))
+            )
+            ->execute()
+            ->fetchColumn();
+
+        return $userCount;
+    }
+
+    /**
+     * Returns extension configuration data from $TYPO3_CONF_VARS (configurable in Extension Manager)
+     *
+     * @param string $mode TYPO3_MODE, whether Configuration for Frontend or Backend should be delivered
+     * @return array Extension configuration data
+     * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
+     */
+    public static function returnExtConf($mode = TYPO3_MODE)
+    {
+        trigger_error('This method is obsolete and will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        $currentConfiguration = self::returnExtConfDefaults();
+        if (isset($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['saltedpasswords'])) {
+            $extensionConfiguration = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('saltedpasswords');
+            // Merge default configuration with modified configuration:
+            if (isset($extensionConfiguration[$mode])) {
+                $currentConfiguration = array_merge($currentConfiguration, $extensionConfiguration[$mode]);
+            }
+        }
+        return $currentConfiguration;
+    }
+
+    /**
+     * Returns default configuration of this extension.
+     *
+     * @return array Default extension configuration data for localconf.php
+     * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
+     */
+    public static function returnExtConfDefaults()
+    {
+        trigger_error('This method is obsolete and will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return [
+            'saltedPWHashingMethod' => \TYPO3\CMS\Core\Crypto\PasswordHashing\PhpassPasswordHash::class,
+        ];
+    }
+
+    /**
+     * Function determines the default(=configured) type of
+     * salted hashing method to be used.
+     *
+     * @param string $mode (optional) The TYPO3 mode (FE or BE) saltedpasswords shall be used for
+     * @return string Classname of object to be used
+     * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
+     */
+    public static function getDefaultSaltingHashingMethod($mode = TYPO3_MODE)
+    {
+        trigger_error('This method is obsolete and will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        $extConf = self::returnExtConf($mode);
+        $classNameToUse = \TYPO3\CMS\Core\Crypto\PasswordHashing\Md5PasswordHash::class;
+        if (in_array(
+            $extConf['saltedPWHashingMethod'],
+            \TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory::getRegisteredSaltedHashingMethods(),
+            true
+        )) {
+            $classNameToUse = $extConf['saltedPWHashingMethod'];
+        }
+        return $classNameToUse;
+    }
+
+    /**
+     * Returns information if salted password hashes are
+     * indeed used in the TYPO3_MODE.
+     *
+     * @return bool TRUE, if salted password hashes are used in the TYPO3_MODE, otherwise FALSE
+     * @deprecated in TYPO3 v9, will be removed in TYPO3 v10
+     */
+    public static function isUsageEnabled()
+    {
+        trigger_error(
+            'Method isUsageEnabled() has been deprecated with core v9, always returns true and will be removed with v10.',
+            E_USER_DEPRECATED
+        );
+        return true;
+    }
+}
index e0dd241..4789e28 100644 (file)
@@ -35,6 +35,8 @@ use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidPointerFieldValueExce
 use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
 use TYPO3\CMS\Core\Configuration\Richtext;
 use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
 use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\QueryHelper;
@@ -60,8 +62,6 @@ use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
 use TYPO3\CMS\Core\Versioning\VersionState;
-use TYPO3\CMS\Saltedpasswords\Exception\InvalidSaltException;
-use TYPO3\CMS\Saltedpasswords\Salt\SaltFactory;
 
 /**
  * The main data handler class which takes care of correctly updating and inserting records.
@@ -2954,16 +2954,16 @@ class DataHandler implements LoggerAwareInterface
                     $hashMethod = substr($value, 0, 2);
                     // The old scheduler task turned existing non-salted passwords into salted hashes by taking the simple md5
                     // and using that as 'password' and make a salted md5 from given hash. Those where then prefixed with 'M'.
-                    // SaltFactory->get($value) only recognizes these salts if we cut off the M again.
+                    // PasswordHashFactory->get($value) only recognizes these salts if we cut off the M again.
                     // @todo @deprecated: $isDeprecatedSaltedHash should be removed in v10.0 as dedicated breaking patch, similar
                     // @todo to authUser() of AuthenticationService::class
                     $isDeprecatedSaltedHash = $hashMethod === 'M$';
                     $tempValue = $isDeprecatedSaltedHash ? substr($value, 1) : $value;
-                    $hashFactory = GeneralUtility::makeInstance(SaltFactory::class);
+                    $hashFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
                     $mode = $table === 'fe_users' ? 'FE' : 'BE';
                     try {
                         $hashFactory->get($tempValue, $mode);
-                    } catch (InvalidSaltException $e) {
+                    } catch (InvalidPasswordHashException $e) {
                         // We got no salted password instance, incoming value must be a new plaintext password
                         // Get an instance of the current configured salted password strategy and hash the value
                         $newHashInstance = $hashFactory->getDefaultHashInstance($mode);
index 4cfb1bd..ebbf037 100644 (file)
@@ -84,6 +84,17 @@ class LocalizationFactory implements \TYPO3\CMS\Core\SingletonInterface
             trigger_error('There is a reference to "' . $fileReference . '", which has been moved to "EXT:' . $mapping[$filePath] . '". This fallback will be removed with TYPO3 v10.', E_USER_DEPRECATED);
             $fileReference = 'EXT:' . $mapping[$filePath];
         }
+        // @deprecated since TYPO3 v9, will be removed with TYPO3 v10
+        // this is a fallback to convert references to old 'saltedpasswords' locallang files to the new location
+        if (strpos($fileReference, 'EXT:saltedpasswords/Resources/Private/Language/') === 0) {
+            $mapping = [
+                'saltedpasswords/Resources/Private/Language/locallang.xlf' => 'core/Resources/Private/Language/locallang_deprecated_saltedpasswords.xlf',
+                'saltedpasswords/Resources/Private/Language/locallang_em.xlf' => 'core/Resources/Private/Language/locallang_deprecated_saltedpasswords_em.xlf',
+            ];
+            $filePath = substr($fileReference, 4);
+            trigger_error('There is a reference to "' . $fileReference . '", which has been moved to "EXT:' . $mapping[$filePath] . '". This fallback will be removed with TYPO3 v10.', E_USER_DEPRECATED);
+            $fileReference = 'EXT:' . $mapping[$filePath];
+        }
 
         $hash = md5($fileReference . $languageKey);
 
index d08d066..21a8108 100644 (file)
@@ -105,12 +105,12 @@ return [
         'reverseProxySSL' => '',
         'reverseProxyPrefixSSL' => '',
         'availablePasswordHashAlgorithms' => [
-            \TYPO3\CMS\Saltedpasswords\Salt\Argon2iSalt::class,
-            \TYPO3\CMS\Saltedpasswords\Salt\BcryptSalt::class,
-            \TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt::class,
-            \TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt::class,
-            \TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt::class,
-            \TYPO3\CMS\Saltedpasswords\Salt\Md5Salt::class,
+            \TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash::class,
+            \TYPO3\CMS\Core\Crypto\PasswordHashing\BcryptPasswordHash::class,
+            \TYPO3\CMS\Core\Crypto\PasswordHashing\Pbkdf2PasswordHash::class,
+            \TYPO3\CMS\Core\Crypto\PasswordHashing\PhpassPasswordHash::class,
+            \TYPO3\CMS\Core\Crypto\PasswordHashing\BlowfishPasswordHash::class,
+            \TYPO3\CMS\Core\Crypto\PasswordHashing\Md5PasswordHash::class,
         ],
         'caching' => [
             'cacheConfigurations' => [
@@ -1223,7 +1223,7 @@ return [
             ]
         ],
         'passwordHashing' => [
-            'className' => \TYPO3\CMS\Saltedpasswords\Salt\Argon2iSalt::class,
+            'className' => \TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash::class,
             'options' => [],
         ],
     ],
@@ -1283,7 +1283,7 @@ return [
             'unknown' => \TYPO3\CMS\Frontend\Typolink\LegacyLinkBuilder::class,
         ],
         'passwordHashing' => [
-            'className' => \TYPO3\CMS\Saltedpasswords\Salt\Argon2iSalt::class,
+            'className' => \TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash::class,
             'options' => [],
         ],
     ],
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-85833-ExtensionSaltedpasswordsMergedIntoCoreExtension.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-85833-ExtensionSaltedpasswordsMergedIntoCoreExtension.rst
new file mode 100644 (file)
index 0000000..28dde89
--- /dev/null
@@ -0,0 +1,62 @@
+.. include:: ../../Includes.txt
+
+==========================================================================
+Deprecation: #85833 - Extension saltedpasswords merged into core extension
+==========================================================================
+
+See :issue:`85833`
+
+Description
+===========
+
+The `saltedpasswords` extension has been merged into the `core` extension. All
+classes have been moved to the PHP namespace :php:`TYPO3\CMS\Core\Crypto\PasswordHashing`.
+
+The documentation has been moved to the Core API document and can be found
+`online <https://docs.typo3.org/typo3cms/CoreApiReference/stable/ApiOverview/PasswordHashing/>`_.
+
+Classes that have been marked as deprecated have been moved to the same namespace and will vanish in v10.
+
+The following classes have been renamed:
+
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Argon2iSalt::class` to :php:`TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash::class`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\BcryptSalt::class` to :php:`TYPO3\CMS\Core\Crypto\PasswordHashing\BcryptPasswordHash::class`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt::class` to :php:`TYPO3\CMS\Core\Crypto\PasswordHashing\BlowfishPasswordHash::class`
+* :php:`TYPO3\CMS\Saltedpasswords\Exception\InvalidSaltException::class` to :php:`TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException::class`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Md5Salt::class` to :php:`TYPO3\CMS\Core\Crypto\PasswordHashing\Md5PasswordHash::class`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\SaltFactory::class` to :php:`TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory::class`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\SaltInterface::class` to :php:`TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashInterface::class`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt::class` to :php:`TYPO3\CMS\Core\Crypto\PasswordHashing\Pbkdf2PasswordHash::class`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt::class` to :php:`TYPO3\CMS\Core\Crypto\PasswordHashing\PhpassPasswordHash::class`
+* (deprecated) :php:`TYPO3\CMS\Saltedpasswords\Salt\AbstractComposedSalt::class` to :php:`TYPO3\CMS\Core\Crypto\PasswordHashing\AbstractComposedSalt::class`
+* (deprecated) :php:`TYPO3\CMS\Saltedpasswords\Salt\ComposedSaltInterface::class` to :php:`TYPO3\CMS\Core\Crypto\PasswordHashing\ComposedPasswordHashInterface::class`
+* (deprecated) :php:`TYPO3\CMS\Saltedpasswords\Utility\ExensionManagerConfigurationUtility::class` to :php:`TYPO3\CMS\Core\Crypto\PasswordHashing\ExtensionManagerConfigurationUtility::class`
+* (deprecated) :php:`TYPO3\CMS\Saltedpasswords\SaltedPasswordsService::class` to :php:`TYPO3\CMS\Core\Crypto\PasswordHashing\SaltedPasswordService::class`
+* (deprecated) :php:`TYPO3\CMS\Saltedpasswords\Utility\SaltedPasswordsUtility::class` to :php:`TYPO3\CMS\Core\Crypto\PasswordHashing\SaltedPasswordsUtility::class`
+
+The following language files have been moved:
+
+* (deprecated) :file:`saltedpasswords/Resources/Private/Language/locallang.xlf` to :file:`core/Resources/Private/Language/locallang_deprecated_saltedpasswords.xlf`
+* (deprecated) :file:`saltedpasswords/Resources/Private/Language/locallang_em.xlf` to :file:`core/Resources/Private/Language/locallang_deprecated_saltedpasswords_em.xlf`
+
+Impact
+======
+
+This change is usually transparent for TYPO3 instances. The old class names have been defined as
+aliases to the new names. They will continue to work in core v9 and dropped only in v10.
+
+
+Affected Installations
+======================
+
+Almost no instance is directly affected by this change, most instances need no configuration change.
+In rare cases, if extensions directly deal with password hashing, class namespaces may need to be adapted.
+The extension scanner will find usages of old class names.
+
+
+Migration
+=========
+
+Use the new class names and drop usages of deprecated classes.
+
+.. index:: Backend, PHP-API, FullyScanned, ext:saltedpasswords
\ No newline at end of file
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Important-85833-SaltedpasswordsExtensionMergedIntoCoreExtension.rst b/typo3/sysext/core/Documentation/Changelog/master/Important-85833-SaltedpasswordsExtensionMergedIntoCoreExtension.rst
new file mode 100644 (file)
index 0000000..88ac12c
--- /dev/null
@@ -0,0 +1,34 @@
+.. include:: ../../Includes.txt
+
+========================================================================
+Important: #85833 - saltedpasswords extension merged into core extension
+========================================================================
+
+See :issue:`85833`
+
+Description
+===========
+
+The previously available system extension `saltedpasswords` has been removed as its
+functionality has been merged into `core`. All functionality is still available but
+renamed related to a better Hashing API.
+
+This resolves a hard cross-dependency between `EXT:core` and `EXT:saltedpasswords`
+since TYPO3 v6 as it was a sane default to properly use password hashes.
+
+Backwards compatibility is given by automatic upgrades of settings when visiting
+the Install Tool.
+
+For composer-based installations this means the dependency to `typo3/cms-saltedpasswords`
+can safely be removed via `composer remove typo3/cms-saltedpasswords`, although this is
+not mandatory due to the fact that `typo3/cms-core` is noted as a replacement for
+saltedpasswords in its composer.json file.
+
+In some edge-cases for non-composer-based installations it might be necessary to remove
+the `saltedpasswords` entry from :php:`typo3conf/PackagesStates.php`.
+
+Any checks for :php:`ExtensionManagementUtility::isLoaded('saltedpasswords')` in
+third-party extensions which were not necessary (as saltedpasswords had to be installed
+at any time anyways), can safely be removed.
+
+.. index:: Backend, NotScanned, ext:saltedpasswords
\ No newline at end of file
index 2582136..2382616 100644 (file)
@@ -7,4 +7,19 @@ return [
     'TYPO3\\CMS\\Core\\Tree\\TableConfiguration\\ExtJsArrayTreeRenderer' => \TYPO3\CMS\Core\Tree\TableConfiguration\ArrayTreeRenderer::class,
     'TYPO3\\CMS\\Core\\History\\RecordHistory' => \TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore::class,
     'TYPO3\\CMS\\Backend\\Routing\\PageUriBuilder' => \TYPO3\CMS\Core\Routing\PageUriBuilder::class,
+    'TYPO3\\CMS\\Saltedpasswords\\Salt\\AbstractSalt' => \TYPO3\CMS\Core\Crypto\PasswordHashing\AbstractComposedSalt::class,
+    'TYPO3\\CMS\\Saltedpasswords\\Salt\\AbstractComposedSalt' => \TYPO3\CMS\Core\Crypto\PasswordHashing\AbstractComposedSalt::class,
+    'TYPO3\\CMS\\Saltedpasswords\\Salt\\Argon2iSalt' => \TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash::class,
+    'TYPO3\\CMS\\Saltedpasswords\\Salt\\BcryptSalt' => \TYPO3\CMS\Core\Crypto\PasswordHashing\BcryptPasswordHash::class,
+    'TYPO3\\CMS\\Saltedpasswords\\Salt\\BlowfishSalt' => \TYPO3\CMS\Core\Crypto\PasswordHashing\BlowfishPasswordHash::class,
+    'TYPO3\\CMS\\Saltedpasswords\\Salt\\ComposedSaltInterface' => \TYPO3\CMS\Core\Crypto\PasswordHashing\ComposedPasswordHashInterface::class,
+    'TYPO3\\CMS\\Saltedpasswords\\Utility\\ExensionManagerConfigurationUtility' => \TYPO3\CMS\Core\Crypto\PasswordHashing\ExtensionManagerConfigurationUtility::class,
+    'TYPO3\\CMS\\Saltedpasswords\\Exception\\InvalidSaltException' => \TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException::class,
+    'TYPO3\\CMS\\Saltedpasswords\\Salt\\Md5Salt' => \TYPO3\CMS\Core\Crypto\PasswordHashing\Md5PasswordHash::class,
+    'TYPO3\\CMS\\Saltedpasswords\\Salt\\SaltFactory' => \TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory::class,
+    'TYPO3\\CMS\\Saltedpasswords\\Salt\\SaltInterface' => \TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashInterface::class,
+    'TYPO3\\CMS\\Saltedpasswords\\Salt\\Pbkdf2Salt' => \TYPO3\CMS\Core\Crypto\PasswordHashing\Pbkdf2PasswordHash::class,
+    'TYPO3\\CMS\\Saltedpasswords\\Salt\\PhpassSalt' => \TYPO3\CMS\Core\Crypto\PasswordHashing\PhpassPasswordHash::class,
+    'TYPO3\\CMS\\Saltedpasswords\\SaltedPasswordsService' => \TYPO3\CMS\Core\Crypto\PasswordHashing\SaltedPasswordService::class,
+    'TYPO3\\CMS\\Saltedpasswords\\Utility\\SaltedPasswordsUtility' => \TYPO3\CMS\Core\Crypto\PasswordHashing\SaltedPasswordsUtility::class,
 ];
index 0e31e02..60579af 100644 (file)
@@ -23,3 +23,60 @@ namespace TYPO3\CMS\Sv {
     {
     }
 }
+
+namespace TYPO3\CMS\Saltedpasswords {
+    class SaltedPasswordsService extends \TYPO3\CMS\Core\Crypto\PasswordHashing\SaltedPasswordService
+    {
+    }
+}
+
+namespace TYPO3\CMS\Saltedpasswords\Exception {
+    class InvalidSaltException extends \TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException
+    {
+    }
+}
+
+namespace TYPO3\CMS\Saltedpasswords\Salt {
+    class AbstractSalt extends \TYPO3\CMS\Core\Crypto\PasswordHashing\AbstractComposedSalt
+    {
+    }
+    class AbstractComposedSalt extends \TYPO3\CMS\Core\Crypto\PasswordHashing\AbstractComposedSalt
+    {
+    }
+    class Argon2iSalt extends \TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash
+    {
+    }
+    class BcryptSalt extends \TYPO3\CMS\Core\Crypto\PasswordHashing\BcryptPasswordHash
+    {
+    }
+    class BlowfishSalt extends \TYPO3\CMS\Core\Crypto\PasswordHashing\BlowfishPasswordHash
+    {
+    }
+    class ComposedSaltInterface extends \TYPO3\CMS\Core\Crypto\PasswordHashing\ComposedPasswordHashInterface
+    {
+    }
+    class Md5Salt extends \TYPO3\CMS\Core\Crypto\PasswordHashing\Md5PasswordHash
+    {
+    }
+    class SaltFactory extends \TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory
+    {
+    }
+    class SaltInterface extends \TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashInterface
+    {
+    }
+    class Pbkdf2Salt extends \TYPO3\CMS\Core\Crypto\PasswordHashing\Pbkdf2PasswordHash
+    {
+    }
+    class PhpassSalt extends \TYPO3\CMS\Core\Crypto\PasswordHashing\PhpassPasswordHash
+    {
+    }
+}
+
+namespace TYPO3\CMS\Saltedpasswords\Utility {
+    class ExensionManagerConfigurationUtility extends \TYPO3\CMS\Core\Crypto\PasswordHashing\ExtensionManagerConfigurationUtility
+    {
+    }
+    class SaltedPasswordsUtility extends \TYPO3\CMS\Core\Crypto\PasswordHashing\SaltedPasswordsUtility
+    {
+    }
+}
diff --git a/typo3/sysext/core/Resources/Private/Language/locallang_deprecated_saltedpasswords.xlf b/typo3/sysext/core/Resources/Private/Language/locallang_deprecated_saltedpasswords.xlf
new file mode 100644 (file)
index 0000000..6fdfda8
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xliff version="1.0" xmlns:t3="http://typo3.org/schemas/xliff">
+       <file t3:id="1415814981" source-language="en" datatype="plaintext" original="messages" date="2011-10-17T20:22:37Z" product-name="saltedpasswords">
+               <header/>
+               <body>
+                       <trans-unit id="ext.saltedpasswords.title.tx_saltedpasswords_salts_phpass">
+                               <source>Portable PHP password hashing (phpass)</source>
+                       </trans-unit>
+                       <trans-unit id="ext.saltedpasswords.title.tx_saltedpasswords_salts_md5">
+                               <source>MD5 salted hashing (secure)</source>
+                       </trans-unit>
+                       <trans-unit id="ext.saltedpasswords.title.tx_saltedpasswords_salts_blowfish">
+                               <source>Blowfish salted hashing (advanced)</source>
+                       </trans-unit>
+                       <trans-unit id="ext.saltedpasswords.title.phpasssalt">
+                               <source>Portable PHP password hashing (phpass)</source>
+                       </trans-unit>
+                       <trans-unit id="ext.saltedpasswords.title.md5salt">
+                               <source>MD5 salted hashing (secure)</source>
+                       </trans-unit>
+                       <trans-unit id="ext.saltedpasswords.title.blowfishsalt">
+                               <source>Blowfish salted hashing (advanced)</source>
+                       </trans-unit>
+                       <trans-unit id="ext.saltedpasswords.title.pbkdf2salt">
+                               <source>PBKDF2 key derivation (advanced)</source>
+                       </trans-unit>
+                       <trans-unit id="ext.saltedpasswords.title.bcryptsalt">
+                               <source>Bcrypt password hashing (PHP native)</source>
+                       </trans-unit>
+                       <trans-unit id="ext.saltedpasswords.title.argon2isalt">
+                               <source>Argon2i password hashing (PHP native)</source>
+                       </trans-unit>
+               </body>
+       </file>
+</xliff>
diff --git a/typo3/sysext/core/Resources/Private/Language/locallang_deprecated_saltedpasswords_em.xlf b/typo3/sysext/core/Resources/Private/Language/locallang_deprecated_saltedpasswords_em.xlf
new file mode 100644 (file)
index 0000000..1977ab1
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xliff version="1.0" xmlns:t3="http://typo3.org/schemas/xliff">
+       <file t3:id="1415814983" source-language="en" datatype="plaintext" original="messages" date="2011-10-17T20:22:32Z" product-name="saltedpasswords">
+               <header/>
+               <body>
+                       <trans-unit id="saltedpasswords.config.FE.saltedPWHashingMethod">
+                               <source>Hashing method for the frontend: Defines salted hashing method to use. Choose "Portable PHP password hashing" to stay compatible with other CMS (e.g. Drupal, Wordpress). Choose "MD5 salted hashing" to reuse TYPO3 passwords for OS level authentication (other servers could use TYPO3 passwords). Choose "Blowfish salted hashing" for advanced security to reuse passwords on OS level (Blowfish might not be supported on your system TODO).</source>
+                       </trans-unit>
+                       <trans-unit id="saltedpasswords.config.BE.saltedPWHashingMethod">
+                               <source>Hashing method for the backend: Defines salted hashing method to use. Choose "Portable PHP password hashing" to stay compatible with other CMS (e.g. Drupal, Wordpress). Choose "MD5 salted hashing" to reuse TYPO3 passwords for OS level authentication (other servers could use TYPO3 passwords). Choose "Blowfish salted hashing" for advanced security to reuse passwords on OS level (Blowfish might not be supported on your system TODO).</source>
+                       </trans-unit>
+               </body>
+       </file>
+</xliff>
diff --git a/typo3/sysext/core/Tests/Functional/Crypto/PasswordHashing/Fixtures/be_users.xml b/typo3/sysext/core/Tests/Functional/Crypto/PasswordHashing/Fixtures/be_users.xml
new file mode 100644 (file)
index 0000000..884707e
--- /dev/null
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<dataset>
+       <be_users>
+               <uid>1</uid>
+               <username>user_with_salted_password_1</username>
+               <password>$P$CDmA2Juu2h9/9MNaKaxtgzZgIVmjkh/</password> <!-- salted hash starting with $ -->
+               <disable>0</disable>
+               <deleted>0</deleted>
+       </be_users>
+       <be_users>
+               <uid>2</uid>
+               <username>user_with_salted_password_2</username>
+               <password>M$v2AultVYItaCpb.tpdx2aGAue8eL3/</password> <!-- salted hash starting with M$ -->
+               <disable>0</disable>
+               <deleted>0</deleted>
+       </be_users>
+       <be_users>
+               <uid>3</uid>
+               <username>user_with_insecure_password_1</username>
+               <password>5f4dcc3b5aa765d61d8327deb882cf99</password> <!-- simple md5 hash -->
+               <disable>0</disable>
+               <deleted>0</deleted>
+       </be_users>
+       <be_users>
+               <uid>4</uid>
+               <username>user_with_empty_password</username>
+               <password></password> <!-- empty password -->
+               <disable>0</disable>
+               <deleted>0</deleted>
+       </be_users>
+       <be_users>
+               <uid>5</uid>
+               <username>deleted_user_with_insecure_password</username>
+               <password>819b0643d6b89dc9b579fdfc9094f28e</password> <!-- simple md5 hash -->
+               <disable>0</disable>
+               <deleted>1</deleted> <!-- user is marked as deleted -->
+       </be_users>
+       <be_users>
+               <uid>6</uid>
+               <username>disabled_user_with_insecure_password</username>
+               <password>34cc93ece0ba9e3f6f235d4af979b16c</password> <!-- simple md5 hash -->
+               <disable>1</disable> <!-- user is marked as disabled -->
+               <deleted>0</deleted>
+       </be_users>
+</dataset>
diff --git a/typo3/sysext/core/Tests/Functional/Crypto/PasswordHashing/Fixtures/fe_users.xml b/typo3/sysext/core/Tests/Functional/Crypto/PasswordHashing/Fixtures/fe_users.xml
new file mode 100644 (file)
index 0000000..598fb5a
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<dataset>
+       <fe_users>
+               <uid>1</uid>
+               <username>user_with_salted_password</username>
+               <password>$P$CDmA2Juu2h9/9MNaKaxtgzZgIVmjkh/</password> <!-- salted hash starting with $ -->
+               <deleted>0</deleted>
+               <disable>0</disable>
+       </fe_users>
+       <fe_users>
+               <uid>2</uid>
+               <username>user_with_unsecured_password</username>
+               <password>5f4dcc3b5aa765d61d8327deb882cf99</password>
+               <deleted>0</deleted>
+               <disable>0</disable>
+       </fe_users>
+</dataset>
diff --git a/typo3/sysext/core/Tests/Functional/Crypto/PasswordHashing/SaltedPasswordServiceTest.php b/typo3/sysext/core/Tests/Functional/Crypto/PasswordHashing/SaltedPasswordServiceTest.php
new file mode 100644 (file)
index 0000000..bbb83eb
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Functional\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\SaltedPasswordService;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Test case for \TYPO3\CMS\Core\Crypto\PasswordHashing\SaltedPasswordService
+ */
+class SaltedPasswordServiceTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTestCase
+{
+
+    /**
+     * XML database fixtures to be loaded into database.
+     *
+     * @var array
+     */
+    protected $xmlDatabaseFixtures = [
+        'typo3/sysext/core/Tests/Functional/Crypto/PasswordHashing/Fixtures/be_users.xml'
+    ];
+
+    /**
+     * @var SaltedPasswordService
+     */
+    protected $subject;
+
+    /**
+     * Sets up this test suite.
+     */
+    protected function setUp()
+    {
+        parent::setUp();
+        foreach ($this->xmlDatabaseFixtures as $fixture) {
+            $this->importDataSet($fixture);
+        }
+        $this->subject = GeneralUtility::makeInstance(SaltedPasswordService::class);
+    }
+
+    /**
+     * Check if service updates backend user password
+     *
+     * @test
+     */
+    public function checkIfServiceUpdatesBackendUserPassword()
+    {
+        $newPassword = ['password' => '008c5926ca861023c1d2a36653fd88e2'];
+
+        $this->subject->pObj = new \stdClass();
+        $this->subject->pObj->user_table = 'be_users';
+
+        $this->callInaccessibleMethod($this->subject, 'updatePassword', 3, $newPassword);
+
+        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
+
+        $currentPassword = $queryBuilder
+            ->select('password')
+            ->from('be_users')
+            ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter(3, \PDO::PARAM_INT)))
+            ->execute()
+            ->fetchColumn();
+
+        $this->assertEquals($newPassword['password'], $currentPassword);
+    }
+}
diff --git a/typo3/sysext/core/Tests/Functional/Crypto/PasswordHashing/SaltedPasswordsUtilityTest.php b/typo3/sysext/core/Tests/Functional/Crypto/PasswordHashing/SaltedPasswordsUtilityTest.php
new file mode 100644 (file)
index 0000000..b70fc20
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Functional\Crypto\PasswordHashing\Utility;
+
+/*
+ * 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\SaltedPasswordsUtility;
+
+/**
+ * Test case for \TYPO3\CMS\Core\Crypto\PasswordHashing\SaltedPasswordsUtility
+ */
+class SaltedPasswordsUtilityTest extends \TYPO3\TestingFramework\Core\Functional\FunctionalTestCase
+{
+
+    /**
+     * XML database fixtures to be loaded into database.
+     *
+     * @var array
+     */
+    protected $xmlDatabaseFixtures = [
+        'typo3/sysext/core/Tests/Functional/Crypto/PasswordHashing/Fixtures/be_users.xml'
+    ];
+
+    /**
+     * Sets up this test suite.
+     */
+    protected function setUp()
+    {
+        parent::setUp();
+        foreach ($this->xmlDatabaseFixtures as $fixture) {
+            $this->importDataSet($fixture);
+        }
+    }
+
+    /**
+     * Check if salted password utility returns the correct number of backend users with insecure passwords
+     *
+     * @test
+     */
+    public function checkIfNumberOfBackendUsersWithInsecurePasswordsIsFetchedCorrectly()
+    {
+        $this->assertEquals(3, SaltedPasswordsUtility::getNumberOfBackendUsersWithInsecurePassword());
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/Argon2iPasswordHashTest.php b/typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/Argon2iPasswordHashTest.php
new file mode 100644 (file)
index 0000000..398ac24
--- /dev/null
@@ -0,0 +1,224 @@
+<?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\Argon2iPasswordHash;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class Argon2iPasswordHashTest extends UnitTestCase
+{
+    /**
+     * @var Argon2iPasswordHash
+     */
+    protected $subject;
+
+    /**
+     * Sets up the subject for this test case.
+     */
+    protected function setUp()
+    {
+        $options = [
+            'memory_cost' => 1024,
+            'time_cost' => 2,
+            'threads' => 2,
+        ];
+        $this->subject = new Argon2iPasswordHash($options);
+    }
+
+    /**
+     * @test
+     */
+    public function constructorThrowsExceptionIfMemoryCostIsTooLow()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1533899612);
+        new Argon2iPasswordHash(['memory_cost' => 1]);
+    }
+
+    /**
+     * @test
+     */
+    public function constructorThrowsExceptionIfTimeCostIsTooLow()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1533899613);
+        new Argon2iPasswordHash(['time_cost' => 1]);
+    }
+
+    /**
+     * @test
+     */
+    public function constructorThrowsExceptionIfThreadsIsTooLow()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1533899614);
+        new Argon2iPasswordHash(['threads' => 1]);
+    }
+
+    /**
+     * @test
+     */
+    public function getHashedPasswordReturnsNullOnEmptyPassword()
+    {
+        $this->assertNull($this->subject->getHashedPassword(''));
+    }
+
+    /**
+     * @test
+     */
+    public function getHashedPasswordReturnsString()
+    {
+        $hash = $this->subject->getHashedPassword('password');
+        $this->assertNotNull($hash);
+        $this->assertTrue(is_string($hash));
+    }
+
+    /**
+     * @test
+     */
+    public function isValidSaltedPwValidatesHastCreatedByGetHashedPassword()
+    {
+        $hash = $this->subject->getHashedPassword('password');
+        $this->assertTrue($this->subject->isValidSaltedPW($hash));
+    }
+
+    /**
+     * Tests authentication procedure with alphabet characters.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueForHashedPasswordWithValidAlphaCharClassPassword()
+    {
+        $password = 'aEjOtY';
+        $hash = $this->subject->getHashedPassword($password);
+        $this->assertTrue($this->subject->checkPassword($password, $hash));
+    }
+
+    /**
+     * Tests authentication procedure with numeric characters.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueForHashedPasswordWithValidNumericCharClassPassword()
+    {
+        $password = '01369';
+        $hash = $this->subject->getHashedPassword($password);
+        $this->assertTrue($this->subject->checkPassword($password, $hash));
+    }
+
+    /**
+     * Tests authentication procedure with US-ASCII special characters.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueForHashedPasswordWithValidAsciiSpecialCharClassPassword()
+    {
+        $password = ' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~';
+        $hash = $this->subject->getHashedPassword($password);
+        $this->assertTrue($this->subject->checkPassword($password, $hash));
+    }
+
+    /**
+     * Tests authentication procedure with latin1 special characters.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueForHashedPasswordWithValidLatin1SpecialCharClassPassword()
+    {
+        $password = '';
+        for ($i = 160; $i <= 191; $i++) {
+            $password .= chr($i);
+        }
+        $password .= chr(215) . chr(247);
+        $hash = $this->subject->getHashedPassword($password);
+        $this->assertTrue($this->subject->checkPassword($password, $hash));
+    }
+
+    /**
+     * Tests authentication procedure with latin1 umlauts.
+     *
+     * @test
+     */
+    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);
+        $this->assertTrue($this->subject->checkPassword($password, $hash));
+    }
+
+    /**
+     * @test
+     */
+    public function checkPasswordReturnsTrueForHashedPasswordWithNonValidPassword()
+    {
+        $password = 'password';
+        $password1 = $password . 'INVALID';
+        $hash = $this->subject->getHashedPassword($password);
+        $this->assertFalse($this->subject->checkPassword($password1, $hash));
+    }
+
+    /**
+     * @test
+     */
+    public function isHashUpdateNeededReturnsFalseForJustGeneratedHash()
+    {
+        $password = 'password';
+        $hash = $this->subject->getHashedPassword($password);
+        $this->assertFalse($this->subject->isHashUpdateNeeded($hash));
+    }
+
+    /**
+     * @test
+     */
+    public function isHashUpdateNeededReturnsTrueForHashGeneratedWithOldOptions()
+    {
+        $originalOptions = [
+            'memory_cost' => 1024,
+            'time_cost' => 2,
+            'threads' => 2,
+        ];
+        $subject = new Argon2iPasswordHash($originalOptions);
+        $hash = $subject->getHashedPassword('password');
+
+        // Change $memoryCost
+        $newOptions = $originalOptions;
+        $newOptions['memory_cost'] = $newOptions['memory_cost'] + 1;
+        $subject = new Argon2iPasswordHash($newOptions);
+        $this->assertTrue($subject->isHashUpdateNeeded($hash));
+
+        // Change $timeCost
+        $newOptions = $originalOptions;
+        $newOptions['time_cost'] = $newOptions['time_cost'] + 1;
+        $subject = new Argon2iPasswordHash($newOptions);
+        $this->assertTrue($subject->isHashUpdateNeeded($hash));
+
+        // Change $threads
+        $newOptions = $originalOptions;
+        $newOptions['threads'] = $newOptions['threads'] + 1;
+        $subject = new Argon2iPasswordHash($newOptions);
+        $this->assertTrue($subject->isHashUpdateNeeded($hash));
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/BcryptPasswordHashTest.php b/typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/BcryptPasswordHashTest.php
new file mode 100644 (file)
index 0000000..d0b0f32
--- /dev/null
@@ -0,0 +1,218 @@
+<?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\BcryptPasswordHash;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class BcryptPasswordHashTest extends UnitTestCase
+{
+    /**
+     * @var BcryptPasswordHash
+     */
+    protected $subject;
+
+    /**
+     * Sets up the fixtures for this testcase.
+     */
+    protected function setUp()
+    {
+        // Set a low cost to speed up tests
+        $options = [
+            'cost' => 10,
+        ];
+        $this->subject = new BcryptPasswordHash($options);
+    }
+
+    /**
+     * @test
+     */
+    public function constructorThrowsExceptionIfMemoryCostIsTooLow()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1533902002);
+        new BcryptPasswordHash(['cost' => 9]);
+    }
+
+    /**
+     * @test
+     */
+    public function constructorThrowsExceptionIfMemoryCostIsTooHigh()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1533902002);
+        new BcryptPasswordHash(['cost' => 32]);
+    }
+
+    /**
+     * @test
+     */
+    public function getHashedPasswordReturnsNullOnEmptyPassword()
+    {
+        $this->assertNull($this->subject->getHashedPassword(''));
+    }
+
+    /**
+     * @test
+     */
+    public function getHashedPasswordReturnsString()
+    {
+        $hash = $this->subject->getHashedPassword('password');
+        $this->assertNotNull($hash);
+        $this->assertTrue(is_string($hash));
+    }
+
+    /**
+     * @test
+     */
+    public function isValidSaltedPwValidatesHastCreatedByGetHashedPassword()
+    {
+        $hash = $this->subject->getHashedPassword('password');
+        $this->assertTrue($this->subject->isValidSaltedPW($hash));
+    }
+
+    /**
+     * Tests authentication procedure with alphabet characters.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueForHashedPasswordWithValidAlphaCharClassPassword()
+    {
+        $password = 'aEjOtY';
+        $hash = $this->subject->getHashedPassword($password);
+        $this->assertTrue($this->subject->checkPassword($password, $hash));
+    }
+
+    /**
+     * Tests authentication procedure with numeric characters.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueForHashedPasswordWithValidNumericCharClassPassword()
+    {
+        $password = '01369';
+        $hash = $this->subject->getHashedPassword($password);
+        $this->assertTrue($this->subject->checkPassword($password, $hash));
+    }
+
+    /**
+     * Tests authentication procedure with US-ASCII special characters.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueForHashedPasswordWithValidAsciiSpecialCharClassPassword()
+    {
+        $password = ' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~';
+        $hash = $this->subject->getHashedPassword($password);
+        $this->assertTrue($this->subject->checkPassword($password, $hash));
+    }
+
+    /**
+     * Tests authentication procedure with latin1 special characters.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueForHashedPasswordWithValidLatin1SpecialCharClassPassword()
+    {
+        $password = '';
+        for ($i = 160; $i <= 191; $i++) {
+            $password .= chr($i);
+        }
+        $password .= chr(215) . chr(247);
+        $hash = $this->subject->getHashedPassword($password);
+        $this->assertTrue($this->subject->checkPassword($password, $hash));
+    }
+
+    /**
+     * Tests authentication procedure with latin1 umlauts.
+     *
+     * @test
+     */
+    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);
+        $this->assertTrue($this->subject->checkPassword($password, $hash));
+    }
+
+    /**
+     * @test
+     */
+    public function checkPasswordReturnsTrueForHashedPasswordWithNonValidPassword()
+    {
+        $password = 'password';
+        $password1 = $password . 'INVALID';
+        $hash = $this->subject->getHashedPassword($password);
+        $this->assertFalse($this->subject->checkPassword($password1, $hash));
+    }
+
+    /**
+     * @test
+     */
+    public function isHashUpdateNeededReturnsFalseForJustGeneratedHash()
+    {
+        $hash = $this->subject->getHashedPassword('password');
+        $this->assertFalse($this->subject->isHashUpdateNeeded($hash));
+    }
+
+    /**
+     * @test
+     */
+    public function isHashUpdateNeededReturnsTrueForHashGeneratedWithOldOptions()
+    {
+        $subject = new BcryptPasswordHash(['cost' => 10]);
+        $hash = $subject->getHashedPassword('password');
+        $subject = new BcryptPasswordHash(['cost' => 11]);
+        $this->assertTrue($subject->isHashUpdateNeeded($hash));
+    }
+
+    /**
+     * Bcrypt truncates on NUL characters by default
+     *
+     * @test
+     */
+    public function getHashedPasswordDoesNotTruncateOnNul()
+    {
+        $password1 = 'pass' . "\x00" . 'word';
+        $password2 = 'pass' . "\x00" . 'phrase';
+        $hash = $this->subject->getHashedPassword($password1);
+        $this->assertFalse($this->subject->checkPassword($password2, $hash));
+    }
+
+    /**
+     * Bcrypt truncates after 72 characters by default
+     *
+     * @test
+     */
+    public function getHashedPasswordDoesNotTruncateAfter72Chars()
+    {
+        $prefix = str_repeat('a', 72);
+        $password1 = $prefix . 'one';
+        $password2 = $prefix . 'two';
+        $hash = $this->subject->getHashedPassword($password1);
+        $this->assertFalse($this->subject->checkPassword($password2, $hash));
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/BlowfishPasswordHashTest.php b/typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/BlowfishPasswordHashTest.php
new file mode 100644 (file)
index 0000000..0e70278
--- /dev/null
@@ -0,0 +1,238 @@
+<?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\BlowfishPasswordHash;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class BlowfishPasswordHashTest extends UnitTestCase
+{
+    /**
+     * Sets up the fixtures for this testcase.
+     */
+    protected function setUp()
+    {
+        if (!CRYPT_BLOWFISH) {
+            $this->markTestSkipped('Blowfish is not supported on your platform.');
+        }
+    }
+
+    /**
+     * @test
+     */
+    public function constructorThrowsExceptionIfHashCountIsTooLow()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1533903545);
+        new BlowfishPasswordHash(['hash_count' => 3]);
+    }
+
+    /**
+     * @test
+     */
+    public function constructorThrowsExceptionIfHashCountIsTooHigh()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1533903545);
+        new BlowfishPasswordHash(['hash_count' => 18]);
+    }
+
+    /**
+     * @test
+     */
+    public function getHashedPasswordWithEmptyPasswordResultsInNullSaltedPassword()
+    {
+        $password = '';
+        $this->assertNull((new BlowfishPasswordHash(['hash_count' => 4]))->getHashedPassword($password));
+    }
+
+    /**
+     * @test
+     */
+    public function getHashedPasswordWithNonEmptyPasswordResultsInNonNullSaltedPassword()
+    {
+        $password = 'a';
+        $this->assertNotNull((new BlowfishPasswordHash(['hash_count' => 4]))->getHashedPassword($password));
+    }
+
+    /**
+     * @test
+     */
+    public function getHashedPasswordValidates()
+    {
+        $password = 'password';
+        $subject = new BlowfishPasswordHash(['hash_count' => 4]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->isValidSaltedPW($saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with fixed password and fixed (pre-generated) hash.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same fixed salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidAlphaCharClassPasswordAndFixedHash()
+    {
+        $password = 'password';
+        $saltedHashPassword = '$2a$07$Rvtl6CyMhR8GZGhHypjwOuydeN0nKFAlgo1LmmGrLowtIrtkov5Na';
+        $this->assertTrue((new BlowfishPasswordHash(['hash_count' => 4]))->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests that authentication procedure fails with broken hash to compare to
+     *
+     * @test
+     */
+    public function checkPasswordReturnsFalseFailsWithBrokenHash()
+    {
+        $password = 'password';
+        $saltedHashPassword = '$2a$07$Rvtl6CyMhR8GZGhHypjwOuydeN0nKFAlgo1LmmGrLowtIrtkov5N';
+        $this->assertFalse((new BlowfishPasswordHash(['hash_count' => 4]))->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with alphabet characters.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidAlphaCharClassPassword()
+    {
+        $password = 'aEjOtY';
+        $subject = new BlowfishPasswordHash(['hash_count' => 4]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with numeric characters.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidNumericCharClassPassword()
+    {
+        $password = '01369';
+        $subject = new BlowfishPasswordHash(['hash_count' => 4]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with US-ASCII special characters.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidAsciiSpecialCharClassPassword()
+    {
+        $password = ' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~';
+        $subject = new BlowfishPasswordHash(['hash_count' => 4]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with latin1 special characters.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidLatin1SpecialCharClassPassword()
+    {
+        $password = '';
+        for ($i = 160; $i <= 191; $i++) {
+            $password .= chr($i);
+        }
+        $password .= chr(215) . chr(247);
+        $subject = new BlowfishPasswordHash(['hash_count' => 4]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with latin1 umlauts.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsReturnsTrueWithValidLatin1UmlautCharClassPassword()
+    {
+        $password = '';
+        for ($i = 192; $i <= 214; $i++) {
+            $password .= chr($i);
+        }
+        for ($i = 216; $i <= 246; $i++) {
+            $password .= chr($i);
+        }
+        for ($i = 248; $i <= 255; $i++) {
+            $password .= chr($i);
+        }
+        $subject = new BlowfishPasswordHash(['hash_count' => 4]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * @test
+     */
+    public function checkPasswordReturnsFalseWithNonValidPassword()
+    {
+        $password = 'password';
+        $password1 = $password . 'INVALID';
+        $subject = new BlowfishPasswordHash(['hash_count' => 4]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertFalse($subject->checkPassword($password1, $saltedHashPassword));
+    }
+
+    /**
+     * @test
+     */
+    public function isHashUpdateNeededReturnsFalseForValidSaltedPassword()
+    {
+        $password = 'password';
+        $subject = new BlowfishPasswordHash(['hash_count' => 4]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertFalse($subject->isHashUpdateNeeded($saltedHashPassword));
+    }
+
+    /**
+     * @test
+     */
+    public function isHashUpdateNeededReturnsTrueForHashGeneratedWithOldOptions()
+    {
+        $subject = new BlowfishPasswordHash(['hash_count' => 4]);
+        $hash = $subject->getHashedPassword('password');
+        $subject = new BlowfishPasswordHash(['hash_count' => 5]);
+        $this->assertTrue($subject->isHashUpdateNeeded($hash));
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/Fixtures/TestPasswordHash.php b/typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/Fixtures/TestPasswordHash.php
new file mode 100644 (file)
index 0000000..0f3ead9
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\Unit\Crypto\PasswordHashing\Fixtures;
+
+/*
+ * 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!
+ */
+
+/**
+ * Fixture salt class to check if constructor is called with options
+ */
+class TestPasswordHash
+{
+    /**
+     * TestPasswordHash constructor.
+     *
+     * @param array $options
+     */
+    public function __construct(array $options = [])
+    {
+        if ($options === [ 'foo' => 'bar' ]) {
+            throw new \RuntimeException('This should be thrown', 1533950385);
+        }
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/Md5PasswordHashTest.php b/typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/Md5PasswordHashTest.php
new file mode 100644 (file)
index 0000000..d31a602
--- /dev/null
@@ -0,0 +1,205 @@
+<?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\Md5PasswordHash;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class Md5PasswordHashTest extends UnitTestCase
+{
+    /**
+     * Sets up the fixtures for this testcase.
+     */
+    protected function setUp()
+    {
+        if (!CRYPT_MD5) {
+            $this->markTestSkipped('Blowfish is not supported on your platform.');
+        }
+    }
+
+    /**
+     * @test
+     */
+    public function getHashedPasswordReturnsNullWithEmptyPassword()
+    {
+        $this->assertNull((new Md5PasswordHash())->getHashedPassword(''));
+    }
+
+    /**
+     * @test
+     */
+    public function getHashedPasswordReturnsNotNullWithNonEmptyPassword()
+    {
+        $this->assertNotNull((new Md5PasswordHash())->getHashedPassword('a'));
+    }
+
+    /**
+     * @test
+     */
+    public function getHashedPasswordCreatesAHashThatValidates()
+    {
+        $password = 'password';
+        $subject = new Md5PasswordHash();
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->isValidSaltedPW($saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with fixed password and fixed (pre-generated) hash.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same fixed salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidAlphaCharClassPasswordAndFixedHash()
+    {
+        $password = 'password';
+        $saltedHashPassword = '$1$GNu9HdMt$RwkPb28pce4nXZfnplVZY/';
+        $this->assertTrue((new Md5PasswordHash())->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests that authentication procedure fails with broken hash to compare to
+     *
+     * @test
+     */
+    public function checkPasswordReturnsFalseWithBrokenHash()
+    {
+        $password = 'password';
+        $saltedHashPassword = '$1$GNu9HdMt$RwkPb28pce4nXZfnplVZY';
+        $this->assertFalse((new Md5PasswordHash())->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with alphabet characters.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidAlphaCharClassPassword()
+    {
+        $password = 'aEjOtY';
+        $subject = new Md5PasswordHash();
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with numeric characters.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidNumericCharClassPassword()
+    {
+        $password = '01369';
+        $subject = new Md5PasswordHash();
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with US-ASCII special characters.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidAsciiSpecialCharClassPassword()
+    {
+        $password = ' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~';
+        $subject = new Md5PasswordHash();
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with latin1 special characters.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidLatin1SpecialCharClassPassword()
+    {
+        $password = '';
+        for ($i = 160; $i <= 191; $i++) {
+            $password .= chr($i);
+        }
+        $password .= chr(215) . chr(247);
+        $subject = new Md5PasswordHash();
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with latin1 umlauts.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidLatin1UmlautCharClassPassword()
+    {
+        $password = '';
+        for ($i = 192; $i <= 214; $i++) {
+            $password .= chr($i);
+        }
+        for ($i = 216; $i <= 246; $i++) {
+            $password .= chr($i);
+        }
+        for ($i = 248; $i <= 255; $i++) {
+            $password .= chr($i);
+        }
+        $subject = new Md5PasswordHash();
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * @test
+     */
+    public function checkPasswordReturnsFalseWithNonValidPassword()
+    {
+        $password = 'password';
+        $password1 = $password . 'INVALID';
+        $subject = new Md5PasswordHash();
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertFalse($subject->checkPassword($password1, $saltedHashPassword));
+    }
+
+    /**
+     * @test
+     */
+    public function isHashUpdateNeededReturnsFalse()
+    {
+        $password = 'password';
+        $subject = new Md5PasswordHash();
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertFalse($subject->isHashUpdateNeeded($saltedHashPassword));
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/PasswordHashFactoryTest.php b/typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/PasswordHashFactoryTest.php
new file mode 100644 (file)
index 0000000..6c86e1c
--- /dev/null
@@ -0,0 +1,273 @@
+<?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\Argon2iPasswordHash;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\PhpassPasswordHash;
+use TYPO3\CMS\Core\Tests\Unit\Crypto\PasswordHashing\Fixtures\TestPasswordHash;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class PasswordHashFactoryTest extends UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function getThrowsExceptionIfModeIsNotBeOrFe(): void
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1533948312);
+        (new PasswordHashFactory())->get('ThisIsNotAValidHash', 'foo');
+    }
+
+    /**
+     * @test
+     */
+    public function getThrowsExceptionWithBrokenClassNameModeConfiguration(): void
+    {
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionCode(1533949053);
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing']['className'] = '';
+        (new PasswordHashFactory())->get('ThisIsNotAValidHash', 'FE');
+    }
+
+    /**
+     * @test
+     */
+    public function getThrowsExceptionWithBrokenOptionsModeConfiguration(): void
+    {
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionCode(1533949053);
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing']['options'] = '';
+        (new PasswordHashFactory())->get('ThisIsNotAValidHash', 'FE');
+    }
+
+    /**
+     * @test
+     */
+    public function getThrowsExceptionIfARegisteredHashDoesNotImplementSaltInterface(): void
+    {
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['availablePasswordHashAlgorithms'] = [ \stdClass::class ];
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionCode(1533818569);
+        (new PasswordHashFactory())->get('ThisIsNotAValidHash', 'BE');
+    }
+
+    /**
+     * @test
+     */
+    public function getThrowsExceptionIfNoClassIsFoundThatHandlesGivenHash(): void
+    {
+        $this->expectException(InvalidPasswordHashException::class);
+        $this->expectExceptionCode(1533818591);
+        (new PasswordHashFactory())->get('ThisIsNotAValidHash', 'BE');
+    }
+
+    /**
+     * @test
+     */
+    public function getThrowsExceptionIfClassThatHandlesAHashIsNotAvailable(): void
+    {
+        $phpassProphecy = $this->prophesize(PhpassPasswordHash::class);
+        GeneralUtility::addInstance(PhpassPasswordHash::class, $phpassProphecy->reveal());
+        $phpassProphecy->isAvailable()->shouldBeCalled()->willReturn(false);
+        $this->expectException(InvalidPasswordHashException::class);
+        $this->expectExceptionCode(1533818591);
+        (new PasswordHashFactory())->get('$P$C7u7E10SBEie/Jbdz0jDtUcWhzgOPF.', 'BE');
+    }
+
+    /**
+     * @test
+     */
+    public function getThrowsExceptionIfClassThatHandlesAHashSaysNoToHash(): void
+    {
+        $phpassProphecy = $this->prophesize(PhpassPasswordHash::class);
+        GeneralUtility::addInstance(PhpassPasswordHash::class, $phpassProphecy->reveal());
+        $phpassProphecy->isAvailable()->shouldBeCalled()->willReturn(true);
+        $hash = '$P$C7u7E10SBEie/Jbdz0jDtUcWhzgOPF.';
+        $phpassProphecy->isValidSaltedPW($hash)->shouldBeCalled()->willReturn(false);
+        $this->expectException(InvalidPasswordHashException::class);
+        $this->expectExceptionCode(1533818591);
+        (new PasswordHashFactory())->get($hash, 'BE');
+    }
+
+    /**
+     * @test
+     */
+    public function getHandsConfiguredOptionsToHashClassIfMethodIsConfiguredDefaultForMode(): void
+    {
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['availablePasswordHashAlgorithms'] = [ TestPasswordHash::class ];
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing'] = [
+            'className' => TestPasswordHash::class,
+            'options' => [
+                'foo' => 'bar'
+            ],
+        ];
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionCode(1533950385);
+        (new PasswordHashFactory())->get('someHash', 'FE');
+    }
+
+    /**
+     * @test
+     */
+    public function getReturnsInstanceOfHashClassThatHandlesHash(): void
+    {
+        $phpassProphecy = $this->prophesize(PhpassPasswordHash::class);
+        $phpassRevelation = $phpassProphecy->reveal();
+        GeneralUtility::addInstance(PhpassPasswordHash::class, $phpassRevelation);
+        $phpassProphecy->isAvailable()->shouldBeCalled()->willReturn(true);
+        $hash = '$P$C7u7E10SBEie/Jbdz0jDtUcWhzgOPF.';
+        $phpassProphecy->isValidSaltedPW($hash)->shouldBeCalled()->willReturn(true);
+        $this->assertSame($phpassRevelation, (new PasswordHashFactory())->get($hash, 'BE'));
+    }
+
+    /**
+     * @test
+     */
+    public function getDefaultHashInstanceThrowsExceptionIfModeIsNotBeOrFe(): void
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1533820041);
+        (new PasswordHashFactory())->getDefaultHashInstance('foo');
+    }
+
+    /**
+     * @test
+     */
+    public function getDefaultHashInstanceThrowsExceptionWithBrokenClassNameModeConfiguration(): void
+    {
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionCode(1533950622);
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing']['className'] = '';
+        (new PasswordHashFactory())->getDefaultHashInstance('FE');
+    }
+
+    /**
+     * @test
+     */
+    public function getDefaultHashInstanceThrowsExceptionWithBrokenOptionsModeConfiguration(): void
+    {
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionCode(1533950622);
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing']['options'] = '';
+        (new PasswordHashFactory())->getDefaultHashInstance('FE');
+    }
+
+    /**
+     * @test
+     */
+    public function getDefaultHashReturnsInstanceOfConfiguredDefaultFeMethod(): void
+    {
+        $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['saltedpasswords']['FE']['saltedPWHashingMethod'] = Argon2iPasswordHash::class;
+        $hashInstance = (new PasswordHashFactory())->getDefaultHashInstance('FE');
+        $this->assertInstanceOf(Argon2iPasswordHash::class, $hashInstance);
+    }
+
+    /**
+     * @test
+     */
+    public function getDefaultHashReturnsInstanceOfConfiguredDefaultBeMethod(): void
+    {
+        $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['saltedpasswords']['BE']['saltedPWHashingMethod'] = Argon2iPasswordHash::class;
+        $hashInstance = (new PasswordHashFactory())->getDefaultHashInstance('BE');
+        $this->assertInstanceOf(Argon2iPasswordHash::class, $hashInstance);
+    }
+
+    /**
+     * @test
+     */
+    public function getDefaultHashThrowsExceptionIfDefaultHashMethodDoesNotImplementSaltInterface(): void
+    {
+        $GLOBALS['TYPO3_CONF_VARS']['BE']['passwordHashing']['className'] = \stdClass::class;
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['availablePasswordHashAlgorithms'] = [ \stdClass::class ];
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionCode(1533820281);
+        (new PasswordHashFactory())->getDefaultHashInstance('BE');
+    }
+
+    /**
+     * @test
+     */
+    public function getDefaultHashThrowsExceptionIfDefaultHashMethodIsNotRegistered(): void
+    {
+        $GLOBALS['TYPO3_CONF_VARS']['BE']['passwordHashing']['className'] = \stdClass::class;
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['availablePasswordHashAlgorithms'] = [ Argon2iPasswordHash::class ];
+        $this->expectException(InvalidPasswordHashException::class);
+        $this->expectExceptionCode(1533820194);
+        (new PasswordHashFactory())->getDefaultHashInstance('BE');
+    }
+
+    /**
+     * @test
+     */
+    public function getDefaultHashThrowsExceptionIfDefaultHashMethodIsNotAvailable(): void
+    {
+        $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['saltedpasswords']['BE']['saltedPWHashingMethod'] = Argon2iPasswordHash::class;
+        $argonProphecy = $this->prophesize(Argon2iPasswordHash::class);
+        GeneralUtility::addInstance(Argon2iPasswordHash::class, $argonProphecy->reveal());
+        $argonProphecy->isAvailable()->shouldBeCalled()->willReturn(false);
+        $this->expectException(InvalidPasswordHashException::class);
+        $this->expectExceptionCode(1533822084);
+        (new PasswordHashFactory())->getDefaultHashInstance('BE');
+    }
+
+    /**
+     * @test
+     */
+    public function getDefaultHoshHandsConfiguredOptionsToHashClass(): void
+    {
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['availablePasswordHashAlgorithms'] = [ TestPasswordHash::class ];
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing'] = [
+            'className' => TestPasswordHash::class,
+            'options' => [
+                'foo' => 'bar'
+            ],
+        ];
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionCode(1533950385);
+        (new PasswordHashFactory())->getDefaultHashInstance('FE');
+    }
+
+    /**
+     * @test
+     */
+    public function getRegisteredSaltedHashingMethodsReturnsRegisteredMethods(): void
+    {
+        $methods = [
+            'foo',
+            'bar'
+        ];
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['availablePasswordHashAlgorithms'] = $methods;
+        $this->assertSame($methods, PasswordHashFactory::getRegisteredSaltedHashingMethods());
+    }
+
+    /**
+     * @test
+     */
+    public function getRegisteredSaltedHashingMethodsThrowsExceptionIfNoMethodIsConfigured(): void
+    {
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionCode(1533948733);
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['availablePasswordHashAlgorithms'] = [];
+        PasswordHashFactory::getRegisteredSaltedHashingMethods();
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/Pbkdf2PaswordHashTest.php b/typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/Pbkdf2PaswordHashTest.php
new file mode 100644 (file)
index 0000000..7d1da4f
--- /dev/null
@@ -0,0 +1,242 @@
+<?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\Pbkdf2PasswordHash;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class Pbkdf2PaswordHashTest extends UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function constructorThrowsExceptionIfHashCountIsTooLow()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1533903544);
+        new Pbkdf2PasswordHash(['hash_count' => 999]);
+    }
+
+    /**
+     * @test
+     */
+    public function constructorThrowsExceptionIfHashCountIsTooHigh()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1533903544);
+        new Pbkdf2PasswordHash(['hash_count' => 10000001]);
+    }
+
+    /**
+     * @test
+     */
+    public function getHashedPasswordReturnsNullWithEmptyPassword()
+    {
+        $password = '';
+        $subject = new Pbkdf2PasswordHash(['hash_count' => 1000]);
+        $this->assertNull($subject->getHashedPassword($password));
+    }
+
+    /**
+     * @test
+     */
+    public function getHashedPasswordReturnsNotNullWithNullPassword()
+    {
+        $password = 'a';
+        $subject = new Pbkdf2PasswordHash(['hash_count' => 1000]);
+        $this->assertNotNull($subject->getHashedPassword($password));
+    }
+
+    /**
+     * @test
+     */
+    public function getHashedPasswordValidates()
+    {
+        $password = 'password';
+        $subject = new Pbkdf2PasswordHash(['hash_count' => 1000]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->isValidSaltedPW($saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with fixed password and fixed (pre-generated) hash.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same fixed salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidAlphaCharClassPasswordAndFixedHash()
+    {
+        $password = 'password';
+        $saltedHashPassword = '$pbkdf2-sha256$1000$woPhT0yoWm3AXJXSjuxJ3w$iZ6EvTulMqXlzr0NO8z5EyrklFcJk5Uw2Fqje68FfaQ';
+        $subject = new Pbkdf2PasswordHash(['hash_count' => 1000]);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests that authentication procedure fails with broken hash to compare to
+     *
+     * @test
+     */
+    public function checkPasswordReturnsFalseWithBrokenHash()
+    {
+        $password = 'password';
+        $saltedHashPassword = '$pbkdf2-sha256$1000$woPhT0yoWm3AXJXSjuxJ3w$iZ6EvTulMqXlzr0NO8z5EyrklFcJk5Uw2Fqje68Ffa';
+        $subject = new Pbkdf2PasswordHash(['hash_count' => 1000]);
+        $this->assertFalse($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with alphabet characters.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidAlphaCharClassPassword()
+    {
+        $password = 'aEjOtY';
+        $subject = new Pbkdf2PasswordHash(['hash_count' => 1000]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with numeric characters.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidNumericCharClassPassword()
+    {
+        $password = '01369';
+        $subject = new Pbkdf2PasswordHash(['hash_count' => 1000]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with US-ASCII special characters.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidAsciiSpecialCharClassPassword()
+    {
+        $password = ' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~';
+        $subject = new Pbkdf2PasswordHash(['hash_count' => 1000]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with latin1 special characters.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidLatin1SpecialCharClassPassword()
+    {
+        $password = '';
+        for ($i = 160; $i <= 191; $i++) {
+            $password .= chr($i);
+        }
+        $password .= chr(215) . chr(247);
+        $subject = new Pbkdf2PasswordHash(['hash_count' => 1000]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with latin1 umlauts.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidLatin1UmlautCharClassPassword()
+    {
+        $password = '';
+        for ($i = 192; $i <= 214; $i++) {
+            $password .= chr($i);
+        }
+        for ($i = 216; $i <= 246; $i++) {
+            $password .= chr($i);
+        }
+        for ($i = 248; $i <= 255; $i++) {
+            $password .= chr($i);
+        }
+        $subject = new Pbkdf2PasswordHash(['hash_count' => 1000]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * @test
+     */
+    public function checkPasswordReturnsFalseWithNonValidPassword()
+    {
+        $password = 'password';
+        $password1 = $password . 'INVALID';
+        $subject = new Pbkdf2PasswordHash(['hash_count' => 1000]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertFalse($subject->checkPassword($password1, $saltedHashPassword));
+    }
+
+    /**
+     * @test
+     */
+    public function isHashUpdateNeededReturnsFalseForValidSaltedPassword()
+    {
+        $password = 'password';
+        $subject = new Pbkdf2PasswordHash(['hash_count' => 1000]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertFalse($subject->isHashUpdateNeeded($saltedHashPassword));
+    }
+
+    /**
+     * @test
+     */
+    public function isHashUpdateNeededReturnsTrueWithChangedHashCount()
+    {
+        $subject = new Pbkdf2PasswordHash(['hash_count' => 1000]);
+        $saltedHashPassword = $subject->getHashedPassword('password');
+        $subject = new Pbkdf2PasswordHash(['hash_count' => 1001]);
+        $this->assertTrue($subject->isHashUpdateNeeded($saltedHashPassword));
+    }
+
+    /**
+     * @test
+     */
+    public function checkPasswordIsCompatibleWithPythonPasslibHashes()
+    {
+        $passlibSaltedHash= '$pbkdf2-sha256$6400$.6UI/S.nXIk8jcbdHx3Fhg$98jZicV16ODfEsEZeYPGHU3kbrUrvUEXOPimVSQDD44';
+        $subject = new Pbkdf2PasswordHash(['hash_count' => 1000]);
+        $this->assertTrue($subject->checkPassword('password', $passlibSaltedHash));
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/PhpassPasswordHashTest.php b/typo3/sysext/core/Tests/Unit/Crypto/PasswordHashing/PhpassPasswordHashTest.php
new file mode 100644 (file)
index 0000000..db140aa
--- /dev/null
@@ -0,0 +1,231 @@
+<?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\PhpassPasswordHash;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class PhpassPasswordHashTest extends UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function constructorThrowsExceptionIfHashCountIsTooLow()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1533940454);
+        new PhpassPasswordHash(['hash_count' => 6]);
+    }
+
+    /**
+     * @test
+     */
+    public function constructorThrowsExceptionIfHashCountIsTooHigh()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1533940454);
+        new PhpassPasswordHash(['hash_count' => 25]);
+    }
+
+    /**
+     * @test
+     */
+    public function getHashedPasswordReturnsNullWithEmptyPassword()
+    {
+        $subject = new PhpassPasswordHash(['hash_count' => 7]);
+        $this->assertNull($subject->getHashedPassword(''));
+    }
+
+    /**
+     * @test
+     */
+    public function getHashedPasswordReturnsNotNullWithNotEmptyPassword()
+    {
+        $subject = new PhpassPasswordHash(['hash_count' => 7]);
+        $this->assertNotNull($subject->getHashedPassword('a'));
+    }
+
+    /**
+     * @test
+     */
+    public function getHashedPasswordValidates()
+    {
+        $password = 'password';
+        $subject = new PhpassPasswordHash(['hash_count' => 7]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->isValidSaltedPW($saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with fixed password and fixed (pre-generated) hash.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same fixed salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidAlphaCharClassPasswordAndFixedHash()
+    {
+        $password = 'password';
+        $saltedHashPassword = '$P$C7u7E10SBEie/Jbdz0jDtUcWhzgOPF.';
+        $subject = new PhpassPasswordHash(['hash_count' => 7]);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests that authentication procedure fails with broken hash to compare to
+     *
+     * @test
+     */
+    public function checkPasswordReturnsFalseWithBrokenHash()
+    {
+        $password = 'password';
+        $saltedHashPassword = '$P$C7u7E10SBEie/Jbdz0jDtUcWhzgOPF';
+        $subject = new PhpassPasswordHash(['hash_count' => 7]);
+        $this->assertFalse($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with alphabet characters.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidAlphaCharClassPassword()
+    {
+        $password = 'aEjOtY';
+        $subject = new PhpassPasswordHash(['hash_count' => 7]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with numeric characters.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidNumericCharClassPassword()
+    {
+        $password = '01369';
+        $subject = new PhpassPasswordHash(['hash_count' => 7]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with US-ASCII special characters.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidAsciiSpecialCharClassPassword()
+    {
+        $password = ' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~';
+        $subject = new PhpassPasswordHash(['hash_count' => 7]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with latin1 special characters.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidLatin1SpecialCharClassPassword()
+    {
+        $password = '';
+        for ($i = 160; $i <= 191; $i++) {
+            $password .= chr($i);
+        }
+        $password .= chr(215) . chr(247);
+        $subject = new PhpassPasswordHash(['hash_count' => 7]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * Tests authentication procedure with latin1 umlauts.
+     *
+     * Checks if a "plain-text password" is every time mapped to the
+     * same "salted password hash" when using the same salt.
+     *
+     * @test
+     */
+    public function checkPasswordReturnsTrueWithValidLatin1UmlautCharClassPassword()
+    {
+        $password = '';
+        for ($i = 192; $i <= 214; $i++) {
+            $password .= chr($i);
+        }
+        for ($i = 216; $i <= 246; $i++) {
+            $password .= chr($i);
+        }
+        for ($i = 248; $i <= 255; $i++) {
+            $password .= chr($i);
+        }
+        $subject = new PhpassPasswordHash(['hash_count' => 7]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
+    }
+
+    /**
+     * @test
+     */
+    public function checkPasswordReturnsFalseWithNonValidPassword()
+    {
+        $password = 'password';
+        $password1 = $password . 'INVALID';
+        $subject = new PhpassPasswordHash(['hash_count' => 7]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertFalse($subject->checkPassword($password1, $saltedHashPassword));
+    }
+
+    /**
+     * @test
+     */
+    public function isHashUpdateNeededReturnsFalseForValidSaltedPassword()
+    {
+        $password = 'password';
+        $subject = new PhpassPasswordHash(['hash_count' => 7]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertFalse($subject->isHashUpdateNeeded($saltedHashPassword));
+    }
+
+    /**
+     * @test
+     */
+    public function isHashUpdateNeededReturnsFalseForChangedHashCountSaltedPassword()
+    {
+        $password = 'password';
+        $subject = new PhpassPasswordHash(['hash_count' => 7]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $subject = new PhpassPasswordHash(['hash_count' => 8]);
+        $this->assertTrue($subject->isHashUpdateNeeded($saltedHashPassword));
+    }
+}
diff --git a/typo3/sysext/core/Tests/UnitDeprecated/Crypto/PasswordHashing/Argon2iPasswordHashTest.php b/typo3/sysext/core/Tests/UnitDeprecated/Crypto/PasswordHashing/Argon2iPasswordHashTest.php
new file mode 100644 (file)
index 0000000..08e8843
--- /dev/null
@@ -0,0 +1,73 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\UnitDeprecated\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\Argon2iPasswordHash;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class Argon2iPasswordHashTest extends UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function getOptionsReturnsPreviouslySetOptions()
+    {
+        $options = [
+            'memory_cost' => 2048,
+            'time_cost' => 4,
+            'threads' => 4,
+        ];
+        $subject = new Argon2iPasswordHash();
+        $subject->setOptions($options);
+        $this->assertSame($subject->getOptions(), $options);
+    }
+
+    /**
+     * @test
+     */
+    public function setOptionsThrowsExceptionWithTooLowMemoryCost()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1526042080);
+        $subject = new Argon2iPasswordHash();
+        $subject->setOptions(['memory_cost' => 1]);
+    }
+
+    /**
+     * @test
+     */
+    public function setOptionsThrowsExceptionWithTooLowTimeCost()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1526042081);
+        $subject = new Argon2iPasswordHash();
+        $subject->setOptions(['time_cost' => 1]);
+    }
+
+    /**
+     * @test
+     */
+    public function setOptionsThrowsExceptionWithTooLowThreads()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1526042082);
+        $subject = new Argon2iPasswordHash();
+        $subject->setOptions(['threads' => 1]);
+    }
+}
diff --git a/typo3/sysext/core/Tests/UnitDeprecated/Crypto/PasswordHashing/BcryptPasswordHashTest.php b/typo3/sysext/core/Tests/UnitDeprecated/Crypto/PasswordHashing/BcryptPasswordHashTest.php
new file mode 100644 (file)
index 0000000..a13ebde
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\UnitDeprecated\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\BcryptPasswordHash;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class BcryptPasswordHashTest extends UnitTestCase
+{
+    /**
+     * @var BcryptPasswordHash
+     */
+    protected $subject;
+
+    /**
+     * Sets up the fixtures for this testcase.
+     */
+    protected function setUp()
+    {
+        // Set a low cost to speed up tests
+        $options = [
+            'cost' => 10,
+        ];
+        $this->subject = new BcryptPasswordHash($options);
+    }
+
+    /**
+     * @test
+     */
+    public function getOptionsReturnsPreviouslySetOptions()
+    {
+        $options = [
+            'cost' => 11,
+        ];
+        $this->subject->setOptions($options);
+        $this->assertSame($this->subject->getOptions(), $options);
+    }
+
+    /**
+     * @test
+     */
+    public function setOptionsThrowsExceptionOnTooLowCostValue()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1526042084);
+        $this->subject->setOptions(['cost' => 9]);
+    }
+}
diff --git a/typo3/sysext/core/Tests/UnitDeprecated/Crypto/PasswordHashing/BlowfishPasswordHashTest.php b/typo3/sysext/core/Tests/UnitDeprecated/Crypto/PasswordHashing/BlowfishPasswordHashTest.php
new file mode 100644 (file)
index 0000000..155b2e2
--- /dev/null
@@ -0,0 +1,148 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\UnitDeprecated\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\Random;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class BlowfishPasswordHashTest extends UnitTestCase
+{
+    /**
+     * Keeps instance of object to test.
+     *
+     * @var \TYPO3\CMS\Core\Crypto\PasswordHashing\BlowfishPasswordHash
+     */
+    protected $objectInstance;
+
+    /**
+     * Sets up the fixtures for this testcase.
+     */
+    protected function setUp()
+    {
+        if (!CRYPT_BLOWFISH) {
+            $this->markTestSkipped('Blowfish is not supported on your platform.');
+        }
+        $this->objectInstance = $this->getMockBuilder(\TYPO3\CMS\Core\Crypto\PasswordHashing\BlowfishPasswordHash::class)
+            ->setMethods(['dummy'])
+            ->getMock();
+    }
+
+    /**
+     * @test
+     */
+    public function nonZeroSaltLength()
+    {
+        $this->assertTrue($this->objectInstance->getSaltLength() > 0);
+    }
+
+    /**
+     * @test
+     */
+    public function createdSaltedHashOfProperStructureForCustomSaltWithoutSetting()
+    {
+        $password = 'password';
+        // custom salt without setting
+        $randomBytes = (new Random())->generateRandomBytes($this->objectInstance->getSaltLength());
+        $salt = $this->objectInstance->base64Encode($randomBytes, $this->objectInstance->getSaltLength());
+        $this->assertTrue($this->objectInstance->isValidSalt($salt));
+        $saltedHashPassword = $this->objectInstance->getHashedPassword($password, $salt);
+        $this->assertTrue($this->objectInstance->isValidSaltedPW($saltedHashPassword));
+    }
+
+    /**
+     * @test
+     */
+    public function createdSaltedHashOfProperStructureForMinimumHashCount()
+    {
+        $password = 'password';
+        $minHashCount = $this->objectInstance->getMinHashCount();
+        $this->objectInstance->setHashCount($minHashCount);
+        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
+        $this->assertTrue($this->objectInstance->isValidSaltedPW($saltedHashPassword));
+        // reset hashcount
+        $this->objectInstance->setHashCount(null);
+    }
+
+    /**
+     * @test
+     */
+    public function passwordVariationsResultInDifferentHashes()
+    {
+        $pad = 'a';
+        $criticalPwLength = 0;
+        // We're using a constant salt.
+        $saltedHashPasswordCurrent = $salt = $this->objectInstance->getHashedPassword($pad);
+        for ($i = 0; $i <= 128; $i += 8) {
+            $password = str_repeat($pad, max($i, 1));
+            $saltedHashPasswordPrevious = $saltedHashPasswordCurrent;
+            $saltedHashPasswordCurrent = $this->objectInstance->getHashedPassword($password, $salt);
+            if ($i > 0 && $saltedHashPasswordPrevious === $saltedHashPasswordCurrent) {
+                $criticalPwLength = $i;
+                break;
+            }
+        }
+        $this->assertTrue($criticalPwLength == 0 || $criticalPwLength > 32, 'Duplicates of hashed passwords with plaintext password of length ' . $criticalPwLength . '+.');
+    }
+
+    /**
+     * @test
+     */
+    public function modifiedHashCount()
+    {
+        $hashCount = $this->objectInstance->getHashCount();
+        $this->objectInstance->setMaxHashCount($hashCount + 1);
+        $this->objectInstance->setHashCount($hashCount + 1);
+        $this->assertTrue($this->objectInstance->getHashCount() > $hashCount);
+        $this->objectInstance->setMinHashCount($hashCount - 1);
+        $this->objectInstance->setHashCount($hashCount - 1);
+        $this->assertTrue($this->objectInstance->getHashCount() < $hashCount);
+        // reset hashcount
+        $this->objectInstance->setHashCount(null);
+    }
+
+    /**
+     * @test
+     */
+    public function updateNecessityForIncreasedHashcount()
+    {
+        $password = 'password';
+        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
+        $increasedHashCount = $this->objectInstance->getHashCount() + 1;
+        $this->objectInstance->setMaxHashCount($increasedHashCount);
+        $this->objectInstance->setHashCount($increasedHashCount);
+        $this->assertTrue($this->objectInstance->isHashUpdateNeeded($saltedHashPassword));
+        // reset hashcount
+        $this->objectInstance->setHashCount(null);
+    }
+
+    /**
+     * @test
+     */
+    public function updateNecessityForDecreasedHashcount()
+    {
+        $password = 'password';
+        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
+        $decreasedHashCount = $this->objectInstance->getHashCount() - 1;
+        $this->objectInstance->setMinHashCount($decreasedHashCount);
+        $this->objectInstance->setHashCount($decreasedHashCount);
+        $this->assertFalse($this->objectInstance->isHashUpdateNeeded($saltedHashPassword));
+        // reset hashcount
+        $this->objectInstance->setHashCount(null);
+    }
+}
diff --git a/typo3/sysext/core/Tests/UnitDeprecated/Crypto/PasswordHashing/Md5PasswordHashTest.php b/typo3/sysext/core/Tests/UnitDeprecated/Crypto/PasswordHashing/Md5PasswordHashTest.php
new file mode 100644 (file)
index 0000000..39254f6
--- /dev/null
@@ -0,0 +1,89 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\UnitDeprecated\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\Md5PasswordHash;
+use TYPO3\CMS\Core\Crypto\Random;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class Md5PasswordHashTest extends UnitTestCase
+{
+    /**
+     * Keeps instance of object to test.
+     *
+     * @var Md5PasswordHash
+     */
+    protected $objectInstance;
+
+    /**
+     * Sets up the fixtures for this testcase.
+     */
+    protected function setUp()
+    {
+        if (!CRYPT_MD5) {
+            $this->markTestSkipped('Blowfish is not supported on your platform.');
+        }
+        $this->objectInstance = $this->getMockBuilder(Md5PasswordHash::class)
+            ->setMethods(['dummy'])
+            ->getMock();
+    }
+
+    /**
+     * @test
+     */
+    public function nonZeroSaltLength()
+    {
+        $this->assertTrue($this->objectInstance->getSaltLength() > 0);
+    }
+
+    /**
+     * @test
+     */
+    public function createdSaltedHashOfProperStructureForCustomSaltWithoutSetting()
+    {
+        $password = 'password';
+        // custom salt without setting
+        $randomBytes = (new Random())->generateRandomBytes($this->objectInstance->getSaltLength());
+        $salt = $this->objectInstance->base64Encode($randomBytes, $this->objectInstance->getSaltLength());
+        $this->assertTrue($this->objectInstance->isValidSalt($salt));
+        $saltedHashPassword = $this->objectInstance->getHashedPassword($password, $salt);
+        $this->assertTrue($this->objectInstance->isValidSaltedPW($saltedHashPassword));
+    }
+
+    /**
+     * @test
+     */
+    public function passwordVariationsResultInDifferentHashes()
+    {
+        $pad = 'a';
+        $criticalPwLength = 0;
+        // We're using a constant salt.
+        $saltedHashPasswordCurrent = $salt = $this->objectInstance->getHashedPassword($pad);
+        for ($i = 0; $i <= 128; $i += 8) {
+            $password = str_repeat($pad, max($i, 1));
+            $saltedHashPasswordPrevious = $saltedHashPasswordCurrent;
+            $saltedHashPasswordCurrent = $this->objectInstance->getHashedPassword($password, $salt);
+            if ($i > 0 && $saltedHashPasswordPrevious === $saltedHashPasswordCurrent) {
+                $criticalPwLength = $i;
+                break;
+            }
+        }
+        $this->assertTrue($criticalPwLength == 0 || $criticalPwLength > 32, 'Duplicates of hashed passwords with plaintext password of length ' . $criticalPwLength . '+.');
+    }
+}
diff --git a/typo3/sysext/core/Tests/UnitDeprecated/Crypto/PasswordHashing/PasswordHashFactoryTest.php b/typo3/sysext/core/Tests/UnitDeprecated/Crypto/PasswordHashing/PasswordHashFactoryTest.php
new file mode 100644 (file)
index 0000000..d7c1503
--- /dev/null
@@ -0,0 +1,104 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\UnitDeprecated\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\PasswordHashFactory;
+use TYPO3\CMS\Core\Crypto\Random;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class PasswordHashFactoryTest extends UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function abstractComposedSaltBase64EncodeReturnsProperLength()
+    {
+        $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['saltedpasswords'] = [
+            'BE' => [
+                'saltedPWHashingMethod' => \TYPO3\CMS\Core\Crypto\PasswordHashing\Pbkdf2PasswordHash::class,
+            ],
+            'FE' => [
+                'saltedPWHashingMethod' => \TYPO3\CMS\Core\Crypto\PasswordHashing\Pbkdf2PasswordHash::class,
+            ],
+        ];
+
+        // set up an instance that extends AbstractComposedSalt first
+        $saltPbkdf2 = '$pbkdf2-sha256$6400$0ZrzXitFSGltTQnBWOsdAw$Y11AchqV4b0sUisdZd0Xr97KWoymNE0LNNrnEgY4H9M';
+        $objectInstance = PasswordHashFactory::getSaltingInstance($saltPbkdf2);
+
+        // 3 Bytes should result in a 6 char length base64 encoded string
+        // used for MD5 and PHPass salted hashing
+        $byteLength = 3;
+        $reqLengthBase64 = (int)ceil($byteLength * 8 / 6);
+        $randomBytes = (new Random())->generateRandomBytes($byteLength);
+        $this->assertTrue(strlen($objectInstance->base64Encode($randomBytes, $byteLength)) == $reqLengthBase64);
+        // 16 Bytes should result in a 22 char length base64 encoded string
+        // used for Blowfish salted hashing
+        $byteLength = 16;
+        $reqLengthBase64 = (int)ceil($byteLength * 8 / 6);
+        $randomBytes = (new Random())->generateRandomBytes($byteLength);
+        $this->assertTrue(strlen($objectInstance->base64Encode($randomBytes, $byteLength)) == $reqLengthBase64);
+    }
+
+    /**
+     * @test
+     */
+    public function objectInstanceForPhpPasswordHashBcryptSalts()
+    {
+        $saltBcrypt = '$2y$12$Tz.al0seuEgRt61u0bzqAOWu67PgG2ThG25oATJJ0oS5KLCPCgBOe';
+        $objectInstance = PasswordHashFactory::getSaltingInstance($saltBcrypt);
+        $this->assertInstanceOf(\TYPO3\CMS\Core\Crypto\PasswordHashing\BcryptPasswordHash::class, $objectInstance);
+    }
+
+    /**
+     * @test
+     */
+    public function objectInstanceForPhpPasswordHashArgon2iSalts()
+    {
+        $saltArgon2i = '$argon2i$v=19$m=8,t=1,p=1$djZiNkdEa3lOZm1SSmZsdQ$9iiRjpLZAT7kfHwS1xU9cqSU7+nXy275qpB/eKjI1ig';
+        $objectInstance = PasswordHashFactory::getSaltingInstance($saltArgon2i);
+        $this->assertInstanceOf(\TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash::class, $objectInstance);
+    }
+
+    /**
+     * @test
+     */
+    public function resettingFactoryInstanceSucceeds()
+    {
+        $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['saltedpasswords'] = [
+            'BE' => [
+                'saltedPWHashingMethod' => \TYPO3\CMS\Core\Crypto\PasswordHashing\Pbkdf2PasswordHash::class,
+            ],
+            'FE' => [
+                'saltedPWHashingMethod' => \TYPO3\CMS\Core\Crypto\PasswordHashing\Pbkdf2PasswordHash::class,
+            ],
+        ];
+
+        $defaultClassNameToUse = \TYPO3\CMS\Core\Crypto\PasswordHashing\SaltedPasswordsUtility::getDefaultSaltingHashingMethod();
+        if ($defaultClassNameToUse == \TYPO3\CMS\Core\Crypto\PasswordHashing\Md5PasswordHash::class) {
+            $saltedPW = '$P$CWF13LlG/0UcAQFUjnnS4LOqyRW43c.';
+        } else {
+            $saltedPW = '$1$rasmusle$rISCgZzpwk3UhDidwXvin0';
+        }
+        $objectInstance = PasswordHashFactory::getSaltingInstance($saltedPW);
+        // resetting
+        $objectInstance = PasswordHashFactory::getSaltingInstance(null);
+        $this->assertTrue(get_class($objectInstance) == $defaultClassNameToUse || is_subclass_of($objectInstance, $defaultClassNameToUse));
+    }
+}
diff --git a/typo3/sysext/core/Tests/UnitDeprecated/Crypto/PasswordHashing/Pbkdf2PasswordHashTest.php b/typo3/sysext/core/Tests/UnitDeprecated/Crypto/PasswordHashing/Pbkdf2PasswordHashTest.php
new file mode 100644 (file)
index 0000000..1db12ab
--- /dev/null
@@ -0,0 +1,138 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\UnitDeprecated\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\Pbkdf2PasswordHash;
+use TYPO3\CMS\Core\Crypto\Random;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class Pbkdf2PasswordHashTest extends UnitTestCase
+{
+    /**
+     * Keeps instance of object to test.
+     *
+     * @var Pbkdf2PasswordHash
+     */
+    protected $subject;
+
+    /**
+     * Sets up the fixtures for this testcase.
+     */
+    protected function setUp()
+    {
+        $this->subject = new Pbkdf2PasswordHash(['hash_count' => 1001]);
+    }
+
+    /**
+     * @test
+     */
+    public function nonZeroSaltLength()
+    {
+        $this->assertTrue($this->subject->getSaltLength() > 0);
+    }
+
+    /**
+     * @test
+     */
+    public function createdSaltedHashOfProperStructureForCustomSaltWithoutSetting()
+    {
+        $password = 'password';
+        // custom salt without setting
+        $randomBytes = (new Random())->generateRandomBytes($this->subject->getSaltLength());
+        $salt = $this->subject->base64Encode($randomBytes, $this->subject->getSaltLength());
+        $this->assertTrue($this->subject->isValidSalt($salt));
+        $saltedHashPassword = $this->subject->getHashedPassword($password, '6400$' . $salt);
+        $this->assertTrue($this->subject->isValidSaltedPW($saltedHashPassword));
+    }
+
+    /**
+     * @test
+     */
+    public function createdSaltedHashOfProperStructureForMinimumHashCount()
+    {
+        $password = 'password';
+        $minHashCount = $this->subject->getMinHashCount();
+        $this->subject->setHashCount($minHashCount);
+        $saltedHashPassword = $this->subject->getHashedPassword($password);
+        $this->assertTrue($this->subject->isValidSaltedPW($saltedHashPassword));
+        // reset hashcount
+        $this->subject->setHashCount(null);
+    }
+
+    /**
+     * @test
+     */
+    public function passwordVariationsResultInDifferentHashes()
+    {
+        $pad = 'a';
+        $criticalPwLength = 0;
+        // We're using a constant salt.
+        $saltedHashPasswordCurrent = $salt = $this->subject->getHashedPassword($pad);
+        for ($i = 0; $i <= 128; $i += 8) {
+            $password = str_repeat($pad, max($i, 1));
+            $saltedHashPasswordPrevious = $saltedHashPasswordCurrent;
+            $saltedHashPasswordCurrent = $this->subject->getHashedPassword($password, $salt);
+            if ($i > 0 && $saltedHashPasswordPrevious === $saltedHashPasswordCurrent) {
+                $criticalPwLength = $i;
+                break;
+            }
+        }
+        $this->assertTrue($criticalPwLength == 0 || $criticalPwLength > 32, 'Duplicates of hashed passwords with plaintext password of length ' . $criticalPwLength . '+.');
+    }
+
+    /**
+     * @test
+     */
+    public function modifiedHashCount()
+    {
+        $hashCount = $this->subject->getHashCount();
+        $this->subject->setMaxHashCount($hashCount + 1);
+        $this->subject->setHashCount($hashCount + 1);
+        $this->assertTrue($this->subject->getHashCount() > $hashCount);
+        $this->subject->setMinHashCount($hashCount - 1);
+        $this->subject->setHashCount($hashCount - 1);
+        $this->assertTrue($this->subject->getHashCount() < $hashCount);
+    }
+
+    /**
+     * @test
+     */
+    public function updateNecessityForIncreasedHashcount()
+    {
+        $password = 'password';
+        $saltedHashPassword = $this->subject->getHashedPassword($password);
+        $increasedHashCount = $this->subject->getHashCount() + 1;
+        $this->subject->setMaxHashCount($increasedHashCount);
+        $this->subject->setHashCount($increasedHashCount);
+        $this->assertTrue($this->subject->isHashUpdateNeeded($saltedHashPassword));
+    }
+
+    /**
+     * @test
+     */
+    public function updateNecessityForDecreasedHashcount()
+    {
+        $password = 'password';
+        $saltedHashPassword = $this->subject->getHashedPassword($password);
+        $decreasedHashCount = $this->subject->getHashCount() - 1;
+        $this->subject->setMinHashCount($decreasedHashCount);
+        $this->subject->setHashCount($decreasedHashCount);
+        $this->assertFalse($this->subject->isHashUpdateNeeded($saltedHashPassword));
+    }
+}
diff --git a/typo3/sysext/core/Tests/UnitDeprecated/Crypto/PasswordHashing/PhpassPasswordHashTest.php b/typo3/sysext/core/Tests/UnitDeprecated/Crypto/PasswordHashing/PhpassPasswordHashTest.php
new file mode 100644 (file)
index 0000000..2e75be1
--- /dev/null
@@ -0,0 +1,146 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Tests\UnitDeprecated\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\PhpassPasswordHash;
+use TYPO3\CMS\Core\Crypto\Random;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class PhpassPasswordHashTest extends UnitTestCase
+{
+    /**
+     * Keeps instance of object to test.
+     *
+     * @var PhpassPasswordHash
+     */
+    protected $objectInstance;
+
+    /**
+     * Sets up the fixtures for this testcase.
+     */
+    protected function setUp()
+    {
+        $this->objectInstance = $this->getMockBuilder(PhpassPasswordHash::class)
+            ->setMethods(['dummy'])
+            ->getMock();
+    }
+
+    /**
+     * @test
+     */
+    public function nonZeroSaltLength()
+    {
+        $this->assertTrue($this->objectInstance->getSaltLength() > 0);
+    }
+
+    /**
+     * @test
+     */
+    public function createdSaltedHashOfProperStructureForCustomSaltWithoutSetting()
+    {
+        $password = 'password';
+        // custom salt without setting
+        $randomBytes = (new Random())->generateRandomBytes($this->objectInstance->getSaltLength());
+        $salt = $this->objectInstance->base64Encode($randomBytes, $this->objectInstance->getSaltLength());
+        $this->assertTrue($this->objectInstance->isValidSalt($salt));
+        $saltedHashPassword = $this->objectInstance->getHashedPassword($password, $salt);
+        $this->assertTrue($this->objectInstance->isValidSaltedPW($saltedHashPassword));
+    }
+
+    /**
+     * @test
+     */
+    public function createdSaltedHashOfProperStructureForMinimumHashCount()
+    {
+        $password = 'password';
+        $minHashCount = $this->objectInstance->getMinHashCount();
+        $this->objectInstance->setHashCount($minHashCount);
+        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
+        $this->assertTrue($this->objectInstance->isValidSaltedPW($saltedHashPassword));
+        // reset hashcount
+        $this->objectInstance->setHashCount(null);
+    }
+
+    /**
+     * @test
+     */
+    public function passwordVariationsResultInDifferentHashes()
+    {
+        $pad = 'a';
+        $criticalPwLength = 0;
+        // We're using a constant salt.
+        $saltedHashPasswordCurrent = $salt = $this->objectInstance->getHashedPassword($pad);
+        for ($i = 0; $i <= 128; $i += 8) {
+            $password = str_repeat($pad, max($i, 1));
+            $saltedHashPasswordPrevious = $saltedHashPasswordCurrent;
+            $saltedHashPasswordCurrent = $this->objectInstance->getHashedPassword($password, $salt);
+            if ($i > 0 && $saltedHashPasswordPrevious === $saltedHashPasswordCurrent) {
+                $criticalPwLength = $i;
+                break;
+            }
+        }
+        $this->assertTrue($criticalPwLength == 0 || $criticalPwLength > 32, 'Duplicates of hashed passwords with plaintext password of length ' . $criticalPwLength . '+.');
+    }
+
+    /**
+     * @test
+     */
+    public function modifiedHashCount()
+    {
+        $hashCount = $this->objectInstance->getHashCount();
+        $this->objectInstance->setMaxHashCount($hashCount + 1);
+        $this->objectInstance->setHashCount($hashCount + 1);
+        $this->assertTrue($this->objectInstance->getHashCount() > $hashCount);
+        $this->objectInstance->setMinHashCount($hashCount - 1);
+        $this->objectInstance->setHashCount($hashCount - 1);
+        $this->assertTrue($this->objectInstance->getHashCount() < $hashCount);
+        // reset hashcount
+        $this->objectInstance->setHashCount(null);
+    }
+
+    /**
+     * @test
+     */
+    public function updateNecessityForIncreasedHashcount()
+    {
+        $password = 'password';
+        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
+        $increasedHashCount = $this->objectInstance->getHashCount() + 1;
+        $this->objectInstance->setMaxHashCount($increasedHashCount);
+        $this->objectInstance->setHashCount($increasedHashCount);
+        $this->assertTrue($this->objectInstance->isHashUpdateNeeded($saltedHashPassword));
+        // reset hashcount
+        $this->objectInstance->setHashCount(null);
+    }
+
+    /**
+     * @test
+     */
+    public function updateNecessityForDecreasedHashcount()
+    {
+        $password = 'password';
+        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
+        $decreasedHashCount = $this->objectInstance->getHashCount() - 1;
+        $this->objectInstance->setMinHashCount($decreasedHashCount);
+        $this->objectInstance->setHashCount($decreasedHashCount);
+        $this->assertFalse($this->objectInstance->isHashUpdateNeeded($saltedHashPassword));
+        // reset hashcount
+        $this->objectInstance->setHashCount(null);
+    }
+}
index f42f147..c05e16d 100644 (file)
@@ -68,6 +68,7 @@
        "replace": {
                "core": "*",
                "typo3/cms-lang": "*",
+               "typo3/cms-saltedpasswords": "*",
                "typo3/cms-sv": "*"
        },
        "extra": {
index 648c38d..7b49f38 100644 (file)
@@ -18,6 +18,7 @@ use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerAwareTrait;
 use TYPO3\CMS\Core\Authentication\LoginType;
 use TYPO3\CMS\Core\Context\Context;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
 use TYPO3\CMS\Core\Crypto\Random;
 use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
@@ -25,7 +26,6 @@ use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
 use TYPO3\CMS\Frontend\Plugin\AbstractPlugin;
-use TYPO3\CMS\Saltedpasswords\Salt\SaltFactory;
 
 /**
  * Plugin 'Website User Login' for the 'felogin' extension.
@@ -353,7 +353,7 @@ class FrontendLoginController extends AbstractPlugin implements LoggerAwareInter
                         );
                     } else {
                         // Hash password using configured salted passwords hash mechanism for FE
-                        $hashInstance = GeneralUtility::makeInstance(SaltFactory::class)->getDefaultHashInstance('FE');
+                        $hashInstance = GeneralUtility::makeInstance(PasswordHashFactory::class)->getDefaultHashInstance('FE');
                         $newPass = $hashInstance->getHashedPassword($postData['password1']);
 
                         // Call a hook for further password processing
index 23d8412..d4f4294 100644 (file)
@@ -16,11 +16,11 @@ namespace TYPO3\CMS\Install\Authentication;
  */
 
 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
 use TYPO3\CMS\Core\Mail\MailMessage;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Install\Service\SessionService;
-use TYPO3\CMS\Saltedpasswords\Exception\InvalidSaltException;
-use TYPO3\CMS\Saltedpasswords\Salt\SaltFactory;
 
 /**
  * Authenticates a user (currently comparing it through the install tool password, but could be extended)
@@ -51,11 +51,11 @@ class AuthenticationService
         $validPassword = false;
         if ($password !== null && $password !== '') {
             $installToolPassword = $GLOBALS['TYPO3_CONF_VARS']['BE']['installToolPassword'];
-            $hashFactory = GeneralUtility::makeInstance(SaltFactory::class);
+            $hashFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
             try {
                 $hashInstance = $hashFactory->get($installToolPassword, 'BE');
                 $validPassword = $hashInstance->checkPassword($password, $installToolPassword);
-            } catch (InvalidSaltException $e) {
+            } catch (InvalidPasswordHashException $e) {
                 // Given hash in global configuration is not a valid salted password
                 if (md5($password) === $installToolPassword) {
                     // Update configured install tool hash if it is still "MD5" and password matches
index 74b1264..0adb3c9 100644 (file)
@@ -15,9 +15,9 @@ namespace TYPO3\CMS\Install\Configuration\PasswordHashing;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Install\Configuration\AbstractPreset;
-use TYPO3\CMS\Saltedpasswords\Salt\Argon2iSalt;
 
 /**
  * Preset for password hashing method "argon2i"
@@ -38,9 +38,9 @@ class Argon2iPreset extends AbstractPreset
      * @var array Configuration values handled by this preset
      */
     protected $configurationValues = [
-        'BE/passwordHashing/className' => Argon2iSalt::class,
+        'BE/passwordHashing/className' => Argon2iPasswordHash::class,
         'BE/passwordHashing/options' => [],
-        'FE/passwordHashing/className' => Argon2iSalt::class,
+        'FE/passwordHashing/className' => Argon2iPasswordHash::class,
         'FE/passwordHashing/options' => [],
     ];
 
@@ -49,8 +49,8 @@ class Argon2iPreset extends AbstractPreset
      *
      * @return bool
      */
-    public function isAvailable()
+    public function isAvailable(): bool
     {
-        return GeneralUtility::makeInstance(Argon2iSalt::class)->isAvailable();
+        return GeneralUtility::makeInstance(Argon2iPasswordHash::class)->isAvailable();
     }
 }
index 4924407..54fca73 100644 (file)
@@ -15,9 +15,9 @@ namespace TYPO3\CMS\Install\Configuration\PasswordHashing;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Crypto\PasswordHashing\BcryptPasswordHash;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Install\Configuration\AbstractPreset;
-use TYPO3\CMS\Saltedpasswords\Salt\BcryptSalt;
 
 /**
  * Preset for password hashing method "bcrypt"
@@ -38,9 +38,9 @@ class BcryptPreset extends AbstractPreset
      * @var array Configuration values handled by this preset
      */
     protected $configurationValues = [
-        'BE/passwordHashing/className' => BcryptSalt::class,
+        'BE/passwordHashing/className' => BcryptPasswordHash::class,
         'BE/passwordHashing/options' => [],
-        'FE/passwordHashing/className' => BcryptSalt::class,
+        'FE/passwordHashing/className' => BcryptPasswordHash::class,
         'FE/passwordHashing/options' => [],
     ];
 
@@ -49,8 +49,8 @@ class BcryptPreset extends AbstractPreset
      *
      * @return bool
      */
-    public function isAvailable()
+    public function isAvailable(): bool
     {
-        return GeneralUtility::makeInstance(BcryptSalt::class)->isAvailable();
+        return GeneralUtility::makeInstance(BcryptPasswordHash::class)->isAvailable();
     }
 }
index 76b7073..26f172b 100644 (file)
@@ -31,7 +31,7 @@ class CustomPreset extends AbstractCustomPreset implements CustomPresetInterface
      *
      * @return array Current custom configuration values
      */
-    public function getConfigurationValues()
+    public function getConfigurationValues(): array
     {
         $configurationValues = [];
         $configurationValues['BE/passwordHashing/className'] =
index afec9ce..04a4306 100644 (file)
@@ -15,9 +15,9 @@ namespace TYPO3\CMS\Install\Configuration\PasswordHashing;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Crypto\PasswordHashing\Pbkdf2PasswordHash;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Install\Configuration\AbstractPreset;
-use TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt;
 
 /**
  * Preset for password hashing method "PBKDF2"
@@ -38,9 +38,9 @@ class Pbkdf2Preset extends AbstractPreset
      * @var array Configuration values handled by this preset
      */
     protected $configurationValues = [
-        'BE/passwordHashing/className' => Pbkdf2Salt::class,
+        'BE/passwordHashing/className' => Pbkdf2PasswordHash::class,
         'BE/passwordHashing/options' => [],
-        'FE/passwordHashing/className' => Pbkdf2Salt::class,
+        'FE/passwordHashing/className' => Pbkdf2PasswordHash::class,
         'FE/passwordHashing/options' => [],
     ];
 
@@ -49,8 +49,8 @@ class Pbkdf2Preset extends AbstractPreset
      *
      * @return bool
      */
-    public function isAvailable()
+    public function isAvailable(): bool
     {
-        return GeneralUtility::makeInstance(Pbkdf2Salt::class)->isAvailable();
+        return GeneralUtility::makeInstance(Pbkdf2PasswordHash::class)->isAvailable();
     }
 }
index b60cb13..51a98e1 100644 (file)
@@ -15,9 +15,9 @@ namespace TYPO3\CMS\Install\Configuration\PasswordHashing;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Crypto\PasswordHashing\PhpassPasswordHash;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Install\Configuration\AbstractPreset;
-use TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt;
 
 /**
  * Preset for password hashing method "phpass"
@@ -38,9 +38,9 @@ class PhpassPreset extends AbstractPreset
      * @var array Configuration values handled by this preset
      */
     protected $configurationValues = [
-        'BE/passwordHashing/className' => PhpassSalt::class,
+        'BE/passwordHashing/className' => PhpassPasswordHash::class,
         'BE/passwordHashing/options' => [],
-        'FE/passwordHashing/className' => PhpassSalt::class,
+        'FE/passwordHashing/className' => PhpassPasswordHash::class,
         'FE/passwordHashing/options' => [],
     ];
 
@@ -49,8 +49,8 @@ class PhpassPreset extends AbstractPreset
      *
      * @return bool
      */
-    public function isAvailable()
+    public function isAvailable(): bool
     {
-        return GeneralUtility::makeInstance(PhpassSalt::class)->isAvailable();
+        return GeneralUtility::makeInstance(PhpassPasswordHash::class)->isAvailable();
     }
 }
index 44d13eb..9707763 100644 (file)
@@ -23,6 +23,12 @@ use TYPO3\CMS\Core\Configuration\ConfigurationManager;
 use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
 use TYPO3\CMS\Core\Core\Bootstrap;
 use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\BcryptPasswordHash;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashInterface;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\Pbkdf2PasswordHash;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\PhpassPasswordHash;
 use TYPO3\CMS\Core\Crypto\Random;
 use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
@@ -47,12 +53,6 @@ use TYPO3\CMS\Install\Service\Exception\ConfigurationChangedException;
 use TYPO3\CMS\Install\Service\SilentConfigurationUpgradeService;
 use TYPO3\CMS\Install\SystemEnvironment\Check;
 use TYPO3\CMS\Install\SystemEnvironment\SetupCheck;
-use TYPO3\CMS\Saltedpasswords\Salt\Argon2iSalt;
-use TYPO3\CMS\Saltedpasswords\Salt\BcryptSalt;
-use TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt;
-use TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt;
-use TYPO3\CMS\Saltedpasswords\Salt\SaltFactory;
-use TYPO3\CMS\Saltedpasswords\Salt\SaltInterface;
 
 /**
  * Install step controller, dispatcher class of step actions.
@@ -1100,24 +1100,24 @@ For each website you need a TypoScript template on the main page of your website
      * This function returns a salted hashed key for new backend user password and install tool password.
      *
      * This method is executed during installation *before* the preset did set up proper hash method
-     * selection in LocalConfiguration. So SaltFactory is not usable at this point. We thus loop through
+     * 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
      * executeDefaultConfigurationAction() basically does the same later.
      *
      * @param string $password Plain text password
      * @return string Hashed password
-     * @throws \LogicException If no hash method has been found, should never happen PhpassSalt is always available
+     * @throws \LogicException If no hash method has been found, should never happen PhpassPasswordHash is always available
      */
     protected function getHashedPassword($password)
     {
         $okHashMethods = [
-            Argon2iSalt::class,
-            BcryptSalt::class,
-            Pbkdf2Salt::class,
-            PhpassSalt::class,
+            Argon2iPasswordHash::class,
+            BcryptPasswordHash::class,
+            Pbkdf2PasswordHash::class,
+            PhpassPasswordHash::class,
         ];
         foreach ($okHashMethods as $className) {
-            /** @var SaltInterface $instance */
+            /** @var PasswordHashInterface $instance */
             $instance = GeneralUtility::makeInstance($className);
             if ($instance->isAvailable()) {
                 return $instance->getHashedPassword($password);
index 8767a39..b8a34c0 100644 (file)
@@ -20,6 +20,7 @@ use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
 use TYPO3\CMS\Core\Core\ClassLoadingInformation;
 use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Schema\Exception\StatementException;
 use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
@@ -36,7 +37,6 @@ use TYPO3\CMS\Install\Service\ClearCacheService;
 use TYPO3\CMS\Install\Service\ClearTableService;
 use TYPO3\CMS\Install\Service\LanguagePackService;
 use TYPO3\CMS\Install\Service\Typo3tempFileService;
-use TYPO3\CMS\Saltedpasswords\Salt\SaltFactory;
 
 /**
  * Maintenance controller
@@ -478,7 +478,7 @@ class MaintenanceController extends AbstractController
                     FlashMessage::ERROR
                 ));
             } else {
-                $hashInstance = GeneralUtility::makeInstance(SaltFactory::class)->getDefaultHashInstance('BE');
+                $hashInstance = GeneralUtility::makeInstance(PasswordHashFactory::class)->getDefaultHashInstance('BE');
                 $hashedPassword = $hashInstance->getHashedPassword($password);
                 $adminUserFields = [
                     'username' => $username,
index a1055bb..8f1c06a 100644 (file)
@@ -20,6 +20,7 @@ use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
 use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
 use TYPO3\CMS\Core\Core\Bootstrap;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
 use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
@@ -34,7 +35,6 @@ use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\Install\Configuration\FeatureManager;
 use TYPO3\CMS\Install\Service\ExtensionConfigurationService;
 use TYPO3\CMS\Install\Service\LocalConfigurationValueService;
-use TYPO3\CMS\Saltedpasswords\Salt\SaltFactory;
 
 /**
  * Settings controller
@@ -100,7 +100,7 @@ class SettingsController extends AbstractController
                 FlashMessage::ERROR
             ));
         } else {
-            $hashInstance = GeneralUtility::makeInstance(SaltFactory::class)->getDefaultHashInstance('BE');
+            $hashInstance = GeneralUtility::makeInstance(PasswordHashFactory::class)->getDefaultHashInstance('BE');
             $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
             $configurationManager->setLocalConfigurationValueByPath(
                 'BE/installToolPassword',
index bd756f0..17cdc30 100644 (file)
@@ -19,6 +19,7 @@ use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
 use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
 use TYPO3\CMS\Core\FormProtection\InstallToolFormProtection;
 use TYPO3\CMS\Core\Http\HtmlResponse;
@@ -40,7 +41,6 @@ use TYPO3\CMS\Install\Controller\SettingsController;
 use TYPO3\CMS\Install\Controller\UpgradeController;
 use TYPO3\CMS\Install\Service\EnableFileService;
 use TYPO3\CMS\Install\Service\SessionService;
-use TYPO3\CMS\Saltedpasswords\Salt\SaltFactory;
 
 /**
  * Default request handler for all requests inside the TYPO3 Install Tool, which does a simple hardcoded
@@ -138,7 +138,7 @@ class RequestHandler implements RequestHandlerInterface
                         new FlashMessage('Please enter the install tool password', '', FlashMessage::ERROR)
                     );
                 } else {
-                    $hashInstance = GeneralUtility::makeInstance(SaltFactory::class)->getDefaultHashInstance('BE');
+                    $hashInstance = GeneralUtility::makeInstance(PasswordHashFactory::class)->getDefaultHashInstance('BE');
                     $hashedPassword = $hashInstance->getHashedPassword($password);
                     $messageQueue = (new FlashMessageQueue('install'))->enqueue(
                         new FlashMessage(
index c810744..34a57c7 100644 (file)
@@ -15,11 +15,11 @@ namespace TYPO3\CMS\Install\Report;
  */
 
 use TYPO3\CMS\Core\Core\Environment;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Install\Service\EnableFileService;
 use TYPO3\CMS\Reports\Status;
-use TYPO3\CMS\Saltedpasswords\Exception\InvalidSaltException;
-use TYPO3\CMS\Saltedpasswords\Salt\SaltFactory;
 
 /**
  * Provides an status report of the security of the install tool
@@ -54,10 +54,10 @@ class SecurityStatusReport implements \TYPO3\CMS\Reports\StatusProviderInterface
         $validPassword = true;
         $installToolPassword = $GLOBALS['TYPO3_CONF_VARS']['BE']['installToolPassword'];
         $hashInstance = null;
-        $hashFactory = GeneralUtility::makeInstance(SaltFactory::class);
+        $hashFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
         try {
             $hashInstance = $hashFactory->get($installToolPassword, 'BE');
-        } catch (InvalidSaltException $e) {
+        } catch (InvalidPasswordHashException $e) {
             // $hashInstance stays null
         }
         if ($installToolPassword !== '' && $hashInstance === null) {
index a9f405c..501eb07 100644 (file)
@@ -15,16 +15,16 @@ namespace TYPO3\CMS\Install\Service;
  */
 
 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\BcryptPasswordHash;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashInterface;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\Pbkdf2PasswordHash;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\PhpassPasswordHash;
 use TYPO3\CMS\Core\Crypto\Random;
 use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Install\Service\Exception\ConfigurationChangedException;
-use TYPO3\CMS\Saltedpasswords\Salt\Argon2iSalt;
-use TYPO3\CMS\Saltedpasswords\Salt\BcryptSalt;
-use TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt;
-use TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt;
-use TYPO3\CMS\Saltedpasswords\Salt\SaltInterface;
 
 /**
  * Execute "silent" LocalConfiguration upgrades if needed.
@@ -991,15 +991,15 @@ class SilentConfigurationUpgradeService
         // to some different hash mechanism will not be touched again after first upgrade.
         // Phpass is always available, so we have some last fallback if the others don't kick in
         $okHashMethods = [
-            Argon2iSalt::class,
-            BcryptSalt::class,
-            Pbkdf2Salt::class,
-            PhpassSalt::class,
+            Argon2iPasswordHash::class,
+            BcryptPasswordHash::class,
+            Pbkdf2PasswordHash::class,
+            PhpassPasswordHash::class,
         ];
         $newMethods = [];
         foreach (['BE', 'FE'] as $mode) {
             foreach ($okHashMethods as $className) {
-                /** @var SaltInterface $instance */
+                /** @var PasswordHashInterface $instance */
                 $instance = GeneralUtility::makeInstance($className);
                 if ($instance->isAvailable()) {
                     $newMethods[$mode] = $className;
@@ -1009,10 +1009,10 @@ class SilentConfigurationUpgradeService
         }
         // We only need to write to LocalConfiguration if method is different than Argon2i from DefaultConfiguration
         $newConfig = [];
-        if ($newMethods['BE'] !== Argon2iSalt::class) {
+        if ($newMethods['BE'] !== Argon2iPasswordHash::class) {
             $newConfig['BE/passwordHashing/className'] = $newMethods['BE'];
         }
-        if ($newMethods['FE'] !== Argon2iSalt::class) {
+        if ($newMethods['FE'] !== Argon2iPasswordHash::class) {
             $newConfig['FE/passwordHashing/className'] = $newMethods['FE'];
         }
         if (!empty($newConfig)) {
index b79c130..c274a05 100644 (file)
@@ -719,24 +719,94 @@ return [
             'Deprecation-85802-MoveFlexFormServiceFromEXTextbaseToEXTcore.rst',
         ],
     ],
-    'TYPO3\CMS\Saltedpasswords\Salt\ComposedSaltInterface' => [
+    'TYPO3\CMS\Core\Crypto\PasswordHashing\Salt\ComposedPasswordHashInterface' => [
         'restFiles' => [
             'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst'
         ],
     ],
-    'TYPO3\CMS\Saltedpasswords\Salt\AbstractComposedSalt' => [
+    'TYPO3\CMS\Core\Crypto\PasswordHashing\Salt\AbstractComposedSalt' => [
         'restFiles' => [
             'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst'
         ],
     ],
-    'TYPO3\CMS\Saltedpasswords\Salt\Utility\ExtensionManagerConfigurationUtility' => [
+    'TYPO3\CMS\Core\Crypto\PasswordHashing\Utility\ExtensionManagerConfigurationUtility' => [
         'restFiles' => [
             'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst'
         ],
     ],
-    'TYPO3\CMS\Saltedpasswords\Salt\Utility\SaltedPasswordsUtility' => [
+    'TYPO3\CMS\Core\Crypto\PasswordHashing\Utility\SaltedPasswordsUtility' => [
         'restFiles' => [
             'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst'
         ],
     ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Argon2ISalt' => [
+        'restFiles' => [
+            'Deprecation-85833-ExtensionSaltedpasswordsMergedIntoCoreExtension.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\BcryptSalt' => [
+        'restFiles' => [
+            'Deprecation-85833-ExtensionSaltedpasswordsMergedIntoCoreExtension.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt' => [
+        'restFiles' => [
+            'Deprecation-85833-ExtensionSaltedpasswordsMergedIntoCoreExtension.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Exception\InvalidSaltException' => [
+        'restFiles' => [
+            'Deprecation-85833-ExtensionSaltedpasswordsMergedIntoCoreExtension.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Md5Salt' => [
+        'restFiles' => [
+            'Deprecation-85833-ExtensionSaltedpasswordsMergedIntoCoreExtension.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\SaltFactory' => [
+        'restFiles' => [
+            'Deprecation-85833-ExtensionSaltedpasswordsMergedIntoCoreExtension.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\SaltInterface' => [
+        'restFiles' => [
+            'Deprecation-85833-ExtensionSaltedpasswordsMergedIntoCoreExtension.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt' => [
+        'restFiles' => [
+            'Deprecation-85833-ExtensionSaltedpasswordsMergedIntoCoreExtension.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt' => [
+        'restFiles' => [
+            'Deprecation-85833-ExtensionSaltedpasswordsMergedIntoCoreExtension.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\AbstractComposedSalt' => [
+        'restFiles' => [
+            'Deprecation-85833-ExtensionSaltedpasswordsMergedIntoCoreExtension.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\ComposedSaltInterface' => [
+        'restFiles' => [
+            'Deprecation-85833-ExtensionSaltedpasswordsMergedIntoCoreExtension.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Utility\ExensionManagerConfigurationUtility' => [
+        'restFiles' => [
+            'Deprecation-85833-ExtensionSaltedpasswordsMergedIntoCoreExtension.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\SaltedPasswordsService' => [
+        'restFiles' => [
+            'Deprecation-85833-ExtensionSaltedpasswordsMergedIntoCoreExtension.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Utility\SaltedPasswordsUtility' => [
+        'restFiles' => [
+            'Deprecation-85833-ExtensionSaltedpasswordsMergedIntoCoreExtension.rst',
+        ],
+    ],
 ];
index 4d00b0c..7fec82e 100644 (file)
@@ -18,6 +18,8 @@ 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\Argon2iPasswordHash;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\BcryptPasswordHash;
 use TYPO3\CMS\Core\Package\PackageManager;
 use TYPO3\CMS\Core\Tests\Unit\Utility\AccessibleProxies\ExtensionManagementUtilityAccessibleProxy;
 use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
@@ -25,8 +27,6 @@ use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Install\Service\Exception\ConfigurationChangedException;
 use TYPO3\CMS\Install\Service\SilentConfigurationUpgradeService;
-use TYPO3\CMS\Saltedpasswords\Salt\Argon2iSalt;
-use TYPO3\CMS\Saltedpasswords\Salt\BcryptSalt;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 /**
@@ -831,12 +831,12 @@ class SilentConfigurationUpgradeServiceTest extends UnitTestCase
             ->shouldBeCalled()->willReturn(['thereIs' => 'something']);
         $configurationManagerProphecy->getLocalConfigurationValueByPath('EXT/extConf/saltedpasswords')
             ->shouldBeCalled()->willThrow($configurationManagerException);
-        $argonBeProphecy = $this->prophesize(Argon2iSalt::class);
+        $argonBeProphecy = $this->prophesize(Argon2iPasswordHash::class);
         $argonBeProphecy->isAvailable()->shouldBeCalled()->willReturn(true);
-        GeneralUtility::addInstance(Argon2iSalt::class, $argonBeProphecy->reveal());
-        $argonFeProphecy = $this->prophesize(Argon2iSalt::class);
+        GeneralUtility::addInstance(Argon2iPasswordHash::class, $argonBeProphecy->reveal());
+        $argonFeProphecy = $this->prophesize(Argon2iPasswordHash::class);
         $argonFeProphecy->isAvailable()->shouldBeCalled()->willReturn(true);
-        GeneralUtility::addInstance(Argon2iSalt::class, $argonFeProphecy->reveal());
+        GeneralUtility::addInstance(Argon2iPasswordHash::class, $argonFeProphecy->reveal());
         $configurationManagerProphecy->removeLocalConfigurationKeysByPath(['EXTENSIONS/saltedpasswords'])
             ->shouldBeCalled();
         $silentConfigurationUpgradeService = $this->getAccessibleMock(
@@ -860,12 +860,12 @@ class SilentConfigurationUpgradeServiceTest extends UnitTestCase
             ->shouldBeCalled()->willThrow($configurationManagerException);
         $configurationManagerProphecy->getLocalConfigurationValueByPath('EXT/extConf/saltedpasswords')
             ->shouldBeCalled()->willReturn('someConfiguration');
-        $argonBeProphecy = $this->prophesize(Argon2iSalt::class);
+        $argonBeProphecy = $this->prophesize(Argon2iPasswordHash::class);
         $argonBeProphecy->isAvailable()->shouldBeCalled()->willReturn(true);
-        GeneralUtility::addInstance(Argon2iSalt::class, $argonBeProphecy->reveal());
-        $argonFeProphecy = $this->prophesize(Argon2iSalt::class);
+        GeneralUtility::addInstance(Argon2iPasswordHash::class, $argonBeProphecy->reveal());
+        $argonFeProphecy = $this->prophesize(Argon2iPasswordHash::class);
         $argonFeProphecy->isAvailable()->shouldBeCalled()->willReturn(true);
-        GeneralUtility::addInstance(Argon2iSalt::class, $argonFeProphecy->reveal());
+        GeneralUtility::addInstance(Argon2iPasswordHash::class, $argonFeProphecy->reveal());
         $configurationManagerProphecy->removeLocalConfigurationKeysByPath(['EXT/extConf/saltedpasswords'])
             ->shouldBeCalled();
         $silentConfigurationUpgradeService = $this->getAccessibleMock(
@@ -888,21 +888,21 @@ class SilentConfigurationUpgradeServiceTest extends UnitTestCase
             ->shouldBeCalled()->willReturn(['thereIs' => 'something']);
         $configurationManagerProphecy->getLocalConfigurationValueByPath('EXT/extConf/saltedpasswords')
             ->shouldBeCalled()->willReturn('someConfiguration');
-        $argonBeProphecy = $this->prophesize(Argon2iSalt::class);
+        $argonBeProphecy = $this->prophesize(Argon2iPasswordHash::class);
         $argonBeProphecy->isAvailable()->shouldBeCalled()->willReturn(false);
-        GeneralUtility::addInstance(Argon2iSalt::class, $argonBeProphecy->reveal());
-        $bcryptBeProphecy = $this->prophesize(BcryptSalt::class);
+        GeneralUtility::addInstance(Argon2iPasswordHash::class, $argonBeProphecy->reveal());
+        $bcryptBeProphecy = $this->prophesize(BcryptPasswordHash::class);
         $bcryptBeProphecy->isAvailable()->shouldBeCalled()->willReturn(true);
-        GeneralUtility::addInstance(BcryptSalt::class, $bcryptBeProphecy->reveal());
-        $argonFeProphecy = $this->prophesize(Argon2iSalt::class);
+        GeneralUtility::addInstance(BcryptPasswordHash::class, $bcryptBeProphecy->reveal());
+        $argonFeProphecy = $this->prophesize(Argon2iPasswordHash::class);
         $argonFeProphecy->isAvailable()->shouldBeCalled()->willReturn(false);
-        GeneralUtility::addInstance(Argon2iSalt::class, $argonFeProphecy->reveal());
-        $bcryptFeProphecy = $this->prophesize(BcryptSalt::class);
+        GeneralUtility::addInstance(Argon2iPasswordHash::class, $argonFeProphecy->reveal());
+        $bcryptFeProphecy = $this->prophesize(BcryptPasswordHash::class);
         $bcryptFeProphecy->isAvailable()->shouldBeCalled()->willReturn(true);
-        GeneralUtility::addInstance(BcryptSalt::class, $bcryptFeProphecy->reveal());
+        GeneralUtility::addInstance(BcryptPasswordHash::class, $bcryptFeProphecy->reveal());
         $configurationManagerProphecy->setLocalConfigurationValuesByPathValuePairs([
-            'BE/passwordHashing/className' => BcryptSalt::class,
-            'FE/passwordHashing/className' => BcryptSalt::class,
+            'BE/passwordHashing/className' => BcryptPasswordHash::class,
+            'FE/passwordHashing/className' => BcryptPasswordHash::class,
         ])->shouldBeCalled();
         $configurationManagerProphecy->removeLocalConfigurationKeysByPath(['EXTENSIONS/saltedpasswords', 'EXT/extConf/saltedpasswords'])
             ->shouldBeCalled();
index 62ba704..a05187c 100644 (file)
@@ -17,14 +17,14 @@ namespace TYPO3\CMS\Reports\Report\Status;
 
 use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Backend\Routing\UriBuilder;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException;
+use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
 use TYPO3\CMS\Core\Localization\LanguageService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Reports\RequestAwareStatusProviderInterface;
 use TYPO3\CMS\Reports\Status as ReportStatus;
-use TYPO3\CMS\Saltedpasswords\Exception\InvalidSaltException;
-use TYPO3\CMS\Saltedpasswords\Salt\SaltFactory;
 
 /**
  * Performs several checks about the system's health
@@ -162,7 +162,7 @@ class SecurityStatus implements RequestAwareStatusProviderInterface
 
         if (!empty($row)) {
             try {
-                $hashInstance = GeneralUtility::makeInstance(SaltFactory::class)->get($row['password'], 'BE');
+                $hashInstance = GeneralUtility::makeInstance(PasswordHashFactory::class)->get($row['password'], 'BE');
                 if ($hashInstance->checkPassword('password', $row['password'])) {
                     // If the password for 'admin' user is 'password': bad idea!
                     // We're checking since the (very) old installer created instances like this in dark old times.
@@ -182,7 +182,7 @@ class SecurityStatus implements RequestAwareStatusProviderInterface
                         '</a>'
                     );
                 }
-            } catch (InvalidSaltException $e) {
+            } catch (InvalidPasswordHashException $e) {
                 // No hash class handling for current hash could be found. Not good, but ok in this case.
             }
         }
index 2b69fe4..9d383f5 100644 (file)
                        <trans-unit id="status_encryptionKey">
                                <source>Encryption Key</source>
                        </trans-unit>
-                       <trans-unit id="status_saltedPasswords">
-                               <source>Backend user password hashes</source>
-                       </trans-unit>
-                       <trans-unit id="status_saltedPasswords_infoText">
-                               <source>During the configuration check of saltedpasswords the following issues have been found</source>
-                       </trans-unit>
-                       <trans-unit id="status_saltedPasswords_notAllPasswordsHashed">
-                               <source>Some backend user passwords are found to be only md5 hashed. Run the scheduler task to convert all passwords to salted hashes.</source>
-                       </trans-unit>
                        <trans-unit id="status_fileDenyPattern">
                                <source>File Deny Pattern</source>
                        </trans-unit>
diff --git a/typo3/sysext/saltedpasswords/.gitattributes b/typo3/sysext/saltedpasswords/.gitattributes
deleted file mode 100644 (file)
index 434f281..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-/.gitattributes export-ignore
-/Resources/Private/TypeScript/ export-ignore
-/Tests/ export-ignore
diff --git a/typo3/sysext/saltedpasswords/Classes/Exception/InvalidSaltException.php b/typo3/sysext/saltedpasswords/Classes/Exception/InvalidSaltException.php
deleted file mode 100644 (file)
index 5f15c9b..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Saltedpasswords\Exception;
-
-/*
- * 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!
- */
-
-/**
- * InvalidSaltException thrown if salting went wrong.
- */
-class InvalidSaltException extends \Exception
-{
-}
diff --git a/typo3/sysext/saltedpasswords/Classes/Salt/AbstractComposedSalt.php b/typo3/sysext/saltedpasswords/Classes/Salt/AbstractComposedSalt.php
deleted file mode 100644 (file)
index 6baff1a..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Saltedpasswords\Salt;
-
-/*
- * 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!
- */
-
-/**
- * Abstract class with methods needed to be extended
- * in a salted hashing class that composes an own salted password hash.
- *
- * @deprecated and will be removed in TYPO3 v10.0.
- */
-abstract class AbstractComposedSalt
-{
-    /**
-     * Returns a string for mapping an int to the corresponding base 64 character.
-     *
-     * @return string String for mapping an int to the corresponding base 64 character
-     */
-    abstract protected function getItoa64(): string;
-
-    /**
-     * Encodes bytes into printable base 64 using the *nix standard from crypt().
-     *
-     * @param string $input The string containing bytes to encode.
-     * @param int $count The number of characters (bytes) to encode.
-     * @return string Encoded string
-     * @deprecated and will be removed in TYPO3 v10.0.
-     */
-    public function base64Encode(string $input, int $count): string
-    {
-        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
-        $output = '';
-        $i = 0;
-        $itoa64 = $this->getItoa64();
-        do {
-            $value = ord($input[$i++]);
-            $output .= $itoa64[$value & 63];
-            if ($i < $count) {
-                $value |= ord($input[$i]) << 8;
-            }
-            $output .= $itoa64[$value >> 6 & 63];
-            if ($i++ >= $count) {
-                break;
-            }
-            if ($i < $count) {
-                $value |= ord($input[$i]) << 16;
-            }
-            $output .= $itoa64[$value >> 12 & 63];
-            if ($i++ >= $count) {
-                break;
-            }
-            $output .= $itoa64[$value >> 18 & 63];
-        } while ($i < $count);
-        return $output;
-    }
-
-    /**
-     * Method determines required length of base64 characters for a given
-     * length of a byte string.
-     *
-     * @param int $byteLength Length of bytes to calculate in base64 chars
-     * @return int Required length of base64 characters
-     * @deprecated and will be removed in TYPO3 v10.0.
-     */
-    protected function getLengthBase64FromBytes(int $byteLength): int
-    {
-        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
-        // Calculates bytes in bits in base64
-        return (int)ceil($byteLength * 8 / 6);
-    }
-}
diff --git a/typo3/sysext/saltedpasswords/Classes/Salt/Argon2iSalt.php b/typo3/sysext/saltedpasswords/Classes/Salt/Argon2iSalt.php
deleted file mode 100644 (file)
index f2ee2db..0000000
+++ /dev/null
@@ -1,227 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Saltedpasswords\Salt;
-
-/*
- * 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\Saltedpasswords\Exception\InvalidSaltException;
-
-/**
- * This class implements the 'argon2i' flavour of the php password api.
- *
- * Hashes are identified by the prefix '$argon2i$'.
- *
- * The length of a argon2i password hash (in the form it is received from
- * PHP) depends on the environment.
- *
- * @see PASSWORD_ARGON2I in https://secure.php.net/manual/en/password.constants.php
- */
-class Argon2iSalt implements SaltInterface
-{
-    /**
-     * Prefix for the password hash.
-     */
-    protected const PREFIX = '$argon2i$';
-
-    /**
-     * The PHP defaults are rather low ('memory_cost' => 1024, 'time_cost' => 2, 'threads' => 2)
-     * 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.
-     *
-     * Note the default values are set again in 'setOptions' below if needed.
-     *
-     * @var array
-     */
-    protected $options = [
-        'memory_cost' => 16384,
-        'time_cost' => 16,
-        'threads' => 2
-    ];
-
-    /**
-     * Constructor sets options if given
-     *
-     * @param array $options
-     */
-    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
-     */
-    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('PASSWORD_ARGON2I') && PASSWORD_ARGON2I;
-    }
-
-    /**
-     * Creates a salted hash for a given plaintext password
-     *
-     * @param string $password Plaintext password to create a salted hash from
-     * @param string $salt Deprecated optional custom salt to use
-     * @return string|null Salted hashed password
-     */
-    public function getHashedPassword(string $password, string $salt = null)
-    {
-        if ($salt !== null) {
-            trigger_error(static::class . ': using a custom salt is deprecated in PHP password api and ignored', E_USER_DEPRECATED);
-        }
-        $hashedPassword = null;
-        if ($password !== '') {
-            $hashedPassword = password_hash($password, PASSWORD_ARGON2I, $this->options);
-            if (!is_string($hashedPassword) || empty($hashedPassword)) {
-                throw new InvalidSaltException('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
-     */
-    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;
-    }
-
-    /**
-     * @return array
-     * @deprecated and will be removed in TYPO3 v10.0.
-     */
-    public function getOptions(): array
-    {
-        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
-        return $this->options;
-    }
-
-    /**
-     * Set new memory_cost, time_cost, and thread values.
-     *
-     * @param array $options
-     * @deprecated and will be removed in TYPO3 v10.0.
-     */
-    public function setOptions(array $options): void
-    {
-        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
-        $newOptions = [];
-
-        // Check options for validity, else use hard coded defaults
-        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,
-                    1526042080
-                );
-            }
-            $newOptions['memory_cost'] = (int)$options['memory_cost'];
-        } else {
-            $newOptions['memory_cost'] = 16384;
-        }
-
-        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,
-                    1526042081
-                );
-            }
-            $newOptions['time_cost'] = (int)$options['time_cost'];
-        } else {
-            $newOptions['time_cost'] = 16;
-        }
-
-        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,
-                    1526042082
-                );
-            }
-            $newOptions['threads'] = (int)$options['threads'];
-        } else {
-            $newOptions['threads'] = 2;
-        }
-
-        $this->options = $newOptions;
-    }
-}
diff --git a/typo3/sysext/saltedpasswords/Classes/Salt/BcryptSalt.php b/typo3/sysext/saltedpasswords/Classes/Salt/BcryptSalt.php
deleted file mode 100644 (file)
index 139fbc1..0000000
+++ /dev/null
@@ -1,215 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Saltedpasswords\Salt;
-
-/*
- * 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\Saltedpasswords\Exception\InvalidSaltException;
-
-/**
- * This class implements the 'bcrypt' flavour of the php password api.
- *
- * Hashes are identified by the prefix '$2y$'.
- *
- * To workaround the limitations of bcrypt (accepts not more than 72
- * chars and truncates on NUL bytes), the plain password is pre-hashed
- * before the actual password-hash is generated/verified.
- *
- * @see PASSWORD_BCRYPT in https://secure.php.net/manual/en/password.constants.php
- */
-class BcryptSalt implements SaltInterface
-{
-    /**
-     * Prefix for the password hash
-     */
-    protected const PREFIX = '$2y$';
-
-    /**
-     * Raise default PHP cost (10). At the time of this writing, this leads to
-     * 150-200ms computing time on a casual I7 CPU.
-     *
-     * Note the default values are set again in 'setOptions' below if needed.
-     *
-     * @var array
-     */
-    protected $options = [
-        'cost' => 12,
-    ];
-
-    /**
-     * Constructor sets options if given
-     *
-     * @param array $options
-     */
-    public function __construct(array $options = [])
-    {
-        $newOptions = $this->options;
-        // Check options for validity
-        if (isset($options['cost'])) {
-            if (!$this->isValidBcryptCost((int)$options['cost'])) {
-                throw new \InvalidArgumentException(
-                    'cost must not be lower than ' . PASSWORD_BCRYPT_DEFAULT_COST . ' or higher than 31',
-                    1533902002
-                );
-            }
-            $newOptions['cost'] = (int)$options['cost'];
-        }
-        $this->options = $newOptions;
-    }
-
-    /**
-     * Returns true if sha384 for pre-hashing and bcrypt itself is available.
-     *
-     * @return bool
-     */
-    public function isAvailable(): bool
-    {
-        return defined('PASSWORD_BCRYPT')
-            && PASSWORD_BCRYPT
-            && function_exists('hash')
-            && function_exists('hash_algos')
-            && in_array('sha384', hash_algos());
-    }
-
-    /**
-     * 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
-     */
-    public function checkPassword(string $plainPW, string $saltedHashPW): bool
-    {
-        return password_verify($this->processPlainPassword($plainPW), $saltedHashPW);
-    }
-
-    /**
-     * Extend parent method to workaround bcrypt limitations.
-     *
-     * @param string $password Plaintext password to create a salted hash from
-     * @param string $salt Deprecated optional custom salt to use
-     * @return string Salted hashed password
-     */
-    public function getHashedPassword(string $password, string $salt = null)
-    {
-        if ($salt !== null) {
-            trigger_error(static::class . ': using a custom salt is deprecated in PHP password api and thus ignored', E_USER_DEPRECATED);
-        }
-        $hashedPassword = null;
-        if ($password !== '') {
-            $password = $this->processPlainPassword($password);
-            $hashedPassword = password_hash($password, PASSWORD_BCRYPT, $this->options);
-            if (!is_string($hashedPassword) || empty($hashedPassword)) {
-                throw new InvalidSaltException('Cannot generate password, probably invalid options', 1517174114);
-            }
-        }
-        return $hashedPassword;
-    }
-
-    /**
-     * Determines if a given string is a valid salted hashed password.
-     *
-     * @param string $saltedPW String to check
-     * @return bool TRUE if it's valid salted hashed password, otherwise FALSE
-     */
-    public function isValidSaltedPW(string $saltedPW): bool
-    {
-        $result = false;
-        $passwordInfo = password_get_info($saltedPW);
-        // Validate the cost value, password_get_info() does not check it
-        $cost = (int)substr($saltedPW, 4, 2);
-        if (isset($passwordInfo['algo'])
-            && $passwordInfo['algo'] === PASSWORD_BCRYPT
-            && strncmp($saltedPW, static::PREFIX, strlen(static::PREFIX)) === 0
-            && $this->isValidBcryptCost($cost)
-        ) {
-            $result = true;
-        }
-        return $result;
-    }
-    /**
-     * Checks whether a user's hashed password needs to be replaced with a new hash.
-     *
-     * @param string $passString Salted hash to check if it needs an update
-     * @return bool TRUE if salted hash needs an update, otherwise FALSE
-     */
-    public function isHashUpdateNeeded(string $passString): bool
-    {
-        return password_needs_rehash($passString, PASSWORD_BCRYPT, $this->options);
-    }
-
-    /**
-     * The plain password is processed through sha384 and then base64
-     * encoded. This will produce a 64 characters input to use with
-     * password_* functions, which has some advantages:
-     * 1. It is close to the (bcrypt-) maximum of 72 character keyspace
-     * 2. base64 will never produce NUL bytes (bcrypt truncates on NUL bytes)
-     * 3. sha384 is resistant to length extension attacks
-     *
-     * @param string $password
-     * @return string
-     */
-    protected function processPlainPassword(string $password): string
-    {
-        return base64_encode(hash('sha384', $password, true));
-    }
-
-    /**
-     * @see https://github.com/php/php-src/blob/php-7.2.0/ext/standard/password.c#L441-L444
-     * @param int $cost
-     * @return bool
-     */
-    protected function isValidBcryptCost(int $cost): bool
-    {
-        return $cost >= PASSWORD_BCRYPT_DEFAULT_COST && $cost <= 31;
-    }
-
-    /**
-     * @return array
-     * @deprecated and will be removed in TYPO3 v10.0.
-     */
-    public function getOptions(): array
-    {
-        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
-        return $this->options;
-    }
-
-    /**
-     * Set new memory_cost, time_cost, and thread values.
-     *
-     * @param array $options
-     * @deprecated and will be removed in TYPO3 v10.0.
-     */
-    public function setOptions(array $options): void
-    {
-        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
-        $newOptions = [];
-
-        // Check options for validity, else use hard coded defaults
-        if (isset($options['cost'])) {
-            if (!$this->isValidBcryptCost((int)$options['cost'])) {
-                throw new \InvalidArgumentException(
-                    'cost must not be lower than ' . PASSWORD_BCRYPT_DEFAULT_COST . ' or higher than 31',
-                    1526042084
-                );
-            }
-            $newOptions['cost'] = (int)$options['cost'];
-        } else {
-            $newOptions['cost'] = 12;
-        }
-
-        $this->options = $newOptions;
-    }
-}
diff --git a/typo3/sysext/saltedpasswords/Classes/Salt/BlowfishSalt.php b/typo3/sysext/saltedpasswords/Classes/Salt/BlowfishSalt.php
deleted file mode 100644 (file)
index ab6fc53..0000000
+++ /dev/null
@@ -1,422 +0,0 @@
-<?php
-declare(strict_types = 1);
-namespace TYPO3\CMS\Saltedpasswords\Salt;
-
-/*
- * 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\Compatibility\PublicMethodDeprecationTrait;
-use TYPO3\CMS\Core\Crypto\Random;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-
-/**
- * Class that implements Blowfish salted hashing based on PHP's
- * crypt() function.
- *
- * Warning: Blowfish salted hashing with PHP's crypt() is not available
- * on every system.
- */
-class BlowfishSalt implements SaltInterface
-{
-    use PublicMethodDeprecationTrait;
-
-    /**
-     * @var array
-     */
-    private $deprecatedPublicMethods = [
-        'isValidSalt' => 'Using BlowfishSalt::isValidSalt() is deprecated and will not be possible anymore in TYPO3 v10.',
-        'base64Encode' => 'Using BlowfishSalt::base64Encode() is deprecated and will not be possible anymore in TYPO3 v10.',
-    ];
-
-    /**
-     * Prefix for the password hash.
-     */
-    protected const PREFIX = '$2a$';
-
-    /**
-     * @var array The default log2 number of iterations for password stretching.
-     */
-    protected $options = [
-        'hash_count' => 7
-    ];
-
-    /**
-     * Keeps a string for mapping an int to the corresponding
-     * base 64 character.
-     *
-     * @deprecated and will be removed in TYPO3 v10.0.
-     */
-    const ITOA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
-
-    /**
-     * The default log2 number of iterations for password stretching.
-     *
-     * @deprecated and will be removed in TYPO3 v10.0.
-     */
-    const HASH_COUNT = 7;
-
-    /**
-     * The default maximum allowed log2 number of iterations for
-     * password stretching.
-     *
-     * @deprecated and will be removed in TYPO3 v10.0.
-     */
-    const MAX_HASH_COUNT = 17;
-
-    /**
-     * The default minimum allowed log2 number of iterations for
-     * password stretching.
-     *
-     * @deprecated and will be removed in TYPO3 v10.0.
-     */
-    const MIN_HASH_COUNT = 4;
-
-    /**
-     * Constructor sets options if given
-     *
-     * @param array $options
-     */
-    public function __construct(array $options = [])
-    {
-        $newOptions = $this->options;
-        if (isset($options['hash_count'])) {
-            if ((int)$options['hash_count'] < 4 || (int)$options['hash_count'] > 17) {
-                throw new \InvalidArgumentException(
-                    'hash_count must not be lower than 4 or bigger than 17',
-                    1533903545
-                );
-            }
-            $newOptions['hash_count'] = (int)$options['hash_count'];
-        }
-        $this->options = $newOptions;
-    }
-
-    /**
-     * Method checks if a given plaintext password is correct by comparing it with
-     * a given salted hashed password.
-     *
-     * @param string $plainPW plain-text password to compare with salted hash
-     * @param string $saltedHashPW salted hash to compare plain-text password with
-     * @return bool TRUE, if plain-text password matches the salted hash, otherwise FALSE
-     */
-    public function checkPassword(string $plainPW, string $saltedHashPW): bool
-    {
-        $isCorrect = false;
-        if ($this->isValidSalt($saltedHashPW)) {
-            $isCorrect = \password_verify($plainPW, $saltedHashPW);
-        }
-        return $isCorrect;
-    }
-
-    /**
-     * Returns whether all prerequisites for the hashing methods are matched
-     *
-     * @return bool Method available
-     */
-    public function isAvailable(): bool
-    {
-        return (bool)CRYPT_BLOWFISH;
-    }
-
-    /**
-     * Method creates a salted hash for a given plaintext password
-     *
-     * @param string $password plaintext password to create a salted hash from
-     * @param string $salt Deprecated optional custom salt with setting to use
-     * @return string Salted hashed password
-     */
-    public function getHashedPassword(string $password, string $salt = null)
-    {
-        if ($salt !== null) {
-            trigger_error(static::class . ': using a custom salt is deprecated.', E_USER_DEPRECATED);
-        }
-        $saltedPW = null;
-        if (!empty($password)) {
-            if (empty($salt) || !$this->isValidSalt($salt)) {
-                $salt = $this->getGeneratedSalt();
-            }
-            $saltedPW = crypt($password, $this->applySettingsToSalt($salt));
-        }
-        return $saltedPW;
-    }
-
-    /**
-     * Checks whether a user's hashed password needs to be replaced with a new hash.
-     *
-     * This is typically called during the login process when the plain text
-     * password is available.  A new hash is needed when the desired iteration
-     * count has changed through a change in the variable $hashCount or
-     * HASH_COUNT.
-     *
-     * @param string $saltedPW Salted hash to check if it needs an update
-     * @return bool TRUE if salted hash needs an update, otherwise FALSE
-     */
-    public function isHashUpdateNeeded(string $saltedPW): bool
-    {
-        // Check whether the iteration count used differs from the standard number.
-        $countLog2 = $this->getCountLog2($saltedPW);
-        return $countLog2 !== null && $countLog2 < $this->options['hash_count'];
-    }
-
-    /**
-     * Method determines if a given string is a valid salted hashed password.
-     *
-     * @param string $saltedPW String to check
-     * @return bool TRUE if it's valid salted hashed password, otherwise FALSE
-     */
-    public function isValidSaltedPW(string $saltedPW): bool
-    {
-        $isValid = !strncmp(self::PREFIX, $saltedPW, strlen(self::PREFIX));
-        if ($isValid) {
-            $isValid = $this->isValidSalt($saltedPW);
-        }
-        return $isValid;
-    }
-
-    /**
-     * Generates a random base 64-encoded salt prefixed and suffixed with settings for the hash.
-     *
-     * Proper use of salts may defeat a number of attacks, including:
-     * - The ability to try candidate passwords against multiple hashes at once.
-     * - The ability to use pre-hashed lists of candidate passwords.
-     * - The ability to determine whether two users have the same (or different)
-     * password without actually having to guess one of the passwords.
-     *
-     * @return string A character string containing settings and a random salt
-     */
-    protected function getGeneratedSalt(): string
-    {
-        $randomBytes = GeneralUtility::makeInstance(Random::class)->generateRandomBytes(16);
-        return $this->base64Encode($randomBytes, 16);
-    }
-
-    /**
-     * Method applies settings (prefix, hash count) to a salt.
-     *
-     * @param string $salt A salt to apply setting to
-     * @return string Salt with setting
-     */
-    protected function applySettingsToSalt(string $salt): string
-    {
-        $saltWithSettings = $salt;
-        $reqLenBase64 = $this->getLengthBase64FromBytes(16);
-        // salt without setting
-        if (strlen($salt) == $reqLenBase64) {
-            $saltWithSettings = self::PREFIX . sprintf('%02u', $this->options['hash_count']) . '$' . $salt;
-        }
-        return $saltWithSettings;
-    }
-
-    /**
-     * Parses the log2 iteration count from a stored hash or setting string.
-     *
-     * @param string $setting Complete hash or a hash's setting string or to get log2 iteration count from
-     * @return int Used hashcount for given hash string
-     */
-    protected function getCountLog2(string $setting): int
-    {
-        $countLog2 = null;
-        $setting = substr($setting, strlen(self::PREFIX));
-        $firstSplitPos = strpos($setting, '$');
-        // Hashcount existing
-        if ($firstSplitPos !== false && $firstSplitPos <= 2 && is_numeric(substr($setting, 0, $firstSplitPos))) {
-            $countLog2 = (int)substr($setting, 0, $firstSplitPos);
-        }
-        return $countLog2;
-    }
-
-    /**
-     * Returns a string for mapping an int to the corresponding base 64 character.
-     *
-     * @return string String for mapping an int to the corresponding base 64 character
-     */
-    protected function getItoa64(): string
-    {
-        return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
-    }
-
-    /**
-     * Method determines if a given string is a valid salt.
-     *
-     * @param string $salt String to check
-     * @return bool TRUE if it's valid salt, otherwise FALSE
-     */
-    protected function isValidSalt(string $salt): bool
-    {
-        $isValid = ($skip = false);
-        $reqLenBase64 = $this->getLengthBase64FromBytes(16);
-        if (strlen($salt) >= $reqLenBase64) {
-            // Salt with prefixed setting
-            if (!strncmp('$', $salt, 1)) {
-                if (!strncmp(self::PREFIX, $salt, strlen(self::PREFIX))) {
-                    $isValid = true;
-                    $salt = substr($salt, strrpos($salt, '$') + 1);
-                } else {
-                    $skip = true;
-