diff options
Diffstat (limited to 'lib/private/Security/Hasher.php')
-rw-r--r-- | lib/private/Security/Hasher.php | 160 |
1 files changed, 103 insertions, 57 deletions
diff --git a/lib/private/Security/Hasher.php b/lib/private/Security/Hasher.php index 3bc546fa0a2..722fdab902f 100644 --- a/lib/private/Security/Hasher.php +++ b/lib/private/Security/Hasher.php @@ -1,26 +1,11 @@ <?php + +declare(strict_types=1); /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OC\Security; use OCP\IConfig; @@ -37,32 +22,34 @@ use OCP\Security\IHasher; * * Usage: * // Hashing a message - * $hash = \OC::$server->getHasher()->hash('MessageToHash'); + * $hash = \OC::$server->get(\OCP\Security\IHasher::class)->hash('MessageToHash'); * // Verifying a message - $newHash will contain the newly calculated hash * $newHash = null; - * var_dump(\OC::$server->getHasher()->verify('a', '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8', $newHash)); + * var_dump(\OC::$server->get(\OCP\Security\IHasher::class)->verify('a', '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8', $newHash)); * var_dump($newHash); * * @package OC\Security */ class Hasher implements IHasher { - /** @var IConfig */ - private $config; - /** @var array Options passed to password_hash and password_needs_rehash */ - private $options = array(); - /** @var string Salt used for legacy passwords */ - private $legacySalt = null; - /** @var int Current version of the generated hash */ - private $currentVersion = 1; - - /** - * @param IConfig $config - */ - function __construct(IConfig $config) { - $this->config = $config; + /** Options passed to password_hash and password_needs_rehash */ + private array $options = []; + /** Salt used for legacy passwords */ + private ?string $legacySalt = null; + + public function __construct( + private IConfig $config, + ) { + if (\defined('PASSWORD_ARGON2ID') || \defined('PASSWORD_ARGON2I')) { + // password_hash fails, when the minimum values are undershot. + // In this case, apply minimum. + $this->options['threads'] = max($this->config->getSystemValueInt('hashingThreads', PASSWORD_ARGON2_DEFAULT_THREADS), 1); + // The minimum memory cost is 8 KiB per thread. + $this->options['memory_cost'] = max($this->config->getSystemValueInt('hashingMemoryCost', PASSWORD_ARGON2_DEFAULT_MEMORY_COST), $this->options['threads'] * 8); + $this->options['time_cost'] = max($this->config->getSystemValueInt('hashingTimeCost', PASSWORD_ARGON2_DEFAULT_TIME_COST), 1); + } $hashingCost = $this->config->getSystemValue('hashingCost', null); - if(!is_null($hashingCost)) { + if (!\is_null($hashingCost)) { $this->options['cost'] = $hashingCost; } } @@ -75,20 +62,30 @@ class Hasher implements IHasher { * @param string $message Message to generate hash from * @return string Hash of the message with appended version parameter */ - public function hash($message) { - return $this->currentVersion . '|' . password_hash($message, PASSWORD_DEFAULT, $this->options); + public function hash(string $message): string { + $alg = $this->getPrefferedAlgorithm(); + + if (\defined('PASSWORD_ARGON2ID') && $alg === PASSWORD_ARGON2ID) { + return 3 . '|' . password_hash($message, PASSWORD_ARGON2ID, $this->options); + } + + if (\defined('PASSWORD_ARGON2I') && $alg === PASSWORD_ARGON2I) { + return 2 . '|' . password_hash($message, PASSWORD_ARGON2I, $this->options); + } + + return 1 . '|' . password_hash($message, PASSWORD_BCRYPT, $this->options); } /** * Get the version and hash from a prefixedHash * @param string $prefixedHash - * @return null|array Null if the hash is not prefixed, otherwise array('version' => 1, 'hash' => 'foo') + * @return null|array{version: int, hash: string} Null if the hash is not prefixed, otherwise array('version' => 1, 'hash' => 'foo') */ - protected function splitHash($prefixedHash) { + protected function splitHash(string $prefixedHash): ?array { $explodedString = explode('|', $prefixedHash, 2); - if(sizeof($explodedString) === 2) { - if((int)$explodedString[0] > 0) { - return array('version' => (int)$explodedString[0], 'hash' => $explodedString[1]); + if (\count($explodedString) === 2) { + if ((int)$explodedString[0] > 0) { + return ['version' => (int)$explodedString[0], 'hash' => $explodedString[1]]; } } @@ -102,15 +99,24 @@ class Hasher implements IHasher { * @param null|string &$newHash Reference will contain the updated hash * @return bool Whether $hash is a valid hash of $message */ - protected function legacyHashVerify($message, $hash, &$newHash = null) { - if(empty($this->legacySalt)) { + protected function legacyHashVerify($message, $hash, &$newHash = null): bool { + if (empty($this->legacySalt)) { $this->legacySalt = $this->config->getSystemValue('passwordsalt', ''); } // Verify whether it matches a legacy PHPass or SHA1 string - $hashLength = strlen($hash); - if($hashLength === 60 && password_verify($message.$this->legacySalt, $hash) || - $hashLength === 40 && hash_equals($hash, sha1($message))) { + $hashLength = \strlen($hash); + if (($hashLength === 60 && password_verify($message . $this->legacySalt, $hash)) + || ($hashLength === 40 && hash_equals($hash, sha1($message)))) { + $newHash = $this->hash($message); + return true; + } + + // Verify whether it matches a legacy PHPass or SHA1 string + // Retry with empty passwordsalt for cases where it was not set + $hashLength = \strlen($hash); + if (($hashLength === 60 && password_verify($message, $hash)) + || ($hashLength === 40 && hash_equals($hash, sha1($message)))) { $newHash = $this->hash($message); return true; } @@ -119,15 +125,17 @@ class Hasher implements IHasher { } /** - * Verify V1 hashes + * Verify V1 (blowfish) hashes + * Verify V2 (argon2i) hashes + * Verify V3 (argon2id) hashes * @param string $message Message to verify * @param string $hash Assumed hash of the message * @param null|string &$newHash Reference will contain the updated hash if necessary. Update the existing hash with this one. * @return bool Whether $hash is a valid hash of $message */ - protected function verifyHashV1($message, $hash, &$newHash = null) { - if(password_verify($message, $hash)) { - if(password_needs_rehash($hash, PASSWORD_DEFAULT, $this->options)) { + protected function verifyHash(string $message, string $hash, &$newHash = null): bool { + if (password_verify($message, $hash)) { + if ($this->needsRehash($hash)) { $newHash = $this->hash($message); } return true; @@ -142,20 +150,58 @@ class Hasher implements IHasher { * @param null|string &$newHash Reference will contain the updated hash if necessary. Update the existing hash with this one. * @return bool Whether $hash is a valid hash of $message */ - public function verify($message, $hash, &$newHash = null) { + public function verify(string $message, string $hash, &$newHash = null): bool { $splittedHash = $this->splitHash($hash); - if(isset($splittedHash['version'])) { + if (isset($splittedHash['version'])) { switch ($splittedHash['version']) { + case 3: + case 2: case 1: - return $this->verifyHashV1($message, $splittedHash['hash'], $newHash); + return $this->verifyHash($message, $splittedHash['hash'], $newHash); } } else { return $this->legacyHashVerify($message, $hash, $newHash); } - return false; } + private function needsRehash(string $hash): bool { + $algorithm = $this->getPrefferedAlgorithm(); + + return password_needs_rehash($hash, $algorithm, $this->options); + } + + private function getPrefferedAlgorithm(): string { + $default = PASSWORD_BCRYPT; + if (\defined('PASSWORD_ARGON2I')) { + $default = PASSWORD_ARGON2I; + } + + if (\defined('PASSWORD_ARGON2ID')) { + $default = PASSWORD_ARGON2ID; + } + + // Check if we should use PASSWORD_DEFAULT + if ($this->config->getSystemValueBool('hashing_default_password', false)) { + $default = PASSWORD_DEFAULT; + } + + return $default; + } + + public function validate(string $prefixedHash): bool { + $splitHash = $this->splitHash($prefixedHash); + if (empty($splitHash)) { + return false; + } + $validVersions = [3, 2, 1]; + $version = $splitHash['version']; + if (!in_array($version, $validVersions, true)) { + return false; + } + $algoName = password_get_info($splitHash['hash'])['algoName']; + return $algoName !== 'unknown'; + } } |