[TASK] Make password hash selection an install tool preset 50/57850/18
authorChristian Kuhn <lolli@schwarzbu.ch>
Thu, 9 Aug 2018 22:41:39 +0000 (00:41 +0200)
committerChristian Kuhn <lolli@schwarzbu.ch>
Sat, 11 Aug 2018 16:22:26 +0000 (18:22 +0200)
With this change, the password hash code in salted passwords is
reduced to the SaltFactory with two methods and the single hash
classes that implement SaltInterface without further public
methods. Everything else including the utility classes is
deprecated.
The change moves the LocalConfiguration.php config options around,
adds a settings preset for hash mechanism selection, adds according
silent upgrades, adds 'best available' hash mechanism selection
at installation time and drops the last saltedpasswords
ext_conf_template.txt option.

Details:
* Remove the password hash selection from saltedpasswords config
  namespace and put to TYPO3_CONF_VARS/BE/passwordHashing/className
  and TYPO3_CONF_VARS/FE/passwordHashing/className
* Move available password hash registry from
  TYPO3_CONF_VARS/SC_OPTIONS/ext/saltedpasswords/saltMethods
  to TYPO3_CONF_VARS/SYS/availablePasswordHashAlgorithms
* Add a setting preset to select one of argon2i (preferred),
  bcrypt, pbkdf2 or phpass (last fallback)
* Use 'best matching preset' during installation to select a good
  salt mechanism by default
* Silently upgrade existing password hash selection and upgrade
  to one of the four hash algorithms above
* Allow algorithm specific options in
  TYPO3_CONF_VARS/BE/passwordHashing/options and
  TYPO3_CONF_VARS/FE/passwordHashing/options for admins who
  know what they are doing and need to fiddle with hash details.
* Simplify and refactor the single password hash classes. Deprecate
  a huge list of methods along the way.

Change-Id: I773e2ee27a121c9f0d5302695ebf4aa561170400
Resolves: #85804
Resolves: #83760
Releases: master
Reviewed-on: https://review.typo3.org/57850
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Tested-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
61 files changed:
typo3/sysext/core/Classes/Authentication/AuthenticationService.php
typo3/sysext/core/Classes/DataHandling/DataHandler.php
typo3/sysext/core/Configuration/DefaultConfiguration.php
typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml
typo3/sysext/core/Documentation/Changelog/master/Deprecation-85804-SaltedPasswordHashClassDeprecations.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Authentication/AuthenticationServiceTest.php
typo3/sysext/install/Classes/Authentication/AuthenticationService.php
typo3/sysext/install/Classes/Configuration/FeatureManager.php
typo3/sysext/install/Classes/Configuration/Image/ImageFeature.php
typo3/sysext/install/Classes/Configuration/PasswordHashing/Argon2iPreset.php [new file with mode: 0644]
typo3/sysext/install/Classes/Configuration/PasswordHashing/BcryptPreset.php [new file with mode: 0644]
typo3/sysext/install/Classes/Configuration/PasswordHashing/CustomPreset.php [new file with mode: 0644]
typo3/sysext/install/Classes/Configuration/PasswordHashing/PasswordHashingFeature.php [new file with mode: 0644]
typo3/sysext/install/Classes/Configuration/PasswordHashing/Pbkdf2Preset.php [new file with mode: 0644]
typo3/sysext/install/Classes/Configuration/PasswordHashing/PhpassPreset.php [new file with mode: 0644]
typo3/sysext/install/Classes/Controller/InstallerController.php
typo3/sysext/install/Classes/Report/SecurityStatusReport.php
typo3/sysext/install/Classes/Service/SilentConfigurationUpgradeService.php
typo3/sysext/install/Configuration/ExtensionScanner/Php/ArrayDimensionMatcher.php
typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassConstantMatcher.php
typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php
typo3/sysext/install/Configuration/ExtensionScanner/Php/InterfaceMethodChangedMatcher.php
typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodArgumentDroppedMatcher.php
typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php
typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing.html [new file with mode: 0644]
typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Argon2i.html [new file with mode: 0644]
typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Bcrypt.html [new file with mode: 0644]
typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Custom.html [new file with mode: 0644]
typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Pbkdf2.html [new file with mode: 0644]
typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Phpass.html [new file with mode: 0644]
typo3/sysext/install/Tests/Unit/Service/SilentConfigurationUpgradeServiceTest.php
typo3/sysext/reports/Classes/Report/Status/SecurityStatus.php
typo3/sysext/saltedpasswords/Classes/Salt/AbstractComposedSalt.php
typo3/sysext/saltedpasswords/Classes/Salt/Argon2iSalt.php
typo3/sysext/saltedpasswords/Classes/Salt/BcryptSalt.php
typo3/sysext/saltedpasswords/Classes/Salt/BlowfishSalt.php
typo3/sysext/saltedpasswords/Classes/Salt/ComposedSaltInterface.php
typo3/sysext/saltedpasswords/Classes/Salt/Md5Salt.php
typo3/sysext/saltedpasswords/Classes/Salt/Pbkdf2Salt.php
typo3/sysext/saltedpasswords/Classes/Salt/PhpassSalt.php
typo3/sysext/saltedpasswords/Classes/Salt/SaltFactory.php
typo3/sysext/saltedpasswords/Classes/Salt/SaltInterface.php
typo3/sysext/saltedpasswords/Classes/Utility/ExtensionManagerConfigurationUtility.php
typo3/sysext/saltedpasswords/Classes/Utility/SaltedPasswordsUtility.php
typo3/sysext/saltedpasswords/Tests/Unit/Salt/Argon2iSaltTest.php
typo3/sysext/saltedpasswords/Tests/Unit/Salt/BcryptSaltTest.php
typo3/sysext/saltedpasswords/Tests/Unit/Salt/BlowfishSaltTest.php
typo3/sysext/saltedpasswords/Tests/Unit/Salt/Fixtures/TestSalt.php [new file with mode: 0644]
typo3/sysext/saltedpasswords/Tests/Unit/Salt/Md5SaltTest.php
typo3/sysext/saltedpasswords/Tests/Unit/Salt/Pbkdf2SaltTest.php
typo3/sysext/saltedpasswords/Tests/Unit/Salt/PhpassSaltTest.php
typo3/sysext/saltedpasswords/Tests/Unit/Salt/SaltFactoryTest.php
typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/Argon2iSaltTest.php [new file with mode: 0644]
typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/BcryptSaltTest.php [new file with mode: 0644]
typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/BlowfishSaltTest.php [new file with mode: 0644]
typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/Md5SaltTest.php [new file with mode: 0644]
typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/Pbkdf2SaltTest.php [new file with mode: 0644]
typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/PhpassSaltTest.php [new file with mode: 0644]
typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/SaltFactoryTest.php
typo3/sysext/saltedpasswords/ext_conf_template.txt [deleted file]
typo3/sysext/setup/Classes/Controller/SetupModuleController.php

