HasherTest.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  6. * SPDX-License-Identifier: AGPL-3.0-or-later
  7. */
  8. namespace Test\Security;
  9. use OC\Security\Hasher;
  10. use OCP\IConfig;
  11. /**
  12. * Class HasherTest
  13. */
  14. class HasherTest extends \Test\TestCase {
  15. /**
  16. * @return array
  17. */
  18. public function versionHashProvider() {
  19. return [
  20. ['asf32äà$$a.|3', null],
  21. ['asf32äà$$a.|3|5', null],
  22. ['1|2|3|4', ['version' => 1, 'hash' => '2|3|4']],
  23. ['1|我看|这本书。 我看這本書', ['version' => 1, 'hash' => '我看|这本书。 我看這本書']],
  24. ['2|newhash', ['version' => 2, 'hash' => 'newhash']],
  25. ];
  26. }
  27. public function hashProviders70_71(): array {
  28. return [
  29. // Valid SHA1 strings
  30. ['password', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', true],
  31. ['owncloud.com', '27a4643e43046c3569e33b68c1a4b15d31306d29', true],
  32. // Invalid SHA1 strings
  33. ['InvalidString', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', false],
  34. ['AnotherInvalidOne', '27a4643e43046c3569e33b68c1a4b15d31306d29', false],
  35. // Valid legacy password string with password salt "6Wow67q1wZQZpUUeI6G2LsWUu4XKx"
  36. ['password', '$2a$08$emCpDEl.V.QwPWt5gPrqrOhdpH6ailBmkj2Hd2vD5U8qIy20HBe7.', true],
  37. ['password', '$2a$08$yjaLO4ev70SaOsWZ9gRS3eRSEpHVsmSWTdTms1949mylxJ279hzo2', true],
  38. ['password', '$2a$08$.jNRG/oB4r7gHJhAyb.mDupNUAqTnBIW/tWBqFobaYflKXiFeG0A6', true],
  39. ['owncloud.com', '$2a$08$YbEsyASX/hXVNMv8hXQo7ezreN17T8Jl6PjecGZvpX.Ayz2aUyaZ2', true],
  40. ['owncloud.com', '$2a$11$cHdDA2IkUP28oNGBwlL7jO/U3dpr8/0LIjTZmE8dMPA7OCUQsSTqS', true],
  41. ['owncloud.com', '$2a$08$GH.UoIfJ1e.qeZ85KPqzQe6NR8XWRgJXWIUeE1o/j1xndvyTA1x96', true],
  42. // Invalid legacy passwords
  43. ['password', '$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false],
  44. // Valid passwords "6Wow67q1wZQZpUUeI6G2LsWUu4XKx"
  45. ['password', '1|$2a$05$ezAE0dkwk57jlfo6z5Pql.gcIK3ReXT15W7ITNxVS0ksfhO/4E4Kq', true],
  46. ['password', '1|$2a$05$4OQmloFW4yTVez2MEWGIleDO9Z5G9tWBXxn1vddogmKBQq/Mq93pe', true],
  47. ['password', '1|$2a$11$yj0hlp6qR32G9exGEXktB.yW2rgt2maRBbPgi3EyxcDwKrD14x/WO', true],
  48. ['owncloud.com', '1|$2a$10$Yiss2WVOqGakxuuqySv5UeOKpF8d8KmNjuAPcBMiRJGizJXjA2bKm', true],
  49. ['owncloud.com', '1|$2a$10$v9mh8/.mF/Ut9jZ7pRnpkuac3bdFCnc4W/gSumheQUi02Sr.xMjPi', true],
  50. ['owncloud.com', '1|$2a$05$ST5E.rplNRfDCzRpzq69leRzsTGtY7k88h9Vy2eWj0Ug/iA9w5kGK', true],
  51. // Invalid passwords
  52. ['password', '0|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false],
  53. ['password', '1|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false],
  54. ['password', '2|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false],
  55. ];
  56. }
  57. public function hashProviders72(): array {
  58. return [
  59. // Valid ARGON2 hashes
  60. ['password', '2|$argon2i$v=19$m=1024,t=2,p=2$T3JGcEkxVFNOVktNSjZUcg$4/hyLtSejxNgAuzSFFV/HLM3qRQKBwEtKw61qPN4zWA', true],
  61. ['password', '2|$argon2i$v=19$m=1024,t=2,p=2$Zk52V24yNjMzTkhyYjJKOQ$vmqHkCaOD6SiiiFKD1GeKLg/D1ynWpyZbx4XA2yed34', true],
  62. ['password', '2|$argon2i$v=19$m=1024,t=2,p=2$R1pRcUZKamVlNndBc3l5ag$ToRhR8SiZc7fGMpOYfSc5haS5t9+Y00rljPJV7+qLkM', true],
  63. ['nextcloud.com', '2|$argon2i$v=19$m=1024,t=2,p=2$NC9xM0FFaDlzM01QM3kudg$fSfndwtO2mKMZlKdsT8XAtPY51cSS6pLSGS3xMqeJhg', true],
  64. ['nextcloud.com', '2|$argon2i$v=19$m=1024,t=2,p=2$UjkvUjEuL042WWl1cmdHOA$FZivLkBdZnloQsW6qq/jqWK95JSYUHW9rwQC4Ff9GN0', true],
  65. ['nextcloud.com', '2|$argon2i$v=19$m=1024,t=2,p=2$ZnpNdUlzMEpUTW40OVpiMQ$c+yHT9dtSYsjtVGsa7UKOsxxgQAMiUc781d9WsFACqs', true],
  66. //Invalid ARGON2 hashes
  67. ['password', '2|$argon2i$v=19$m=1024,t=2,p=2$UjFDUDg3cjBvM3FkbXVOWQ$7Y5xqFxSERnYn+2+7WChUpWZWMa5BEIhSHWnDgJ71Jk', false],
  68. ['password', '2|$argon2i$v=19$m=1024,t=2,p=2$ZUxSUi5aQklXdkcyMG1uVA$sYjoSvXg/CS/aS6Xnas/o9a/OPVcGKldzzmuiCD1Fxo', false],
  69. ['password', '2|$argon2i$v=19$m=1024,t=2,p=2$ZHQ5V0xMOFNmUC52by44Sg$DzQFk3bJTX0J4PVGwW6rMvtnBJRalBkbtpDIXR+d4A0', false],
  70. ];
  71. }
  72. public function hashProviders73(): array {
  73. return [
  74. // Valid ARGON2ID hashes
  75. ['password', '2|$argon2id$v=19$m=65536,t=4,p=1$TEtIMnhUczliQzI0Y01WeA$BpMUDrApy25iagIogUAnlc0rNTPJmGs8lOEeVHujJ9Q', true],
  76. ['password', '2|$argon2id$v=19$m=65536,t=4,p=1$RzdUdDNvbHhZalVQa2VIcQ$Wo8CGasVCBcSe69ldPdoVKTWEDQkET2cgQJSUiKcIzs', true],
  77. ['password', '2|$argon2id$v=19$m=65536,t=4,p=1$djlDMTVkL3VnMlNZNWZPeg$PCMpdAjB+OtwGpM75IGWmYHh1h2I7l5P8YabYtKubWg', true],
  78. ['nextcloud.com', '2|$argon2id$v=19$m=65536,t=4,p=1$VGhGL05rcUI3d3k3WVhibQ$CSy0ShUnamZQhu8oeZfUTTd/S3z966zuQ/uz1Y80Rss', true],
  79. ['nextcloud.com', '2|$argon2id$v=19$m=65536,t=4,p=1$ZVlZTVlCaTZhRlZHOGFpYQ$xd1TtMz1Mi0SuZrP+VWB3v/hwoC7HfSVsUYmzOo2DUU', true],
  80. ['nextcloud.com', '2|$argon2id$v=19$m=65536,t=4,p=1$OG1wZUtzZ0tnLjF2MUZVMA$CBluq8W8ISmZ9QumeWsVhaVREP0Zcq8rwk2NrA9d4YE', true],
  81. //Invalid ARGON2ID hashes
  82. ['password', '2|$argon2id$v=19$m=65536,t=4,p=1$V3ovTHlvc0Eyb24xenVRNQ$iY/A0Yf24c2DToedj2rj9+KeoJBGsJYQOlJMoa0SFXk', false],
  83. ['password', '2|$argon2id$v=19$m=65536,t=4,p=1$NlYuMlQ0ODIudTRkZDhYUw$/Z71ckOIuydujedUGK73iXC9vbLzlH/iXkG9+gGgn+c', false],
  84. ['password', '2|$argon2id$v=19$m=65536,t=4,p=1$b09kNFZTZWFjS05aTkl6ZA$llE4TnIYYrC0H7wkTL1JsIwAAgoMJERlqtFcHHQcXTs', false],
  85. ];
  86. }
  87. /** @var Hasher */
  88. protected $hasher;
  89. /** @var IConfig */
  90. protected $config;
  91. protected function setUp(): void {
  92. parent::setUp();
  93. $this->config = $this->createMock(IConfig::class);
  94. $this->config->method('getSystemValueInt')
  95. ->willReturnCallback(function ($name, $default) {
  96. return $default;
  97. });
  98. $this->hasher = new Hasher($this->config);
  99. }
  100. public function testHash() {
  101. $hash = $this->hasher->hash('String To Hash');
  102. $this->assertNotNull($hash);
  103. }
  104. /**
  105. * @dataProvider versionHashProvider
  106. */
  107. public function testSplitHash($hash, $expected) {
  108. $relativePath = self::invokePrivate($this->hasher, 'splitHash', [$hash]);
  109. $this->assertSame($expected, $relativePath);
  110. }
  111. /**
  112. * @dataProvider hashProviders70_71
  113. */
  114. public function testVerify($password, $hash, $expected) {
  115. $this->config
  116. ->expects($this->any())
  117. ->method('getSystemValue')
  118. ->willReturnCallback(function ($key, $default) {
  119. if ($key === 'passwordsalt') {
  120. return '6Wow67q1wZQZpUUeI6G2LsWUu4XKx';
  121. }
  122. return $default;
  123. });
  124. $result = $this->hasher->verify($password, $hash);
  125. $this->assertSame($expected, $result);
  126. }
  127. /**
  128. * @dataProvider hashProviders72
  129. */
  130. public function testVerifyArgon2i($password, $hash, $expected) {
  131. if (!\defined('PASSWORD_ARGON2I')) {
  132. $this->markTestSkipped('Need ARGON2 support to test ARGON2 hashes');
  133. }
  134. $result = $this->hasher->verify($password, $hash);
  135. $this->assertSame($expected, $result);
  136. }
  137. /**
  138. * @dataProvider hashProviders73
  139. */
  140. public function testVerifyArgon2id(string $password, string $hash, bool $expected) {
  141. if (!\defined('PASSWORD_ARGON2ID')) {
  142. $this->markTestSkipped('Need ARGON2ID support to test ARGON2ID hashes');
  143. }
  144. $result = $this->hasher->verify($password, $hash);
  145. $this->assertSame($expected, $result);
  146. }
  147. public function testUpgradeHashBlowFishToArgon2() {
  148. if (!\defined('PASSWORD_ARGON2I')) {
  149. $this->markTestSkipped('Need ARGON2 support to test ARGON2 hashes');
  150. }
  151. $message = 'mysecret';
  152. $blowfish = 1 . '|' . password_hash($message, PASSWORD_BCRYPT, []);
  153. $argon2 = 2 . '|' . password_hash($message, PASSWORD_ARGON2I, []);
  154. $newAlg = PASSWORD_ARGON2I;
  155. if (\defined('PASSWORD_ARGON2ID')) {
  156. $newAlg = PASSWORD_ARGON2ID;
  157. $argon2 = 2 . '|' . password_hash($message, PASSWORD_ARGON2ID, []);
  158. }
  159. $this->assertTrue($this->hasher->verify($message, $blowfish, $newHash));
  160. $this->assertTrue($this->hasher->verify($message, $argon2));
  161. $relativePath = self::invokePrivate($this->hasher, 'splitHash', [$newHash]);
  162. $this->assertFalse(password_needs_rehash($relativePath['hash'], $newAlg, []));
  163. }
  164. public function testUsePasswordDefaultArgon2iVerify() {
  165. if (!\defined('PASSWORD_ARGON2I')) {
  166. $this->markTestSkipped('Need ARGON2 support to test ARGON2 hashes');
  167. }
  168. $this->config->method('getSystemValueBool')
  169. ->with('hashing_default_password')
  170. ->willReturn(true);
  171. $message = 'mysecret';
  172. $argon2i = 2 . '|' . password_hash($message, PASSWORD_ARGON2I, []);
  173. $newHash = null;
  174. $this->assertTrue($this->hasher->verify($message, $argon2i, $newHash));
  175. $this->assertNotNull($newHash);
  176. $relativePath = self::invokePrivate($this->hasher, 'splitHash', [$newHash]);
  177. $this->assertEquals(1, $relativePath['version']);
  178. $this->assertEquals(PASSWORD_BCRYPT, password_get_info($relativePath['hash'])['algo']);
  179. $this->assertFalse(password_needs_rehash($relativePath['hash'], PASSWORD_BCRYPT));
  180. $this->assertTrue(password_verify($message, $relativePath['hash']));
  181. }
  182. public function testDoNotUsePasswordDefaultArgon2idVerify() {
  183. if (!\defined('PASSWORD_ARGON2ID')) {
  184. $this->markTestSkipped('Need ARGON2ID support to test ARGON2ID hashes');
  185. }
  186. $this->config->method('getSystemValueBool')
  187. ->with('hashing_default_password')
  188. ->willReturn(false);
  189. $message = 'mysecret';
  190. $argon2id = 3 . '|' . password_hash($message, PASSWORD_ARGON2ID, []);
  191. $newHash = null;
  192. $this->assertTrue($this->hasher->verify($message, $argon2id, $newHash));
  193. $this->assertNull($newHash);
  194. }
  195. public function testHashUsePasswordDefault() {
  196. if (!\defined('PASSWORD_ARGON2I')) {
  197. $this->markTestSkipped('Need ARGON2 support to test ARGON2 hashes');
  198. }
  199. $this->config->method('getSystemValueBool')
  200. ->with('hashing_default_password')
  201. ->willReturn(true);
  202. $message = 'mysecret';
  203. $hash = $this->hasher->hash($message);
  204. $relativePath = self::invokePrivate($this->hasher, 'splitHash', [$hash]);
  205. $this->assertSame(1, $relativePath['version']);
  206. $info = password_get_info($relativePath['hash']);
  207. $this->assertEquals(PASSWORD_BCRYPT, $info['algo']);
  208. }
  209. public function testValidHash() {
  210. $hash = '3|$argon2id$v=19$m=65536,t=4,p=1$czFCSjk3LklVdXppZ2VCWA$li0NgdXe2/jwSRxgteGQPWlzJU0E0xdtfHbCbrpych0';
  211. $isValid = $this->hasher->validate($hash);
  212. $this->assertTrue($isValid);
  213. }
  214. public function testValidGeneratedHash() {
  215. $message = 'secret';
  216. $hash = $this->hasher->hash($message);
  217. $isValid = $this->hasher->validate($hash);
  218. $this->assertTrue($isValid);
  219. }
  220. public function testInvalidHash() {
  221. $invalidHash = 'someInvalidHash';
  222. $isValid = $this->hasher->validate($invalidHash);
  223. $this->assertFalse($isValid);
  224. }
  225. }