1
0

Hasher.php 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  6. * SPDX-License-Identifier: AGPL-3.0-only
  7. */
  8. namespace OC\Security;
  9. use OCP\IConfig;
  10. use OCP\Security\IHasher;
  11. /**
  12. * Class Hasher provides some basic hashing functions. Furthermore, it supports legacy hashes
  13. * used by previous versions of ownCloud and helps migrating those hashes to newer ones.
  14. *
  15. * The hashes generated by this class are prefixed (version|hash) with a version parameter to allow possible
  16. * updates in the future.
  17. * Possible versions:
  18. * - 1 (Initial version)
  19. *
  20. * Usage:
  21. * // Hashing a message
  22. * $hash = \OC::$server->get(\OCP\Security\IHasher::class)->hash('MessageToHash');
  23. * // Verifying a message - $newHash will contain the newly calculated hash
  24. * $newHash = null;
  25. * var_dump(\OC::$server->get(\OCP\Security\IHasher::class)->verify('a', '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8', $newHash));
  26. * var_dump($newHash);
  27. *
  28. * @package OC\Security
  29. */
  30. class Hasher implements IHasher {
  31. /** Options passed to password_hash and password_needs_rehash */
  32. private array $options = [];
  33. /** Salt used for legacy passwords */
  34. private ?string $legacySalt = null;
  35. public function __construct(
  36. private IConfig $config,
  37. ) {
  38. if (\defined('PASSWORD_ARGON2ID') || \defined('PASSWORD_ARGON2I')) {
  39. // password_hash fails, when the minimum values are undershot.
  40. // In this case, apply minimum.
  41. $this->options['threads'] = max($this->config->getSystemValueInt('hashingThreads', PASSWORD_ARGON2_DEFAULT_THREADS), 1);
  42. // The minimum memory cost is 8 KiB per thread.
  43. $this->options['memory_cost'] = max($this->config->getSystemValueInt('hashingMemoryCost', PASSWORD_ARGON2_DEFAULT_MEMORY_COST), $this->options['threads'] * 8);
  44. $this->options['time_cost'] = max($this->config->getSystemValueInt('hashingTimeCost', PASSWORD_ARGON2_DEFAULT_TIME_COST), 1);
  45. }
  46. $hashingCost = $this->config->getSystemValue('hashingCost', null);
  47. if (!\is_null($hashingCost)) {
  48. $this->options['cost'] = $hashingCost;
  49. }
  50. }
  51. /**
  52. * Hashes a message using PHP's `password_hash` functionality.
  53. * Please note that the size of the returned string is not guaranteed
  54. * and can be up to 255 characters.
  55. *
  56. * @param string $message Message to generate hash from
  57. * @return string Hash of the message with appended version parameter
  58. */
  59. public function hash(string $message): string {
  60. $alg = $this->getPrefferedAlgorithm();
  61. if (\defined('PASSWORD_ARGON2ID') && $alg === PASSWORD_ARGON2ID) {
  62. return 3 . '|' . password_hash($message, PASSWORD_ARGON2ID, $this->options);
  63. }
  64. if (\defined('PASSWORD_ARGON2I') && $alg === PASSWORD_ARGON2I) {
  65. return 2 . '|' . password_hash($message, PASSWORD_ARGON2I, $this->options);
  66. }
  67. return 1 . '|' . password_hash($message, PASSWORD_BCRYPT, $this->options);
  68. }
  69. /**
  70. * Get the version and hash from a prefixedHash
  71. * @param string $prefixedHash
  72. * @return null|array{version: int, hash: string} Null if the hash is not prefixed, otherwise array('version' => 1, 'hash' => 'foo')
  73. */
  74. protected function splitHash(string $prefixedHash): ?array {
  75. $explodedString = explode('|', $prefixedHash, 2);
  76. if (\count($explodedString) === 2) {
  77. if ((int)$explodedString[0] > 0) {
  78. return ['version' => (int)$explodedString[0], 'hash' => $explodedString[1]];
  79. }
  80. }
  81. return null;
  82. }
  83. /**
  84. * Verify legacy hashes
  85. * @param string $message Message to verify
  86. * @param string $hash Assumed hash of the message
  87. * @param null|string &$newHash Reference will contain the updated hash
  88. * @return bool Whether $hash is a valid hash of $message
  89. */
  90. protected function legacyHashVerify($message, $hash, &$newHash = null): bool {
  91. if (empty($this->legacySalt)) {
  92. $this->legacySalt = $this->config->getSystemValue('passwordsalt', '');
  93. }
  94. // Verify whether it matches a legacy PHPass or SHA1 string
  95. $hashLength = \strlen($hash);
  96. if (($hashLength === 60 && password_verify($message . $this->legacySalt, $hash)) ||
  97. ($hashLength === 40 && hash_equals($hash, sha1($message)))) {
  98. $newHash = $this->hash($message);
  99. return true;
  100. }
  101. // Verify whether it matches a legacy PHPass or SHA1 string
  102. // Retry with empty passwordsalt for cases where it was not set
  103. $hashLength = \strlen($hash);
  104. if (($hashLength === 60 && password_verify($message, $hash)) ||
  105. ($hashLength === 40 && hash_equals($hash, sha1($message)))) {
  106. $newHash = $this->hash($message);
  107. return true;
  108. }
  109. return false;
  110. }
  111. /**
  112. * Verify V1 (blowfish) hashes
  113. * Verify V2 (argon2i) hashes
  114. * Verify V3 (argon2id) hashes
  115. * @param string $message Message to verify
  116. * @param string $hash Assumed hash of the message
  117. * @param null|string &$newHash Reference will contain the updated hash if necessary. Update the existing hash with this one.
  118. * @return bool Whether $hash is a valid hash of $message
  119. */
  120. protected function verifyHash(string $message, string $hash, &$newHash = null): bool {
  121. if (password_verify($message, $hash)) {
  122. if ($this->needsRehash($hash)) {
  123. $newHash = $this->hash($message);
  124. }
  125. return true;
  126. }
  127. return false;
  128. }
  129. /**
  130. * @param string $message Message to verify
  131. * @param string $hash Assumed hash of the message
  132. * @param null|string &$newHash Reference will contain the updated hash if necessary. Update the existing hash with this one.
  133. * @return bool Whether $hash is a valid hash of $message
  134. */
  135. public function verify(string $message, string $hash, &$newHash = null): bool {
  136. $splittedHash = $this->splitHash($hash);
  137. if (isset($splittedHash['version'])) {
  138. switch ($splittedHash['version']) {
  139. case 3:
  140. case 2:
  141. case 1:
  142. return $this->verifyHash($message, $splittedHash['hash'], $newHash);
  143. }
  144. } else {
  145. return $this->legacyHashVerify($message, $hash, $newHash);
  146. }
  147. return false;
  148. }
  149. private function needsRehash(string $hash): bool {
  150. $algorithm = $this->getPrefferedAlgorithm();
  151. return password_needs_rehash($hash, $algorithm, $this->options);
  152. }
  153. private function getPrefferedAlgorithm(): string {
  154. $default = PASSWORD_BCRYPT;
  155. if (\defined('PASSWORD_ARGON2I')) {
  156. $default = PASSWORD_ARGON2I;
  157. }
  158. if (\defined('PASSWORD_ARGON2ID')) {
  159. $default = PASSWORD_ARGON2ID;
  160. }
  161. // Check if we should use PASSWORD_DEFAULT
  162. if ($this->config->getSystemValueBool('hashing_default_password', false)) {
  163. $default = PASSWORD_DEFAULT;
  164. }
  165. return $default;
  166. }
  167. public function validate(string $prefixedHash): bool {
  168. $splitHash = $this->splitHash($prefixedHash);
  169. if (empty($splitHash)) {
  170. return false;
  171. }
  172. $validVersions = [3, 2, 1];
  173. $version = $splitHash['version'];
  174. if (!in_array($version, $validVersions, true)) {
  175. return false;
  176. }
  177. $algoName = password_get_info($splitHash['hash'])['algoName'];
  178. return $algoName !== 'unknown';
  179. }
  180. }