diff --git a/lib/private/Security/Hasher.php b/lib/private/Security/Hasher.php index c6c9109b33..e20de729f4 100644 --- a/lib/private/Security/Hasher.php +++ b/lib/private/Security/Hasher.php @@ -51,11 +51,9 @@ class Hasher implements IHasher { /** @var IConfig */ private $config; /** @var array Options passed to password_hash and password_needs_rehash */ - private $options = array(); + private $options = []; /** @var string Salt used for legacy passwords */ private $legacySalt = null; - /** @var int Current version of the generated hash */ - private $currentVersion = 1; /** * @param IConfig $config @@ -78,7 +76,11 @@ class Hasher implements IHasher { * @return string Hash of the message with appended version parameter */ public function hash(string $message): string { - return $this->currentVersion . '|' . password_hash($message, PASSWORD_DEFAULT, $this->options); + if (\defined('PASSWORD_ARGON2I')) { + return 2 . '|' . password_hash($message, PASSWORD_ARGON2I, $this->options); + } else { + return 1 . '|' . password_hash($message, PASSWORD_BCRYPT, $this->options); + } } /** @@ -90,7 +92,7 @@ class Hasher implements IHasher { $explodedString = explode('|', $prefixedHash, 2); if(\count($explodedString) === 2) { if((int)$explodedString[0] > 0) { - return array('version' => (int)$explodedString[0], 'hash' => $explodedString[1]); + return ['version' => (int)$explodedString[0], 'hash' => $explodedString[1]]; } } @@ -111,8 +113,8 @@ class Hasher implements IHasher { // 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))) { + if(($hashLength === 60 && password_verify($message.$this->legacySalt, $hash)) || + ($hashLength === 40 && hash_equals($hash, sha1($message)))) { $newHash = $this->hash($message); return true; } @@ -121,7 +123,7 @@ class Hasher implements IHasher { } /** - * Verify V1 hashes + * Verify V1 (blowfish) 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. @@ -129,7 +131,30 @@ class Hasher implements IHasher { */ protected function verifyHashV1(string $message, string $hash, &$newHash = null): bool { if(password_verify($message, $hash)) { - if(password_needs_rehash($hash, PASSWORD_DEFAULT, $this->options)) { + $algo = PASSWORD_BCRYPT; + if (\defined('PASSWORD_ARGON2I')) { + $algo = PASSWORD_ARGON2I; + } + + if(password_needs_rehash($hash, $algo, $this->options)) { + $newHash = $this->hash($message); + } + return true; + } + + return false; + } + + /** + * Verify V2 (argon2i) 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 verifyHashV2(string $message, string $hash, &$newHash = null) : bool { + if(password_verify($message, $hash)) { + if(password_needs_rehash($hash, PASSWORD_ARGON2I, $this->options)) { $newHash = $this->hash($message); } return true; @@ -149,6 +174,8 @@ class Hasher implements IHasher { if(isset($splittedHash['version'])) { switch ($splittedHash['version']) { + case 2: + return $this->verifyHashV2($message, $splittedHash['hash'], $newHash); case 1: return $this->verifyHashV1($message, $splittedHash['hash'], $newHash); } diff --git a/tests/lib/Security/HasherTest.php b/tests/lib/Security/HasherTest.php index 86d4ef6ca0..c994b68f78 100644 --- a/tests/lib/Security/HasherTest.php +++ b/tests/lib/Security/HasherTest.php @@ -21,52 +21,74 @@ class HasherTest extends \Test\TestCase { */ public function versionHashProvider() { - return array( - array('asf32äà$$a.|3', null), - array('asf32äà$$a.|3|5', null), - array('1|2|3|4', array('version' => 1, 'hash' => '2|3|4')), - array('1|我看|这本书。 我看這本書', array('version' => 1, 'hash' => '我看|这本书。 我看這本書')) - ); + return [ + ['asf32äà$$a.|3', null], + ['asf32äà$$a.|3|5', null], + ['1|2|3|4', ['version' => 1, 'hash' => '2|3|4']], + ['1|我看|这本书。 我看這本書', ['version' => 1, 'hash' => '我看|这本书。 我看這本書']], + ['2|newhash', ['version' => 2, 'hash' => 'newhash']], + ]; } /** * @return array */ - public function allHashProviders() + public function hashProviders70_71() { - return array( + return [ // Valid SHA1 strings - array('password', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', true), - array('owncloud.com', '27a4643e43046c3569e33b68c1a4b15d31306d29', true), + ['password', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', true], + ['owncloud.com', '27a4643e43046c3569e33b68c1a4b15d31306d29', true], // Invalid SHA1 strings - array('InvalidString', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', false), - array('AnotherInvalidOne', '27a4643e43046c3569e33b68c1a4b15d31306d29', false), + ['InvalidString', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', false], + ['AnotherInvalidOne', '27a4643e43046c3569e33b68c1a4b15d31306d29', false], // Valid legacy password string with password salt "6Wow67q1wZQZpUUeI6G2LsWUu4XKx" - array('password', '$2a$08$emCpDEl.V.QwPWt5gPrqrOhdpH6ailBmkj2Hd2vD5U8qIy20HBe7.', true), - array('password', '$2a$08$yjaLO4ev70SaOsWZ9gRS3eRSEpHVsmSWTdTms1949mylxJ279hzo2', true), - array('password', '$2a$08$.jNRG/oB4r7gHJhAyb.mDupNUAqTnBIW/tWBqFobaYflKXiFeG0A6', true), - array('owncloud.com', '$2a$08$YbEsyASX/hXVNMv8hXQo7ezreN17T8Jl6PjecGZvpX.Ayz2aUyaZ2', true), - array('owncloud.com', '$2a$11$cHdDA2IkUP28oNGBwlL7jO/U3dpr8/0LIjTZmE8dMPA7OCUQsSTqS', true), - array('owncloud.com', '$2a$08$GH.UoIfJ1e.qeZ85KPqzQe6NR8XWRgJXWIUeE1o/j1xndvyTA1x96', true), + ['password', '$2a$08$emCpDEl.V.QwPWt5gPrqrOhdpH6ailBmkj2Hd2vD5U8qIy20HBe7.', true], + ['password', '$2a$08$yjaLO4ev70SaOsWZ9gRS3eRSEpHVsmSWTdTms1949mylxJ279hzo2', true], + ['password', '$2a$08$.jNRG/oB4r7gHJhAyb.mDupNUAqTnBIW/tWBqFobaYflKXiFeG0A6', true], + ['owncloud.com', '$2a$08$YbEsyASX/hXVNMv8hXQo7ezreN17T8Jl6PjecGZvpX.Ayz2aUyaZ2', true], + ['owncloud.com', '$2a$11$cHdDA2IkUP28oNGBwlL7jO/U3dpr8/0LIjTZmE8dMPA7OCUQsSTqS', true], + ['owncloud.com', '$2a$08$GH.UoIfJ1e.qeZ85KPqzQe6NR8XWRgJXWIUeE1o/j1xndvyTA1x96', true], // Invalid legacy passwords - array('password', '$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false), + ['password', '$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false], // Valid passwords "6Wow67q1wZQZpUUeI6G2LsWUu4XKx" - array('password', '1|$2a$05$ezAE0dkwk57jlfo6z5Pql.gcIK3ReXT15W7ITNxVS0ksfhO/4E4Kq', true), - array('password', '1|$2a$05$4OQmloFW4yTVez2MEWGIleDO9Z5G9tWBXxn1vddogmKBQq/Mq93pe', true), - array('password', '1|$2a$11$yj0hlp6qR32G9exGEXktB.yW2rgt2maRBbPgi3EyxcDwKrD14x/WO', true), - array('owncloud.com', '1|$2a$10$Yiss2WVOqGakxuuqySv5UeOKpF8d8KmNjuAPcBMiRJGizJXjA2bKm', true), - array('owncloud.com', '1|$2a$10$v9mh8/.mF/Ut9jZ7pRnpkuac3bdFCnc4W/gSumheQUi02Sr.xMjPi', true), - array('owncloud.com', '1|$2a$05$ST5E.rplNRfDCzRpzq69leRzsTGtY7k88h9Vy2eWj0Ug/iA9w5kGK', true), + ['password', '1|$2a$05$ezAE0dkwk57jlfo6z5Pql.gcIK3ReXT15W7ITNxVS0ksfhO/4E4Kq', true], + ['password', '1|$2a$05$4OQmloFW4yTVez2MEWGIleDO9Z5G9tWBXxn1vddogmKBQq/Mq93pe', true], + ['password', '1|$2a$11$yj0hlp6qR32G9exGEXktB.yW2rgt2maRBbPgi3EyxcDwKrD14x/WO', true], + ['owncloud.com', '1|$2a$10$Yiss2WVOqGakxuuqySv5UeOKpF8d8KmNjuAPcBMiRJGizJXjA2bKm', true], + ['owncloud.com', '1|$2a$10$v9mh8/.mF/Ut9jZ7pRnpkuac3bdFCnc4W/gSumheQUi02Sr.xMjPi', true], + ['owncloud.com', '1|$2a$05$ST5E.rplNRfDCzRpzq69leRzsTGtY7k88h9Vy2eWj0Ug/iA9w5kGK', true], // Invalid passwords - array('password', '0|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false), - array('password', '1|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false), - array('password', '2|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false), - ); + ['password', '0|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false], + ['password', '1|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false], + ['password', '2|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false], + ]; + } + + + /** + * @return array + */ + public function hashProviders72() { + return [ + // Valid ARGON2 hashes + ['password', '2|$argon2i$v=19$m=1024,t=2,p=2$T3JGcEkxVFNOVktNSjZUcg$4/hyLtSejxNgAuzSFFV/HLM3qRQKBwEtKw61qPN4zWA', true], + ['password', '2|$argon2i$v=19$m=1024,t=2,p=2$Zk52V24yNjMzTkhyYjJKOQ$vmqHkCaOD6SiiiFKD1GeKLg/D1ynWpyZbx4XA2yed34', true], + ['password', '2|$argon2i$v=19$m=1024,t=2,p=2$R1pRcUZKamVlNndBc3l5ag$ToRhR8SiZc7fGMpOYfSc5haS5t9+Y00rljPJV7+qLkM', true], + ['nextcloud.com', '2|$argon2i$v=19$m=1024,t=2,p=2$NC9xM0FFaDlzM01QM3kudg$fSfndwtO2mKMZlKdsT8XAtPY51cSS6pLSGS3xMqeJhg', true], + ['nextcloud.com', '2|$argon2i$v=19$m=1024,t=2,p=2$UjkvUjEuL042WWl1cmdHOA$FZivLkBdZnloQsW6qq/jqWK95JSYUHW9rwQC4Ff9GN0', true], + ['nextcloud.com', '2|$argon2i$v=19$m=1024,t=2,p=2$ZnpNdUlzMEpUTW40OVpiMQ$c+yHT9dtSYsjtVGsa7UKOsxxgQAMiUc781d9WsFACqs', true], + + //Invalid ARGON2 hashes + ['password', '2|$argon2i$v=19$m=1024,t=2,p=2$UjFDUDg3cjBvM3FkbXVOWQ$7Y5xqFxSERnYn+2+7WChUpWZWMa5BEIhSHWnDgJ71Jk', false], + ['password', '2|$argon2i$v=19$m=1024,t=2,p=2$ZUxSUi5aQklXdkcyMG1uVA$sYjoSvXg/CS/aS6Xnas/o9a/OPVcGKldzzmuiCD1Fxo', false], + ['password', '2|$argon2i$v=19$m=1024,t=2,p=2$ZHQ5V0xMOFNmUC52by44Sg$DzQFk3bJTX0J4PVGwW6rMvtnBJRalBkbtpDIXR+d4A0', false], + ]; } /** @var Hasher */ @@ -78,13 +100,12 @@ class HasherTest extends \Test\TestCase { protected function setUp() { parent::setUp(); - $this->config = $this->getMockBuilder(IConfig::class) - ->disableOriginalConstructor()->getMock(); + $this->config = $this->createMock(IConfig::class); $this->hasher = new Hasher($this->config); } - function testHash() { + public function testHash() { $hash = $this->hasher->hash('String To Hash'); $this->assertNotNull($hash); } @@ -92,16 +113,16 @@ class HasherTest extends \Test\TestCase { /** * @dataProvider versionHashProvider */ - function testSplitHash($hash, $expected) { - $relativePath = self::invokePrivate($this->hasher, 'splitHash', array($hash)); + public function testSplitHash($hash, $expected) { + $relativePath = self::invokePrivate($this->hasher, 'splitHash', [$hash]); $this->assertSame($expected, $relativePath); } /** - * @dataProvider allHashProviders + * @dataProvider hashProviders70_71 */ - function testVerify($password, $hash, $expected) { + public function testVerify($password, $hash, $expected) { $this->config ->expects($this->any()) ->method('getSystemValue') @@ -112,4 +133,33 @@ class HasherTest extends \Test\TestCase { $this->assertSame($expected, $result); } + /** + * @dataProvider hashProviders72 + */ + public function testVerifyArgon2i($password, $hash, $expected) { + if (!\defined('PASSWORD_ARGON2I')) { + $this->markTestSkipped('Need ARGON2 support to test ARGON2 hashes'); + } + + $result = $this->hasher->verify($password, $hash); + $this->assertSame($expected, $result); + } + + public function testUpgradeHashBlowFishToArgon2i() { + if (!\defined('PASSWORD_ARGON2I')) { + $this->markTestSkipped('Need ARGON2 support to test ARGON2 hashes'); + } + + $message = 'mysecret'; + + $blowfish = 1 . '|' . password_hash($message, PASSWORD_BCRYPT, []); + $argon2i = 2 . '|' . password_hash($message, PASSWORD_ARGON2I, []); + + $this->assertTrue($this->hasher->verify($message, $blowfish,$newHash)); + $this->assertTrue($this->hasher->verify($message, $argon2i)); + + $relativePath = self::invokePrivate($this->hasher, 'splitHash', [$newHash]); + + $this->assertFalse(password_needs_rehash($relativePath['hash'], PASSWORD_ARGON2I, [])); + } }