Allow selecting the hashing algorithm
Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
This commit is contained in:
parent
9bd4322558
commit
9687febed7
3 changed files with 100 additions and 13 deletions
|
@ -1435,6 +1435,16 @@ $CONFIG = array(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hashing
|
* Hashing
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default Nextcloud will use the Argon2 password hashing if available.
|
||||||
|
* However if for whatever reason you want to stick with the PASSWORD_DEFAULT
|
||||||
|
* of your php version. Then set the setting to true.
|
||||||
|
*/
|
||||||
|
'hashing_default_password' => false,
|
||||||
|
|
||||||
|
/**
|
||||||
*
|
*
|
||||||
* Nextcloud uses the Argon2 algorithm (with PHP >= 7.2) to create hashes by its
|
* Nextcloud uses the Argon2 algorithm (with PHP >= 7.2) to create hashes by its
|
||||||
* own and exposes its configuration options as following. More information can
|
* own and exposes its configuration options as following. More information can
|
||||||
|
|
|
@ -92,11 +92,13 @@ 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 {
|
||||||
if (\defined('PASSWORD_ARGON2I')) {
|
$alg = $this->getPrefferedAlgorithm();
|
||||||
|
|
||||||
|
if (\defined('PASSWORD_ARGON2I') && $alg === PASSWORD_ARGON2I) {
|
||||||
return 2 . '|' . password_hash($message, PASSWORD_ARGON2I, $this->options);
|
return 2 . '|' . password_hash($message, PASSWORD_ARGON2I, $this->options);
|
||||||
} else {
|
|
||||||
return 1 . '|' . password_hash($message, PASSWORD_BCRYPT, $this->options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return 1 . '|' . password_hash($message, PASSWORD_BCRYPT, $this->options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -147,12 +149,7 @@ 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)) {
|
||||||
$algo = PASSWORD_BCRYPT;
|
if ($this->needsRehash($hash)) {
|
||||||
if (\defined('PASSWORD_ARGON2I')) {
|
|
||||||
$algo = PASSWORD_ARGON2I;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(password_needs_rehash($hash, $algo, $this->options)) {
|
|
||||||
$newHash = $this->hash($message);
|
$newHash = $this->hash($message);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -170,7 +167,7 @@ class Hasher implements IHasher {
|
||||||
*/
|
*/
|
||||||
protected function verifyHashV2(string $message, string $hash, &$newHash = null) : bool {
|
protected function verifyHashV2(string $message, string $hash, &$newHash = null) : bool {
|
||||||
if(password_verify($message, $hash)) {
|
if(password_verify($message, $hash)) {
|
||||||
if(password_needs_rehash($hash, PASSWORD_ARGON2I, $this->options)) {
|
if($this->needsRehash($hash)) {
|
||||||
$newHash = $this->hash($message);
|
$newHash = $this->hash($message);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -199,8 +196,27 @@ class Hasher implements IHasher {
|
||||||
return $this->legacyHashVerify($message, $hash, $newHash);
|
return $this->legacyHashVerify($message, $hash, $newHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function needsRehash(string $hash): bool {
|
||||||
|
$algorithm = $this->getPrefferedAlgorithm();
|
||||||
|
|
||||||
|
return password_needs_rehash($hash, $algorithm, $this->options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPrefferedAlgorithm() {
|
||||||
|
$default = PASSWORD_BCRYPT;
|
||||||
|
if (\defined('PASSWORD_ARGON2I')) {
|
||||||
|
$default = PASSWORD_ARGON2I;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we should use PASSWORD_DEFAULT
|
||||||
|
if ($this->config->getSystemValue('hashing_default_password', false) === true) {
|
||||||
|
$default = PASSWORD_DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,8 +126,12 @@ class HasherTest extends \Test\TestCase {
|
||||||
$this->config
|
$this->config
|
||||||
->expects($this->any())
|
->expects($this->any())
|
||||||
->method('getSystemValue')
|
->method('getSystemValue')
|
||||||
->with('passwordsalt', null)
|
->willReturnCallback(function ($key, $default) {
|
||||||
->will($this->returnValue('6Wow67q1wZQZpUUeI6G2LsWUu4XKx'));
|
if($key === 'passwordsalt') {
|
||||||
|
return '6Wow67q1wZQZpUUeI6G2LsWUu4XKx';
|
||||||
|
}
|
||||||
|
return $default;
|
||||||
|
});
|
||||||
|
|
||||||
$result = $this->hasher->verify($password, $hash);
|
$result = $this->hasher->verify($password, $hash);
|
||||||
$this->assertSame($expected, $result);
|
$this->assertSame($expected, $result);
|
||||||
|
@ -162,4 +166,61 @@ class HasherTest extends \Test\TestCase {
|
||||||
|
|
||||||
$this->assertFalse(password_needs_rehash($relativePath['hash'], PASSWORD_ARGON2I, []));
|
$this->assertFalse(password_needs_rehash($relativePath['hash'], PASSWORD_ARGON2I, []));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testUsePasswordDefaultArgon2iVerify() {
|
||||||
|
if (!\defined('PASSWORD_ARGON2I')) {
|
||||||
|
$this->markTestSkipped('Need ARGON2 support to test ARGON2 hashes');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->config->method('getSystemValue')
|
||||||
|
->with('hashing_default_password')
|
||||||
|
->willReturn(true);
|
||||||
|
|
||||||
|
$message = 'mysecret';
|
||||||
|
|
||||||
|
$argon2i = 2 . '|' . password_hash($message, PASSWORD_ARGON2I, []);
|
||||||
|
|
||||||
|
$newHash = null;
|
||||||
|
$this->assertTrue($this->hasher->verify($message, $argon2i, $newHash));
|
||||||
|
$this->assertNotNull($newHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDoNotUserPasswordDefaultArgon2iVerify() {
|
||||||
|
if (!\defined('PASSWORD_ARGON2I')) {
|
||||||
|
$this->markTestSkipped('Need ARGON2 support to test ARGON2 hashes');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->config->method('getSystemValue')
|
||||||
|
->with('hashing_default_password')
|
||||||
|
->willReturn(false);
|
||||||
|
|
||||||
|
$message = 'mysecret';
|
||||||
|
|
||||||
|
$argon2i = 2 . '|' . password_hash($message, PASSWORD_ARGON2I, []);
|
||||||
|
|
||||||
|
$newHash = null;
|
||||||
|
$this->assertTrue($this->hasher->verify($message, $argon2i, $newHash));
|
||||||
|
$this->assertNull($newHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHashUsePasswordDefault() {
|
||||||
|
if (!\defined('PASSWORD_ARGON2I')) {
|
||||||
|
$this->markTestSkipped('Need ARGON2 support to test ARGON2 hashes');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->config->method('getSystemValue')
|
||||||
|
->with('hashing_default_password')
|
||||||
|
->willReturn(true);
|
||||||
|
|
||||||
|
$message = 'mysecret';
|
||||||
|
|
||||||
|
$hash = $this->hasher->hash($message);
|
||||||
|
$relativePath = self::invokePrivate($this->hasher, 'splitHash', [$hash]);
|
||||||
|
|
||||||
|
$this->assertSame(1, $relativePath['version']);
|
||||||
|
|
||||||
|
$info = password_get_info($relativePath['hash']);
|
||||||
|
$this->assertEquals(PASSWORD_BCRYPT, $info['algo']);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue