Merge pull request #9074 from nextcloud/ARGON2I
Add ARGON2I support to the hasher
This commit is contained in:
commit
185a260f6b
|
@ -51,11 +51,9 @@ class Hasher implements IHasher {
|
||||||
/** @var IConfig */
|
/** @var IConfig */
|
||||||
private $config;
|
private $config;
|
||||||
/** @var array Options passed to password_hash and password_needs_rehash */
|
/** @var array Options passed to password_hash and password_needs_rehash */
|
||||||
private $options = array();
|
private $options = [];
|
||||||
/** @var string Salt used for legacy passwords */
|
/** @var string Salt used for legacy passwords */
|
||||||
private $legacySalt = null;
|
private $legacySalt = null;
|
||||||
/** @var int Current version of the generated hash */
|
|
||||||
private $currentVersion = 1;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param IConfig $config
|
* @param IConfig $config
|
||||||
|
@ -78,7 +76,11 @@ class Hasher implements IHasher {
|
||||||
* @return string Hash of the message with appended version parameter
|
* @return string Hash of the message with appended version parameter
|
||||||
*/
|
*/
|
||||||
public function hash(string $message): string {
|
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);
|
$explodedString = explode('|', $prefixedHash, 2);
|
||||||
if(\count($explodedString) === 2) {
|
if(\count($explodedString) === 2) {
|
||||||
if((int)$explodedString[0] > 0) {
|
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
|
// Verify whether it matches a legacy PHPass or SHA1 string
|
||||||
$hashLength = \strlen($hash);
|
$hashLength = \strlen($hash);
|
||||||
if($hashLength === 60 && password_verify($message.$this->legacySalt, $hash) ||
|
if(($hashLength === 60 && password_verify($message.$this->legacySalt, $hash)) ||
|
||||||
$hashLength === 40 && hash_equals($hash, sha1($message))) {
|
($hashLength === 40 && hash_equals($hash, sha1($message)))) {
|
||||||
$newHash = $this->hash($message);
|
$newHash = $this->hash($message);
|
||||||
return true;
|
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 $message Message to verify
|
||||||
* @param string $hash Assumed hash of the message
|
* @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.
|
* @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 {
|
protected function verifyHashV1(string $message, string $hash, &$newHash = null): bool {
|
||||||
if(password_verify($message, $hash)) {
|
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);
|
$newHash = $this->hash($message);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -149,6 +174,8 @@ class Hasher implements IHasher {
|
||||||
|
|
||||||
if(isset($splittedHash['version'])) {
|
if(isset($splittedHash['version'])) {
|
||||||
switch ($splittedHash['version']) {
|
switch ($splittedHash['version']) {
|
||||||
|
case 2:
|
||||||
|
return $this->verifyHashV2($message, $splittedHash['hash'], $newHash);
|
||||||
case 1:
|
case 1:
|
||||||
return $this->verifyHashV1($message, $splittedHash['hash'], $newHash);
|
return $this->verifyHashV1($message, $splittedHash['hash'], $newHash);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,52 +21,74 @@ class HasherTest extends \Test\TestCase {
|
||||||
*/
|
*/
|
||||||
public function versionHashProvider()
|
public function versionHashProvider()
|
||||||
{
|
{
|
||||||
return array(
|
return [
|
||||||
array('asf32äà$$a.|3', null),
|
['asf32äà$$a.|3', null],
|
||||||
array('asf32äà$$a.|3|5', null),
|
['asf32äà$$a.|3|5', null],
|
||||||
array('1|2|3|4', array('version' => 1, 'hash' => '2|3|4')),
|
['1|2|3|4', ['version' => 1, 'hash' => '2|3|4']],
|
||||||
array('1|我看|这本书。 我看這本書', array('version' => 1, 'hash' => '我看|这本书。 我看這本書'))
|
['1|我看|这本书。 我看這本書', ['version' => 1, 'hash' => '我看|这本书。 我看這本書']],
|
||||||
);
|
['2|newhash', ['version' => 2, 'hash' => 'newhash']],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function allHashProviders()
|
public function hashProviders70_71()
|
||||||
{
|
{
|
||||||
return array(
|
return [
|
||||||
// Valid SHA1 strings
|
// Valid SHA1 strings
|
||||||
array('password', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', true),
|
['password', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', true],
|
||||||
array('owncloud.com', '27a4643e43046c3569e33b68c1a4b15d31306d29', true),
|
['owncloud.com', '27a4643e43046c3569e33b68c1a4b15d31306d29', true],
|
||||||
|
|
||||||
// Invalid SHA1 strings
|
// Invalid SHA1 strings
|
||||||
array('InvalidString', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', false),
|
['InvalidString', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', false],
|
||||||
array('AnotherInvalidOne', '27a4643e43046c3569e33b68c1a4b15d31306d29', false),
|
['AnotherInvalidOne', '27a4643e43046c3569e33b68c1a4b15d31306d29', false],
|
||||||
|
|
||||||
// Valid legacy password string with password salt "6Wow67q1wZQZpUUeI6G2LsWUu4XKx"
|
// Valid legacy password string with password salt "6Wow67q1wZQZpUUeI6G2LsWUu4XKx"
|
||||||
array('password', '$2a$08$emCpDEl.V.QwPWt5gPrqrOhdpH6ailBmkj2Hd2vD5U8qIy20HBe7.', true),
|
['password', '$2a$08$emCpDEl.V.QwPWt5gPrqrOhdpH6ailBmkj2Hd2vD5U8qIy20HBe7.', true],
|
||||||
array('password', '$2a$08$yjaLO4ev70SaOsWZ9gRS3eRSEpHVsmSWTdTms1949mylxJ279hzo2', true),
|
['password', '$2a$08$yjaLO4ev70SaOsWZ9gRS3eRSEpHVsmSWTdTms1949mylxJ279hzo2', true],
|
||||||
array('password', '$2a$08$.jNRG/oB4r7gHJhAyb.mDupNUAqTnBIW/tWBqFobaYflKXiFeG0A6', true),
|
['password', '$2a$08$.jNRG/oB4r7gHJhAyb.mDupNUAqTnBIW/tWBqFobaYflKXiFeG0A6', true],
|
||||||
array('owncloud.com', '$2a$08$YbEsyASX/hXVNMv8hXQo7ezreN17T8Jl6PjecGZvpX.Ayz2aUyaZ2', true),
|
['owncloud.com', '$2a$08$YbEsyASX/hXVNMv8hXQo7ezreN17T8Jl6PjecGZvpX.Ayz2aUyaZ2', true],
|
||||||
array('owncloud.com', '$2a$11$cHdDA2IkUP28oNGBwlL7jO/U3dpr8/0LIjTZmE8dMPA7OCUQsSTqS', true),
|
['owncloud.com', '$2a$11$cHdDA2IkUP28oNGBwlL7jO/U3dpr8/0LIjTZmE8dMPA7OCUQsSTqS', true],
|
||||||
array('owncloud.com', '$2a$08$GH.UoIfJ1e.qeZ85KPqzQe6NR8XWRgJXWIUeE1o/j1xndvyTA1x96', true),
|
['owncloud.com', '$2a$08$GH.UoIfJ1e.qeZ85KPqzQe6NR8XWRgJXWIUeE1o/j1xndvyTA1x96', true],
|
||||||
|
|
||||||
// Invalid legacy passwords
|
// Invalid legacy passwords
|
||||||
array('password', '$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false),
|
['password', '$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false],
|
||||||
|
|
||||||
// Valid passwords "6Wow67q1wZQZpUUeI6G2LsWUu4XKx"
|
// Valid passwords "6Wow67q1wZQZpUUeI6G2LsWUu4XKx"
|
||||||
array('password', '1|$2a$05$ezAE0dkwk57jlfo6z5Pql.gcIK3ReXT15W7ITNxVS0ksfhO/4E4Kq', true),
|
['password', '1|$2a$05$ezAE0dkwk57jlfo6z5Pql.gcIK3ReXT15W7ITNxVS0ksfhO/4E4Kq', true],
|
||||||
array('password', '1|$2a$05$4OQmloFW4yTVez2MEWGIleDO9Z5G9tWBXxn1vddogmKBQq/Mq93pe', true),
|
['password', '1|$2a$05$4OQmloFW4yTVez2MEWGIleDO9Z5G9tWBXxn1vddogmKBQq/Mq93pe', true],
|
||||||
array('password', '1|$2a$11$yj0hlp6qR32G9exGEXktB.yW2rgt2maRBbPgi3EyxcDwKrD14x/WO', true),
|
['password', '1|$2a$11$yj0hlp6qR32G9exGEXktB.yW2rgt2maRBbPgi3EyxcDwKrD14x/WO', true],
|
||||||
array('owncloud.com', '1|$2a$10$Yiss2WVOqGakxuuqySv5UeOKpF8d8KmNjuAPcBMiRJGizJXjA2bKm', true),
|
['owncloud.com', '1|$2a$10$Yiss2WVOqGakxuuqySv5UeOKpF8d8KmNjuAPcBMiRJGizJXjA2bKm', true],
|
||||||
array('owncloud.com', '1|$2a$10$v9mh8/.mF/Ut9jZ7pRnpkuac3bdFCnc4W/gSumheQUi02Sr.xMjPi', true),
|
['owncloud.com', '1|$2a$10$v9mh8/.mF/Ut9jZ7pRnpkuac3bdFCnc4W/gSumheQUi02Sr.xMjPi', true],
|
||||||
array('owncloud.com', '1|$2a$05$ST5E.rplNRfDCzRpzq69leRzsTGtY7k88h9Vy2eWj0Ug/iA9w5kGK', true),
|
['owncloud.com', '1|$2a$05$ST5E.rplNRfDCzRpzq69leRzsTGtY7k88h9Vy2eWj0Ug/iA9w5kGK', true],
|
||||||
|
|
||||||
// Invalid passwords
|
// Invalid passwords
|
||||||
array('password', '0|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false),
|
['password', '0|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false],
|
||||||
array('password', '1|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false),
|
['password', '1|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false],
|
||||||
array('password', '2|$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 */
|
/** @var Hasher */
|
||||||
|
@ -78,13 +100,12 @@ class HasherTest extends \Test\TestCase {
|
||||||
protected function setUp() {
|
protected function setUp() {
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
$this->config = $this->getMockBuilder(IConfig::class)
|
$this->config = $this->createMock(IConfig::class);
|
||||||
->disableOriginalConstructor()->getMock();
|
|
||||||
|
|
||||||
$this->hasher = new Hasher($this->config);
|
$this->hasher = new Hasher($this->config);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testHash() {
|
public function testHash() {
|
||||||
$hash = $this->hasher->hash('String To Hash');
|
$hash = $this->hasher->hash('String To Hash');
|
||||||
$this->assertNotNull($hash);
|
$this->assertNotNull($hash);
|
||||||
}
|
}
|
||||||
|
@ -92,16 +113,16 @@ class HasherTest extends \Test\TestCase {
|
||||||
/**
|
/**
|
||||||
* @dataProvider versionHashProvider
|
* @dataProvider versionHashProvider
|
||||||
*/
|
*/
|
||||||
function testSplitHash($hash, $expected) {
|
public function testSplitHash($hash, $expected) {
|
||||||
$relativePath = self::invokePrivate($this->hasher, 'splitHash', array($hash));
|
$relativePath = self::invokePrivate($this->hasher, 'splitHash', [$hash]);
|
||||||
$this->assertSame($expected, $relativePath);
|
$this->assertSame($expected, $relativePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dataProvider allHashProviders
|
* @dataProvider hashProviders70_71
|
||||||
*/
|
*/
|
||||||
function testVerify($password, $hash, $expected) {
|
public function testVerify($password, $hash, $expected) {
|
||||||
$this->config
|
$this->config
|
||||||
->expects($this->any())
|
->expects($this->any())
|
||||||
->method('getSystemValue')
|
->method('getSystemValue')
|
||||||
|
@ -112,4 +133,33 @@ class HasherTest extends \Test\TestCase {
|
||||||
$this->assertSame($expected, $result);
|
$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, []));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue