'ed', 'password' => $hash, 'email' => 'ed@example.org'); } private function storedHash(): string { return SiteData::$tables['editors'][0]['password']; } public function testAlgorithmIsArgon2idWhenAvailable(): void { $this->assertSame(defined('PASSWORD_ARGON2ID') ? PASSWORD_ARGON2ID : PASSWORD_BCRYPT, password_algorithm()); } public function testEditorPasswordValid(): void { $hash = password_hash('secret', password_algorithm()); $this->assertTrue(editor_password_valid('secret', $hash)); $this->assertFalse(editor_password_valid('wrong', $hash)); $this->assertFalse(editor_password_valid('', '')); $this->assertFalse(editor_password_valid('secret', NULL)); $this->assertFalse(editor_password_valid('', NULL)); } // A fast answer for an unknown username would show which usernames exist public function testAnUnknownEditorTakesAsLongAsAWrongPassword(): void { $hash = password_hash('secret', password_algorithm()); $time = function ($stored) { $best = INF; for ($i = 0; $i < 3; $i++) { $start = hrtime(TRUE); editor_password_valid('wrong', $stored); $best = min($best, hrtime(TRUE) - $start); } return $best; }; $this->assertGreaterThan($time($hash) / 2, $time(NULL)); } public function testHashIsStoredAndChecked(): void { $this->addEditor('old'); $hash = password_hash('secret', password_algorithm()); $this->assertTrue(store_password_hash('ed', $hash)); $this->assertSame($hash, $this->storedHash()); $this->assertTrue(password_hash_is_stored('ed', $hash)); $this->assertFalse(password_hash_is_stored('ed', 'other')); $this->assertFalse(password_hash_is_stored('nobody', $hash)); } public function testCutHashIsNotStored(): void { $this->addEditor('old'); SiteData::$passwordLength = 60; $hash = str_repeat('h', 97); $this->assertFalse(store_password_hash('ed', $hash)); } public function testOldMd5CryptHashIsUpgraded(): void { $old = crypt('secret', '$1$abcdefgh$'); $this->assertTrue(password_verify('secret', $old), 'password_verify() still accepts the old hashes'); $this->addEditor($old); rehash_password('ed', 'secret', $old); $new = $this->storedHash(); $this->assertNotSame($old, $new); $this->assertTrue(password_verify('secret', $new)); $this->assertFalse(password_needs_rehash($new, password_algorithm())); } public function testCurrentHashIsLeftAlone(): void { $hash = password_hash('secret', password_algorithm()); $this->addEditor($hash); rehash_password('ed', 'secret', $hash); $this->assertSame($hash, $this->storedHash()); $this->assertSame('', $this->loggedErrors()); } public function testShortColumnKeepsTheOldHash(): void { $old = crypt('secret', '$1$abcdefgh$'); $this->addEditor($old); SiteData::$passwordLength = 50; rehash_password('ed', 'secret', $old); $this->assertSame($old, $this->storedHash()); $this->assertStringContainsString('could not store a new password hash for ed; widen editors.password', $this->loggedErrors()); } }