Added support for legacy passwords

This commit is contained in:
Marcin Kurczewski 2014-10-05 22:26:56 +02:00
parent a1dfebea3b
commit 185f820e21
10 changed files with 91 additions and 13 deletions

View file

@ -12,6 +12,7 @@ class UserEntityConverter extends AbstractEntityConverter implements IEntityConv
'email' => $entity->getEmail(), 'email' => $entity->getEmail(),
'emailUnconfirmed' => $entity->getEmailUnconfirmed(), 'emailUnconfirmed' => $entity->getEmailUnconfirmed(),
'passwordHash' => $entity->getPasswordHash(), 'passwordHash' => $entity->getPasswordHash(),
'passwordSalt' => $entity->getPasswordSalt(),
'accessRank' => $entity->getAccessRank(), 'accessRank' => $entity->getAccessRank(),
'registrationTime' => $this->entityTimeToDbTime($entity->getRegistrationTime()), 'registrationTime' => $this->entityTimeToDbTime($entity->getRegistrationTime()),
'lastLoginTime' => $this->entityTimeToDbTime($entity->getLastLoginTime()), 'lastLoginTime' => $this->entityTimeToDbTime($entity->getLastLoginTime()),
@ -29,6 +30,7 @@ class UserEntityConverter extends AbstractEntityConverter implements IEntityConv
$entity->setEmail($array['email']); $entity->setEmail($array['email']);
$entity->setEmailUnconfirmed($array['emailUnconfirmed']); $entity->setEmailUnconfirmed($array['emailUnconfirmed']);
$entity->setPasswordHash($array['passwordHash']); $entity->setPasswordHash($array['passwordHash']);
$entity->setPasswordSalt($array['passwordSalt']);
$entity->setAccessRank(intval($array['accessRank'])); $entity->setAccessRank(intval($array['accessRank']));
$entity->setRegistrationTime($this->dbTimeToEntityTime($array['registrationTime'])); $entity->setRegistrationTime($this->dbTimeToEntityTime($array['registrationTime']));
$entity->setLastLoginTime($this->dbTimeToEntityTime($array['lastLoginTime'])); $entity->setLastLoginTime($this->dbTimeToEntityTime($array['lastLoginTime']));

View file

@ -20,6 +20,7 @@ final class User extends Entity
protected $email; protected $email;
protected $emailUnconfirmed; protected $emailUnconfirmed;
protected $passwordHash; protected $passwordHash;
protected $passwordSalt;
protected $accessRank; protected $accessRank;
protected $registrationTime; protected $registrationTime;
protected $lastLoginTime; protected $lastLoginTime;
@ -88,6 +89,16 @@ final class User extends Entity
$this->passwordHash = $passwordHash; $this->passwordHash = $passwordHash;
} }
public function getPasswordSalt()
{
return $this->passwordSalt;
}
public function setPasswordSalt($passwordSalt)
{
$this->passwordSalt = $passwordSalt;
}
public function getAccessRank() public function getAccessRank()
{ {
return $this->accessRank; return $this->accessRank;

View file

@ -48,8 +48,12 @@ class AuthService
$user = $this->userService->getByNameOrEmail($formData->userNameOrEmail); $user = $this->userService->getByNameOrEmail($formData->userNameOrEmail);
$this->doFinalChecksOnUser($user); $this->doFinalChecksOnUser($user);
$passwordHash = $this->passwordService->getHash($formData->password); $hashValid = $this->passwordService->isHashValid(
if ($user->getPasswordHash() !== $passwordHash) $formData->password,
$user->getPasswordSalt(),
$user->getPasswordHash());
if (!$hashValid)
throw new \InvalidArgumentException('Specified password is invalid.'); throw new \InvalidArgumentException('Specified password is invalid.');
$this->loginToken = $this->createAndSaveLoginToken($user); $this->loginToken = $this->createAndSaveLoginToken($user);

View file

@ -19,9 +19,25 @@ class PasswordService
$this->pattern = str_split('cvcvnncvcv'); $this->pattern = str_split('cvcvnncvcv');
} }
public function getHash($password) public function getLegacyHash($password, $salt)
{ {
return hash('sha256', $this->config->security->secret . '/' . $password); //hash used by old szurubooru version
return sha1('1A2/$_4xVa' . $salt . $password);
}
public function getHash($password, $salt)
{
return hash('sha256', $this->config->security->secret . $salt . $password);
}
public function isHashValid($password, $salt, $expectedPasswordHash)
{
$hashes =
[
$this->getLegacyHash($password, $salt),
$this->getHash($password, $salt),
];
return in_array($expectedPasswordHash, $hashes);
} }
public function getRandomPassword() public function getRandomPassword()

View file

@ -100,6 +100,7 @@ class UserService
$user->setAccessRank($this->userDao->hasAnyUsers() $user->setAccessRank($this->userDao->hasAnyUsers()
? \Szurubooru\Entities\User::ACCESS_RANK_REGULAR_USER ? \Szurubooru\Entities\User::ACCESS_RANK_REGULAR_USER
: \Szurubooru\Entities\User::ACCESS_RANK_ADMINISTRATOR); : \Szurubooru\Entities\User::ACCESS_RANK_ADMINISTRATOR);
$user->setPasswordSalt($this->passwordService->getRandomPassword());
$this->updateUserName($user, $formData->userName); $this->updateUserName($user, $formData->userName);
$this->updateUserPassword($user, $formData->password); $this->updateUserPassword($user, $formData->password);
@ -173,7 +174,7 @@ class UserService
$user = $this->getByName($token->getAdditionalData()); $user = $this->getByName($token->getAdditionalData());
$newPassword = $this->passwordService->getRandomPassword(); $newPassword = $this->passwordService->getRandomPassword();
$user->setPasswordHash($this->passwordService->getHash($newPassword)); $this->updateUserPassword($user, $newPassword);
$this->userDao->save($user); $this->userDao->save($user);
$this->tokenService->invalidateByName($token->getName()); $this->tokenService->invalidateByName($token->getName());
return $newPassword; return $newPassword;
@ -231,7 +232,7 @@ class UserService
private function updateUserPassword(\Szurubooru\Entities\User $user, $newPassword) private function updateUserPassword(\Szurubooru\Entities\User $user, $newPassword)
{ {
$user->setPasswordHash($this->passwordService->getHash($newPassword)); $user->setPasswordHash($this->passwordService->getHash($newPassword, $user->getPasswordSalt()));
} }
private function updateUserEmail(\Szurubooru\Entities\User $user, $newEmail) private function updateUserEmail(\Szurubooru\Entities\User $user, $newEmail)

View file

@ -0,0 +1,13 @@
<?php
namespace Szurubooru\Upgrades;
class Upgrade17 implements IUpgrade
{
public function run(\Szurubooru\DatabaseConnection $databaseConnection)
{
$pdo = $databaseConnection->getPDO();
$pdo->exec('ALTER TABLE users ADD COLUMN passwordSalt VARCHAR(32)');
$pdo->exec('UPDATE users SET passwordSalt = "/"');
}
}

View file

@ -32,6 +32,7 @@ return [
$container->get(\Szurubooru\Upgrades\Upgrade14::class), $container->get(\Szurubooru\Upgrades\Upgrade14::class),
$container->get(\Szurubooru\Upgrades\Upgrade15::class), $container->get(\Szurubooru\Upgrades\Upgrade15::class),
$container->get(\Szurubooru\Upgrades\Upgrade16::class), $container->get(\Szurubooru\Upgrades\Upgrade16::class),
$container->get(\Szurubooru\Upgrades\Upgrade17::class),
]; ];
}), }),

View file

@ -22,11 +22,12 @@ class AuthServiceTest extends \Szurubooru\Tests\AbstractTestCase
public function testInvalidPassword() public function testInvalidPassword()
{ {
$this->configMock->set('security/needEmailActivationToRegister', false); $this->configMock->set('security/needEmailActivationToRegister', false);
$this->passwordServiceMock->expects($this->once())->method('getHash')->willReturn('unmatchingHash'); $this->passwordServiceMock->expects($this->once())->method('isHashValid')->with('godzilla', 'salt', 'hash')->willReturn(false);
$testUser = new \Szurubooru\Entities\User(); $testUser = new \Szurubooru\Entities\User();
$testUser->setName('dummy'); $testUser->setName('dummy');
$testUser->setPasswordHash('hash'); $testUser->setPasswordHash('hash');
$testUser->setPasswordSalt('salt');
$this->userServiceMock->expects($this->once())->method('getByNameOrEmail')->willReturn($testUser); $this->userServiceMock->expects($this->once())->method('getByNameOrEmail')->willReturn($testUser);
$this->setExpectedException(\Exception::class, 'Specified password is invalid'); $this->setExpectedException(\Exception::class, 'Specified password is invalid');
@ -40,11 +41,12 @@ class AuthServiceTest extends \Szurubooru\Tests\AbstractTestCase
public function testValidCredentials() public function testValidCredentials()
{ {
$this->configMock->set('security/needEmailActivationToRegister', false); $this->configMock->set('security/needEmailActivationToRegister', false);
$this->passwordServiceMock->expects($this->once())->method('getHash')->willReturn('hash'); $this->passwordServiceMock->expects($this->once())->method('isHashValid')->with('godzilla', 'salt', 'hash')->willReturn(true);
$testUser = new \Szurubooru\Entities\User('an unusual database identifier'); $testUser = new \Szurubooru\Entities\User('an unusual database identifier');
$testUser->setName('dummy'); $testUser->setName('dummy');
$testUser->setPasswordHash('hash'); $testUser->setPasswordHash('hash');
$testUser->setPasswordSalt('salt');
$this->userServiceMock->expects($this->once())->method('getByNameOrEmail')->willReturn($testUser); $this->userServiceMock->expects($this->once())->method('getByNameOrEmail')->willReturn($testUser);
$testToken = new \Szurubooru\Entities\Token(); $testToken = new \Szurubooru\Entities\Token();
@ -68,7 +70,7 @@ class AuthServiceTest extends \Szurubooru\Tests\AbstractTestCase
public function testValidCredentialsUnconfirmedEmail() public function testValidCredentialsUnconfirmedEmail()
{ {
$this->configMock->set('security/needEmailActivationToRegister', true); $this->configMock->set('security/needEmailActivationToRegister', true);
$this->passwordServiceMock->expects($this->never())->method('getHash')->willReturn('hash'); $this->passwordServiceMock->expects($this->never())->method('isHashValid')->willReturn('hash');
$testUser = new \Szurubooru\Entities\User(); $testUser = new \Szurubooru\Entities\User();
$testUser->setName('dummy'); $testUser->setName('dummy');

View file

@ -3,10 +3,31 @@ namespace Szurubooru\Tests\Service;
class PasswordServiceTest extends \Szurubooru\Tests\AbstractTestCase class PasswordServiceTest extends \Szurubooru\Tests\AbstractTestCase
{ {
private $configMock;
public function setUp()
{
parent::setUp();
$this->configMock = $this->mockConfig();
}
public function testLegacyPasswordValidation()
{
$passwordService = $this->getPasswordService();
$this->configMock->set('security/secret', 'doesnt matter');
$this->assertTrue($passwordService->isHashValid('testt', 'ac63e0bcdf20b82db509d123166c4592', '2602572e077d48b35af39d1cff84bfcaa5363116'));
}
public function testPasswordValidation()
{
$passwordService = $this->getPasswordService();
$this->configMock->set('security/secret', 'change');
$this->assertTrue($passwordService->isHashValid('testt', '/', '4f4f8b836cd65f3f1d0b7751fc442f79595c23439cd8a928af15e10807bf08cc'));
}
public function testGeneratingPasswords() public function testGeneratingPasswords()
{ {
$configMock = $this->mockConfig(); $passwordService = $this->getPasswordService();
$passwordService = new \Szurubooru\Services\PasswordService($configMock);
$sampleCount = 10000; $sampleCount = 10000;
$distribution = []; $distribution = [];
@ -44,4 +65,9 @@ class PasswordServiceTest extends \Szurubooru\Tests\AbstractTestCase
$mean = array_sum($sample) / count($sample); $mean = array_sum($sample) / count($sample);
return 100 * $this->getStandardDeviation($sample) / $mean; return 100 * $this->getStandardDeviation($sample) / $mean;
} }
private function getPasswordService()
{
return new \Szurubooru\Services\PasswordService($this->configMock);
}
} }

View file

@ -73,7 +73,8 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
$formData->email = 'human@people.gov'; $formData->email = 'human@people.gov';
$this->configMock->set('security/needEmailActivationToRegister', false); $this->configMock->set('security/needEmailActivationToRegister', false);
$this->passwordServiceMock->expects($this->once())->method('getHash')->willReturn('hash'); $this->passwordServiceMock->expects($this->once())->method('getRandomPassword')->willReturn('salt');
$this->passwordServiceMock->expects($this->once())->method('getHash')->with('password', 'salt')->willReturn('hash');
$this->timeServiceMock->expects($this->once())->method('getCurrentTime')->willReturn('now'); $this->timeServiceMock->expects($this->once())->method('getCurrentTime')->willReturn('now');
$this->userDaoMock->expects($this->once())->method('hasAnyUsers')->willReturn(true); $this->userDaoMock->expects($this->once())->method('hasAnyUsers')->willReturn(true);
$this->userDaoMock->expects($this->once())->method('save')->will($this->returnArgument(0)); $this->userDaoMock->expects($this->once())->method('save')->will($this->returnArgument(0));
@ -99,7 +100,8 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
$formData->email = 'human@people.gov'; $formData->email = 'human@people.gov';
$this->configMock->set('security/needEmailActivationToRegister', true); $this->configMock->set('security/needEmailActivationToRegister', true);
$this->passwordServiceMock->expects($this->once())->method('getHash')->willReturn('hash'); $this->passwordServiceMock->expects($this->once())->method('getRandomPassword')->willReturn('salt');
$this->passwordServiceMock->expects($this->once())->method('getHash')->with('password', 'salt')->willReturn('hash');
$this->timeServiceMock->expects($this->once())->method('getCurrentTime')->willReturn('now'); $this->timeServiceMock->expects($this->once())->method('getCurrentTime')->willReturn('now');
$this->userDaoMock->expects($this->once())->method('hasAnyUsers')->willReturn(true); $this->userDaoMock->expects($this->once())->method('hasAnyUsers')->willReturn(true);
$this->userDaoMock->expects($this->once())->method('save')->will($this->returnArgument(0)); $this->userDaoMock->expects($this->once())->method('save')->will($this->returnArgument(0));