index c085418..c92f75d 100644 (file)
@@ -122,7 +122,7 @@ class AuthenticationService extends AbstractAuthenticationService
 
         // Get a hashed password instance for the hash stored in db of this user
         try {
-            $hashInstance = $saltFactory->get($passwordHashInDatabase);
+            $hashInstance = $saltFactory->get($passwordHashInDatabase, TYPO3_MODE);
         } catch (InvalidSaltException $e) {
             // This can be refactored if the 'else' part below is gone in v10: Log and return 100 here
             $hashInstance = null;
@@ -158,7 +158,7 @@ class AuthenticationService extends AbstractAuthenticationService
                 // 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
                 try {
-                    $hashInstance = $saltFactory->get(substr($passwordHashInDatabase, 1));
+                    $hashInstance = $saltFactory->get(substr($passwordHashInDatabase, 1), TYPO3_MODE);
                     $isSaltedPassword = true;
                     $isValidPassword = $hashInstance->checkPassword(md5($submittedPassword), substr($passwordHashInDatabase, 1));
                     if ($isValidPassword) {
index 3d1cba2..e0dd241 100644 (file)
@@ -2960,16 +2960,13 @@ class DataHandler implements LoggerAwareInterface
                     $isDeprecatedSaltedHash = $hashMethod === 'M$';
                     $tempValue = $isDeprecatedSaltedHash ? substr($value, 1) : $value;
                     $hashFactory = GeneralUtility::makeInstance(SaltFactory::class);
+                    $mode = $table === 'fe_users' ? 'FE' : 'BE';
                     try {
-                        $hashFactory->get($tempValue);
+                        $hashFactory->get($tempValue, $mode);
                     } catch (InvalidSaltException $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
-                        if ($table === 'fe_users') {
-                            $newHashInstance = $hashFactory->getDefaultHashInstance('FE');
-                        } else {
-                            $newHashInstance = $hashFactory->getDefaultHashInstance('BE');
-                        }
+                        $newHashInstance = $hashFactory->getDefaultHashInstance($mode);
                         $value = $newHashInstance->getHashedPassword($value);
                     }
                     break;
index 814678c..d08d066 100644 (file)
@@ -104,6 +104,14 @@ return [
         'reverseProxyPrefix' => '',
         '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,
+        ],
         'caching' => [
             'cacheConfigurations' => [
                 // The cache_core cache is is for core php code only and must
@@ -1214,6 +1222,10 @@ return [
                 'Headers' => ['clickJackingProtection' => 'X-Frame-Options: SAMEORIGIN']
             ]
         ],
+        'passwordHashing' => [
+            'className' => \TYPO3\CMS\Saltedpasswords\Salt\Argon2iSalt::class,
+            'options' => [],
+        ],
     ],
     'FE' => [ // Configuration for the TypoScript frontend (FE). Nothing here relates to the administration backend!
         'addAllowedPaths' => '',
@@ -1270,6 +1282,10 @@ return [
             'record' => \TYPO3\CMS\Frontend\Typolink\DatabaseRecordLinkBuilder::class,
             'unknown' => \TYPO3\CMS\Frontend\Typolink\LegacyLinkBuilder::class,
         ],
+        'passwordHashing' => [
+            'className' => \TYPO3\CMS\Saltedpasswords\Salt\Argon2iSalt::class,
+            'options' => [],
+        ],
     ],
     'MAIL' => [ // Mail configurations to tune how \TYPO3\CMS\Core\Mail\ classes will send their mails.
         'transport' => 'mail',
index ee7acbc..d3f5019 100644 (file)
@@ -218,6 +218,9 @@ SYS:
               unifiedPageTranslationHandling:
                 type: bool
                 description: 'If activated, TCA configuration for pages_language_overlay will never be loaded, and the database table "pages_language_overlay" will not be created.'
+        availablePasswordHashAlgorithms:
+            type: array
+            description: 'A list of available password hash mechanisms. Extensions may register additional mechanisms here. This is usually not extended in LocalConfiguration.php.'
 EXT:
     type: container
     description: 'Extension Installation'
@@ -351,6 +354,19 @@ BE:
         debug:
             type: bool
             description: 'If enabled, the loginrefresh is disabled and pageRenderer is set to debug mode. Furthermore the fieldname is appended to the label of fields. Use this to debug the backend only!'
+        passwordHashing:
+            type: container
+            items:
+                className:
+                    type: dropdown
+                    allowedValues:
+                        'TYPO3\CMS\Saltedpasswords\Salt\Argon2iSalt': 'Good password hash mechanism. Used by default if available.'
+                        'TYPO3\CMS\Saltedpasswords\Salt\BcryptSalt': 'Good password hash mechanism.'
+                        'TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt': 'Fallback hash mechanism if argon and bcrypt are not available.'
+                        'TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt': 'Fallback hash mechanism if none of the above are avalaible.'
+                options:
+                    type: array
+                    description: 'Special settings for specific hashes.'
 FE:
     type: container
     description: 'Frontend'
@@ -478,6 +494,19 @@ FE:
               Allows to automatically include a version number (timestamp of the file) to referred CSS and JS filenames
               on the rendered page. This will make browsers and proxies reload the files if they change (thus avoiding caching issues).
               <strong>IMPORTANT</strong>: ''embed'' requires extra <code>.htaccess</code> rules to work (please refer to the <code>_.htaccess</code> file shipped with TYPO3)'
+        passwordHashing:
+            type: container
+            items:
+                className:
+                    type: dropdown
+                    allowedValues:
+                        'TYPO3\CMS\Saltedpasswords\Salt\Argon2iSalt': 'Good password hash mechanism. Used by default if available.'
+                        'TYPO3\CMS\Saltedpasswords\Salt\BcryptSalt': 'Good password hash mechanism.'
+                        'TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt': 'Fallback hash mechanism if argon and bcrypt are not available.'
+                        'TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt': 'Fallback hash mechanism if none of the above are avalaible.'
+                options:
+                    type: array
+                    description: 'Special settings for specific hashes.'
 MAIL:
     type: container
     description: 'Mail'
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-85804-SaltedPasswordHashClassDeprecations.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-85804-SaltedPasswordHashClassDeprecations.rst
new file mode 100644 (file)
index 0000000..a3f1d54
--- /dev/null
@@ -0,0 +1,136 @@
+.. include:: ../../Includes.txt
+
+=============================================================
+Deprecation: #85804 - Salted password hash class deprecations
+=============================================================
+
+See :issue:`85804`
+
+Description
+===========
+
+Selecting the hash algorithm used to store frontend and backend user hashes is
+now a "preset" and can be changed using "Admin tools" -> "Settings" -> "Configuration Presets".
+
+Existing settings are updated automatically when upgrading from an older TYPO3 version to
+core version v9. The detail list below is only interesting for instances that need to
+run custom hash mechanisms.
+
+The password hash mechanism used for backend user passwords has been moved from
+:php:`$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['saltedpasswords']['BE']['saltedPWHashingMethod']
+to :php:`$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordHashing']['className']. Options for a specific
+hash algorithms can be defined using :php:`$GLOBALS['TYPO3_CONF_VARS']['BE']['passwordHashing']['options'].
+
+The password hash mechanism used for frontend user passwords has been moved from
+:php:`$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['saltedpasswords']['FE']['saltedPWHashingMethod']
+to :php:`$GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing']['className']. Options for a specific
+hash algorithms can be defined using :php:`$GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing']['options'].
+
+Custom password hash algorithms should now be registered in
+:php:`$GLOBALS['TYPO3_CONF_VARS']['SYS']['availablePasswordHashAlgorithms']`, using
+:php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/saltedpasswords']['saltMethods']` has been deprecated.
+
+These interfaces and classes have been deprecated and should not be implemented any longer:
+
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\ComposedSaltInterface`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\AbstractComposedSalt`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Utility\ExtensionManagerConfigurationUtility`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Utility\SaltedPasswordsUtility`
+
+An interface has been changed:
+
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\SaltInterface->getHashedPassword(string $password)` - The
+  second argument has been dropped. Classes implementing the interface should remove the second argument.
+
+These methods have been deprecated:
+
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Argon2iSalt->getOptions()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Argon2iSalt->setOptions()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\BcryptSalt->getOptions()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\BcryptSalt->setOptions()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->getHashCount()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->getMaxHashCount()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->getMinHashCount()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->getSaltLength()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->getSetting()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->setHashCount()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->setMaxHashCount()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->setMinHashCount()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Md5Salt->getSetting()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Md5Salt->getSaltLength()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->getHashCount()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->getMaxHashCount()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->getMinHashCount()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->getSaltLength()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->getSetting()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->setHashCount()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->setMaxHashCount()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->setMinHashCount()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->getHashCount()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->getMaxHashCount()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->getMinHashCount()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->getSaltLength()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->getSetting()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->setHashCount()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->setMaxHashCount()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->setMinHashCount()`
+
+These methods changed their signature:
+
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->getHashedPassword()` - Second argument deprecated
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Md5Salt->getHashedPassword()` - Second argument deprecated
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->getHashedPassword()` - Second argument deprecated
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->getHashedPassword()` - Second argument deprecated
+
+These methods changed their visibility from public to protected:
+
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->isValidSalt()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->base64Encode()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Md5Salt->isValidSalt()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Md5Salt->base64Encode()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->isValidSalt()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->base64Encode()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->base64Decode()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->isValidSalt()`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->base64Encode()`
+
+These class constants have been deprecated and will be removed in v10:
+
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt::ITOA64`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt::HASH_COUNT`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt::MAX_HASH_COUNT`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt::MIN_HASH_COUNT`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Md5Salt::ITOA64`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt::ITOA64`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt::HASH_COUNT`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt::MAX_HASH_COUNT`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt::MIN_HASH_COUNT`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt::ITOA64`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt::HASH_COUNT`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt::MAX_HASH_COUNT`
+* :php:`TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt::MIN_HASH_COUNT`
+
+
+Impact
+======
+
+Using functionality from the above list will log deprecation log entries.
+
+
+Affected Installations
+======================
+
+Almost all TYPO3 instances are not directly affected by the changes outlined above. A configuration
+upgrade is in place to move from old to new settings when calling the install tool the first time
+after upgrade without further user interaction.
+
+If in rare cases an existing TYPO3 instances runs custom salt mechanisms, the extension scanner
+will find affected code places that should be adapted.
+
+
+Migration
+=========
+
+If the extension scanner finds affected code, adapt the method calls, class constant usages and interface usages.
+
+.. index:: PHP-API, FullyScanned, ext:saltedpasswords
\ No newline at end of file
index c268cfa..9ac0e40 100644 (file)
@@ -192,8 +192,8 @@ class AuthenticationServiceTest extends UnitTestCase
             $pObjProphecy->reveal()
         );
         $dbUser = [
-            // an phpass hash of 'myPassword'
-            'password' => '$P$C/2Vr3ywuuPo5C7cs75YBnVhgBWpMP1',
+            // an argon2i hash of 'myPassword'
+            'password' => '$argon2i$v=19$m=16384,t=16,p=2$Ty9zOFVWdDBVQmlWTldVbg$kiVbkrYeTvgNg84i97WZBMQszmza66IohBxUtOnzRvU',
             'lockToDomain' => ''
         ];
         $this->assertSame(200, $subject->authUser($dbUser));
@@ -225,8 +225,8 @@ class AuthenticationServiceTest extends UnitTestCase
             $pObjProphecy->reveal()
         );
         $dbUser = [
-            // an phpass hash of 'myPassword'
-            'password' => '$P$C/2Vr3ywuuPo5C7cs75YBnVhgBWpMP1',
+            // an argon2i hash of 'myPassword'
+            'password' => '$argon2i$v=19$m=16384,t=16,p=2$Ty9zOFVWdDBVQmlWTldVbg$kiVbkrYeTvgNg84i97WZBMQszmza66IohBxUtOnzRvU',
             'username' => 'lolli',
             'lockToDomain' => 'not.example.com'
         ];
index b7bd761..23d8412 100644 (file)
@@ -53,7 +53,7 @@ class AuthenticationService
             $installToolPassword = $GLOBALS['TYPO3_CONF_VARS']['BE']['installToolPassword'];
             $hashFactory = GeneralUtility::makeInstance(SaltFactory::class);
             try {
-                $hashInstance = $hashFactory->get($installToolPassword);
+                $hashInstance = $hashFactory->get($installToolPassword, 'BE');
                 $validPassword = $hashInstance->checkPassword($password, $installToolPassword);
             } catch (InvalidSaltException $e) {
                 // Given hash in global configuration is not a valid salted password
index 567b883..6a47a61 100644 (file)
@@ -18,6 +18,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Install\Configuration\Context\ContextFeature;
 use TYPO3\CMS\Install\Configuration\Image\ImageFeature;
 use TYPO3\CMS\Install\Configuration\Mail\MailFeature;
+use TYPO3\CMS\Install\Configuration\PasswordHashing\PasswordHashingFeature;
 
 /**
  * Instantiate and configure all known features and presets
@@ -31,6 +32,7 @@ class FeatureManager
         ContextFeature::class,
         ImageFeature::class,
         MailFeature::class,
+        PasswordHashingFeature::class,
     ];
 
     /**
index 9e4d763..1aa71ac 100644 (file)
@@ -36,7 +36,7 @@ class ImageFeature extends Configuration\AbstractFeature implements Configuratio
     ];
 
     /**
-     * Image feature can be feeded with an additional path to search for executables,
+     * Image feature can be fed with an additional path to search for executables,
      * this getter returns the given input string (for Fluid)
      *
      * @return string
diff --git a/typo3/sysext/install/Classes/Configuration/PasswordHashing/Argon2iPreset.php b/typo3/sysext/install/Classes/Configuration/PasswordHashing/Argon2iPreset.php
new file mode 100644 (file)
index 0000000..74b1264
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Install\Configuration\PasswordHashing;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Configuration\AbstractPreset;
+use TYPO3\CMS\Saltedpasswords\Salt\Argon2iSalt;
+
+/**
+ * Preset for password hashing method "argon2i"
+ */
+class Argon2iPreset extends AbstractPreset
+{
+    /**
+     * @var string Name of preset
+     */
+    protected $name = 'Argon2i';
+
+    /**
+     * @var int Priority of preset
+     */
+    protected $priority = 70;
+
+    /**
+     * @var array Configuration values handled by this preset
+     */
+    protected $configurationValues = [
+        'BE/passwordHashing/className' => Argon2iSalt::class,
+        'BE/passwordHashing/options' => [],
+        'FE/passwordHashing/className' => Argon2iSalt::class,
+        'FE/passwordHashing/options' => [],
+    ];
+
+    /**
+     * Find out if Argon2i is available on this system
+     *
+     * @return bool
+     */
+    public function isAvailable()
+    {
+        return GeneralUtility::makeInstance(Argon2iSalt::class)->isAvailable();
+    }
+}
diff --git a/typo3/sysext/install/Classes/Configuration/PasswordHashing/BcryptPreset.php b/typo3/sysext/install/Classes/Configuration/PasswordHashing/BcryptPreset.php
new file mode 100644 (file)
index 0000000..4924407
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Install\Configuration\PasswordHashing;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Configuration\AbstractPreset;
+use TYPO3\CMS\Saltedpasswords\Salt\BcryptSalt;
+
+/**
+ * Preset for password hashing method "bcrypt"
+ */
+class BcryptPreset extends AbstractPreset
+{
+    /**
+     * @var string Name of preset
+     */
+    protected $name = 'Bcrypt';
+
+    /**
+     * @var int Priority of preset
+     */
+    protected $priority = 60;
+
+    /**
+     * @var array Configuration values handled by this preset
+     */
+    protected $configurationValues = [
+        'BE/passwordHashing/className' => BcryptSalt::class,
+        'BE/passwordHashing/options' => [],
+        'FE/passwordHashing/className' => BcryptSalt::class,
+        'FE/passwordHashing/options' => [],
+    ];
+
+    /**
+     * Find out if bcrypt is available on this system
+     *
+     * @return bool
+     */
+    public function isAvailable()
+    {
+        return GeneralUtility::makeInstance(BcryptSalt::class)->isAvailable();
+    }
+}
diff --git a/typo3/sysext/install/Classes/Configuration/PasswordHashing/CustomPreset.php b/typo3/sysext/install/Classes/Configuration/PasswordHashing/CustomPreset.php
new file mode 100644 (file)
index 0000000..76b7073
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Install\Configuration\PasswordHashing;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Install\Configuration\AbstractCustomPreset;
+use TYPO3\CMS\Install\Configuration\CustomPresetInterface;
+
+/**
+ * Preset used if custom password hashing configuration has been applied.
+ * Note this custom preset does not allow manipulation via gui, this has to be done manually.
+ * This preset only find out if it is active and shows the current values.
+ */
+class CustomPreset extends AbstractCustomPreset implements CustomPresetInterface
+{
+    /**
+     * Get configuration values is used in fluid to show configuration options.
+     * They are fetched from LocalConfiguration / DefaultConfiguration.
+     *
+     * @return array Current custom configuration values
+     */
+    public function getConfigurationValues()
+    {
+        $configurationValues = [];
+        $configurationValues['BE/passwordHashing/className'] =
+            $this->configurationManager->getConfigurationValueByPath('BE/passwordHashing/className');
+        $options = (array)$this->configurationManager->getConfigurationValueByPath('BE/passwordHashing/options');
+        foreach ($options as $optionName => $optionValue) {
+            $configurationValues['BE/passwordHashing/options/' . $optionName] = $optionValue;
+        }
+        $configurationValues['FE/passwordHashing/className'] =
+            $this->configurationManager->getConfigurationValueByPath('FE/passwordHashing/className');
+        $options = (array)$this->configurationManager->getConfigurationValueByPath('FE/passwordHashing/options');
+        foreach ($options as $optionName => $optionValue) {
+            $configurationValues['FE/passwordHashing/options/' . $optionName] = $optionValue;
+        }
+        return $configurationValues;
+    }
+}
diff --git a/typo3/sysext/install/Classes/Configuration/PasswordHashing/PasswordHashingFeature.php b/typo3/sysext/install/Classes/Configuration/PasswordHashing/PasswordHashingFeature.php
new file mode 100644 (file)
index 0000000..520888e
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Install\Configuration\PasswordHashing;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Install\Configuration\AbstractFeature;
+use TYPO3\CMS\Install\Configuration\FeatureInterface;
+
+/**
+ * Password hashing feature detects password hashing capabilities of the system
+ */
+class PasswordHashingFeature extends AbstractFeature implements FeatureInterface
+{
+    /**
+     * @var string Name of feature
+     */
+    protected $name = 'PasswordHashing';
+
+    /**
+     * @var array List of preset classes
+     */
+    protected $presetRegistry = [
+        Argon2iPreset::class,
+        BcryptPreset::class,
+        Pbkdf2Preset::class,
+        PhpassPreset::class,
+        CustomPreset::class,
+    ];
+}
diff --git a/typo3/sysext/install/Classes/Configuration/PasswordHashing/Pbkdf2Preset.php b/typo3/sysext/install/Classes/Configuration/PasswordHashing/Pbkdf2Preset.php
new file mode 100644 (file)
index 0000000..afec9ce
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Install\Configuration\PasswordHashing;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Configuration\AbstractPreset;
+use TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt;
+
+/**
+ * Preset for password hashing method "PBKDF2"
+ */
+class Pbkdf2Preset extends AbstractPreset
+{
+    /**
+     * @var string Name of preset
+     */
+    protected $name = 'Pbkdf2';
+
+    /**
+     * @var int Priority of preset
+     */
+    protected $priority = 50;
+
+    /**
+     * @var array Configuration values handled by this preset
+     */
+    protected $configurationValues = [
+        'BE/passwordHashing/className' => Pbkdf2Salt::class,
+        'BE/passwordHashing/options' => [],
+        'FE/passwordHashing/className' => Pbkdf2Salt::class,
+        'FE/passwordHashing/options' => [],
+    ];
+
+    /**
+     * Find out if PBKDF2 is available on this system
+     *
+     * @return bool
+     */
+    public function isAvailable()
+    {
+        return GeneralUtility::makeInstance(Pbkdf2Salt::class)->isAvailable();
+    }
+}
diff --git a/typo3/sysext/install/Classes/Configuration/PasswordHashing/PhpassPreset.php b/typo3/sysext/install/Classes/Configuration/PasswordHashing/PhpassPreset.php
new file mode 100644 (file)
index 0000000..b60cb13
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Install\Configuration\PasswordHashing;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Configuration\AbstractPreset;
+use TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt;
+
+/**
+ * Preset for password hashing method "phpass"
+ */
+class PhpassPreset extends AbstractPreset
+{
+    /**
+     * @var string Name of preset
+     */
+    protected $name = 'Phpass';
+
+    /**
+     * @var int Priority of preset
+     */
+    protected $priority = 40;
+
+    /**
+     * @var array Configuration values handled by this preset
+     */
+    protected $configurationValues = [
+        'BE/passwordHashing/className' => PhpassSalt::class,
+        'BE/passwordHashing/options' => [],
+        'FE/passwordHashing/className' => PhpassSalt::class,
+        'FE/passwordHashing/options' => [],
+    ];
+
+    /**
+     * Find out if Phpass is available on this system
+     *
+     * @return bool
+     */
+    public function isAvailable()
+    {
+        return GeneralUtility::makeInstance(PhpassSalt::class)->isAvailable();
+    }
+}
index 67638a9..44d13eb 100644 (file)
@@ -47,7 +47,12 @@ 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.
@@ -1092,14 +1097,33 @@ 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 function returns a salted hashed key for new backend user password and install tool password.
      *
-     * @param string $password
-     * @return string
+     * 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
+     * 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
      */
     protected function getHashedPassword($password)
     {
-        return GeneralUtility::makeInstance(SaltFactory::class)->getDefaultHashInstance('BE')->getHashedPassword($password);
+        $okHashMethods = [
+            Argon2iSalt::class,
+            BcryptSalt::class,
+            Pbkdf2Salt::class,
+            PhpassSalt::class,
+        ];
+        foreach ($okHashMethods as $className) {
+            /** @var SaltInterface $instance */
+            $instance = GeneralUtility::makeInstance($className);
+            if ($instance->isAvailable()) {
+                return $instance->getHashedPassword($password);
+            }
+        }
+        throw new \LogicException('No suitable hash method found', 1533988846);
     }
 
     /**
index d7e06de..c810744 100644 (file)
@@ -56,7 +56,7 @@ class SecurityStatusReport implements \TYPO3\CMS\Reports\StatusProviderInterface
         $hashInstance = null;
         $hashFactory = GeneralUtility::makeInstance(SaltFactory::class);
         try {
-            $hashInstance = $hashFactory->get($installToolPassword);
+            $hashInstance = $hashFactory->get($installToolPassword, 'BE');
         } catch (InvalidSaltException $e) {
             // $hashInstance stays null
         }
index 7beb55f..a9f405c 100644 (file)
@@ -20,6 +20,11 @@ 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.
@@ -140,6 +145,8 @@ class SilentConfigurationUpgradeService
     /**
      * Executed configuration upgrades. Single upgrade methods must throw a
      * ConfigurationChangedException if something was written to LocalConfiguration.
+     *
+     * @throws ConfigurationChangedException
      */
     public function execute()
     {
@@ -158,8 +165,9 @@ class SilentConfigurationUpgradeService
         $this->migrateCacheHashOptions();
         $this->migrateExceptionErrors();
         $this->migrateDisplayErrorsSetting();
+        $this->migrateSaltedPasswordsSettings();
 
-        // Should run at the end to prevent that obsolete settings are removed before migration
+        // Should run at the end to prevent obsolete settings are removed before migration
         $this->removeObsoleteLocalConfigurationSettings();
     }
 
@@ -168,6 +176,8 @@ class SilentConfigurationUpgradeService
      * and have no impact on the core anymore.
      * To keep the configuration clean, those old settings are just silently
      * removed from LocalConfiguration if set.
+     *
+     * @throws ConfigurationChangedException
      */
     protected function removeObsoleteLocalConfigurationSettings()
     {
@@ -183,6 +193,8 @@ class SilentConfigurationUpgradeService
      * Backend login security is set to rsa if rsaauth
      * is installed (but not used) otherwise the default value "normal" has to be used.
      * This forces either 'normal' or 'rsa' to be set in LocalConfiguration.
+     *
+     * @throws ConfigurationChangedException
      */
     protected function configureBackendLoginSecurity()
     {
@@ -211,6 +223,8 @@ class SilentConfigurationUpgradeService
      * and the whole TYPO3 link rendering later on. A random key is set here in
      * LocalConfiguration if it does not exist yet. This might possible happen
      * during upgrading and will happen during first install.
+     *
+     * @throws ConfigurationChangedException
      */
     protected function generateEncryptionKeyIfNeeded()
     {
@@ -230,6 +244,8 @@ class SilentConfigurationUpgradeService
 
     /**
      * Parse old curl and HTTP options and set new HTTP options, related to Guzzle
+     *
+     * @throws ConfigurationChangedException
      */
     protected function transferHttpSettings()
     {
@@ -428,6 +444,8 @@ class SilentConfigurationUpgradeService
      * "Configuration presets" in install tool is not type safe, so value
      * comparisons here are not type safe too, to not trigger changes to
      * LocalConfiguration again.
+     *
+     * @throws ConfigurationChangedException
      */
     protected function disableImageMagickDetailSettingsIfImageMagickIsDisabled()
     {
@@ -489,6 +507,8 @@ class SilentConfigurationUpgradeService
      * "Configuration presets" in install tool is not type safe, so value
      * comparisons here are not type safe too, to not trigger changes to
      * LocalConfiguration again.
+     *
+     * @throws ConfigurationChangedException
      */
     protected function setImageMagickDetailSettings()
     {
@@ -529,6 +549,8 @@ class SilentConfigurationUpgradeService
     /**
      * Migrate the definition of the image processor from the configuration value
      * im_version_5 to the setting processor.
+     *
+     * @throws ConfigurationChangedException
      */
     protected function migrateImageProcessorSetting()
     {
@@ -612,6 +634,8 @@ class SilentConfigurationUpgradeService
 
     /**
      * Migrate the configuration value thumbnails_png to a boolean value.
+     *
+     * @throws ConfigurationChangedException
      */
     protected function migrateThumbnailsPngSetting()
     {
@@ -633,6 +657,8 @@ class SilentConfigurationUpgradeService
 
     /**
      * Migrate the configuration setting BE/lockSSL to boolean if set in the LocalConfiguration.php file
+     *
+     * @throws ConfigurationChangedException
      */
     protected function migrateLockSslSetting()
     {
@@ -650,6 +676,8 @@ class SilentConfigurationUpgradeService
 
     /**
      * Move the database connection settings to a "Default" connection
+     *
+     * @throws ConfigurationChangedException
      */
     protected function migrateDatabaseConnectionSettings()
     {
@@ -770,6 +798,8 @@ class SilentConfigurationUpgradeService
     /**
      * Migrate the configuration setting DB/Connections/Default/charset to 'utf8' as
      * 'utf-8' is not supported by all MySQL versions.
+     *
+     * @throws ConfigurationChangedException
      */
     protected function migrateDatabaseConnectionCharset()
     {
@@ -788,6 +818,8 @@ class SilentConfigurationUpgradeService
 
     /**
      * Migrate the configuration setting DB/Connections/Default/driverOptions to array type.
+     *
+     * @throws ConfigurationChangedException
      */
     protected function migrateDatabaseDriverOptions()
     {
@@ -799,6 +831,7 @@ class SilentConfigurationUpgradeService
                     'DB/Connections/Default/driverOptions',
                     ['flags' => (int)$options]
                 );
+                $this->throwConfigurationChangedException();
             }
         } catch (MissingArrayPathException $e) {
             // no driver options found, nothing needs to be modified
@@ -807,6 +840,8 @@ class SilentConfigurationUpgradeService
 
     /**
      * Migrate the configuration setting BE/lang/debug if set in the LocalConfiguration.php file
+     *
+     * @throws ConfigurationChangedException
      */
     protected function migrateLangDebug()
     {
@@ -816,6 +851,7 @@ class SilentConfigurationUpgradeService
             // check if the current option is set and boolean
             if (isset($currentOption) && is_bool($currentOption)) {
                 $confManager->setLocalConfigurationValueByPath('BE/languageDebug', $currentOption);
+                $this->throwConfigurationChangedException();
             }
         } catch (MissingArrayPathException $e) {
             // no change inside the LocalConfiguration.php found, so nothing needs to be modified
@@ -824,6 +860,8 @@ class SilentConfigurationUpgradeService
 
     /**
      * Migrate single cache hash related options under "FE" into "FE/cacheHash"
+     *
+     * @throws ConfigurationChangedException
      */
     protected function migrateCacheHashOptions()
     {
@@ -883,6 +921,8 @@ class SilentConfigurationUpgradeService
 
     /**
      * Migrate SYS/exceptionalErrors to not contain E_USER_DEPRECATED
+     *
+     * @throws ConfigurationChangedException
      */
     protected function migrateExceptionErrors()
     {
@@ -892,6 +932,7 @@ class SilentConfigurationUpgradeService
             // make sure E_USER_DEPRECATED is not part of the exceptionalErrors
             if ($currentOption & E_USER_DEPRECATED) {
                 $confManager->setLocalConfigurationValueByPath('SYS/exceptionalErrors', $currentOption & ~E_USER_DEPRECATED);
+                $this->throwConfigurationChangedException();
             }
         } catch (MissingArrayPathException $e) {
             // no change inside the LocalConfiguration.php found, so nothing needs to be modified
@@ -900,6 +941,8 @@ class SilentConfigurationUpgradeService
 
     /**
      * Migrate SYS/displayErrors to not contain 2
+     *
+     * @throws ConfigurationChangedException
      */
     protected function migrateDisplayErrorsSetting()
     {
@@ -909,9 +952,73 @@ class SilentConfigurationUpgradeService
             // make sure displayErrors is set to 2
             if ($currentOption === 2) {
                 $confManager->setLocalConfigurationValueByPath('SYS/displayErrors', -1);
+                $this->throwConfigurationChangedException();
             }
         } catch (MissingArrayPathException $e) {
             // no change inside the LocalConfiguration.php found, so nothing needs to be modified
         }
     }
+
+    /**
+     * Migrate salted passwords extension configuration settings to BE/passwordHashing and FE/passwordHashing
+     *
+     * @throws ConfigurationChangedException
+     */
+    protected function migrateSaltedPasswordsSettings()
+    {
+        $confManager = $this->configurationManager;
+        $configsToRemove = [];
+        try {
+            $extensionConfiguration = (array)$confManager->getLocalConfigurationValueByPath('EXTENSIONS/saltedpasswords');
+            $configsToRemove[] = 'EXTENSIONS/saltedpasswords';
+        } catch (MissingArrayPathException $e) {
+            $extensionConfiguration = [];
+        }
+        try {
+            // The silent upgrade may be executed before LayoutController synchronized old serialized extConf
+            // settings to EXTENSIONS if upgrading from v8 to v9.
+            $extConfConfiguration = (string)$confManager->getLocalConfigurationValueByPath('EXT/extConf/saltedpasswords');
+            $configsToRemove[] = 'EXT/extConf/saltedpasswords';
+        } catch (MissingArrayPathException $e) {
+            $extConfConfiguration = [];
+        }
+        // Migration already done
+        if (empty($extensionConfiguration) && empty($extConfConfiguration)) {
+            return;
+        }
+        // Upgrade to best available hash method. This is only done once since that code will no longer be reached
+        // after first migration because extConf and EXTENSIONS array entries are gone then. Thus, a manual selection
+        // 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,
+        ];
+        $newMethods = [];
+        foreach (['BE', 'FE'] as $mode) {
+            foreach ($okHashMethods as $className) {
+                /** @var SaltInterface $instance */
+                $instance = GeneralUtility::makeInstance($className);
+                if ($instance->isAvailable()) {
+                    $newMethods[$mode] = $className;
+                    break;
+                }
+            }
+        }
+        // We only need to write to LocalConfiguration if method is different than Argon2i from DefaultConfiguration
+        $newConfig = [];
+        if ($newMethods['BE'] !== Argon2iSalt::class) {
+            $newConfig['BE/passwordHashing/className'] = $newMethods['BE'];
+        }
+        if ($newMethods['FE'] !== Argon2iSalt::class) {
+            $newConfig['FE/passwordHashing/className'] = $newMethods['FE'];
+        }
+        if (!empty($newConfig)) {
+            $confManager->setLocalConfigurationValuesByPathValuePairs($newConfig);
+        }
+        $confManager->removeLocalConfigurationKeysByPath($configsToRemove);
+        $this->throwConfigurationChangedException();
+    }
 }
index 29c4945..33e3f7a 100644 (file)
@@ -165,4 +165,9 @@ return [
             'Deprecation-85124-RedirectingUrlHandlerHookConcept.rst',
         ],
     ],
+    '$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'ext/saltedpasswords\'][\'saltMethods\']' => [
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst'
+        ],
+    ],
 ];
index 597251e..e2428ee 100644 (file)
@@ -55,4 +55,69 @@ return [
             'Deprecation-52694-DeprecatedGeneralUtilitydevLog.rst',
         ],
     ],
+    'TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt::ITOA64' => [
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt::HASH_COUNT' => [
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt::MAX_HASH_COUNT' => [
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt::MIN_HASH_COUNT' => [
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Md5Salt::ITOA64' => [
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt::ITOA64' => [
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt::HASH_COUNT' => [
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt::MAX_HASH_COUNT' => [
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt::MIN_HASH_COUNT' => [
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt::ITOA64' => [
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt::HASH_COUNT' => [
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt::MAX_HASH_COUNT' => [
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt::MIN_HASH_COUNT' => [
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
 ];
index 79fe95e..b79c130 100644 (file)
@@ -719,4 +719,24 @@ return [
             'Deprecation-85802-MoveFlexFormServiceFromEXTextbaseToEXTcore.rst',
         ],
     ],
+    'TYPO3\CMS\Saltedpasswords\Salt\ComposedSaltInterface' => [
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst'
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\AbstractComposedSalt' => [
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst'
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Utility\ExtensionManagerConfigurationUtility' => [
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst'
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Utility\SaltedPasswordsUtility' => [
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst'
+        ],
+    ],
 ];
index 9cdc377..8df25f1 100644 (file)
@@ -1,10 +1,17 @@
 <?php
 return [
     // Arguments removed from interface methods
+    // @todo: Add the interface name to the definition and refactor matcher
     'like' => [
         'newNumberOfArguments' => 2,
         'restFiles' => [
             'Breaking-80700-DeprecatedFunctionalityRemoved.rst',
         ],
     ],
+    'getHashedPassword' => [
+        'newNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst'
+        ],
+    ]
 ];
index f881108..db9f44b 100644 (file)
@@ -177,4 +177,32 @@ return [
             'Breaking-84877-MethodsOfLocalizationRepositoryChanged.rst',
         ],
     ],
+    'TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->getHashedPassword' => [
+        'numberOfMandatoryArguments' => 1,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Md5Salt->getHashedPassword' => [
+        'numberOfMandatoryArguments' => 1,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->getHashedPassword' => [
+        'numberOfMandatoryArguments' => 1,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->getHashedPassword' => [
+        'numberOfMandatoryArguments' => 1,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
 ];
index 0f333c7..f65b24d 100644 (file)
@@ -2571,4 +2571,277 @@ return [
             'Deprecation-85807-DeprecateEnvironmentServiceisEnvironmentInCliMode.rst',
         ],
     ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Argon2iSalt->getOptions' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Argon2iSalt->setOptions' => [
+        'numberOfMandatoryArguments' => 1,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\BcryptSalt->getOptions' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\BcryptSalt->setOptions' => [
+        'numberOfMandatoryArguments' => 1,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->getHashCount' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->getMaxHashCount' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->getMinHashCount' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->getSaltLength' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->getSetting' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->setHashCount' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->setMaxHashCount' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->setMinHashCount' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Md5Salt->getSetting' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Md5Salt->getSaltLength' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->getHashCount' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->getMaxHashCount' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->getMinHashCount' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->getSaltLength' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->getSetting' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->setHashCount' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->setMaxHashCount' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->setMinHashCount' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->getHashCount' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->getMaxHashCount' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->getMinHashCount' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->getSaltLength' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->getSetting' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->setHashCount' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->setMaxHashCount' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->setMinHashCount' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->isValidSalt' => [
+        'numberOfMandatoryArguments' => 1,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt->base64Encode' => [
+        'numberOfMandatoryArguments' => 2,
+        'maximumNumberOfArguments' => 2,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Md5Salt->isValidSalt' => [
+        'numberOfMandatoryArguments' => 1,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Md5Salt->base64Encode' => [
+        'numberOfMandatoryArguments' => 2,
+        'maximumNumberOfArguments' => 2,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->isValidSalt' => [
+        'numberOfMandatoryArguments' => 1,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->base64Encode' => [
+        'numberOfMandatoryArguments' => 2,
+        'maximumNumberOfArguments' => 2,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt->base64Decode' => [
+        'numberOfMandatoryArguments' => 1,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->isValidSalt' => [
+        'numberOfMandatoryArguments' => 1,
+        'maximumNumberOfArguments' => 1,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
+    'TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt->base64Encode' => [
+        'numberOfMandatoryArguments' => 2,
+        'maximumNumberOfArguments' => 2,
+        'restFiles' => [
+            'Deprecation-85804-SaltedPasswordHashClassDeprecations.rst',
+        ],
+    ],
 ];
diff --git a/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing.html b/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing.html
new file mode 100644 (file)
index 0000000..5165ae5
--- /dev/null
@@ -0,0 +1,32 @@
+<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
+
+<div class="panel panel-default">
+       <div class="panel-heading" role="tab" id="headingSix">
+               <h4 class="panel-title">
+                       <a role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseSix" aria-expanded="true" aria-controls="collapseSix" class="collapsed">
+                               <span class="caret"></span>
+                               Password hashing settings
+                       </a>
+               </h4>
+       </div>
+       <div id="collapseSix" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingSix">
+               <div class="panel-body">
+                       <p>
+                               Passwords in TYPO3 for frontend and backend users are never stored in
+                               plain text in the database. A salted one-way hash algorithm is
+                               used.
+                       </p>
+                       <p>
+                               This module detects available password hash algorithms. If in doubt, select the first
+                               selectable one, it represents the most secure hash algorithm available
+                               on this system. The selected algorithm will be used for new frontend and backend users
+                               and existing users are upgraded to the selected algorithm upon their first successful login.
+                       </p>
+                       <f:for each="{feature.presetsOrderedByPriority}" as="preset">
+                               <f:render partial="Settings/Presets/{feature.name}/{preset.name}" arguments="{_all}" />
+                       </f:for>
+               </div>
+       </div>
+</div>
+
+</html>
diff --git a/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Argon2i.html b/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Argon2i.html
new file mode 100644 (file)
index 0000000..c2b9f9a
--- /dev/null
@@ -0,0 +1,33 @@
+<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
+
+<f:be.infobox state="{f:if(condition:'{preset.isAvailable}', then:'0', else:'2')}" disableIcon="true">
+       <input
+               type="radio"
+               class="t3-install-tool-configuration-radio"
+               id="t3-install-tool-configuration-passwordHashing-argon2i"
+               name="install[values][{feature.name}][enable]"
+               value="{preset.name}"
+               {f:if(condition:'{preset.isAvailable}', then:'', else:'disabled="disabled"')}
+               {f:if(condition: preset.isActive, then:'checked="checked"')}
+       />
+       <label for="t3-install-tool-configuration-passwordHashing-argon2i" class="t3-install-tool-configuration-radio-label">
+               <strong>Argon2i</strong> {f:if(condition: preset.isActive, then:' [Active]')}
+       </label>
+       <p>
+               <f:if condition="{preset.isAvailable}">
+                       <f:then>
+                               Select this one if in doubt: Argon2i is a modern key derivation function that was selected as
+                               the winner of the Password Hashing Competition in July 2015.
+                       </f:then>
+                       <f:else>
+                               Argon2i is not available on this system. This is sad since it is a modern password hash
+                               algorithm and the winner of the Password Hashing Competition in July 2015. It is easily
+                               available on all platforms since PHP version 7.2. There is no sane reason to run PHP >7.2
+                               without argon2i. Please reach out to your hoster to fix this and select this hash algorithm
+                               as soon as it is available.
+                       </f:else>
+               </f:if>
+       </p>
+</f:be.infobox>
+
+</html>
diff --git a/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Bcrypt.html b/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Bcrypt.html
new file mode 100644 (file)
index 0000000..3e5ea43
--- /dev/null
@@ -0,0 +1,32 @@
+<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
+
+<f:be.infobox state="{f:if(condition:'{preset.isAvailable}', then:'0', else:'2')}" disableIcon="true">
+       <input
+               type="radio"
+               class="t3-install-tool-configuration-radio"
+               id="t3-install-tool-configuration-passwordHashing-bcrypt"
+               name="install[values][{feature.name}][enable]"
+               value="{preset.name}"
+               {f:if(condition:'{preset.isAvailable}', then:'', else:'disabled="disabled"')}
+               {f:if(condition: preset.isActive, then:'checked="checked"')}
+       />
+       <label for="t3-install-tool-configuration-passwordHashing-bcrypt" class="t3-install-tool-configuration-radio-label">
+               <strong>bcrypt</strong> {f:if(condition: preset.isActive, then:' [Active]')}
+       </label>
+       <p>
+               <f:if condition="{preset.isAvailable}">
+                       <f:then>
+                               bcrypt is a good password hashing algorithm. It however needs some additional quirks
+                               for long passwords in PHP and should only be used if Argon2i is not available.
+                       </f:then>
+                       <f:else>
+                               bcrypt is not available on this system. TYPO3 password storage not only requires bcrypt itself,
+                               but also sha384 to be availble to use this algorithm. One of these or both are missing. bcrypt however
+                               can be used as a fallback if Argon2i is not available, too. Ask reach out to your hoster to fix both
+                               and prefer Argon2i.
+                       </f:else>
+               </f:if>
+       </p>
+</f:be.infobox>
+
+</html>
diff --git a/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Custom.html b/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Custom.html
new file mode 100644 (file)
index 0000000..e1c7ff8
--- /dev/null
@@ -0,0 +1,37 @@
+<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
+
+<f:be.infobox state="1" disableIcon="true">
+       <input
+               type="radio"
+               class="t3-install-tool-configuration-radio"
+               id="t3-install-tool-configuration-passwordHashing-custom"
+               name="install[values][{feature.name}][enable]"
+               value="{preset.name}"
+               disabled="disabled"
+               {f:if(condition: preset.isActive, then:'checked="checked"')}
+       />
+       <label for="t3-install-tool-configuration-passwordHashing-custom" class="t3-install-tool-configuration-radio-label">
+               <strong>Custom configuration</strong> {f:if(condition: preset.isActive, then:' [Active]')}
+       </label>
+       <p>Custom password hash settings. This interface does not allow modification of the values, they are just shown.
+               Configuring custom hash settings is for advanced users who know exactly what they are doing. Refer to the
+               core documentation for details.</p>
+       <f:for each="{preset.configurationValues}" as="configurationValue" key="configurationKey">
+               <div class="form-group">
+                       <label class="col-sm-6 control-label" for="{feature.name}{preset.name}{configurationKey}">{configurationKey}</label>
+                       <div class="col-sm-6">
+                               <input
+                                       id="{feature.name}{preset.name}{configurationKey}"
+                                       type="text"
+                                       name="install[values][{feature.name}][{preset.name}][{configurationKey}]"
+                                       value="{configurationValue}"
+                                       disabled="disabled"
+                                       class="form-control t3js-custom-preset"
+                                       data-radio="t3-install-tool-configuration-passwordHashing-custom"
+                                       />
+                       </div>
+               </div>
+       </f:for>
+</f:be.infobox>
+
+</html>
diff --git a/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Pbkdf2.html b/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Pbkdf2.html
new file mode 100644 (file)
index 0000000..0fdf306
--- /dev/null
@@ -0,0 +1,32 @@
+<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
+
+<f:be.infobox state="{f:if(condition:'{preset.isAvailable}', then:'0', else:'2')}" disableIcon="true">
+       <input
+               type="radio"
+               class="t3-install-tool-configuration-radio"
+               id="t3-install-tool-configuration-passwordHashing-pbkdf2"
+               name="install[values][{feature.name}][enable]"
+               value="{preset.name}"
+               {f:if(condition:'{preset.isAvailable}', then:'', else:'disabled="disabled"')}
+               {f:if(condition: preset.isActive, then:'checked="checked"')}
+       />
+       <label for="t3-install-tool-configuration-passwordHashing-pbkdf2" class="t3-install-tool-configuration-radio-label">
+               <strong>PBKDF2</strong> {f:if(condition: preset.isActive, then:' [Active]')}
+       </label>
+       <p>
+               <f:if condition="{preset.isAvailable}">
+                       <f:then>
+                               PBKDF2 is a key derivation function recommended by IETF in RFC 8018 as part of the PKCS series, even
+                               though newer password hashing functions such as Argon2i are designed to address weaknesses of PBKDF2.
+                               It could be a preferred password hash algorithm if storing passwords in a FIPS compliant way is necessary.
+                               Usually, selecting Argon2i as hash algorithm is good to go.
+                       </f:then>
+                       <f:else>
+                               PBKDF2 is not available on this system. This is very uncommon. If Argon2i and bcrypt are also not available,
+                               you should seriously question the quality of your current hoster and reach them out to fix this as soon as possible.
+                       </f:else>
+               </f:if>
+       </p>
+</f:be.infobox>
+
+</html>
diff --git a/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Phpass.html b/typo3/sysext/install/Resources/Private/Partials/Settings/Presets/PasswordHashing/Phpass.html
new file mode 100644 (file)
index 0000000..af3d0d5
--- /dev/null
@@ -0,0 +1,32 @@
+<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
+
+<f:be.infobox state="{f:if(condition:'{preset.isAvailable}', then:'0', else:'2')}" disableIcon="true">
+       <input
+               type="radio"
+               class="t3-install-tool-configuration-radio"
+               id="t3-install-tool-configuration-passwordHashing-phpass"
+               name="install[values][{feature.name}][enable]"
+               value="{preset.name}"
+               {f:if(condition:'{preset.isAvailable}', then:'', else:'disabled="disabled"')}
+               {f:if(condition: preset.isActive, then:'checked="checked"')}
+       />
+       <label for="t3-install-tool-configuration-passwordHashing-phpass" class="t3-install-tool-configuration-radio-label">
+               <strong>phpass</strong> {f:if(condition: preset.isActive, then:' [Active]')}
+       </label>
+       <p>
+               <f:if condition="{preset.isAvailable}">
+                       <f:then>
+                               In almost all cases, a modern hash algorithm like Argon2i should be preferred and is good to go.
+                               phpass is a portable public domain password hashing framework for use in PHP applications since 2005.
+                               The implementation should work on almost all PHP builds. It might be a suitable password storage hash
+                               method in seldom cases if third party systems must use the same password hash on a low database level
+                               and no sane different authentication service can be used for whatever reason.
+                       </f:then>
+                       <f:else>
+                               That's funny: phpass is always available!
+                       </f:else>
+               </f:if>
+       </p>
+</f:be.infobox>
+
+</html>
index 0478980..4d00b0c 100644 (file)
@@ -15,14 +15,18 @@ namespace TYPO3\CMS\Install\Tests\Unit\Service;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Prophecy\Argument;
 use Prophecy\Prophecy\ObjectProphecy;
 use TYPO3\CMS\Core\Configuration\ConfigurationManager;
 use TYPO3\CMS\Core\Package\PackageManager;
 use TYPO3\CMS\Core\Tests\Unit\Utility\AccessibleProxies\ExtensionManagementUtilityAccessibleProxy;
 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\Install\Service\SilentConfigurationUpgradeService;
+use TYPO3\CMS\Saltedpasswords\Salt\Argon2iSalt;
+use TYPO3\CMS\Saltedpasswords\Salt\BcryptSalt;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 /**
@@ -728,8 +732,10 @@ class SilentConfigurationUpgradeServiceTest extends UnitTestCase
             ->method('setLocalConfigurationValueByPath')
             ->with($this->equalTo('BE/languageDebug'), false);
 
-        $silentConfigurationUpgradeServiceInstance->_set('configurationManager', $this->configurationManager);
+        $this->expectException(ConfigurationChangedException::class);
+        $this->expectExceptionCode(1379024938);
 
+        $silentConfigurationUpgradeServiceInstance->_set('configurationManager', $this->configurationManager);
         $silentConfigurationUpgradeServiceInstance->_call('migrateLangDebug');
     }
 
@@ -754,12 +760,11 @@ class SilentConfigurationUpgradeServiceTest extends UnitTestCase
                 ->willReturn($value);
         }
 
-        $configurationManager->setLocalConfigurationValuesByPathValuePairs(\Prophecy\Argument::cetera())
-            ->shouldBeCalled();
-        $configurationManager->removeLocalConfigurationKeysByPath(\Prophecy\Argument::cetera())
-            ->shouldBeCalled();
+        $configurationManager->setLocalConfigurationValuesByPathValuePairs(Argument::cetera())->shouldBeCalled();
+        $configurationManager->removeLocalConfigurationKeysByPath(Argument::cetera())->shouldBeCalled();
 
         $this->expectException(ConfigurationChangedException::class);
+        $this->expectExceptionCode(1379024938);
 
         /** @var $silentConfigurationUpgradeServiceInstance SilentConfigurationUpgradeService|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\TestingFramework\Core\AccessibleObjectInterface */
         $silentConfigurationUpgradeServiceInstance = $this->getAccessibleMock(
@@ -771,7 +776,143 @@ class SilentConfigurationUpgradeServiceTest extends UnitTestCase
         );
 
         $silentConfigurationUpgradeServiceInstance->_set('configurationManager', $configurationManager->reveal());
-
         $silentConfigurationUpgradeServiceInstance->_call('migrateCacheHashOptions');
     }
+
+    /**
+     * @test
+     */
+    public function migrateSaltedPasswordsSettingsDoesNothingIfExtensionConfigsAreNotSet()
+    {
+        $configurationManagerProphecy = $this->prophesize(ConfigurationManager::class);
+        $configurationManagerException = new MissingArrayPathException('Path does not exist in array', 1533989414);
+        $configurationManagerProphecy->getLocalConfigurationValueByPath('EXTENSIONS/saltedpasswords')
+            ->shouldBeCalled()->willThrow($configurationManagerException);
+        $configurationManagerProphecy->getLocalConfigurationValueByPath('EXT/extConf/saltedpasswords')
+            ->shouldBeCalled()->willThrow($configurationManagerException);
+        $configurationManagerProphecy->setLocalConfigurationValuesByPathValuePairs(Argument::cetera())
+            ->shouldNotBeCalled();
+        $silentConfigurationUpgradeService = $this->getAccessibleMock(
+            SilentConfigurationUpgradeService::class,
+            ['dummy'],
+            [$configurationManagerProphecy->reveal()]
+        );
+        $silentConfigurationUpgradeService->_call('migrateSaltedPasswordsSettings');
+    }
+
+    /**
+     * @test
+     */
+    public function migrateSaltedPasswordsSettingsDoesNothingIfExtensionConfigsAreEmpty()
+    {
+        $configurationManagerProphecy = $this->prophesize(ConfigurationManager::class);
+        $configurationManagerProphecy->getLocalConfigurationValueByPath('EXTENSIONS/saltedpasswords')
+            ->shouldBeCalled()->willReturn([]);
+        $configurationManagerProphecy->getLocalConfigurationValueByPath('EXT/extConf/saltedpasswords')
+            ->shouldBeCalled()->willReturn('');
+        $configurationManagerProphecy->setLocalConfigurationValuesByPathValuePairs(Argument::cetera())
+            ->shouldNotBeCalled();
+        $silentConfigurationUpgradeService = $this->getAccessibleMock(
+            SilentConfigurationUpgradeService::class,
+            ['dummy'],
+            [$configurationManagerProphecy->reveal()]
+        );
+        $silentConfigurationUpgradeService->_call('migrateSaltedPasswordsSettings');
+    }
+
+    /**
+     * @test
+     */
+    public function migrateSaltedPasswordsSettingsRemovesExtensionsConfigAndSetsNothingElseIfArgon2iIsAvailable()
+    {
+        $configurationManagerProphecy = $this->prophesize(ConfigurationManager::class);
+        $configurationManagerException = new MissingArrayPathException('Path does not exist in array', 1533989428);
+        $configurationManagerProphecy->getLocalConfigurationValueByPath('EXTENSIONS/saltedpasswords')
+            ->shouldBeCalled()->willReturn(['thereIs' => 'something']);
+        $configurationManagerProphecy->getLocalConfigurationValueByPath('EXT/extConf/saltedpasswords')
+            ->shouldBeCalled()->willThrow($configurationManagerException);
+        $argonBeProphecy = $this->prophesize(Argon2iSalt::class);
+        $argonBeProphecy->isAvailable()->shouldBeCalled()->willReturn(true);
+        GeneralUtility::addInstance(Argon2iSalt::class, $argonBeProphecy->reveal());
+        $argonFeProphecy = $this->prophesize(Argon2iSalt::class);
+        $argonFeProphecy->isAvailable()->shouldBeCalled()->willReturn(true);
+        GeneralUtility::addInstance(Argon2iSalt::class, $argonFeProphecy->reveal());
+        $configurationManagerProphecy->removeLocalConfigurationKeysByPath(['EXTENSIONS/saltedpasswords'])
+            ->shouldBeCalled();
+        $silentConfigurationUpgradeService = $this->getAccessibleMock(
+            SilentConfigurationUpgradeService::class,
+            ['dummy'],
+            [$configurationManagerProphecy->reveal()]
+        );
+        $this->expectException(ConfigurationChangedException::class);
+        $this->expectExceptionCode(1379024938);
+        $silentConfigurationUpgradeService->_call('migrateSaltedPasswordsSettings');
+    }
+
+    /**
+     * @test
+     */
+    public function migrateSaltedPasswordsSettingsRemovesExtConfAndSetsNothingElseIfArgon2iIsAvailable()
+    {
+        $configurationManagerProphecy = $this->prophesize(ConfigurationManager::class);
+        $configurationManagerException = new MissingArrayPathException('Path does not exist in array', 1533989434);
+        $configurationManagerProphecy->getLocalConfigurationValueByPath('EXTENSIONS/saltedpasswords')
+            ->shouldBeCalled()->willThrow($configurationManagerException);
+        $configurationManagerProphecy->getLocalConfigurationValueByPath('EXT/extConf/saltedpasswords')
+            ->shouldBeCalled()->willReturn('someConfiguration');
+        $argonBeProphecy = $this->prophesize(Argon2iSalt::class);
+        $argonBeProphecy->isAvailable()->shouldBeCalled()->willReturn(true);
+        GeneralUtility::addInstance(Argon2iSalt::class, $argonBeProphecy->reveal());
+        $argonFeProphecy = $this->prophesize(Argon2iSalt::class);
+        $argonFeProphecy->isAvailable()->shouldBeCalled()->willReturn(true);
+        GeneralUtility::addInstance(Argon2iSalt::class, $argonFeProphecy->reveal());
+        $configurationManagerProphecy->removeLocalConfigurationKeysByPath(['EXT/extConf/saltedpasswords'])
+            ->shouldBeCalled();
+        $silentConfigurationUpgradeService = $this->getAccessibleMock(
+            SilentConfigurationUpgradeService::class,
+            ['dummy'],
+            [$configurationManagerProphecy->reveal()]
+        );
+        $this->expectException(ConfigurationChangedException::class);
+        $this->expectExceptionCode(1379024938);
+        $silentConfigurationUpgradeService->_call('migrateSaltedPasswordsSettings');
+    }
+
+    /**
+     * @test
+     */
+    public function migrateSaltedPasswordsSetsSpecificHashMethodIfArgon2iIsNotAvailable()
+    {
+        $configurationManagerProphecy = $this->prophesize(ConfigurationManager::class);
+        $configurationManagerProphecy->getLocalConfigurationValueByPath('EXTENSIONS/saltedpasswords')
+            ->shouldBeCalled()->willReturn(['thereIs' => 'something']);
+        $configurationManagerProphecy->getLocalConfigurationValueByPath('EXT/extConf/saltedpasswords')
+            ->shouldBeCalled()->willReturn('someConfiguration');
+        $argonBeProphecy = $this->prophesize(Argon2iSalt::class);
+        $argonBeProphecy->isAvailable()->shouldBeCalled()->willReturn(false);
+        GeneralUtility::addInstance(Argon2iSalt::class, $argonBeProphecy->reveal());
+        $bcryptBeProphecy = $this->prophesize(BcryptSalt::class);
+        $bcryptBeProphecy->isAvailable()->shouldBeCalled()->willReturn(true);
+        GeneralUtility::addInstance(BcryptSalt::class, $bcryptBeProphecy->reveal());
+        $argonFeProphecy = $this->prophesize(Argon2iSalt::class);
+        $argonFeProphecy->isAvailable()->shouldBeCalled()->willReturn(false);
+        GeneralUtility::addInstance(Argon2iSalt::class, $argonFeProphecy->reveal());
+        $bcryptFeProphecy = $this->prophesize(BcryptSalt::class);
+        $bcryptFeProphecy->isAvailable()->shouldBeCalled()->willReturn(true);
+        GeneralUtility::addInstance(BcryptSalt::class, $bcryptFeProphecy->reveal());
+        $configurationManagerProphecy->setLocalConfigurationValuesByPathValuePairs([
+            'BE/passwordHashing/className' => BcryptSalt::class,
+            'FE/passwordHashing/className' => BcryptSalt::class,
+        ])->shouldBeCalled();
+        $configurationManagerProphecy->removeLocalConfigurationKeysByPath(['EXTENSIONS/saltedpasswords', 'EXT/extConf/saltedpasswords'])
+            ->shouldBeCalled();
+        $silentConfigurationUpgradeService = $this->getAccessibleMock(
+            SilentConfigurationUpgradeService::class,
+            ['dummy'],
+            [$configurationManagerProphecy->reveal()]
+        );
+        $this->expectException(ConfigurationChangedException::class);
+        $this->expectExceptionCode(1379024938);
+        $silentConfigurationUpgradeService->_call('migrateSaltedPasswordsSettings');
+    }
 }
index 9f00a8d..62ba704 100644 (file)
@@ -162,7 +162,7 @@ class SecurityStatus implements RequestAwareStatusProviderInterface
 
         if (!empty($row)) {
             try {
-                $hashInstance = GeneralUtility::makeInstance(SaltFactory::class)->get($row['password']);
+                $hashInstance = GeneralUtility::makeInstance(SaltFactory::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.
index 0b4cf26..6baff1a 100644 (file)
@@ -18,26 +18,12 @@ namespace TYPO3\CMS\Saltedpasswords\Salt;
 /**
  * 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 implements ComposedSaltInterface
+abstract class AbstractComposedSalt
 {
     /**
-     * Method applies settings (prefix, optional hash count, optional suffix)
-     * to a salt.
-     *
-     * @param string $salt A salt to apply setting to
-     * @return string Salt with setting
-     */
-    abstract protected function applySettingsToSalt(string $salt): string;
-
-    /**
-     * Generates a random base salt settings for the hash.
-     *
-     * @return string A string containing settings and a random salt
-     */
-    abstract protected function getGeneratedSalt(): string;
-
-    /**
      * 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
@@ -45,36 +31,16 @@ abstract class AbstractComposedSalt implements ComposedSaltInterface
     abstract protected function getItoa64(): string;
 
     /**
-     * Returns setting string to indicate type of hashing method.
-     *
-     * @return string Setting string of hashing method
-     */
-    abstract protected function getSetting(): string;
-
-    /**
-     * Returns length of required salt.
-     *
-     * @return int Length of required salt
-     */
-    abstract 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
-     */
-    abstract public function isValidSalt(string $salt): bool;
-
-    /**
      * 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();
@@ -106,9 +72,11 @@ abstract class AbstractComposedSalt implements ComposedSaltInterface
      *
      * @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);
     }
index a5d2bdf..f2ee2db 100644 (file)
@@ -50,6 +50,44 @@ class Argon2iSalt implements SaltInterface
     ];
 
     /**
+     * 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.
      *
@@ -70,15 +108,14 @@ class Argon2iSalt implements SaltInterface
      */
     public function isAvailable(): bool
     {
-        return defined('PASSWORD_ARGON2I')
-            && PASSWORD_BCRYPT;
+        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 Optional custom salt to use
+     * @param string $salt Deprecated optional custom salt to use
      * @return string|null Salted hashed password
      */
     public function getHashedPassword(string $password, string $salt = null)
@@ -129,9 +166,11 @@ class Argon2iSalt implements SaltInterface
 
     /**
      * @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;
     }
 
@@ -139,9 +178,11 @@ class Argon2iSalt implements SaltInterface
      * 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
index 30ad1b1..139fbc1 100644 (file)
@@ -48,6 +48,27 @@ class BcryptSalt implements SaltInterface
     ];
 
     /**
+     * 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
@@ -65,6 +86,8 @@ class BcryptSalt implements SaltInterface
      * 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
@@ -75,9 +98,8 @@ class BcryptSalt implements SaltInterface
     /**
      * Extend parent method to workaround bcrypt limitations.
      *
-     * @see \TYPO3\CMS\Saltedpasswords\Salt\BcryptSalt::processPlainPassword()
      * @param string $password Plaintext password to create a salted hash from
-     * @param string $salt Optional custom salt to use
+     * @param string $salt Deprecated optional custom salt to use
      * @return string Salted hashed password
      */
     public function getHashedPassword(string $password, string $salt = null)
@@ -97,22 +119,6 @@ class BcryptSalt implements SaltInterface
     }
 
     /**
-     * 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));
-    }
-
-    /**
      * Determines if a given string is a valid salted hashed password.
      *
      * @param string $saltedPW String to check
@@ -133,7 +139,6 @@ class BcryptSalt implements SaltInterface
         }
         return $result;
     }
-
     /**
      * Checks whether a user's hashed password needs to be replaced with a new hash.
      *
@@ -146,10 +151,38 @@ class BcryptSalt implements SaltInterface
     }
 
     /**
+     * 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;
     }
 
@@ -157,9 +190,11 @@ class BcryptSalt implements SaltInterface
      * 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
@@ -177,14 +212,4 @@ class BcryptSalt implements SaltInterface
 
         $this->options = $newOptions;
     }
-
-    /**
-     * @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;
-    }
 }
index b9e5c2b..ab6fc53 100644 (file)
@@ -15,6 +15,10 @@ namespace TYPO3\CMS\Saltedpasswords\Salt;
  * 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.
@@ -22,68 +26,182 @@ namespace TYPO3\CMS\Saltedpasswords\Salt;
  * Warning: Blowfish salted hashing with PHP's crypt() is not available
  * on every system.
  */
-class BlowfishSalt extends Md5Salt
+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;
 
     /**
-     * Keeps log2 number
-     * of iterations for password stretching.
+     * Constructor sets options if given
      *
-     * @var int
+     * @param array $options
      */
-    protected static $hashCount;
+    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;
+    }
 
     /**
-     * Keeps maximum allowed log2 number
-     * of iterations for password stretching.
+     * Method checks if a given plaintext password is correct by comparing it with
+     * a given salted hashed password.
      *
-     * @var int
+     * @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
      */
-    protected static $maxHashCount;
+    public function checkPassword(string $plainPW, string $saltedHashPW): bool
+    {
+        $isCorrect = false;
+        if ($this->isValidSalt($saltedHashPW)) {
+            $isCorrect = \password_verify($plainPW, $saltedHashPW);
+        }
+        return $isCorrect;
+    }
 
     /**
-     * Keeps minimum allowed log2 number
-     * of iterations for password stretching.
+     * Returns whether all prerequisites for the hashing methods are matched
      *
-     * @var int
+     * @return bool Method available
      */
-    protected static $minHashCount;
+    public function isAvailable(): bool
+    {
+        return (bool)CRYPT_BLOWFISH;
+    }
 
     /**
-     * Keeps length of a Blowfish salt in bytes.
+     * Method creates a salted hash for a given plaintext password
      *
-     * @var int
+     * @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
      */
-    protected static $saltLengthBlowfish = 16;
+    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;
+    }
 
     /**
-     * Setting string to indicate type of hashing method (blowfish).
+     * 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.
      *
-     * @var string
+     * @param string $saltedPW Salted hash to check if it needs an update
+     * @return bool TRUE if salted hash needs an update, otherwise FALSE
      */
-    protected static $settingBlowfish = '$2a$';
+    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 applies settings (prefix, hash count) to a salt.
+     * Method determines if a given string is a valid salted hashed password.
      *
-     * Overwrites {@link Md5Salt::applySettingsToSalt()}
-     * with Blowfish specifics.
+     * @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
@@ -91,10 +209,10 @@ class BlowfishSalt extends Md5Salt
     protected function applySettingsToSalt(string $salt): string
     {
         $saltWithSettings = $salt;
-        $reqLenBase64 = $this->getLengthBase64FromBytes($this->getSaltLength());
+        $reqLenBase64 = $this->getLengthBase64FromBytes(16);
         // salt without setting
         if (strlen($salt) == $reqLenBase64) {
-            $saltWithSettings = $this->getSetting() . sprintf('%02u', $this->getHashCount()) . '$' . $salt;
+            $saltWithSettings = self::PREFIX . sprintf('%02u', $this->options['hash_count']) . '$' . $salt;
         }
         return $saltWithSettings;
     }
@@ -108,7 +226,7 @@ class BlowfishSalt extends Md5Salt
     protected function getCountLog2(string $setting): int
     {
         $countLog2 = null;
-        $setting = substr($setting, strlen($this->getSetting()));
+        $setting = substr($setting, strlen(self::PREFIX));
         $firstSplitPos = strpos($setting, '$');
         // Hashcount existing
         if ($firstSplitPos !== false && $firstSplitPos <= 2 && is_numeric(substr($setting, 0, $firstSplitPos))) {
@@ -118,171 +236,187 @@ class BlowfishSalt extends Md5Salt
     }
 
     /**
-     * Method returns log2 number of iterations for password stretching.
+     * Returns a string for mapping an int to the corresponding base 64 character.
      *
-     * @return int log2 number of iterations for password stretching
-     * @see HASH_COUNT
-     * @see $hashCount
-     * @see setHashCount()
+     * @return string String for mapping an int to the corresponding base 64 character
      */
-    public function getHashCount(): int
+    protected function getItoa64(): string
     {
-        return self::$hashCount ?? self::HASH_COUNT;
+        return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
     }
 
     /**
-     * Method returns maximum allowed log2 number of iterations for password stretching.
+     * Method determines if a given string is a valid salt.
      *
-     * @return int Maximum allowed log2 number of iterations for password stretching
-     * @see MAX_HASH_COUNT
-     * @see $maxHashCount
-     * @see setMaxHashCount()
+     * @param string $salt String to check
+     * @return bool TRUE if it's valid salt, otherwise FALSE
      */
-    public function getMaxHashCount(): int
+    protected function isValidSalt(string $salt): bool
     {
-        return self::$maxHashCount ?? self::MAX_HASH_COUNT;
+        $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;
     }
 
     /**
-     * Returns whether all prerequisites for the hashing methods are matched
+     * Encodes bytes into printable base 64 using the *nix standard from crypt().
      *
-     * @return bool Method available
+     * @param string $input The string containing bytes to encode.
+     * @param int $count The number of characters (bytes) to encode.
+     * @return string Encoded string
      */
-    public function isAvailable(): bool
+    protected function base64Encode(string $input, int $count): string
     {
-        return (bool)CRYPT_BLOWFISH;
+        $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 returns minimum allowed log2 number of iterations for password stretching.
+     * Method determines required length of base64 characters for a given
+     * length of a byte string.
      *
-     * @return int Minimum allowed log2 number of iterations for password stretching
-     * @see MIN_HASH_COUNT
-     * @see $minHashCount
-     * @see setMinHashCount()
+     * @param int $byteLength Length of bytes to calculate in base64 chars
+     * @return int Required length of base64 characters
      */
-    public function getMinHashCount(): int
+    protected function getLengthBase64FromBytes(int $byteLength): int
     {
-        return self::$minHashCount ?? self::MIN_HASH_COUNT;
+        // Calculates bytes in bits in base64
+        return (int)ceil($byteLength * 8 / 6);
     }
 
     /**
-     * Returns length of a Blowfish salt in bytes.
-     *
-     * Overwrites {@link Md5Salt::getSaltLength()}
-     * with Blowfish specifics.
+     * Method returns log2 number of iterations for password stretching.
      *
-     * @return int Length of a Blowfish salt in bytes
+     * @return int log2 number of iterations for password stretching
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
-    public function getSaltLength(): int
+    public function getHashCount(): int
     {
-        return self::$saltLengthBlowfish;
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return $this->options['hash_count'];
     }
 
     /**
-     * Returns setting string of Blowfish salted hashes.
-     *
-     * Overwrites {@link Md5Salt::getSetting()}
-     * with Blowfish specifics.
+     * Method returns maximum allowed log2 number of iterations for password stretching.
      *
-     * @return string Setting string of Blowfish salted hashes
+     * @return int Maximum allowed log2 number of iterations for password stretching
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
-    public function getSetting(): string
+    public function getMaxHashCount(): int
     {
-        return self::$settingBlowfish;
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return 17;
     }
 
     /**
-     * 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.
+     * Method returns minimum allowed log2 number of iterations for password stretching.
      *
-     * @param string $saltedPW Salted hash to check if it needs an update
-     * @return bool TRUE if salted hash needs an update, otherwise FALSE
+     * @return int Minimum allowed log2 number of iterations for password stretching
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
-    public function isHashUpdateNeeded(string $saltedPW): bool
+    public function getMinHashCount(): int
     {
-        // Check whether this was an updated password.
-        if (strncmp($saltedPW, '$2', 2) || !$this->isValidSalt($saltedPW)) {
-            return true;
-        }
-        // Check whether the iteration count used differs from the standard number.
-        $countLog2 = $this->getCountLog2($saltedPW);
-        return $countLog2 !== null && $countLog2 < $this->getHashCount();
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return 4;
     }
 
     /**
-     * Method determines if a given string is a valid salt.
+     * Returns length of a Blowfish salt in bytes.
      *
-     * Overwrites {@link Md5Salt::isValidSalt()} with
-     * Blowfish specifics.
+     * @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.
      *
-     * @param string $salt String to check
-     * @return bool TRUE if it's valid salt, otherwise FALSE
+     * @return string Setting string of Blowfish salted hashes
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
-    public function isValidSalt(string $salt): bool
+    public function getSetting(): string
     {
-        $isValid = ($skip = false);
-        $reqLenBase64 = $this->getLengthBase64FromBytes($this->getSaltLength());
-        if (strlen($salt) >= $reqLenBase64) {
-            // Salt with prefixed setting
-            if (!strncmp('$', $salt, 1)) {
-                if (!strncmp($this->getSetting(), $salt, strlen($this->getSetting()))) {
-                    $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;
+        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
-     * @see HASH_COUNT
-     * @see $hashCount
-     * @see getHashCount()
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
     public function setHashCount(int $hashCount = null)
     {
-        self::$hashCount = $hashCount !== null && $hashCount >= $this->getMinHashCount() && $hashCount <= $this->getMaxHashCount() ? $hashCount : self::HASH_COUNT;
+        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
-     * @see MAX_HASH_COUNT
-     * @see $maxHashCount
-     * @see getMaxHashCount()
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
     public function setMaxHashCount(int $maxHashCount = null)
     {
-        self::$maxHashCount = $maxHashCount ?? self::MAX_HASH_COUNT;
+        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
-     * @see MIN_HASH_COUNT
-     * @see $minHashCount
-     * @see getMinHashCount()
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
     public function setMinHashCount(int $minHashCount = null)
     {
-        self::$minHashCount = $minHashCount ?? self::MIN_HASH_COUNT;
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        // Empty, min hash count is hard coded to 4
     }
 }
index de1cd23..3f4cb41 100644 (file)
@@ -18,6 +18,8 @@ namespace TYPO3\CMS\Saltedpasswords\Salt;
 /**
  * Interface for implementing salts that compose the password-hash string
  * themselves.
+ *
+ * @deprecated and will be removed in TYPO3 v10.0.
  */
 interface ComposedSaltInterface extends SaltInterface
 {
index 3111369..4b17ee3 100644 (file)
@@ -15,6 +15,7 @@ namespace TYPO3\CMS\Saltedpasswords\Salt;
  * 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;
 
@@ -25,51 +26,30 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
  * MD5 salted hashing with PHP's crypt() should be available
  * on most of the systems.
  */
-class Md5Salt extends AbstractComposedSalt
+class Md5Salt implements SaltInterface
 {
-    /**
-     * Keeps a string for mapping an int to the corresponding
-     * base 64 character.
-     */
-    const ITOA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
-
-    /**
-     * Keeps length of a MD5 salt in bytes.
-     *
-     * @var int
-     */
-    protected static $saltLengthMD5 = 6;
+    use PublicMethodDeprecationTrait;
 
     /**
-     * Keeps suffix to be appended to a salt.
-     *
-     * @var string
+     * @var array
      */
-    protected static $saltSuffixMD5 = '$';
+    private $deprecatedPublicMethods = [
+        'isValidSalt' => 'Using Md5Salt::isValidSalt() is deprecated and will not be possible anymore in TYPO3 v10.',
+        'base64Encode' => 'Using Md5Salt::base64Encode() is deprecated and will not be possible anymore in TYPO3 v10.',
+    ];
 
     /**
-     * Setting string to indicate type of hashing method (md5).
-     *
-     * @var string
+     * Prefix for the password hash.
      */
-    protected static $settingMD5 = '$1$';
+    protected const PREFIX = '$1$';
 
     /**
-     * Method applies settings (prefix, suffix) to a salt.
+     * Keeps a string for mapping an int to the corresponding
+     * base 64 character.
      *
-     * @param string $salt A salt to apply setting to
-     * @return string Salt with setting
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
-    protected function applySettingsToSalt(string $salt): string
-    {
-        $saltWithSettings = $salt;
-        $reqLenBase64 = $this->getLengthBase64FromBytes($this->getSaltLength());
-        // Salt without setting
-        if (strlen($salt) == $reqLenBase64) {
-            $saltWithSettings = $this->getSetting() . $salt . $this->getSaltSuffix();
-        }
-        return $saltWithSettings;
-    }
+    const ITOA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
 
     /**
      * Method checks if a given plaintext password is correct by comparing it with
@@ -89,31 +69,27 @@ class Md5Salt extends AbstractComposedSalt
     }
 
     /**
-     * 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.
+     * Returns whether all prerequisites for the hashing methods are matched
      *
-     * @return string A character string containing settings and a random salt
+     * @return bool Method available
      */
-    protected function getGeneratedSalt(): string
+    public function isAvailable(): bool
     {
-        $randomBytes = GeneralUtility::makeInstance(Random::class)->generateRandomBytes($this->getSaltLength());
-        return $this->base64Encode($randomBytes, $this->getSaltLength());
+        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 Optional custom salt with setting to use
+     * @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)) {
@@ -125,68 +101,77 @@ class Md5Salt extends AbstractComposedSalt
     }
 
     /**
-     * Returns a string for mapping an int to the corresponding base 64 character.
+     * Checks whether a user's hashed password needs to be replaced with a new hash.
      *
-     * @return string String for mapping an int to the corresponding base 64 character
-     */
-    protected function getItoa64(): string
-    {
-        return self::ITOA64;
-    }
-
-    /**
-     * Returns whether all prerequisites for the hashing methods are matched
+     * 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.
      *
-     * @return bool Method available
+     * @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 isAvailable(): bool
+    public function isHashUpdateNeeded(string $passString): bool
     {
-        return (bool)CRYPT_MD5;
+        return false;
     }
 
     /**
-     * Returns length of a MD5 salt in bytes.
+     * Method determines if a given string is a valid salted hashed password.
      *
-     * @return int Length of a MD5 salt in bytes
+     * @param string $saltedPW String to check
+     * @return bool TRUE if it's valid salted hashed password, otherwise FALSE
      */
-    public function getSaltLength(): int
+    public function isValidSaltedPW(string $saltedPW): bool
     {
-        return self::$saltLengthMD5;
+        $isValid = !strncmp(self::PREFIX, $saltedPW, strlen(self::PREFIX));
+        if ($isValid) {
+            $isValid = $this->isValidSalt($saltedPW);
+        }
+        return $isValid;
     }
 
     /**
-     * Returns suffix to be appended to a salt.
+     * Generates a random base 64-encoded salt prefixed and suffixed with settings for the hash.
      *
-     * @return string Suffix of a salt
+     * 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 getSaltSuffix(): string
+    protected function getGeneratedSalt(): string
     {
-        return self::$saltSuffixMD5;
+        $randomBytes = GeneralUtility::makeInstance(Random::class)->generateRandomBytes(6);
+        return $this->base64Encode($randomBytes, 6);
     }
 
     /**
-     * Returns setting string of MD5 salted hashes.
+     * Method applies settings (prefix, suffix) to a salt.
      *
-     * @return string Setting string of MD5 salted hashes
+     * @param string $salt A salt to apply setting to
+     * @return string Salt with setting
      */
-    public function getSetting(): string
+    protected function applySettingsToSalt(string $salt): string
     {
-        return self::$settingMD5;
+        $saltWithSettings = $salt;
+        $reqLenBase64 = $this->getLengthBase64FromBytes(6);
+        // Salt without setting
+        if (strlen($salt) == $reqLenBase64) {
+            $saltWithSettings = self::PREFIX . $salt . '$';
+        }
+        return $saltWithSettings;
     }
 
     /**
-     * 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.
+     * Returns a string for mapping an int to the corresponding base 64 character.
      *
-     * @param string $passString Salted hash to check if it needs an update
-     * @return bool TRUE if salted hash needs an update, otherwise FALSE
+     * @return string String for mapping an int to the corresponding base 64 character
      */
-    public function isHashUpdateNeeded(string $passString): bool
+    protected function getItoa64(): string
     {
-        return false;
+        return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
     }
 
     /**
@@ -195,16 +180,16 @@ class Md5Salt extends AbstractComposedSalt
      * @param string $salt String to check
      * @return bool TRUE if it's valid salt, otherwise FALSE
      */
-    public function isValidSalt(string $salt): bool
+    protected function isValidSalt(string $salt): bool
     {
         $isValid = ($skip = false);
-        $reqLenBase64 = $this->getLengthBase64FromBytes($this->getSaltLength());
+        $reqLenBase64 = $this->getLengthBase64FromBytes(6);
         if (strlen($salt) >= $reqLenBase64) {
             // Salt with prefixed setting
             if (!strncmp('$', $salt, 1)) {
-                if (!strncmp($this->getSetting(), $salt, strlen($this->getSetting()))) {
+                if (!strncmp(self::PREFIX, $salt, strlen(self::PREFIX))) {
                     $isValid = true;
-                    $salt = substr($salt, strlen($this->getSetting()));
+                    $salt = substr($salt, strlen(self::PREFIX));
                 } else {
                     $skip = true;
                 }
@@ -220,17 +205,73 @@ class Md5Salt extends AbstractComposedSalt
     }
 
     /**
-     * Method determines if a given string is a valid salted hashed password.
+     * Encodes bytes into printable base 64 using the *nix standard from crypt().
      *
-     * @param string $saltedPW String to check
-     * @return bool TRUE if it's valid salted hashed password, otherwise FALSE
+     * @param string $input The string containing bytes to encode.
+     * @param int $count The number of characters (bytes) to encode.
+     * @return string Encoded string
      */
-    public function isValidSaltedPW(string $saltedPW): bool
+    protected function base64Encode(string $input, int $count): string
     {
-        $isValid = !strncmp($this->getSetting(), $saltedPW, strlen($this->getSetting()));
-        if ($isValid) {
-            $isValid = $this->isValidSalt($saltedPW);
-        }
-        return $isValid;
+        $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;
     }
 }
index 902409a..02fff16 100644 (file)
@@ -15,6 +15,7 @@ namespace TYPO3\CMS\Saltedpasswords\Salt;
  * 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;
 
@@ -22,94 +23,152 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
  * Class that implements PBKDF2 salted hashing based on PHP's
  * hash_pbkdf2() function.
  */
-class Pbkdf2Salt extends AbstractComposedSalt
+class Pbkdf2Salt implements SaltInterface
 {
+    use PublicMethodDeprecationTrait;
+
+    /**
+     * @var array
+     */
+    private $deprecatedPublicMethods = [
+        'isValidSalt' => 'Using Pbkdf2Salt::isValidSalt() is deprecated and will not be possible anymore in TYPO3 v10.',
+        'base64Encode' => 'Using Pbkdf2Salt::base64Encode() is deprecated and will not be possible anymore in TYPO3 v10.',
+        'base64Decode' => 'Using Pbkdf2Salt::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.
-     */
-    const MIN_HASH_COUNT = 1000;
-
-    /**
-     * Keeps number of iterations for password stretching.
      *
-     * @var int
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
-    protected static $hashCount;
+    const MIN_HASH_COUNT = 1000;
 
     /**
-     * Keeps maximum allowed number of iterations for password stretching.
+     * Constructor sets options if given
      *
-     * @var int
+     * @param array $options
      */
-    protected static $maxHashCount;
+    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;
+    }
 
     /**
-     * Keeps minimum allowed number of iterations for password stretching.
+     * Method checks if a given plaintext password is correct by comparing it with
+     * a given salted hashed password.
      *
-     * @var int
+     * @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
      */
-    protected static $minHashCount;
+    public function checkPassword(string $plainPW, string $saltedHashPW): bool
+    {
+        return $this->isValidSalt($saltedHashPW) && hash_equals($this->getHashedPasswordInternal($plainPW, $saltedHashPW), $saltedHashPW);
+    }
 
     /**
-     * Keeps length of a PBKDF2 salt in bytes.
+     * Returns whether all prerequisites for the hashing methods are matched
      *
-     * @var int
+     * @return bool Method available
      */
-    protected static $saltLengthPbkdf2 = 16;
+    public function isAvailable(): bool
+    {
+        return function_exists('hash_pbkdf2');
+    }
 
     /**
-     * Setting string to indicate type of hashing method (PBKDF2).
+     * Method creates a salted hash for a given plaintext password
      *
-     * @var string
+     * @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
      */
-    protected static $settingPbkdf2 = '$pbkdf2-sha256$';
+    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 applies settings (prefix, hash count) to a salt.
-     *
-     * Overwrites {@link Md5Salt::applySettingsToSalt()}
-     * with PBKDF2 specifics.
+     * Method determines if a given string is a valid salted hashed password.
      *
-     * @param string $salt A salt to apply setting to
-     * @return string Salt with setting
+     * @param string $saltedPW String to check
+     * @return bool TRUE if it's valid salted hashed password, otherwise FALSE
      */
-    protected function applySettingsToSalt(string $salt): string
+    public function isValidSaltedPW(string $saltedPW): bool
     {
-        $saltWithSettings = $salt;
-        // salt without setting
-        if (strlen($salt) === $this->getSaltLength()) {
-            $saltWithSettings = $this->getSetting() . sprintf('%02u', $this->getHashCount()) . '$' . $this->base64Encode($salt, $this->getSaltLength());
+        $isValid = !strncmp(self::PREFIX, $saltedPW, strlen(self::PREFIX));
+        if ($isValid) {
+            $isValid = $this->isValidSalt($saltedPW);
         }
-        return $saltWithSettings;
+        return $isValid;
     }
 
     /**
-     * Method checks if a given plaintext password is correct by comparing it with
-     * a given salted hashed password.
+     * Checks whether a user's hashed password needs to be replaced with a new hash.
      *
-     * @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
+     * 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 checkPassword(string $plainPW, string $saltedHashPW): bool
+    public function isHashUpdateNeeded(string $saltedPW): bool
     {
-        return $this->isValidSalt($saltedHashPW) && hash_equals($this->getHashedPassword($plainPW, $saltedHashPW), $saltedHashPW);
+        // 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'];
     }
 
     /**
@@ -121,11 +180,11 @@ class Pbkdf2Salt extends AbstractComposedSalt
     protected function getIterationCount(string $setting)
     {
         $iterationCount = null;
-        $setting = substr($setting, strlen($this->getSetting()));
+        $setting = substr($setting, strlen(self::PREFIX));
         $firstSplitPos = strpos($setting, '$');
         // Hashcount existing
         if ($firstSplitPos !== false
-            && $firstSplitPos <= strlen((string)$this->getMaxHashCount())
+            && $firstSplitPos <= strlen((string)10000000)
             && is_numeric(substr($setting, 0, $firstSplitPos))
         ) {
             $iterationCount = (int)substr($setting, 0, $firstSplitPos);
@@ -134,6 +193,35 @@ class Pbkdf2Salt extends AbstractComposedSalt
     }
 
     /**
+     * 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:
@@ -146,7 +234,7 @@ class Pbkdf2Salt extends AbstractComposedSalt
      */
     protected function getGeneratedSalt(): string
     {
-        return GeneralUtility::makeInstance(Random::class)->generateRandomBytes($this->getSaltLength());
+        return GeneralUtility::makeInstance(Random::class)->generateRandomBytes(16);
     }
 
     /**
@@ -159,7 +247,7 @@ class Pbkdf2Salt extends AbstractComposedSalt
     protected function getStoredSalt(string $salt): string
     {
         if (!strncmp('$', $salt, 1)) {
-            if (!strncmp($this->getSetting(), $salt, strlen($this->getSetting()))) {
+            if (!strncmp(self::PREFIX, $salt, strlen(self::PREFIX))) {
                 $saltParts = GeneralUtility::trimExplode('$', $salt, 4);
                 $salt = $saltParts[2];
             }
@@ -174,239 +262,173 @@ class Pbkdf2Salt extends AbstractComposedSalt
      */
     protected function getItoa64(): string
     {
-        return self::ITOA64;
+        return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
     }
 
     /**
-     * Method creates a salted hash for a given plaintext password
+     * Method determines if a given string is a valid salt.
      *
-     * @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
+     * @param string $salt String to check
+     * @return bool TRUE if it's valid salt, otherwise FALSE
      */
-    public function getHashedPassword(string $password, string $salt = null)
+    protected function isValidSalt(string $salt): bool
     {
-        $saltedPW = null;
-        if ($password !== '') {
-            if (empty($salt) || !$this->isValidSalt($salt)) {
-                $salt = $this->getGeneratedSalt();
-            } else {
-                $this->setHashCount($this->getIterationCount($salt));
-                $salt = $this->getStoredSalt($salt);
+        $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;
+                }
             }
-            $hash = hash_pbkdf2('sha256', $password, $salt, $this->getHashCount(), 0, true);
-            $saltedPW = $this->applySettingsToSalt($salt) . '$' . $this->base64Encode($hash, strlen($hash));
         }
-        return $saltedPW;
-    }
-
-    /**
-     * Method returns number of iterations for password stretching.
-     *
-     * @return int number of iterations for password stretching
-     * @see HASH_COUNT
-     * @see $hashCount
-     * @see setHashCount()
-     */
-    public function getHashCount(): int
-    {
-        return self::$hashCount ?? self::HASH_COUNT;
+        return $isValid;
     }
 
     /**
-     * Method returns maximum allowed number of iterations for password stretching.
+     * Method determines required length of base64 characters for a given
+     * length of a byte string.
      *
-     * @return int Maximum allowed number of iterations for password stretching
-     * @see MAX_HASH_COUNT
-     * @see $maxHashCount
-     * @see setMaxHashCount()
+     * @param int $byteLength Length of bytes to calculate in base64 chars
+     * @return int Required length of base64 characters
      */
-    public function getMaxHashCount(): int
+    protected function getLengthBase64FromBytes(int $byteLength): int
     {
-        return self::$maxHashCount ?? self::MAX_HASH_COUNT;
+        // Calculates bytes in bits in base64
+        return (int)ceil($byteLength * 8 / 6);
     }
 
     /**
-     * Returns whether all prerequisites for the hashing methods are matched
+     * 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.
      *
-     * @return bool Method available
+     * @param string $input The string containing bytes to encode.
+     * @param int $count The number of characters (bytes) to encode.
+     * @return string Encoded string
      */
-    public function isAvailable(): bool
+    protected function base64Encode(string $input, int $count): string
     {
-        return function_exists('hash_pbkdf2');
+        $input = substr($input, 0, $count);
+        return rtrim(str_replace('+', '.', base64_encode($input)), " =\r\n\t\0\x0B");
     }
 
     /**
-     * Method returns minimum allowed number of iterations for password stretching.
+     * 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.
      *
-     * @return int Minimum allowed number of iterations for password stretching
-     * @see MIN_HASH_COUNT
-     * @see $minHashCount
-     * @see setMinHashCount()
+     * @param string $value
+     * @return string
      */
-    public function getMinHashCount(): int
+    protected function base64Decode(string $value): string
     {
-        return self::$minHashCount ?? self::MIN_HASH_COUNT;
+        return base64_decode(str_replace('.', '+', $value));
     }
 
     /**
-     * Returns length of a PBKDF2 salt in bytes.
-     *
-     * Overwrites {@link Md5Salt::getSaltLength()}
-     * with PBKDF2 specifics.
+     * Method returns number of iterations for password stretching.
      *
-     * @return int Length of a PBKDF2 salt in bytes
+     * @return int number of iterations for password stretching
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
-    public function getSaltLength(): int
+    public function getHashCount(): int
     {
-        return self::$saltLengthPbkdf2;
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return $this->options['hash_count'];
     }
 
     /**
-     * Returns setting string of PBKDF2 salted hashes.
-     *
-     * Overwrites {@link Md5Salt::getSetting()}
-     * with PBKDF2 specifics.
+     * Method returns maximum allowed number of iterations for password stretching.
      *
-     * @return string Setting string of PBKDF2 salted hashes
+     * @return int Maximum allowed number of iterations for password stretching
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
-    public function getSetting(): string
+    public function getMaxHashCount(): int
     {
-        return self::$settingPbkdf2;
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return 10000000;
     }
 
     /**
-     * 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.
+     * Method returns minimum allowed number of iterations for password stretching.
      *
-     * @param string $saltedPW Salted hash to check if it needs an update
-     * @return bool TRUE if salted hash needs an update, otherwise FALSE
+     * @return int Minimum allowed number of iterations for password stretching
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
-    public function isHashUpdateNeeded(string $saltedPW): bool
+    public function getMinHashCount(): int
     {
-        // Check whether this was an updated password.
-        if (strncmp($saltedPW, $this->getSetting(), strlen($this->getSetting())) || !$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->getHashCount();
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return 1000;
     }
 
     /**
-     * Method determines if a given string is a valid salt.
-     *
-     * Overwrites {@link Md5Salt::isValidSalt()} with
-     * PBKDF2 specifics.
+     * Returns length of a PBKDF2 salt in bytes.
      *
-     * @param string $salt String to check
-     * @return bool TRUE if it's valid salt, otherwise FALSE
+     * @return int Length of a PBKDF2 salt in bytes
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
-    public function isValidSalt(string $salt): bool
+    public function getSaltLength(): int
     {
-        $isValid = ($skip = false);
-        $reqLenBase64 = $this->getLengthBase64FromBytes($this->getSaltLength());
-        if (strlen($salt) >= $reqLenBase64) {
-            // Salt with prefixed setting
-            if (!strncmp('$', $salt, 1)) {
-                if (!strncmp($this->getSetting(), $salt, strlen($this->getSetting()))) {
-                    $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;
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return 16;
     }
 
     /**
-     * Method determines if a given string is a valid salted hashed password.
+     * Returns setting string of PBKDF2 salted hashes.
      *
-     * @param string $saltedPW String to check
-     * @return bool TRUE if it's valid salted hashed password, otherwise FALSE
+     * @return string Setting string of PBKDF2 salted hashes
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
-    public function isValidSaltedPW(string $saltedPW): bool
+    public function getSetting(): string
     {
-        $isValid = !strncmp($this->getSetting(), $saltedPW, strlen($this->getSetting()));
-        if ($isValid) {
-            $isValid = $this->isValidSalt($saltedPW);
-        }
-        return $isValid;
+        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
-     * @see HASH_COUNT
-     * @see $hashCount
-     * @see getHashCount()
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
     public function setHashCount(int $hashCount = null)
     {
-        self::$hashCount = $hashCount !== null && $hashCount >= $this->getMinHashCount() && $hashCount <= $this->getMaxHashCount() ? $hashCount : self::HASH_COUNT;
+        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
-     * @see MAX_HASH_COUNT
-     * @see $maxHashCount
-     * @see getMaxHashCount()
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
     public function setMaxHashCount(int $maxHashCount = null)
     {
-        self::$maxHashCount = $maxHashCount ?? self::MAX_HASH_COUNT;
+        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
-     * @see MIN_HASH_COUNT
-     * @see $minHashCount
-     * @see getMinHashCount()
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
     public function setMinHashCount(int $minHashCount = null)
     {
-        self::$minHashCount = $minHashCount ?? self::MIN_HASH_COUNT;
-    }
-
-    /**
-     * 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
-     */
-    public 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
-     */
-    public function base64Decode(string $value): string
-    {
-        return base64_decode(str_replace('.', '+', $value));
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        // Empty, max hash count is hard coded to 1000
     }
 }
index 6c05b03..00d0240 100644 (file)
@@ -15,6 +15,7 @@ namespace TYPO3\CMS\Saltedpasswords\Salt;
  * 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;
 
@@ -29,74 +30,164 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
  * @see http://drupal.org/node/29706/
  * @see http://www.openwall.com/phpass/
  */
-class PhpassSalt extends AbstractComposedSalt
+class PhpassSalt implements SaltInterface
 {
+    use PublicMethodDeprecationTrait;
+
+    /**
+     * @var array
+     */
+    private $deprecatedPublicMethods = [
+        'isValidSalt' => 'Using PhpassSalt::isValidSalt() is deprecated and will not be possible anymore in TYPO3 v10.',
+        'base64Encode' => 'Using PhpassSalt::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;
 
     /**
-     * Keeps log2 number
-     * of iterations for password stretching.
+     * Constructor sets options if given
      *
-     * @var int
+     * @param array $options
      */
-    protected static $hashCount;
+    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;
+    }
 
     /**
-     * Keeps maximum allowed log2 number
-     * of iterations for password stretching.
+     * Method checks if a given plaintext password is correct by comparing it with
+     * a given salted hashed password.
      *
-     * @var int
+     * @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
      */
-    protected static $maxHashCount;
+    public function checkPassword(string $plainPW, string $saltedHashPW): bool
+    {
+        $hash = $this->cryptPassword($plainPW, $saltedHashPW);
+        return $hash && hash_equals($hash, $saltedHashPW);
+    }
 
     /**
-     * Keeps minimum allowed log2 number
-     * of iterations for password stretching.
+     * Returns whether all prerequisites for the hashing methods are matched
      *
-     * @var int
+     * @return bool Method available
      */
-    protected static $minHashCount;
+    public function isAvailable(): bool
+    {
+        return true;
+    }
 
     /**
-     * Keeps length of a PHPass salt in bytes.
+     * Method creates a salted hash for a given plaintext password
      *
-     * @var int
+     * @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
      */
-    protected static $saltLengthPhpass = 6;
+    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;
+    }
 
     /**
-     * Setting string to indicate type of hashing method (PHPass).
+     * Checks whether a user's hashed password needs to be replaced with a new hash.
      *
-     * @var string
+     * 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
      */
-    protected static $settingPhpass = '$P$';
+    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 applies settings (prefix, hash count) to a salt.
+     * Method determines if a given string is a valid salted hashed password.
      *
-     * Overwrites {@link Md5Salt::applySettingsToSalt()}
-     * with Blowfish specifics.
+     * @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
@@ -104,42 +195,18 @@ class PhpassSalt extends AbstractComposedSalt
     protected function applySettingsToSalt(string $salt): string
     {
         $saltWithSettings = $salt;
-        $reqLenBase64 = $this->getLengthBase64FromBytes($this->getSaltLength());
+        $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 = $this->getSetting() . $itoa64[$this->getHashCount()];
+            $saltWithSettings = self::PREFIX . $itoa64[$this->options['hash_count']];
             $saltWithSettings .= $salt;
         }
         return $saltWithSettings;
     }
 
     /**
-     * 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;
-    }
-
-    /**
      * Hashes a password using a secure stretched hash.
      *
      * By using a salt and repeated hashing the password is "stretched". Its
@@ -154,13 +221,13 @@ class PhpassSalt extends AbstractComposedSalt
     protected function cryptPassword(string $password, string $setting)
     {
         $saltedPW = null;
-        $reqLenBase64 = $this->getLengthBase64FromBytes($this->getSaltLength());
+        $reqLenBase64 = $this->getLengthBase64FromBytes(6);
         // Retrieving settings with salt
-        $setting = substr($setting, 0, strlen($this->getSetting()) + 1 + $reqLenBase64);
+        $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 >= $this->getMinHashCount() && $count_log2 <= $this->getMaxHashCount()) {
-            $salt = substr($setting, strlen($this->getSetting()) + 1, $reqLenBase64);
+        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
@@ -186,7 +253,7 @@ class PhpassSalt extends AbstractComposedSalt
      */
     protected function getCountLog2(string $setting): int
     {
-        return strpos($this->getItoa64(), $setting[strlen($this->getSetting())]);
+        return strpos($this->getItoa64(), $setting[strlen(self::PREFIX)]);
     }
 
     /**
@@ -202,199 +269,192 @@ class PhpassSalt extends AbstractComposedSalt
      */
     protected function getGeneratedSalt(): string
     {
-        $randomBytes = GeneralUtility::makeInstance(Random::class)->generateRandomBytes($this->getSaltLength());
-        return $this->base64Encode($randomBytes, $this->getSaltLength());
+        $randomBytes = GeneralUtility::makeInstance(Random::class)->generateRandomBytes(6);
+        return $this->base64Encode($randomBytes, 6);
     }
 
     /**
-     * Method returns log2 number of iterations for password stretching.
+     * Returns a string for mapping an int to the corresponding base 64 character.
      *
-     * @return int log2 number of iterations for password stretching
-     * @see HASH_COUNT
-     * @see $hashCount
-     * @see setHashCount()
+     * @return string String for mapping an int to the corresponding base 64 character
      */
-    public function getHashCount(): int
+    protected function getItoa64(): string
     {
-        return self::$hashCount ?? self::HASH_COUNT;
+        return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
     }
 
     /**
-     * Method creates a salted hash for a given plaintext password
+     * Method determines if a given string is a valid salt.
      *
-     * @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
+     * @param string $salt String to check
+     * @return bool TRUE if it's valid salt, otherwise FALSE
      */
-    public function getHashedPassword(string $password, string $salt = null)
+    protected function isValidSalt(string $salt): bool
     {
-        $saltedPW = null;
-        if (!empty($password)) {
-            if (empty($salt) || !$this->isValidSalt($salt)) {
-                $salt = $this->getGeneratedSalt();
+        $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;
+                }
             }
-            $saltedPW = $this->cryptPassword($password, $this->applySettingsToSalt($salt));
         }
-        return $saltedPW;
-    }
-
-    /**
-     * 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 self::ITOA64;
+        return $isValid;
     }
 
     /**
-     * Method returns maximum allowed log2 number of iterations for password stretching.
+     * Encodes bytes into printable base 64 using the *nix standard from crypt().
      *
-     * @return int Maximum allowed log2 number of iterations for password stretching
-     * @see MAX_HASH_COUNT
-     * @see $maxHashCount
-     * @see setMaxHashCount()
+     * @param string $input The string containing bytes to encode.
+     * @param int $count The number of characters (bytes) to encode.
+     * @return string Encoded string
      */
-    public function getMaxHashCount(): int
+    protected function base64Encode(string $input, int $count): string
     {
-        return self::$maxHashCount ?? self::MAX_HASH_COUNT;
+        $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 returns minimum allowed log2 number of iterations for password stretching.
+     * Method determines required length of base64 characters for a given
+     * length of a byte string.
      *
-     * @return int Minimum allowed log2 number of iterations for password stretching
-     * @see MIN_HASH_COUNT
-     * @see $minHashCount
-     * @see setMinHashCount()
+     * @param int $byteLength Length of bytes to calculate in base64 chars
+     * @return int Required length of base64 characters
      */
-    public function getMinHashCount(): int
+    protected function getLengthBase64FromBytes(int $byteLength): int
     {
-        return self::$minHashCount ?? self::MIN_HASH_COUNT;
+        // Calculates bytes in bits in base64
+        return (int)ceil($byteLength * 8 / 6);
     }
 
     /**
-     * Returns length of a Blowfish salt in bytes.
+     * Method returns log2 number of iterations for password stretching.
      *
-     * @return int Length of a Blowfish salt in bytes
+     * @return int log2 number of iterations for password stretching
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
-    public function getSaltLength(): int
+    public function getHashCount(): int
     {
-        return self::$saltLengthPhpass;
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return $this->options['hash_count'];
     }
 
     /**
-     * Returns setting string of PHPass salted hashes.
+     * Method returns maximum allowed log2 number of iterations for password stretching.
      *
-     * @return string Setting string of PHPass salted hashes
+     * @return int Maximum allowed log2 number of iterations for password stretching
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
-    public function getSetting(): string
+    public function getMaxHashCount(): int
     {
-        return self::$settingPhpass;
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return 24;
     }
 
     /**
-     * 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.
+     * Method returns minimum allowed log2 number of iterations for password stretching.
      *
-     * @param string $passString Salted hash to check if it needs an update
-     * @return bool TRUE if salted hash needs an update, otherwise FALSE
+     * @return int Minimum allowed log2 number of iterations for password stretching
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
-    public function isHashUpdateNeeded(string $passString): bool
+    public function getMinHashCount(): int
     {
-        // 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->getHashCount();
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return 7;
     }
 
     /**
-     * Method determines if a given string is a valid salt.
+     * Returns length of a Blowfish salt in bytes.
      *
-     * @param string $salt String to check
-     * @return bool TRUE if it's valid salt, otherwise FALSE
+     * @return int Length of a Blowfish salt in bytes
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
-    public function isValidSalt(string $salt): bool
+    public function getSaltLength(): int
     {
-        $isValid = ($skip = false);
-        $reqLenBase64 = $this->getLengthBase64FromBytes($this->getSaltLength());
-        if (strlen($salt) >= $reqLenBase64) {
-            // Salt with prefixed setting
-            if (!strncmp('$', $salt, 1)) {
-                if (!strncmp($this->getSetting(), $salt, strlen($this->getSetting()))) {
-                    $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;
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        return 6;
     }
 
     /**
-     * Method determines if a given string is a valid salted hashed password.
+     * Returns setting string of PHPass salted hashes.
      *
-     * @param string $saltedPW String to check
-     * @return bool TRUE if it's valid salted hashed password, otherwise FALSE
+     * @return string Setting string of PHPass salted hashes
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
-    public function isValidSaltedPW(string $saltedPW): bool
+    public function getSetting(): string
     {
-        $isValid = !strncmp($this->getSetting(), $saltedPW, strlen($this->getSetting()));
-        if ($isValid) {
-            $isValid = $this->isValidSalt($saltedPW);
-        }
-        return $isValid;
+        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
-     * @see HASH_COUNT
-     * @see $hashCount
-     * @see getHashCount()
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
     public function setHashCount(int $hashCount = null)
     {
-        self::$hashCount = $hashCount !== null && $hashCount >= $this->getMinHashCount() && $hashCount <= $this->getMaxHashCount() ? $hashCount : self::HASH_COUNT;
+        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
-     * @see MAX_HASH_COUNT
-     * @see $maxHashCount
-     * @see getMaxHashCount()
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
     public function setMaxHashCount(int $maxHashCount = null)
     {
-        self::$maxHashCount = $maxHashCount ?? self::MAX_HASH_COUNT;
+        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
-     * @see MIN_HASH_COUNT
-     * @see $minHashCount
-     * @see getMinHashCount()
+     * @deprecated and will be removed in TYPO3 v10.0.
      */
     public function setMinHashCount(int $minHashCount = null)
     {
-        self::$minHashCount = $minHashCount ?? self::MIN_HASH_COUNT;
+        trigger_error('This method will be removed in TYPO3 v10.', E_USER_DEPRECATED);
+        // Empty, max hash count is hard coded to 7
     }
 }
index 3a7da98..59051aa 100644 (file)
@@ -38,17 +38,38 @@ class SaltFactory
      * 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 SaltInterface Object that can handle given hash
-     * @throws \LogicException If a registered hash class does not implement SaltInterface
+     * @throws \LogicException
+     * @throws \InvalidArgumentException
      * @throws InvalidSaltException If no class was found that handles given hash
      */
-    public function get(string $hash): SaltInterface
+    public function get(string $hash, string $mode): SaltInterface
     {
-        // @todo: Refactor $registeredHashClasses when implementing 'preset' and moving config options
+        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) {
-            $hashInstance = GeneralUtility::makeInstance($className);
+            if ($className === $defaultHashClassName) {
+                $hashInstance = GeneralUtility::makeInstance($className, $defaultHashOptions);
+            } else {
+                $hashInstance = GeneralUtility::makeInstance($className);
+            }
             if (!$hashInstance instanceof SaltInterface) {
                 throw new \LogicException('Class ' . $className . ' does not implement SaltInterface', 1533818569);
             }
@@ -65,7 +86,8 @@ class SaltFactory
      *
      * @param string $mode 'FE' for frontend users, 'BE' for backend users
      * @return SaltInterface Class instance that is configured as default hash method
-     * @throws \InvalidArgumentException If configured default hash class does not implement SaltInterface
+     * @throws \InvalidArgumentException
+     * @throws \LogicException
      * @throws InvalidSaltException If configuration is broken
      */
     public function getDefaultHashInstance(string $mode): SaltInterface
@@ -73,20 +95,30 @@ class SaltFactory
         if ($mode !== 'FE' && $mode !== 'BE') {
             throw new \InvalidArgumentException('Mode must be either \'FE\' or \'BE\', ' . $mode . ' given.', 1533820041);
         }
-        $defaultHashClassName = SaltedPasswordsUtility::getDefaultSaltingHashingMethod($mode);
 
-        // @todo: Refactor $availableHashClasses when implementing 'preset' and moving config options
+        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 (!isset($availableHashClasses[$defaultHashClassName])) {
+        if (!in_array($defaultHashClassName, $availableHashClasses, true)) {
             throw new InvalidSaltException(
                 'Configured default hash method ' . $defaultHashClassName . ' is not registered',
                 1533820194
             );
         }
-        $hashInstance =  GeneralUtility::makeInstance($defaultHashClassName);
+        $hashInstance =  GeneralUtility::makeInstance($defaultHashClassName, $defaultHashOptions);
         if (!$hashInstance instanceof SaltInterface) {
-            throw new \RuntimeException(
+            throw new \LogicException(
                 'Configured default hash method ' . $defaultHashClassName . ' is not an instance of SaltInterface',
                 1533820281
             );
@@ -105,28 +137,23 @@ class SaltFactory
      * extension configuration to select the default hashing method.
      *
      * @return array
+     * @throws \RuntimeException
      */
     public static function getRegisteredSaltedHashingMethods(): array
     {
-        $saltMethods = [
-            Md5Salt::class => Md5Salt::class,
-            BlowfishSalt::class => BlowfishSalt::class,
-            PhpassSalt::class => PhpassSalt::class,
-            Pbkdf2Salt::class => Pbkdf2Salt::class,
-            BcryptSalt::class => BcryptSalt::class,
-            Argon2iSalt::class => Argon2iSalt::class,
-        ];
+        $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)) {
-                if (isset($configuredMethods[0])) {
-                    // ensure the key of the array is not numeric, but a class name
-                    foreach ($configuredMethods as $method) {
-                        $saltMethods[$method] = $method;
-                    }
-                } else {
-                    $saltMethods = array_merge($saltMethods, $configuredMethods);
-                }
+                $saltMethods = array_merge($saltMethods, $configuredMethods);
             }
         }
         return $saltMethods;
@@ -164,9 +191,7 @@ class SaltFactory
                 }
             } else {
                 $classNameToUse = SaltedPasswordsUtility::getDefaultSaltingHashingMethod($mode);
-                // Calls deprecated determineSaltingHashingMethod - ok, since getSaltingInstance() is deprecated, too
-                $availableClasses = static::getRegisteredSaltedHashingMethods();
-                self::$instance = GeneralUtility::makeInstance($availableClasses[$classNameToUse]);
+                self::$instance = GeneralUtility::makeInstance($classNameToUse);
             }
         }
         return self::$instance;
@@ -190,10 +215,9 @@ class SaltFactory
         );
         $registeredMethods = static::getRegisteredSaltedHashingMethods();
         $defaultClassName = SaltedPasswordsUtility::getDefaultSaltingHashingMethod($mode);
-        $defaultReference = $registeredMethods[$defaultClassName];
         unset($registeredMethods[$defaultClassName]);
         // place the default method first in the order
-        $registeredMethods = [$defaultClassName => $defaultReference] + $registeredMethods;
+        $registeredMethods = [$defaultClassName => $defaultClassName] + $registeredMethods;
         $methodFound = false;
         foreach ($registeredMethods as $method) {
             $objectInstance = GeneralUtility::makeInstance($method);
index a10745a..1628fb4 100644 (file)
@@ -42,10 +42,9 @@ interface SaltInterface
      * 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 to use
      * @return string Salted hashed password
      */
-    public function getHashedPassword(string $password, string $salt = null);
+    public function getHashedPassword(string $password);
 
     /**
      * Checks whether a user's hashed password needs to be replaced with a new hash.
index aea31ae..f0d9f6e 100644 (file)
@@ -19,6 +19,8 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
  * class providing configuration checks for saltedpasswords.
+ *
+ * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
  */
 class ExtensionManagerConfigurationUtility
 {
@@ -28,6 +30,14 @@ class ExtensionManagerConfigurationUtility
     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()
index af86fa0..4557730 100644 (file)
@@ -25,6 +25,8 @@ class SaltedPasswordsUtility
 {
     /**
      * Keeps this extension's key.
+     *
+     * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
      */
     const EXTKEY = 'saltedpasswords';
 
@@ -56,11 +58,14 @@ class SaltedPasswordsUtility
 
     /**
      * 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');
@@ -76,9 +81,11 @@ class SaltedPasswordsUtility
      * 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\Saltedpasswords\Salt\PhpassSalt::class,
         ];
@@ -90,14 +97,17 @@ class SaltedPasswordsUtility
      *
      * @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\Saltedpasswords\Salt\Md5Salt::class;
-        if (array_key_exists(
+        if (in_array(
             $extConf['saltedPWHashingMethod'],
-            \TYPO3\CMS\Saltedpasswords\Salt\SaltFactory::getRegisteredSaltedHashingMethods()
+            \TYPO3\CMS\Saltedpasswords\Salt\SaltFactory::getRegisteredSaltedHashingMethods(),
+            true
         )) {
             $classNameToUse = $extConf['saltedPWHashingMethod'];
         }
index 9debc7c..02ed37f 100644 (file)
@@ -29,61 +29,46 @@ class Argon2iSaltTest extends UnitTestCase
     protected $subject;
 
     /**
-     * Sets up the fixtures for this testcase.
+     * Sets up the subject for this test case.
      */
     protected function setUp()
     {
-        $this->subject = new Argon2iSalt();
-        // Set low values to speed up tests
-        $this->subject->setOptions([
+        $options = [
             'memory_cost' => 1024,
             'time_cost' => 2,
             'threads' => 2,
-        ]);
-    }
-
-    /**
-     * @test
-     */
-    public function getOptionsReturnsPreviouslySetOptions()
-    {
-        $options = [
-            'memory_cost' => 2048,
-            'time_cost' => 4,
-            'threads' => 4,
         ];
-        $this->subject->setOptions($options);
-        $this->assertSame($this->subject->getOptions(), $options);
+        $this->subject = new Argon2iSalt($options);
     }
 
     /**
      * @test
      */
-    public function setOptionsThrowsExceptionWithTooLowMemoryCost()
+    public function constructorThrowsExceptionIfMemoryCostIsTooLow()
     {
         $this->expectException(\InvalidArgumentException::class);
-        $this->expectExceptionCode(1526042080);
-        $this->subject->setOptions(['memory_cost' => 1]);
+        $this->expectExceptionCode(1533899612);
+        new Argon2iSalt(['memory_cost' => 1]);
     }
 
     /**
      * @test
      */
-    public function setOptionsThrowsExceptionWithTooLowTimeCost()
+    public function constructorThrowsExceptionIfTimeCostIsTooLow()
     {
         $this->expectException(\InvalidArgumentException::class);
-        $this->expectExceptionCode(1526042081);
-        $this->subject->setOptions(['time_cost' => 1]);
+        $this->expectExceptionCode(1533899613);
+        new Argon2iSalt(['time_cost' => 1]);
     }
 
     /**
      * @test
      */
-    public function setOptionsThrowsExceptionWithTooLowThreads()
+    public function constructorThrowsExceptionIfThreadsIsTooLow()
     {
         $this->expectException(\InvalidArgumentException::class);
-        $this->expectExceptionCode(1526042082);
-        $this->subject->setOptions(['threads' => 1]);
+        $this->expectExceptionCode(1533899614);
+        new Argon2iSalt(['threads' => 1]);
     }
 
     /**
@@ -210,27 +195,30 @@ class Argon2iSaltTest extends UnitTestCase
      */
     public function isHashUpdateNeededReturnsTrueForHashGeneratedWithOldOptions()
     {
-        $originalOptions = $this->subject->getOptions();
-        $hash = $this->subject->getHashedPassword('password');
+        $originalOptions = [
+            'memory_cost' => 1024,
+            'time_cost' => 2,
+            'threads' => 2,
+        ];
+        $subject = new Argon2iSalt($originalOptions);
+        $hash = $subject->getHashedPassword('password');
 
+        // Change $memoryCost
         $newOptions = $originalOptions;
         $newOptions['memory_cost'] = $newOptions['memory_cost'] + 1;
-        $this->subject->setOptions($newOptions);
-        $this->assertTrue($this->subject->isHashUpdateNeeded($hash));
-        $this->subject->setOptions($originalOptions);
+        $subject = new Argon2iSalt($newOptions);
+        $this->assertTrue($subject->isHashUpdateNeeded($hash));
 
         // Change $timeCost
         $newOptions = $originalOptions;
         $newOptions['time_cost'] = $newOptions['time_cost'] + 1;
-        $this->subject->setOptions($newOptions);
-        $this->assertTrue($this->subject->isHashUpdateNeeded($hash));
-        $this->subject->setOptions($originalOptions);
+        $subject = new Argon2iSalt($newOptions);
+        $this->assertTrue($subject->isHashUpdateNeeded($hash));
 
         // Change $threads
         $newOptions = $originalOptions;
         $newOptions['threads'] = $newOptions['threads'] + 1;
-        $this->subject->setOptions($newOptions);
-        $this->assertTrue($this->subject->isHashUpdateNeeded($hash));
-        $this->subject->setOptions($originalOptions);
+        $subject = new Argon2iSalt($newOptions);
+        $this->assertTrue($subject->isHashUpdateNeeded($hash));
     }
 }
index 3153ea6..5c2b461 100644 (file)
@@ -33,33 +33,31 @@ class BcryptSaltTest extends UnitTestCase
      */
     protected function setUp()
     {
-        $this->subject = new BcryptSalt();
         // Set a low cost to speed up tests
-        $this->subject->setOptions([
+        $options = [
             'cost' => 10,
-        ]);
+        ];
+        $this->subject = new BcryptSalt($options);
     }
 
     /**
      * @test
      */
-    public function getOptionsReturnsPreviouslySetOptions()
+    public function constructorThrowsExceptionIfMemoryCostIsTooLow()
     {
-        $options = [
-            'cost' => 11,
-        ];
-        $this->subject->setOptions($options);
-        $this->assertSame($this->subject->getOptions(), $options);
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1533902002);
+        new BcryptSalt(['cost' => 9]);
     }
 
     /**
      * @test
      */
-    public function setOptionsThrowsExceptionOnTooLowCostValue()
+    public function constructorThrowsExceptionIfMemoryCostIsTooHigh()
     {
         $this->expectException(\InvalidArgumentException::class);
-        $this->expectExceptionCode(1526042084);
-        $this->subject->setOptions(['cost' => 9]);
+        $this->expectExceptionCode(1533902002);
+        new BcryptSalt(['cost' => 32]);
     }
 
     /**
@@ -185,13 +183,10 @@ class BcryptSaltTest extends UnitTestCase
      */
     public function isHashUpdateNeededReturnsTrueForHashGeneratedWithOldOptions()
     {
-        $originalOptions = $this->subject->getOptions();
-        $hash = $this->subject->getHashedPassword('password');
-
-        $newOptions = $originalOptions;
-        $newOptions['cost'] = $newOptions['cost'] + 1;
-        $this->subject->setOptions($newOptions);
-        $this->assertTrue($this->subject->isHashUpdateNeeded($hash));
+        $subject = new BcryptSalt(['cost' => 10]);
+        $hash = $subject->getHashedPassword('password');
+        $subject = new BcryptSalt(['cost' => 11]);
+        $this->assertTrue($subject->isHashUpdateNeeded($hash));
     }
 
     /**
index 779a858..57d104d 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+declare(strict_types = 1);
 namespace TYPO3\CMS\Saltedpasswords\Tests\Unit\Salt;
 
 /*
@@ -14,35 +15,19 @@ namespace TYPO3\CMS\Saltedpasswords\Tests\Unit\Salt;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Core\Crypto\Random;
+use TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 /**
- * Testcase for BlowfishSalt
+ * Test case
  */
-class BlowfishSaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
+class BlowfishSaltTest extends UnitTestCase
 {
     /**
-     * Keeps instance of object to test.
-     *
-     * @var \TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt
-     */
-    protected $objectInstance;
-
-    /**
      * Sets up the fixtures for this testcase.
      */
     protected function setUp()
     {
-        $this->objectInstance = $this->getMockBuilder(\TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt::class)
-            ->setMethods(['dummy'])
-            ->getMock();
-    }
-
-    /**
-     * Marks tests as skipped if the blowfish method is not available.
-     */
-    protected function skipTestIfBlowfishIsNotAvailable()
-    {
         if (!CRYPT_BLOWFISH) {
             $this->markTestSkipped('Blowfish is not supported on your platform.');
         }
@@ -51,82 +36,50 @@ class BlowfishSaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
     /**
      * @test
      */
-    public function hasCorrectBaseClass()
+    public function constructorThrowsExceptionIfHashCountIsTooLow()
     {
-        $hasCorrectBaseClass = get_class($this->objectInstance) === \TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt::class;
-        // XCLASS ?
-        if (!$hasCorrectBaseClass && false != get_parent_class($this->objectInstance)) {
-            $hasCorrectBaseClass = is_subclass_of($this->objectInstance, \TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt::class);
-        }
-        $this->assertTrue($hasCorrectBaseClass);
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1533903545);
+        new BlowfishSalt(['hash_count' => 3]);
     }
 
     /**
      * @test
      */
-    public function nonZeroSaltLength()
+    public function constructorThrowsExceptionIfHashCountIsTooHigh()
     {
-        $this->assertTrue($this->objectInstance->getSaltLength() > 0);
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1533903545);
+        new BlowfishSalt(['hash_count' => 18]);
     }
 
     /**
      * @test
      */
-    public function emptyPasswordResultsInNullSaltedPassword()
+    public function getHashedPasswordWithEmptyPasswordResultsInNullSaltedPassword()
     {
         $password = '';
-        $this->assertNull($this->objectInstance->getHashedPassword($password));
+        $this->assertNull((new BlowfishSalt(['hash_count' => 4]))->getHashedPassword($password));
     }
 
     /**
      * @test
      */
-    public function nonEmptyPasswordResultsInNonNullSaltedPassword()
+    public function getHashedPasswordWithNonEmptyPasswordResultsInNonNullSaltedPassword()
     {
-        $this->skipTestIfBlowfishIsNotAvailable();
         $password = 'a';
-        $this->assertNotNull($this->objectInstance->getHashedPassword($password));
+        $this->assertNotNull((new BlowfishSalt(['hash_count' => 4]))->getHashedPassword($password));
     }
 
     /**
      * @test
      */
-    public function createdSaltedHashOfProperStructure()
+    public function getHashedPasswordValidates()
     {
-        $this->skipTestIfBlowfishIsNotAvailable();
         $password = 'password';
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertTrue($this->objectInstance->isValidSaltedPW($saltedHashPassword));
-    }
-
-    /**
-     * @test
-     */
-    public function createdSaltedHashOfProperStructureForCustomSaltWithoutSetting()
-    {
-        $this->skipTestIfBlowfishIsNotAvailable();
-        $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()
-    {
-        $this->skipTestIfBlowfishIsNotAvailable();
-        $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);
+        $subject = new BlowfishSalt(['hash_count' => 4]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->isValidSaltedPW($saltedHashPassword));
     }
 
     /**
@@ -137,11 +90,11 @@ class BlowfishSaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidAlphaCharClassPasswordAndFixedHash()
+    public function checkPasswordReturnsTrueWithValidAlphaCharClassPasswordAndFixedHash()
     {
         $password = 'password';
         $saltedHashPassword = '$2a$07$Rvtl6CyMhR8GZGhHypjwOuydeN0nKFAlgo1LmmGrLowtIrtkov5Na';
-        $this->assertTrue($this->objectInstance->checkPassword($password, $saltedHashPassword));
+        $this->assertTrue((new BlowfishSalt(['hash_count' => 4]))->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -149,11 +102,11 @@ class BlowfishSaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      *
      * @test
      */
-    public function authenticationFailsWithBrokenHash()
+    public function checkPasswordReturnsFalseFailsWithBrokenHash()
     {
         $password = 'password';
         $saltedHashPassword = '$2a$07$Rvtl6CyMhR8GZGhHypjwOuydeN0nKFAlgo1LmmGrLowtIrtkov5N';
-        $this->assertFalse($this->objectInstance->checkPassword($password, $saltedHashPassword));
+        $this->assertFalse((new BlowfishSalt(['hash_count' => 4]))->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -164,12 +117,12 @@ class BlowfishSaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidAlphaCharClassPassword()
+    public function checkPasswordReturnsTrueWithValidAlphaCharClassPassword()
     {
-        $this->skipTestIfBlowfishIsNotAvailable();
         $password = 'aEjOtY';
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertTrue($this->objectInstance->checkPassword($password, $saltedHashPassword));
+        $subject = new BlowfishSalt(['hash_count' => 4]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -180,12 +133,12 @@ class BlowfishSaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidNumericCharClassPassword()
+    public function checkPasswordReturnsTrueWithValidNumericCharClassPassword()
     {
-        $this->skipTestIfBlowfishIsNotAvailable();
         $password = '01369';
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertTrue($this->objectInstance->checkPassword($password, $saltedHashPassword));
+        $subject = new BlowfishSalt(['hash_count' => 4]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -196,12 +149,12 @@ class BlowfishSaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidAsciiSpecialCharClassPassword()
+    public function checkPasswordReturnsTrueWithValidAsciiSpecialCharClassPassword()
     {
-        $this->skipTestIfBlowfishIsNotAvailable();
         $password = ' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~';
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertTrue($this->objectInstance->checkPassword($password, $saltedHashPassword));
+        $subject = new BlowfishSalt(['hash_count' => 4]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -212,16 +165,16 @@ class BlowfishSaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidLatin1SpecialCharClassPassword()
+    public function checkPasswordReturnsTrueWithValidLatin1SpecialCharClassPassword()
     {
-        $this->skipTestIfBlowfishIsNotAvailable();
         $password = '';
         for ($i = 160; $i <= 191; $i++) {
             $password .= chr($i);
         }
         $password .= chr(215) . chr(247);
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertTrue($this->objectInstance->checkPassword($password, $saltedHashPassword));
+        $subject = new BlowfishSalt(['hash_count' => 4]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -232,9 +185,8 @@ class BlowfishSaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidLatin1UmlautCharClassPassword()
+    public function checkPasswordReturnsReturnsTrueWithValidLatin1UmlautCharClassPassword()
     {
-        $this->skipTestIfBlowfishIsNotAvailable();
         $password = '';
         for ($i = 192; $i <= 214; $i++) {
             $password .= chr($i);
@@ -245,123 +197,42 @@ class BlowfishSaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
         for ($i = 248; $i <= 255; $i++) {
             $password .= chr($i);
         }
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertTrue($this->objectInstance->checkPassword($password, $saltedHashPassword));
+        $subject = new BlowfishSalt(['hash_count' => 4]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
      * @test
      */
-    public function authenticationWithNonValidPassword()
+    public function checkPasswordReturnsFalseWithNonValidPassword()
     {
-        $this->skipTestIfBlowfishIsNotAvailable();
         $password = 'password';
         $password1 = $password . 'INVALID';
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertFalse($this->objectInstance->checkPassword($password1, $saltedHashPassword));
+        $subject = new BlowfishSalt(['hash_count' => 4]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertFalse($subject->checkPassword($password1, $saltedHashPassword));
     }
 
     /**
      * @test
      */
-    public function passwordVariationsResultInDifferentHashes()
-    {
-        $this->skipTestIfBlowfishIsNotAvailable();
-        $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 modifiedMinHashCount()
-    {
-        $minHashCount = $this->objectInstance->getMinHashCount();
-        $this->objectInstance->setMinHashCount($minHashCount - 1);
-        $this->assertTrue($this->objectInstance->getMinHashCount() < $minHashCount);
-        $this->objectInstance->setMinHashCount($minHashCount + 1);
-        $this->assertTrue($this->objectInstance->getMinHashCount() > $minHashCount);
-    }
-
-    /**
-     * @test
-     */
-    public function modifiedMaxHashCount()
-    {
-        $maxHashCount = $this->objectInstance->getMaxHashCount();
-        $this->objectInstance->setMaxHashCount($maxHashCount + 1);
-        $this->assertTrue($this->objectInstance->getMaxHashCount() > $maxHashCount);
-        $this->objectInstance->setMaxHashCount($maxHashCount - 1);
-        $this->assertTrue($this->objectInstance->getMaxHashCount() < $maxHashCount);
-    }
-
-    /**
-     * @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 updateNecessityForValidSaltedPassword()
-    {
-        $this->skipTestIfBlowfishIsNotAvailable();
-        $password = 'password';
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertFalse($this->objectInstance->isHashUpdateNeeded($saltedHashPassword));
-    }
-
-    /**
-     * @test
-     */
-    public function updateNecessityForIncreasedHashcount()
+    public function isHashUpdateNeededReturnsFalseForValidSaltedPassword()
     {
         $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);
+        $subject = new BlowfishSalt(['hash_count' => 4]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertFalse($subject->isHashUpdateNeeded($saltedHashPassword));
     }
 
     /**
      * @test
      */
-    public function updateNecessityForDecreasedHashcount()
+    public function isHashUpdateNeededReturnsTrueForHashGeneratedWithOldOptions()
     {
-        $this->skipTestIfBlowfishIsNotAvailable();
-        $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);
+        $subject = new BlowfishSalt(['hash_count' => 4]);
+        $hash = $subject->getHashedPassword('password');
+        $subject = new BlowfishSalt(['hash_count' => 5]);
+        $this->assertTrue($subject->isHashUpdateNeeded($hash));
     }
 }
diff --git a/typo3/sysext/saltedpasswords/Tests/Unit/Salt/Fixtures/TestSalt.php b/typo3/sysext/saltedpasswords/Tests/Unit/Salt/Fixtures/TestSalt.php
new file mode 100644 (file)
index 0000000..ec7845f
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Saltedpasswords\Tests\Unit\Salt\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 wits options
+ */
+class TestSalt
+{
+    /**
+     * TestSalt constructor.
+     *
+     * @param array $options
+     */
+    public function __construct(array $options = [])
+    {
+        if ($options === [ 'foo' => 'bar' ]) {
+            throw new \RuntimeException('This should be thrown', 1533950385);
+        }
+    }
+}
index f47d330..8e37565 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+declare(strict_types = 1);
 namespace TYPO3\CMS\Saltedpasswords\Tests\Unit\Salt;
 
 /*
@@ -14,105 +15,49 @@ namespace TYPO3\CMS\Saltedpasswords\Tests\Unit\Salt;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Core\Crypto\Random;
+use TYPO3\CMS\Saltedpasswords\Salt\Md5Salt;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 /**
- * Testcases for Md5Salt
+ * Test case
  */
-class Md5SaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
+class Md5SaltTest extends UnitTestCase
 {
     /**
-     * Keeps instance of object to test.
-     *
-     * @var \TYPO3\CMS\Saltedpasswords\Salt\Md5Salt
-     */
-    protected $objectInstance;
-
-    /**
      * Sets up the fixtures for this testcase.
      */
     protected function setUp()
     {
-        $this->objectInstance = $this->getMockBuilder(\TYPO3\CMS\Saltedpasswords\Salt\Md5Salt::class)
-            ->setMethods(['dummy'])
-            ->getMock();
-    }
-
-    /**
-     * Prepares a message to be shown when a salted hashing is not supported.
-     *
-     * @return string Empty string if salted hashing method is available, otherwise an according warning
-     */
-    protected function getWarningWhenMethodUnavailable()
-    {
-        $warningMsg = '';
         if (!CRYPT_MD5) {
-            $warningMsg = 'MD5 is not supported on your platform. ' . 'Then, some of the md5 tests will fail.';
+            $this->markTestSkipped('Blowfish is not supported on your platform.');
         }
-        return $warningMsg;
-    }
-
-    /**
-     * @test
-     */
-    public function hasCorrectBaseClass()
-    {
-        $hasCorrectBaseClass = get_class($this->objectInstance) === \TYPO3\CMS\Saltedpasswords\Salt\Md5Salt::class;
-        // XCLASS ?
-        if (!$hasCorrectBaseClass && false != get_parent_class($this->objectInstance)) {
-            $hasCorrectBaseClass = is_subclass_of($this->objectInstance, \TYPO3\CMS\Saltedpasswords\Salt\Md5Salt::class);
-        }
-        $this->assertTrue($hasCorrectBaseClass);
-    }
-
-    /**
-     * @test
-     */
-    public function nonZeroSaltLength()
-    {
-        $this->assertTrue($this->objectInstance->getSaltLength() > 0);
     }
 
     /**
      * @test
      */
-    public function emptyPasswordResultsInNullSaltedPassword()
+    public function getHashedPasswordReturnsNullWithEmptyPassword()
     {
-        $password = '';
-        $this->assertNull($this->objectInstance->getHashedPassword($password));
+        $this->assertNull((new Md5Salt())->getHashedPassword(''));
     }
 
     /**
      * @test
      */
-    public function nonEmptyPasswordResultsInNonNullSaltedPassword()
+    public function getHashedPasswordReturnsNotNullWithNonEmptyPassword()
     {
-        $password = 'a';
-        $this->assertNotNull($this->objectInstance->getHashedPassword($password), $this->getWarningWhenMethodUnavailable());
+        $this->assertNotNull((new Md5Salt())->getHashedPassword('a'));
     }
 
     /**
      * @test
      */
-    public function createdSaltedHashOfProperStructure()
+    public function getHashedPasswordCreatesAHashThatValidates()
     {
         $password = 'password';
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertTrue($this->objectInstance->isValidSaltedPW($saltedHashPassword), $this->getWarningWhenMethodUnavailable());
-    }
-
-    /**
-     * @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), $this->getWarningWhenMethodUnavailable());
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password, $salt);
-        $this->assertTrue($this->objectInstance->isValidSaltedPW($saltedHashPassword), $this->getWarningWhenMethodUnavailable());
+        $subject = new Md5Salt();
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->isValidSaltedPW($saltedHashPassword));
     }
 
     /**
@@ -123,11 +68,11 @@ class Md5SaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidAlphaCharClassPasswordAndFixedHash()
+    public function checkPasswordReturnsTrueWithValidAlphaCharClassPasswordAndFixedHash()
     {
         $password = 'password';
         $saltedHashPassword = '$1$GNu9HdMt$RwkPb28pce4nXZfnplVZY/';
-        $this->assertTrue($this->objectInstance->checkPassword($password, $saltedHashPassword), $this->getWarningWhenMethodUnavailable());
+        $this->assertTrue((new Md5Salt())->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -135,11 +80,11 @@ class Md5SaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      *
      * @test
      */
-    public function authenticationFailsWithBrokenHash()
+    public function checkPasswordReturnsFalseWithBrokenHash()
     {
         $password = 'password';
         $saltedHashPassword = '$1$GNu9HdMt$RwkPb28pce4nXZfnplVZY';
-        $this->assertFalse($this->objectInstance->checkPassword($password, $saltedHashPassword), $this->getWarningWhenMethodUnavailable());
+        $this->assertFalse((new Md5Salt())->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -150,11 +95,12 @@ class Md5SaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidAlphaCharClassPassword()
+    public function checkPasswordReturnsTrueWithValidAlphaCharClassPassword()
     {
         $password = 'aEjOtY';
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertTrue($this->objectInstance->checkPassword($password, $saltedHashPassword), $this->getWarningWhenMethodUnavailable());
+        $subject = new Md5Salt();
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -165,11 +111,12 @@ class Md5SaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidNumericCharClassPassword()
+    public function checkPasswordReturnsTrueWithValidNumericCharClassPassword()
     {
         $password = '01369';
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertTrue($this->objectInstance->checkPassword($password, $saltedHashPassword), $this->getWarningWhenMethodUnavailable());
+        $subject = new Md5Salt();
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -180,11 +127,12 @@ class Md5SaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidAsciiSpecialCharClassPassword()
+    public function checkPasswordReturnsTrueWithValidAsciiSpecialCharClassPassword()
     {
         $password = ' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~';
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertTrue($this->objectInstance->checkPassword($password, $saltedHashPassword), $this->getWarningWhenMethodUnavailable());
+        $subject = new Md5Salt();
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -195,15 +143,16 @@ class Md5SaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidLatin1SpecialCharClassPassword()
+    public function checkPasswordReturnsTrueWithValidLatin1SpecialCharClassPassword()
     {
         $password = '';
         for ($i = 160; $i <= 191; $i++) {
             $password .= chr($i);
         }
         $password .= chr(215) . chr(247);
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertTrue($this->objectInstance->checkPassword($password, $saltedHashPassword), $this->getWarningWhenMethodUnavailable());
+        $subject = new Md5Salt();
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -214,7 +163,7 @@ class Md5SaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidLatin1UmlautCharClassPassword()
+    public function checkPasswordReturnsTrueWithValidLatin1UmlautCharClassPassword()
     {
         $password = '';
         for ($i = 192; $i <= 214; $i++) {
@@ -226,49 +175,31 @@ class Md5SaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
         for ($i = 248; $i <= 255; $i++) {
             $password .= chr($i);
         }
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertTrue($this->objectInstance->checkPassword($password, $saltedHashPassword), $this->getWarningWhenMethodUnavailable());
+        $subject = new Md5Salt();
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
      * @test
      */
-    public function authenticationWithNonValidPassword()
+    public function checkPasswordReturnsFalseWithNonValidPassword()
     {
         $password = 'password';
         $password1 = $password . 'INVALID';
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertFalse($this->objectInstance->checkPassword($password1, $saltedHashPassword), $this->getWarningWhenMethodUnavailable());
-    }
-
-    /**
-     * @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, $this->getWarningWhenMethodUnavailable() . 'Duplicates of hashed passwords with plaintext password of length ' . $criticalPwLength . '+.');
+        $subject = new Md5Salt();
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertFalse($subject->checkPassword($password1, $saltedHashPassword));
     }
 
     /**
      * @test
      */
-    public function noUpdateNecessityForMd5()
+    public function isHashUpdateNeededReturnsFalse()
     {
         $password = 'password';
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertFalse($this->objectInstance->isHashUpdateNeeded($saltedHashPassword));
+        $subject = new Md5Salt();
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertFalse($subject->isHashUpdateNeeded($saltedHashPassword));
     }
 }
index 72e3f2f..d8482e4 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+declare(strict_types = 1);
 namespace TYPO3\CMS\Saltedpasswords\Tests\Unit\Salt;
 
 /*
@@ -14,7 +15,6 @@ namespace TYPO3\CMS\Saltedpasswords\Tests\Unit\Salt;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Core\Crypto\Random;
 use TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
@@ -24,86 +24,54 @@ use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 class Pbkdf2SaltTest extends UnitTestCase
 {
     /**
-     * Keeps instance of object to test.
-     *
-     * @var Pbkdf2Salt
-     */
-    protected $subject;
-
-    /**
-     * Sets up the fixtures for this testcase.
+     * @test
      */
-    protected function setUp()
+    public function constructorThrowsExceptionIfHashCountIsTooLow()
     {
-        $this->subject = new Pbkdf2Salt();
-        // Speed up the tests by reducing the iteration count
-        $this->subject->setHashCount(1000);
-        $this->subject->setMinHashCount(1000);
-        $this->subject->setMaxHashCount(10000000);
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1533903544);
+        new Pbkdf2Salt(['hash_count' => 999]);
     }
 
     /**
      * @test
      */
-    public function nonZeroSaltLength()
+    public function constructorThrowsExceptionIfHashCountIsTooHigh()
     {
-        $this->assertTrue($this->subject->getSaltLength() > 0);
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1533903544);
+        new Pbkdf2Salt(['hash_count' => 10000001]);
     }
 
     /**
      * @test
      */
-    public function emptyPasswordResultsInNullSaltedPassword()
+    public function getHashedPasswordReturnsNullWithEmptyPassword()
     {
         $password = '';
-        $this->assertNull($this->subject->getHashedPassword($password));
+        $subject = new Pbkdf2Salt(['hash_count' => 1000]);
+        $this->assertNull($subject->getHashedPassword($password));
     }
 
     /**
      * @test
      */
-    public function nonEmptyPasswordResultsInNonNullSaltedPassword()
+    public function getHashedPasswordReturnsNotNullWithNullPassword()
     {
         $password = 'a';
-        $this->assertNotNull($this->subject->getHashedPassword($password));
-    }
-
-    /**
-     * @test
-     */
-    public function createdSaltedHashOfProperStructure()
-    {
-        $password = 'password';
-        $saltedHashPassword = $this->subject->getHashedPassword($password);
-        $this->assertTrue($this->subject->isValidSaltedPW($saltedHashPassword));
+        $subject = new Pbkdf2Salt(['hash_count' => 1000]);
+        $this->assertNotNull($subject->getHashedPassword($password));
     }
 
     /**
      * @test
      */
-    public function createdSaltedHashOfProperStructureForCustomSaltWithoutSetting()
+    public function getHashedPasswordValidates()
     {
         $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, $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);
+        $subject = new Pbkdf2Salt(['hash_count' => 1000]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->isValidSaltedPW($saltedHashPassword));
     }
 
     /**
@@ -114,11 +82,12 @@ class Pbkdf2SaltTest extends UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidAlphaCharClassPasswordAndFixedHash()
+    public function checkPasswordReturnsTrueWithValidAlphaCharClassPasswordAndFixedHash()
     {
         $password = 'password';
         $saltedHashPassword = '$pbkdf2-sha256$1000$woPhT0yoWm3AXJXSjuxJ3w$iZ6EvTulMqXlzr0NO8z5EyrklFcJk5Uw2Fqje68FfaQ';
-        $this->assertTrue($this->subject->checkPassword($password, $saltedHashPassword));
+        $subject = new Pbkdf2Salt(['hash_count' => 1000]);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -126,11 +95,12 @@ class Pbkdf2SaltTest extends UnitTestCase
      *
      * @test
      */
-    public function authenticationFailsWithBrokenHash()
+    public function checkPasswordReturnsFalseWithBrokenHash()
     {
         $password = 'password';
         $saltedHashPassword = '$pbkdf2-sha256$1000$woPhT0yoWm3AXJXSjuxJ3w$iZ6EvTulMqXlzr0NO8z5EyrklFcJk5Uw2Fqje68Ffa';
-        $this->assertFalse($this->subject->checkPassword($password, $saltedHashPassword));
+        $subject = new Pbkdf2Salt(['hash_count' => 1000]);
+        $this->assertFalse($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -141,11 +111,12 @@ class Pbkdf2SaltTest extends UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidAlphaCharClassPassword()
+    public function checkPasswordReturnsTrueWithValidAlphaCharClassPassword()
     {
         $password = 'aEjOtY';
-        $saltedHashPassword = $this->subject->getHashedPassword($password);
-        $this->assertTrue($this->subject->checkPassword($password, $saltedHashPassword));
+        $subject = new Pbkdf2Salt(['hash_count' => 1000]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -156,11 +127,12 @@ class Pbkdf2SaltTest extends UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidNumericCharClassPassword()
+    public function checkPasswordReturnsTrueWithValidNumericCharClassPassword()
     {
         $password = '01369';
-        $saltedHashPassword = $this->subject->getHashedPassword($password);
-        $this->assertTrue($this->subject->checkPassword($password, $saltedHashPassword));
+        $subject = new Pbkdf2Salt(['hash_count' => 1000]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -171,11 +143,12 @@ class Pbkdf2SaltTest extends UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidAsciiSpecialCharClassPassword()
+    public function checkPasswordReturnsTrueWithValidAsciiSpecialCharClassPassword()
     {
         $password = ' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~';
-        $saltedHashPassword = $this->subject->getHashedPassword($password);
-        $this->assertTrue($this->subject->checkPassword($password, $saltedHashPassword));
+        $subject = new Pbkdf2Salt(['hash_count' => 1000]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -186,15 +159,16 @@ class Pbkdf2SaltTest extends UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidLatin1SpecialCharClassPassword()
+    public function checkPasswordReturnsTrueWithValidLatin1SpecialCharClassPassword()
     {
         $password = '';
         for ($i = 160; $i <= 191; $i++) {
             $password .= chr($i);
         }
         $password .= chr(215) . chr(247);
-        $saltedHashPassword = $this->subject->getHashedPassword($password);
-        $this->assertTrue($this->subject->checkPassword($password, $saltedHashPassword));
+        $subject = new Pbkdf2Salt(['hash_count' => 1000]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -205,7 +179,7 @@ class Pbkdf2SaltTest extends UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidLatin1UmlautCharClassPassword()
+    public function checkPasswordReturnsTrueWithValidLatin1UmlautCharClassPassword()
     {
         $password = '';
         for ($i = 192; $i <= 214; $i++) {
@@ -217,130 +191,52 @@ class Pbkdf2SaltTest extends UnitTestCase
         for ($i = 248; $i <= 255; $i++) {
             $password .= chr($i);
         }
-        $saltedHashPassword = $this->subject->getHashedPassword($password);
-        $this->assertTrue($this->subject->checkPassword($password, $saltedHashPassword));
+        $subject = new Pbkdf2Salt(['hash_count' => 1000]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
      * @test
      */
-    public function authenticationWithNonValidPassword()
+    public function checkPasswordReturnsFalseWithNonValidPassword()
     {
         $password = 'password';
         $password1 = $password . 'INVALID';
-        $saltedHashPassword = $this->subject->getHashedPassword($password);
-        $this->assertFalse($this->subject->checkPassword($password1, $saltedHashPassword));
+        $subject = new Pbkdf2Salt(['hash_count' => 1000]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertFalse($subject->checkPassword($password1, $saltedHashPassword));
     }
 
     /**
      * @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 modifiedMinHashCount()
-    {
-        $minHashCount = $this->subject->getMinHashCount();
-        $this->subject->setMinHashCount($minHashCount - 1);
-        $this->assertTrue($this->subject->getMinHashCount() < $minHashCount);
-        $this->subject->setMinHashCount($minHashCount + 1);
-        $this->assertTrue($this->subject->getMinHashCount() > $minHashCount);
-    }
-
-    /**
-     * @test
-     */
-    public function modifiedMaxHashCount()
-    {
-        $maxHashCount = $this->subject->getMaxHashCount();
-        $this->subject->setMaxHashCount($maxHashCount + 1);
-        $this->assertTrue($this->subject->getMaxHashCount() > $maxHashCount);
-        $this->subject->setMaxHashCount($maxHashCount - 1);
-        $this->assertTrue($this->subject->getMaxHashCount() < $maxHashCount);
-    }
-
-    /**
-     * @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);
-        // reset hashcount
-        $this->subject->setHashCount(null);
-    }
-
-    /**
-     * @test
-     */
-    public function updateNecessityForValidSaltedPassword()
+    public function isHashUpdateNeededReturnsFalseForValidSaltedPassword()
     {
         $password = 'password';
-        $saltedHashPassword = $this->subject->getHashedPassword($password);
-        $this->assertFalse($this->subject->isHashUpdateNeeded($saltedHashPassword));
+        $subject = new Pbkdf2Salt(['hash_count' => 1000]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertFalse($subject->isHashUpdateNeeded($saltedHashPassword));
     }
 
     /**
      * @test
      */
-    public function updateNecessityForIncreasedHashcount()
+    public function isHashUpdateNeededReturnsTrueWithChangedHashCount()
     {
-        $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));
-        // reset hashcount
-        $this->subject->setHashCount(null);
+        $subject = new Pbkdf2Salt(['hash_count' => 1000]);
+        $saltedHashPassword = $subject->getHashedPassword('password');
+        $subject = new Pbkdf2Salt(['hash_count' => 1001]);
+        $this->assertTrue($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));
-        // reset hashcount
-        $this->subject->setHashCount(null);
-    }
-
-    /**
-     * @test
-     */
-    public function isCompatibleWithPythonPasslibHashes()
+    public function checkPasswordIsCompatibleWithPythonPasslibHashes()
     {
         $passlibSaltedHash= '$pbkdf2-sha256$6400$.6UI/S.nXIk8jcbdHx3Fhg$98jZicV16ODfEsEZeYPGHU3kbrUrvUEXOPimVSQDD44';
-        $saltedHashPassword = $this->subject->getHashedPassword('password', $passlibSaltedHash);
-
-        $this->assertSame($passlibSaltedHash, $saltedHashPassword);
+        $subject = new Pbkdf2Salt(['hash_count' => 1000]);
+        $this->assertTrue($subject->checkPassword('password', $passlibSaltedHash));
     }
 }
index 9887c0a..f963434 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+declare(strict_types = 1);
 namespace TYPO3\CMS\Saltedpasswords\Tests\Unit\Salt;
 
 /*
@@ -14,105 +15,61 @@ namespace TYPO3\CMS\Saltedpasswords\Tests\Unit\Salt;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Core\Crypto\Random;
+use TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 /**
- * Testcase for PhpassSalt
+ * Test case
  */
-class PhpassSaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
+class PhpassSaltTest extends UnitTestCase
 {
     /**
-     * Keeps instance of object to test.
-     *
-     * @var \TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt
-     */
-    protected $objectInstance;
-
-    /**
-     * Sets up the fixtures for this testcase.
-     */
-    protected function setUp()
-    {
-        $this->objectInstance = $this->getMockBuilder(\TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt::class)
-            ->setMethods(['dummy'])
-            ->getMock();
-    }
-
-    /**
      * @test
      */
-    public function hasCorrectBaseClass()
+    public function constructorThrowsExceptionIfHashCountIsTooLow()
     {
-        $hasCorrectBaseClass = get_class($this->objectInstance) === \TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt::class;
-        // XCLASS ?
-        if (!$hasCorrectBaseClass && false != get_parent_class($this->objectInstance)) {
-            $hasCorrectBaseClass = is_subclass_of($this->objectInstance, \TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt::class);
-        }
-        $this->assertTrue($hasCorrectBaseClass);
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1533940454);
+        new PhpassSalt(['hash_count' => 6]);
     }
 
     /**
      * @test
      */
-    public function nonZeroSaltLength()
+    public function constructorThrowsExceptionIfHashCountIsTooHigh()
     {
-        $this->assertTrue($this->objectInstance->getSaltLength() > 0);
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1533940454);
+        new PhpassSalt(['hash_count' => 25]);
     }
 
     /**
      * @test
      */
-    public function emptyPasswordResultsInNullSaltedPassword()
+    public function getHashedPasswordReturnsNullWithEmptyPassword()
     {
-        $password = '';
-        $this->assertNull($this->objectInstance->getHashedPassword($password));
+        $subject = new PhpassSalt(['hash_count' => 7]);
+        $this->assertNull($subject->getHashedPassword(''));
     }
 
     /**
      * @test
      */
-    public function nonEmptyPasswordResultsInNonNullSaltedPassword()
+    public function getHashedPasswordReturnsNotNullWithNotEmptyPassword()
     {
-        $password = 'a';
-        $this->assertNotNull($this->objectInstance->getHashedPassword($password));
+        $subject = new PhpassSalt(['hash_count' => 7]);
+        $this->assertNotNull($subject->getHashedPassword('a'));
     }
 
     /**
      * @test
      */
-    public function createdSaltedHashOfProperStructure()
+    public function getHashedPasswordValidates()
     {
         $password = 'password';
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertTrue($this->objectInstance->isValidSaltedPW($saltedHashPassword));
-    }
-
-    /**
-     * @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);
+        $subject = new PhpassSalt(['hash_count' => 7]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->isValidSaltedPW($saltedHashPassword));
     }
 
     /**
@@ -123,11 +80,12 @@ class PhpassSaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidAlphaCharClassPasswordAndFixedHash()
+    public function checkPasswordReturnsTrueWithValidAlphaCharClassPasswordAndFixedHash()
     {
         $password = 'password';
         $saltedHashPassword = '$P$C7u7E10SBEie/Jbdz0jDtUcWhzgOPF.';
-        $this->assertTrue($this->objectInstance->checkPassword($password, $saltedHashPassword));
+        $subject = new PhpassSalt(['hash_count' => 7]);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -135,11 +93,12 @@ class PhpassSaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      *
      * @test
      */
-    public function authenticationFailsWithBrokenHash()
+    public function checkPasswordReturnsFalseWithBrokenHash()
     {
         $password = 'password';
         $saltedHashPassword = '$P$C7u7E10SBEie/Jbdz0jDtUcWhzgOPF';
-        $this->assertFalse($this->objectInstance->checkPassword($password, $saltedHashPassword));
+        $subject = new PhpassSalt(['hash_count' => 7]);
+        $this->assertFalse($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -150,11 +109,12 @@ class PhpassSaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidAlphaCharClassPassword()
+    public function checkPasswordReturnsTrueWithValidAlphaCharClassPassword()
     {
         $password = 'aEjOtY';
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertTrue($this->objectInstance->checkPassword($password, $saltedHashPassword));
+        $subject = new PhpassSalt(['hash_count' => 7]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -165,11 +125,12 @@ class PhpassSaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidNumericCharClassPassword()
+    public function checkPasswordReturnsTrueWithValidNumericCharClassPassword()
     {
         $password = '01369';
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertTrue($this->objectInstance->checkPassword($password, $saltedHashPassword));
+        $subject = new PhpassSalt(['hash_count' => 7]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -180,11 +141,12 @@ class PhpassSaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidAsciiSpecialCharClassPassword()
+    public function checkPasswordReturnsTrueWithValidAsciiSpecialCharClassPassword()
     {
         $password = ' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~';
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertTrue($this->objectInstance->checkPassword($password, $saltedHashPassword));
+        $subject = new PhpassSalt(['hash_count' => 7]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -195,15 +157,16 @@ class PhpassSaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidLatin1SpecialCharClassPassword()
+    public function checkPasswordReturnsTrueWithValidLatin1SpecialCharClassPassword()
     {
         $password = '';
         for ($i = 160; $i <= 191; $i++) {
             $password .= chr($i);
         }
         $password .= chr(215) . chr(247);
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertTrue($this->objectInstance->checkPassword($password, $saltedHashPassword));
+        $subject = new PhpassSalt(['hash_count' => 7]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
@@ -214,7 +177,7 @@ class PhpassSaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
      *
      * @test
      */
-    public function authenticationWithValidLatin1UmlautCharClassPassword()
+    public function checkPasswordReturnsTrueWithValidLatin1UmlautCharClassPassword()
     {
         $password = '';
         for ($i = 192; $i <= 214; $i++) {
@@ -226,119 +189,43 @@ class PhpassSaltTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
         for ($i = 248; $i <= 255; $i++) {
             $password .= chr($i);
         }
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertTrue($this->objectInstance->checkPassword($password, $saltedHashPassword));
+        $subject = new PhpassSalt(['hash_count' => 7]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertTrue($subject->checkPassword($password, $saltedHashPassword));
     }
 
     /**
      * @test
      */
-    public function authenticationWithNonValidPassword()
+    public function checkPasswordReturnsFalseWithNonValidPassword()
     {
         $password = 'password';
         $password1 = $password . 'INVALID';
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertFalse($this->objectInstance->checkPassword($password1, $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 . '+.');
-    }
-
-    /**
-     * @test
-     */
-    public function modifiedMinHashCount()
-    {
-        $minHashCount = $this->objectInstance->getMinHashCount();
-        $this->objectInstance->setMinHashCount($minHashCount - 1);
-        $this->assertTrue($this->objectInstance->getMinHashCount() < $minHashCount);
-        $this->objectInstance->setMinHashCount($minHashCount + 1);
-        $this->assertTrue($this->objectInstance->getMinHashCount() > $minHashCount);
-    }
-
-    /**
-     * @test
-     */
-    public function modifiedMaxHashCount()
-    {
-        $maxHashCount = $this->objectInstance->getMaxHashCount();
-        $this->objectInstance->setMaxHashCount($maxHashCount + 1);
-        $this->assertTrue($this->objectInstance->getMaxHashCount() > $maxHashCount);
-        $this->objectInstance->setMaxHashCount($maxHashCount - 1);
-        $this->assertTrue($this->objectInstance->getMaxHashCount() < $maxHashCount);
-    }
-
-    /**
-     * @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 updateNecessityForValidSaltedPassword()
-    {
-        $password = 'password';
-        $saltedHashPassword = $this->objectInstance->getHashedPassword($password);
-        $this->assertFalse($this->objectInstance->isHashUpdateNeeded($saltedHashPassword));
+        $subject = new PhpassSalt(['hash_count' => 7]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertFalse($subject->checkPassword($password1, $saltedHashPassword));
     }
 
     /**
      * @test
      */
-    public function updateNecessityForIncreasedHashcount()
+    public function isHashUpdateNeededReturnsFalseForValidSaltedPassword()
     {
         $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);
+        $subject = new PhpassSalt(['hash_count' => 7]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $this->assertFalse($subject->isHashUpdateNeeded($saltedHashPassword));
     }
 
     /**
      * @test
      */
-    public function updateNecessityForDecreasedHashcount()
+    public function isHashUpdateNeededReturnsFalseForChangedHashCountSaltedPassword()
     {
         $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);
+        $subject = new PhpassSalt(['hash_count' => 7]);
+        $saltedHashPassword = $subject->getHashedPassword($password);
+        $subject = new PhpassSalt(['hash_count' => 8]);
+        $this->assertTrue($subject->isHashUpdateNeeded($saltedHashPassword));
     }
 }
index 3164cc8..ea1090e 100644 (file)
@@ -20,6 +20,7 @@ use TYPO3\CMS\Saltedpasswords\Exception\InvalidSaltException;
 use TYPO3\CMS\Saltedpasswords\Salt\Argon2iSalt;
 use TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt;
 use TYPO3\CMS\Saltedpasswords\Salt\SaltFactory;
+use TYPO3\CMS\Saltedpasswords\Tests\Unit\Salt\Fixtures\TestSalt;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 /**
@@ -30,12 +31,44 @@ class SaltFactoryTest extends UnitTestCase
     /**
      * @test
      */
+    public function getThrowsExceptionIfModeIsNotBeOrFe(): void
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1533948312);
+        (new SaltFactory())->get('ThisIsNotAValidHash', 'foo');
+    }
+
+    /**
+     * @test
+     */
+    public function getThrowsExceptionWithBrokenClassNameModeConfiguration(): void
+    {
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionCode(1533949053);
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing']['className'] = '';
+        (new SaltFactory())->get('ThisIsNotAValidHash', 'FE');
+    }
+
+    /**
+     * @test
+     */
+    public function getThrowsExceptionWithBrokenOptionsModeConfiguration(): void
+    {
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionCode(1533949053);
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing']['options'] = '';
+        (new SaltFactory())->get('ThisIsNotAValidHash', 'FE');
+    }
+
+    /**
+     * @test
+     */
     public function getThrowsExceptionIfARegisteredHashDoesNotImplementSaltInterface(): void
     {
-        $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/saltedpasswords']['saltMethods'] = [ \stdClass::class ];
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['availablePasswordHashAlgorithms'] = [ \stdClass::class ];
         $this->expectException(\LogicException::class);
         $this->expectExceptionCode(1533818569);
-        (new SaltFactory())->get('ThisIsNotAValidHash');
+        (new SaltFactory())->get('ThisIsNotAValidHash', 'BE');
     }
 
     /**
@@ -45,7 +78,7 @@ class SaltFactoryTest extends UnitTestCase
     {
         $this->expectException(InvalidSaltException::class);
         $this->expectExceptionCode(1533818591);
-        (new SaltFactory())->get('ThisIsNotAValidHash');
+        (new SaltFactory())->get('ThisIsNotAValidHash', 'BE');
     }
 
     /**
@@ -58,7 +91,7 @@ class SaltFactoryTest extends UnitTestCase
         $phpassProphecy->isAvailable()->shouldBeCalled()->willReturn(false);
         $this->expectException(InvalidSaltException::class);
         $this->expectExceptionCode(1533818591);
-        (new SaltFactory())->get('$P$C7u7E10SBEie/Jbdz0jDtUcWhzgOPF.');
+        (new SaltFactory())->get('$P$C7u7E10SBEie/Jbdz0jDtUcWhzgOPF.', 'BE');
     }
 
     /**
@@ -73,7 +106,24 @@ class SaltFactoryTest extends UnitTestCase
         $phpassProphecy->isValidSaltedPW($hash)->shouldBeCalled()->willReturn(false);
         $this->expectException(InvalidSaltException::class);
         $this->expectExceptionCode(1533818591);
-        (new SaltFactory())->get($hash);
+        (new SaltFactory())->get($hash, 'BE');
+    }
+
+    /**
+     * @test
+     */
+    public function getHandsConfiguredOptionsToHashClassIfMethodIsConfiguredDefaultForMode(): void
+    {
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['availablePasswordHashAlgorithms'] = [ TestSalt::class ];
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing'] = [
+            'className' => TestSalt::class,
+            'options' => [
+                'foo' => 'bar'
+            ],
+        ];
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionCode(1533950385);
+        (new SaltFactory())->get('someHash', 'FE');
     }
 
     /**
@@ -87,7 +137,7 @@ class SaltFactoryTest extends UnitTestCase
         $phpassProphecy->isAvailable()->shouldBeCalled()->willReturn(true);
         $hash = '$P$C7u7E10SBEie/Jbdz0jDtUcWhzgOPF.';
         $phpassProphecy->isValidSaltedPW($hash)->shouldBeCalled()->willReturn(true);
-        $this->assertSame($phpassRevelation, (new SaltFactory())->get($hash));
+        $this->assertSame($phpassRevelation, (new SaltFactory())->get($hash, 'BE'));
     }
 
     /**
@@ -103,6 +153,28 @@ class SaltFactoryTest extends UnitTestCase
     /**
      * @test
      */
+    public function getDefaultHashInstanceThrowsExceptionWithBrokenClassNameModeConfiguration(): void
+    {
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionCode(1533950622);
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing']['className'] = '';
+        (new SaltFactory())->getDefaultHashInstance('FE');
+    }
+
+    /**
+     * @test
+     */
+    public function getDefaultHashInstanceThrowsExceptionWithBrokenOptionsModeConfiguration(): void
+    {
+        $this->expectException(\LogicException::class);
+        $this->expectExceptionCode(1533950622);
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing']['options'] = '';
+        (new SaltFactory())->getDefaultHashInstance('FE');
+    }
+
+    /**
+     * @test
+     */
     public function getDefaultHashReturnsInstanceOfConfiguredDefaultFeMethod(): void
     {
         $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['saltedpasswords']['FE']['saltedPWHashingMethod'] = Argon2iSalt::class;
@@ -122,13 +194,12 @@ class SaltFactoryTest extends UnitTestCase
 
     /**
      * @test
-     * @todo: have a test for exception 1533820194 after utility class is no longer used
      */
     public function getDefaultHashThrowsExceptionIfDefaultHashMethodDoesNotImplementSaltInterface(): void
     {
-        $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['saltedpasswords']['BE']['saltedPWHashingMethod'] = \stdClass::class;
-        $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/saltedpasswords']['saltMethods'] = [ \stdClass::class ];
-        $this->expectException(\RuntimeException::class);
+        $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 SaltFactory())->getDefaultHashInstance('BE');
     }
@@ -136,10 +207,21 @@ class SaltFactoryTest extends UnitTestCase
     /**
      * @test
      */
+    public function getDefaultHashThrowsExceptionIfDefaultHashMethodIsNotRegistered(): void
+    {
+        $GLOBALS['TYPO3_CONF_VARS']['BE']['passwordHashing']['className'] = \stdClass::class;
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['availablePasswordHashAlgorithms'] = [ Argon2iSalt::class ];
+        $this->expectException(InvalidSaltException::class);
+        $this->expectExceptionCode(1533820194);
+        (new SaltFactory())->getDefaultHashInstance('BE');
+    }
+
+    /**
+     * @test
+     */
     public function getDefaultHashThrowsExceptionIfDefaultHashMethodIsNotAvailable(): void
     {
         $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['saltedpasswords']['BE']['saltedPWHashingMethod'] = Argon2iSalt::class;
-        $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/saltedpasswords']['saltMethods'] = [ \stdClass::class ];
         $argonProphecy = $this->prophesize(Argon2iSalt::class);
         GeneralUtility::addInstance(Argon2iSalt::class, $argonProphecy->reveal());
         $argonProphecy->isAvailable()->shouldBeCalled()->willReturn(false);
@@ -147,4 +229,45 @@ class SaltFactoryTest extends UnitTestCase
         $this->expectExceptionCode(1533822084);
         (new SaltFactory())->getDefaultHashInstance('BE');
     }
+
+    /**
+     * @test
+     */
+    public function getDefaultHoshHandsConfiguredOptionsToHashClass(): void
+    {
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['availablePasswordHashAlgorithms'] = [ TestSalt::class ];
+        $GLOBALS['TYPO3_CONF_VARS']['FE']['passwordHashing'] = [
+            'className' => TestSalt::class,
+            'options' => [
+                'foo' => 'bar'
+            ],
+        ];
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionCode(1533950385);
+        (new SaltFactory())->getDefaultHashInstance('FE');
+    }
+
+    /**
+     * @test
+     */
+    public function getRegisteredSaltedHashingMethodsReturnsRegisteredMethods(): void
+    {
+        $methods = [
+            'foo',
+            'bar'
+        ];
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['availablePasswordHashAlgorithms'] = $methods;
+        $this->assertSame($methods, SaltFactory::getRegisteredSaltedHashingMethods());
+    }
+
+    /**
+     * @test
+     */
+    public function getRegisteredSaltedHashingMethodsThrowsExceptionIfNoMethodIsConfigured(): void
+    {
+        $this->expectException(\RuntimeException::class);
+        $this->expectExceptionCode(1533948733);
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['availablePasswordHashAlgorithms'] = [];
+        SaltFactory::getRegisteredSaltedHashingMethods();
+    }
 }
diff --git a/typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/Argon2iSaltTest.php b/typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/Argon2iSaltTest.php
new file mode 100644 (file)
index 0000000..360fc56
--- /dev/null
@@ -0,0 +1,73 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Saltedpasswords\Tests\UnitDeprecated\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\Salt\Argon2iSalt;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class Argon2iSaltTest extends UnitTestCase
+{
+    /**
+     * @test
+     */
+    public function getOptionsReturnsPreviouslySetOptions()
+    {
+        $options = [
+            'memory_cost' => 2048,
+            'time_cost' => 4,
+            'threads' => 4,
+        ];
+        $subject = new Argon2iSalt();
+        $subject->setOptions($options);
+        $this->assertSame($subject->getOptions(), $options);
+    }
+
+    /**
+     * @test
+     */
+    public function setOptionsThrowsExceptionWithTooLowMemoryCost()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1526042080);
+        $subject = new Argon2iSalt();
+        $subject->setOptions(['memory_cost' => 1]);
+    }
+
+    /**
+     * @test
+     */
+    public function setOptionsThrowsExceptionWithTooLowTimeCost()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1526042081);
+        $subject = new Argon2iSalt();
+        $subject->setOptions(['time_cost' => 1]);
+    }
+
+    /**
+     * @test
+     */
+    public function setOptionsThrowsExceptionWithTooLowThreads()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1526042082);
+        $subject = new Argon2iSalt();
+        $subject->setOptions(['threads' => 1]);
+    }
+}
diff --git a/typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/BcryptSaltTest.php b/typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/BcryptSaltTest.php
new file mode 100644 (file)
index 0000000..866dd4e
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Saltedpasswords\Tests\UnitDeprecated\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\Salt\BcryptSalt;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class BcryptSaltTest extends UnitTestCase
+{
+    /**
+     * @var BcryptSalt
+     */
+    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 BcryptSalt($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/saltedpasswords/Tests/UnitDeprecated/Salt/BlowfishSaltTest.php b/typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/BlowfishSaltTest.php
new file mode 100644 (file)
index 0000000..13eee32
--- /dev/null
@@ -0,0 +1,148 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Saltedpasswords\Tests\UnitDeprecated\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\Crypto\Random;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class BlowfishSaltTest extends UnitTestCase
+{
+    /**
+     * Keeps instance of object to test.
+     *
+     * @var \TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt
+     */
+    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\Saltedpasswords\Salt\BlowfishSalt::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/saltedpasswords/Tests/UnitDeprecated/Salt/Md5SaltTest.php b/typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/Md5SaltTest.php
new file mode 100644 (file)
index 0000000..6e65e5d
--- /dev/null
@@ -0,0 +1,89 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Saltedpasswords\Tests\UnitDeprecated\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\Crypto\Random;
+use TYPO3\CMS\Saltedpasswords\Salt\Md5Salt;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class Md5SaltTest extends UnitTestCase
+{
+    /**
+     * Keeps instance of object to test.
+     *
+     * @var Md5Salt
+     */
+    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(Md5Salt::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/saltedpasswords/Tests/UnitDeprecated/Salt/Pbkdf2SaltTest.php b/typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/Pbkdf2SaltTest.php
new file mode 100644 (file)
index 0000000..6f708b4
--- /dev/null
@@ -0,0 +1,138 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Saltedpasswords\Tests\UnitDeprecated\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\Crypto\Random;
+use TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class Pbkdf2SaltTest extends UnitTestCase
+{
+    /**
+     * Keeps instance of object to test.
+     *
+     * @var Pbkdf2Salt
+     */
+    protected $subject;
+
+    /**
+     * Sets up the fixtures for this testcase.
+     */
+    protected function setUp()
+    {
+        $this->subject = new Pbkdf2Salt(['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/saltedpasswords/Tests/UnitDeprecated/Salt/PhpassSaltTest.php b/typo3/sysext/saltedpasswords/Tests/UnitDeprecated/Salt/PhpassSaltTest.php
new file mode 100644 (file)
index 0000000..eefbb7d
--- /dev/null
@@ -0,0 +1,146 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Saltedpasswords\Tests\UnitDeprecated\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\Crypto\Random;
+use TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+/**
+ * Test case
+ */
+class PhpassSaltTest extends UnitTestCase
+{
+    /**
+     * Keeps instance of object to test.
+     *
+     * @var PhpassSalt
+     */
+    protected $objectInstance;
+
+    /**
+     * Sets up the fixtures for this testcase.
+     */
+    protected function setUp()
+    {
+        $this->objectInstance = $this->getMockBuilder(PhpassSalt::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 69328d1..6dc4655 100644 (file)
@@ -59,50 +59,6 @@ class SaltFactoryTest extends UnitTestCase
     /**
      * @test
      */
-    public function objectInstanceForMD5Salts()
-    {
-        $saltMD5 = '$1$rasmusle$rISCgZzpwk3UhDidwXvin0';
-        $objectInstance = SaltFactory::getSaltingInstance($saltMD5);
-        $this->assertTrue(get_class($objectInstance) == \TYPO3\CMS\Saltedpasswords\Salt\Md5Salt::class || is_subclass_of($objectInstance, \TYPO3\CMS\Saltedpasswords\Salt\Md5Salt::class));
-        $this->assertInstanceOf(\TYPO3\CMS\Saltedpasswords\Salt\AbstractComposedSalt::class, $objectInstance);
-    }
-
-    /**
-     * @test
-     */
-    public function objectInstanceForBlowfishSalts()
-    {
-        $saltBlowfish = '$2a$07$abcdefghijklmnopqrstuuIdQV69PAxWYTgmnoGpe0Sk47GNS/9ZW';
-        $objectInstance = SaltFactory::getSaltingInstance($saltBlowfish);
-        $this->assertTrue(get_class($objectInstance) == \TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt::class || is_subclass_of($objectInstance, \TYPO3\CMS\Saltedpasswords\Salt\BlowfishSalt::class));
-        $this->assertInstanceOf(\TYPO3\CMS\Saltedpasswords\Salt\AbstractComposedSalt::class, $objectInstance);
-    }
-
-    /**
-     * @test
-     */
-    public function objectInstanceForPhpassSalts()
-    {
-        $saltPhpass = '$P$CWF13LlG/0UcAQFUjnnS4LOqyRW43c.';
-        $objectInstance = SaltFactory::getSaltingInstance($saltPhpass);
-        $this->assertTrue(get_class($objectInstance) == \TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt::class || is_subclass_of($objectInstance, \TYPO3\CMS\Saltedpasswords\Salt\PhpassSalt::class));
-        $this->assertInstanceOf(\TYPO3\CMS\Saltedpasswords\Salt\AbstractComposedSalt::class, $objectInstance);
-    }
-
-    /**
-     * @test
-     */
-    public function objectInstanceForPbkdf2Salts()
-    {
-        $saltPbkdf2 = '$pbkdf2-sha256$6400$0ZrzXitFSGltTQnBWOsdAw$Y11AchqV4b0sUisdZd0Xr97KWoymNE0LNNrnEgY4H9M';
-        $objectInstance = SaltFactory::getSaltingInstance($saltPbkdf2);
-        $this->assertTrue(get_class($objectInstance) == \TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt::class || is_subclass_of($objectInstance, \TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt::class));
-        $this->assertInstanceOf(\TYPO3\CMS\Saltedpasswords\Salt\AbstractComposedSalt::class, $objectInstance);
-    }
-
-    /**
-     * @test
-     */
     public function objectInstanceForPhpPasswordHashBcryptSalts()
     {
         $saltBcrypt = '$2y$12$Tz.al0seuEgRt61u0bzqAOWu67PgG2ThG25oATJJ0oS5KLCPCgBOe';
diff --git a/typo3/sysext/saltedpasswords/ext_conf_template.txt b/typo3/sysext/saltedpasswords/ext_conf_template.txt
deleted file mode 100644 (file)
index e909f2e..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-# cat=Basic/enable; type=user[TYPO3\CMS\Saltedpasswords\Utility\ExtensionManagerConfigurationUtility->buildHashMethodSelectorFE]; label=LLL:EXT:saltedpasswords/Resources/Private/Language/locallang_em.xlf:saltedpasswords.config.FE.saltedPWHashingMethod
-FE.saltedPWHashingMethod = TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt
-
-# cat=Basic/enable; type=user[TYPO3\CMS\Saltedpasswords\Utility\ExtensionManagerConfigurationUtility->buildHashMethodSelectorBE]; label=LLL:EXT:saltedpasswords/Resources/Private/Language/locallang_em.xlf:saltedpasswords.config.BE.saltedPWHashingMethod
-BE.saltedPWHashingMethod = TYPO3\CMS\Saltedpasswords\Salt\Pbkdf2Salt
index c1c2d64..13bc4c5 100644 (file)
@@ -292,7 +292,7 @@ class SetupModuleController
                         $passwordOk = false;
                         $saltFactory = GeneralUtility::makeInstance(SaltFactory::class);
                         try {
-                            $hashInstance = $saltFactory->get($currentPasswordHashed);
+                            $hashInstance = $saltFactory->get($currentPasswordHashed, 'BE');
                             $passwordOk = $hashInstance->checkPassword($be_user_data['passwordCurrent'], $currentPasswordHashed);
                         } catch (InvalidSaltException $e) {
                             // Could not find hash class responsible for existing password. This is a