Skip to content
Snippets Groups Projects
Unverified Commit 8f7ef73c authored by James Gilliland's avatar James Gilliland
Browse files

New service that supports Argon

New password service that supports argon and should generally be future
proof for future algorithms as it offloads options into the service
definition.
parent 1a07f442
No related branches found
No related merge requests found
......@@ -14,6 +14,9 @@ services:
password.php:
class: Drupal\php_password\Password\PhpPassword
arguments: ['%password_hash_cost%']
password.php10:
class: Drupal\php_password\Password\PhpPassword2
arguments: ['%password_hash_cost%']
password.drupal7:
class: Drupal\Core\Password\PhpassHashedPassword
arguments: [16]
......@@ -52,6 +52,7 @@ public function check($password, $hash): bool {
$prefix = substr($hash, 0, 2);
if ($prefix === 'U$') {
$hash = substr($hash, 1);
$prefix = substr($hash, 0, 2);
$password = md5($password);
}
......
......@@ -2,36 +2,12 @@
namespace Drupal\php_password\Password;
use Drupal\Core\Password\PasswordInterface;
/**
* Secure password hashing functions based on native PHP password hashing.
*
* @see http://php.net/manual/en/ref.password.php
*/
class PhpPassword implements PasswordInterface {
/**
* The algorithmic cost that should be used.
*
* This is the same 'cost' option as is used by password_hash().
*
* @var int
*
* @see password_hash().
* @see http://php.net/manual/en/ref.password.php
*/
protected int $cost;
/**
* The algorithm constant used to hash password.
*
* @var int|string|null
*
* @see password_hash().
* @see http://php.net/manual/en/password.constants.php
*/
protected int|string|null $algorithm;
class PhpPassword extends PhpPassword2 {
/**
* Constructs a new password hashing instance.
......@@ -41,56 +17,8 @@ class PhpPassword implements PasswordInterface {
* @param int|string|null $algorithm
* The hashing algorithm to use. Defaults to php default.
*/
public function __construct(
int $cost,
int|string|null $algorithm = PASSWORD_DEFAULT
) {
$this->cost = $cost;
$this->algorithm = $algorithm;
}
/**
* {@inheritdoc}
*/
public function hash($password): bool|string {
// Prevent DoS attacks by refusing to hash large passwords.
if (strlen($password) > static::PASSWORD_MAX_LENGTH) {
return FALSE;
}
return password_hash($password, $this->algorithm, $this->getOptions());
}
/**
* {@inheritdoc}
*/
public function check($password, $hash): bool {
return password_verify($password, $hash);
}
/**
* {@inheritdoc}
*/
public function needsRehash($hash): bool {
// The PHP 5.5 password_needs_rehash() will return TRUE in two cases:
// - The password is a Drupal 6 or 7 password and it has been rehashed
// during the migration. In this case the rehashed legacy hash is prefixed
// to indicate an old Drupal hash and will not comply with the expected
// password_needs_rehash() format.
// - The parameters of hashing engine were changed. For example the
// parameter 'password_hash_cost' (the hashing cost) has been increased in
// core.services.yml.
return password_needs_rehash($hash, PASSWORD_DEFAULT, $this->getOptions());
}
/**
* Returns password options.
*
* @return array
* Associative array with password options.
*/
protected function getOptions(): array {
return ['cost' => $this->cost];
public function __construct($cost, $algorithm = PASSWORD_DEFAULT) {
parent::__construct($algorithm, ['cost' => $cost]);
}
}
<?php
namespace Drupal\php_password\Password;
use Drupal\Core\Password\PasswordInterface;
/**
* Secure password hashing functions based on PHP password hashing functions.
*
* @see http://php.net/manual/en/ref.password.php
*/
class PhpPassword2 implements PasswordInterface {
/**
* The algorithm constant used to hash password.
*
* @see password_hash().
* @see http://php.net/manual/en/password.constants.php
*/
protected string $algorithm;
/**
* Password hash options for the provided algorithm.
*/
protected array $options = [];
/**
* Constructs a new password hashing instance.
*
* @param string $algorithm
* The hashing algorithm to use. Defaults to php default.
* @param array $options
* List of options. Refer to password_hash for available options.
*/
public function __construct(string $algorithm = PASSWORD_DEFAULT, array $options = []) {
$this->algorithm = $algorithm;
$this->options = $options;
}
/**
* {@inheritdoc}
*/
public function hash($password): bool|string {
// Prevent DoS attacks by refusing to hash large passwords.
if (strlen($password) > static::PASSWORD_MAX_LENGTH) {
return FALSE;
}
return password_hash($password, $this->algorithm, $this->options);
}
/**
* {@inheritdoc}
*/
public function check($password, $hash): bool {
return password_verify($password, $hash);
}
/**
* {@inheritdoc}
*/
public function needsRehash($hash): bool {
// The PHP password_needs_rehash() will return TRUE in two cases:
// - The password is a Drupal 6 or 7 password, and it has been rehashed
// during the migration. In this case the rehashed legacy hash is prefixed
// to indicate an old Drupal hash and will not comply with the expected
// password_needs_rehash() format.
// - The parameters of hashing engine were changed. For example the
// parameter 'password_hash_cost' (the hashing cost) has been increased in
// core.services.yml.
return password_needs_rehash($hash, $this->algorithm, $this->options);
}
}
......@@ -51,10 +51,11 @@ public function testHash($password) {
$this->legacyPassword->reveal()
);
$this->phpPassword->hash($password)
->shouldBeCalledTimes(1);
->shouldBeCalledTimes(1)
->willReturn('my hash');
$this->legacyPassword->hash(Argument::any())
->shouldNotBeCalled();
$testPassword->hash($password);
$this->assertEquals('my hash', $testPassword->hash($password));
}
/**
......@@ -77,13 +78,15 @@ public function testCheck(
if ($legacy) {
// This isn't working as expected... Somehow the comparison always fails.
$this->legacyPassword->check($actual_pass, Argument::Any())
->shouldBeCalledTimes(1);
->shouldBeCalledTimes(1)
->willReturn(TRUE);
$this->phpPassword->check(Argument::any(), Argument::any())
->shouldNotBeCalled();
}
else {
$this->phpPassword->check($actual_pass, $actual_hash)
->shouldBeCalledTimes(1);
->shouldBeCalledTimes(1)
->willReturn(TRUE);
$this->legacyPassword->check(Argument::any(), Argument::any())
->shouldNotBeCalled();
}
......@@ -102,10 +105,11 @@ public function testNeedsRehash($password, $hash): void {
$this->legacyPassword->reveal()
);
$this->phpPassword->needsRehash($hash)
->shouldBeCalledTimes(1);
->shouldBeCalledTimes(1)
->willReturn(TRUE);
$this->legacyPassword->needsRehash(Argument::any())
->shouldNotBeCalled();
$testPassword->needsRehash($hash);
$this->assertTrue($testPassword->needsRehash($hash));
}
/**
......@@ -113,64 +117,63 @@ public function testNeedsRehash($password, $hash): void {
*/
public function providePasswordHashes(): array {
return [
// Rehashed D6 md5 passwords.
[
'rehashed d6 md5 S' => [
'password',
'U$S$asdf',
'5f4dcc3b5aa765d61d8327deb882cf99',
'$S$asfd',
TRUE,
],
[
'rehashed d6 md5 H' => [
'password',
'U$H$asdf',
'5f4dcc3b5aa765d61d8327deb882cf99',
'$H$asfd',
TRUE,
],
[
'rehashed d6 md5 P' => [
'password',
'U$P$asdf',
'5f4dcc3b5aa765d61d8327deb882cf99',
'$P$asfd',
TRUE,
],
[
'rehashed d6 md5 2' => [
'password',
'U$2$asfd',
'5f4dcc3b5aa765d61d8327deb882cf99',
'$2$asfd',
FALSE,
],
[
'rehashed d6 md5 2a' => [
'password',
'U$2a$asfd',
'5f4dcc3b5aa765d61d8327deb882cf99',
'$2a$asfd',
FALSE,
],
[
'rehashed d6 md5 2b' => [
'password',
'U$2b$asfd',
'5f4dcc3b5aa765d61d8327deb882cf99',
'$2b$asfd',
FALSE,
],
[
'rehashed d6 md5 2x' => [
'password',
'U$2x$asfd',
'5f4dcc3b5aa765d61d8327deb882cf99',
'$2x$asfd',
FALSE,
],
[
'rehashed d6 md5 2y' => [
'password',
'U$2y$asfd',
'5f4dcc3b5aa765d61d8327deb882cf99',
'$2y$asfd',
FALSE,
],
[
'rehashed d6 md5 argon2i' => [
'password',
'U$argon2i$asfd',
'5f4dcc3b5aa765d61d8327deb882cf99',
......
<?php
namespace Drupal\Tests\php_password\Unit\Password;
use Drupal\php_password\Password\PhpPassword2;
use Drupal\Tests\UnitTestCase;
/**
* Test PhpPasswordTest native password implementation.
*
* @coversDefaultClass \Drupal\php_password\Password\PhpPassword2
* @group php_password
*/
class PhpPassword2Test extends UnitTestCase {
/**
* @covers ::hash
* @dataProvider providePasswordOptions
*/
public function testHash($algo, $options): void {
$password_implementation = new PhpPassword2($algo, $options);
$hash = $password_implementation->hash('mock_password');
// Each hash is unique with a salt, so we verify it validates.
$this->assertTrue(password_verify('mock_password', $hash));
$info = password_get_info($hash);
$this->assertEquals($algo, $info['algo']);
$this->assertEquals($options, $info['options']);
}
/**
* @covers ::hash
* @covers ::getOptions
*/
public function testHashDefault(): void {
$password_implementation = new PhpPassword2();
$hash = $password_implementation->hash('mock_password');
// Each hash is unique with a salt, so we verify it validates.
$this->assertTrue(password_verify('mock_password', $hash));
$info = password_get_info($hash);
$this->assertEquals(PASSWORD_DEFAULT, $info['algo']);
$this->assertEquals(['cost' => PASSWORD_BCRYPT_DEFAULT_COST],
$info['options']);
$password_implementation = new PhpPassword2(PASSWORD_ARGON2I);
$hash = $password_implementation->hash('mock_password');
// Each hash is unique with a salt, so we verify it validates.
$this->assertTrue(password_verify('mock_password', $hash));
$info = password_get_info($hash);
$this->assertEquals(PASSWORD_ARGON2I, $info['algo']);
$this->assertEquals([
'memory_cost' => PASSWORD_ARGON2_DEFAULT_MEMORY_COST,
'time_cost' => PASSWORD_ARGON2_DEFAULT_TIME_COST,
'threads' => PASSWORD_ARGON2_DEFAULT_THREADS,
], $info['options']);
}
/**
* @covers ::needsRehash
*/
public function testNeedsRehash(): void {
$this->markTestIncomplete('needs testing.');
}
/**
* @covers ::check
*/
public function testCheck(): void {
$this->markTestIncomplete('needs testing.');
}
/**
* Data provider for password hashing options.
*/
public function providePasswordOptions(): array {
return [
[PASSWORD_DEFAULT, ['cost' => 5]],
[PASSWORD_DEFAULT, ['cost' => 10]],
[
PASSWORD_ARGON2I,
[
'memory_cost' => 1024,
'time_cost' => 2,
'threads' => 2,
],
],
[
PASSWORD_ARGON2ID,
[
'memory_cost' => 1024,
'time_cost' => 2,
'threads' => 2,
],
],
];
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment