From 84409fc1266b1a6bdd6eaa2d16363558e60ecaff Mon Sep 17 00:00:00 2001 From: Ernestas Kvedaras Date: Sat, 23 Nov 2019 08:15:48 +0100 Subject: [PATCH] Add ability to set custom password strategies + add makeSSHAPassword method to Utilities that can be used as one of the strategies --- .gitignore | 1 + src/Models/User.php | 33 +++++++++++++++++++++++++++++---- src/Utilities.php | 15 +++++++++++++++ tests/Models/UserTest.php | 27 +++++++++++++++++++++++++++ tests/TestCase.php | 9 +++++++++ tests/UtilitiesTest.php | 10 ++++++++++ 6 files changed, 91 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 8b7ef350..2130d097 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +/.idea /vendor composer.lock diff --git a/src/Models/User.php b/src/Models/User.php index 2847c332..b6b04dbf 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -23,6 +23,31 @@ class User extends Entry implements Authenticatable Concerns\HasLastLogonAndLogOff, Concerns\HasUserAccountControl; + /** @var callable|null */ + private static $passwordStrategy; + + /** + * Password will be processed using given callback before saving. + * + * @param callable $strategy + */ + public static function usePasswordStrategy(callable $strategy) + { + self::$passwordStrategy = $strategy; + } + + /** + * Will return user set password strategy or default one. + * + * @return callable + */ + public static function getPasswordStrategy(): callable + { + return self::$passwordStrategy ?? function ($password) { + return Utilities::encodePassword($password); + }; + } + /** * Get the name of the unique identifier for the user. * @@ -788,7 +813,7 @@ public function setPassword($password) { $this->validateSecureConnection(); - $encodedPassword = Utilities::encodePassword($password); + $encodedPassword = call_user_func(self::getPasswordStrategy(), $password); if ($this->exists) { // If the record exists, we need to add a batch replace @@ -861,21 +886,21 @@ public function changePassword($oldPassword, $newPassword, $replaceNotRemove = f $modifications[] = $this->newBatchModification( $attribute, LDAP_MODIFY_BATCH_REPLACE, - [Utilities::encodePassword($newPassword)] + [call_user_func(self::getPasswordStrategy(), $newPassword)] ); } else { // Create batch modification for removing the old password. $modifications[] = $this->newBatchModification( $attribute, LDAP_MODIFY_BATCH_REMOVE, - [Utilities::encodePassword($oldPassword)] + [call_user_func(self::getPasswordStrategy(), $oldPassword)] ); // Create batch modification for adding the new password. $modifications[] = $this->newBatchModification( $attribute, LDAP_MODIFY_BATCH_ADD, - [Utilities::encodePassword($newPassword)] + [call_user_func(self::getPasswordStrategy(), $newPassword)] ); } diff --git a/src/Utilities.php b/src/Utilities.php index 51c176b8..a6f12911 100644 --- a/src/Utilities.php +++ b/src/Utilities.php @@ -152,6 +152,21 @@ public static function encodePassword($password) return iconv('UTF-8', 'UTF-16LE', '"'.$password.'"'); } + /** + * Salt and hash a password to make its SSHA OpenLDAP version. + * + * @param string $password The password to create + * + * @return string + */ + public static function makeSSHAPassword($password) + { + mt_srand((float) microtime() * 1000000); + $salt = pack('CCCC', mt_rand(), mt_rand(), mt_rand(), mt_rand()); + + return '{SSHA}'.base64_encode(pack('H*', sha1($password.$salt)).$salt); + } + /** * Round a Windows timestamp down to seconds and remove * the seconds between 1601-01-01 and 1970-01-01. diff --git a/tests/Models/UserTest.php b/tests/Models/UserTest.php index 031d2cff..0ccf559c 100644 --- a/tests/Models/UserTest.php +++ b/tests/Models/UserTest.php @@ -44,6 +44,33 @@ public function test_set_password_on_new_user() $this->assertEquals($expected, $user->getModifications()); } + public function test_set_password_on_new_user_with_different_password_strategy() + { + $connection = $this->newConnectionMock(); + + $connection->shouldReceive('canChangePasswords')->once()->andReturn(true); + + $user = new User([], $this->newBuilder($connection)); + $rand = mt_rand(); + User::usePasswordStrategy(function ($password) use ($rand) { + return $rand.$password; + }); + + $password = 'password'; + + $user->setPassword($password); + + $expected = [ + [ + 'attrib' => 'unicodepwd', + 'modtype' => 1, + 'values' => [$rand.$password], + ], + ]; + + $this->assertEquals($expected, $user->getModifications()); + } + public function test_set_password_on_existing_user() { $connection = $this->newConnectionMock(); diff --git a/tests/TestCase.php b/tests/TestCase.php index 766ad9dc..9aeccae8 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,6 +3,8 @@ namespace Adldap\Tests; use Mockery; +use Adldap\Utilities; +use Adldap\Models\User; use Adldap\Query\Builder; use Adldap\Query\Grammar; use Adldap\Connections\ConnectionInterface; @@ -33,6 +35,13 @@ public function setUp() } } + protected function tearDown() + { + User::usePasswordStrategy(function ($password) { + return Utilities::encodePassword($password); + }); + } + /** * Mocks a the specified class. * diff --git a/tests/UtilitiesTest.php b/tests/UtilitiesTest.php index eef4d3d8..2e5b477a 100644 --- a/tests/UtilitiesTest.php +++ b/tests/UtilitiesTest.php @@ -43,6 +43,16 @@ public function test_encode_password() $this->assertEquals($expected, bin2hex($encoded)); } + public function test_make_ssha_password() + { + $password = 'password'; + + $encoded = Utilities::makeSSHAPassword($password); + + $this->assertStringStartsWith('{SSHA}', $encoded); + $this->assertGreaterThan(6, strlen($encoded)); + } + public function test_is_valid_sid() { $this->assertTrue(Utilities::isValidSid('S-1-5-21-3623811015-3361044348-30300820-1013'));