summaryrefslogtreecommitdiff
path: root/www/wiki/tests/phpunit/includes/password
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/tests/phpunit/includes/password')
-rw-r--r--www/wiki/tests/phpunit/includes/password/BcryptPasswordTest.php44
-rw-r--r--www/wiki/tests/phpunit/includes/password/EncryptedPasswordTest.php84
-rw-r--r--www/wiki/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php63
-rw-r--r--www/wiki/tests/phpunit/includes/password/MWOldPasswordTest.php24
-rw-r--r--www/wiki/tests/phpunit/includes/password/MWSaltedPasswordTest.php21
-rw-r--r--www/wiki/tests/phpunit/includes/password/PasswordFactoryTest.php110
-rw-r--r--www/wiki/tests/phpunit/includes/password/PasswordPolicyChecksTest.php159
-rw-r--r--www/wiki/tests/phpunit/includes/password/PasswordTest.php41
-rw-r--r--www/wiki/tests/phpunit/includes/password/PasswordTestCase.php112
-rw-r--r--www/wiki/tests/phpunit/includes/password/Pbkdf2PasswordFallbackTest.php29
-rw-r--r--www/wiki/tests/phpunit/includes/password/Pbkdf2PasswordTest.php29
-rw-r--r--www/wiki/tests/phpunit/includes/password/UserPasswordPolicyTest.php232
12 files changed, 948 insertions, 0 deletions
diff --git a/www/wiki/tests/phpunit/includes/password/BcryptPasswordTest.php b/www/wiki/tests/phpunit/includes/password/BcryptPasswordTest.php
new file mode 100644
index 00000000..952f5417
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/password/BcryptPasswordTest.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @group large
+ * @covers BcryptPassword
+ * @covers ParameterizedPassword
+ * @covers Password
+ * @covers PasswordFactory
+ */
+class BcryptPasswordTest extends PasswordTestCase {
+ protected function getTypeConfigs() {
+ return [ 'bcrypt' => [
+ 'class' => BcryptPassword::class,
+ 'cost' => 9,
+ ] ];
+ }
+
+ public static function providePasswordTests() {
+ // phpcs:disable Generic.Files.LineLength
+ return [
+ // Tests from glibc bcrypt implementation
+ [ true, ':bcrypt:5$CCCCCCCCCCCCCCCCCCCCC.$E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW', "U*U" ],
+ [ true, ':bcrypt:5$CCCCCCCCCCCCCCCCCCCCC.$VGOzA784oUp/Z0DY336zx7pLYAy0lwK', "U*U*" ],
+ [ true, ':bcrypt:5$XXXXXXXXXXXXXXXXXXXXXO$AcXxm9kjPGEMsLznoKqmqw7tc8WCx4a', "U*U*U" ],
+ [ true, ':bcrypt:5$abcdefghijklmnopqrstuu$5s2v8.iXieOjg/.AySBTTZIIVFJeBui', "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789chars after 72 are ignored" ],
+ [ true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$CE5elHaaO4EbggVDjb8P19RukzXSM3e', "\xff\xff\xa3" ],
+ [ true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq', "\xa3" ],
+ [ true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$Sa7shbm4.OzKpvFnX1pQLmQW96oUlCq', "\xa3" ],
+ [ true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$o./n25XVfn6oAPaUvHe.Csk4zRfsYPi', "\xff\xa334\xff\xff\xff\xa3345" ],
+ [ true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$nRht2l/HRhr6zmCp9vYUvvsqynflf9e', "\xff\xa3345" ],
+ [ true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$nRht2l/HRhr6zmCp9vYUvvsqynflf9e', "\xff\xa3345" ],
+ [ true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$6IflQkJytoRVc1yuaNtHfiuq.FRlSIS', "\xa3ab" ],
+ [ true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$6IflQkJytoRVc1yuaNtHfiuq.FRlSIS', "\xa3ab" ],
+ [ true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$swQOIzjOiJ9GHEPuhEkvqrUyvWhEMx6', "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaachars after 72 are ignored as usual" ],
+ [ true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$R9xrDjiycxMbQE2bp.vgqlYpW5wx2yy', "\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55" ],
+ [ true, ':bcrypt:5$/OK.fbVrR/bpIqNJ5ianF.$9tQZzcJfm3uj2NvJ/n5xkhpqLrMpWCe', "\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff\x55\xaa\xff" ],
+ [ true, ':bcrypt:5$CCCCCCCCCCCCCCCCCCCCC.$7uG0VCzI2bS7j6ymqJi9CdcdxiRTWNy', "" ],
+ // One or two false sanity tests
+ [ false, ':bcrypt:5$CCCCCCCCCCCCCCCCCCCCC.$E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW', "UXU" ],
+ [ false, ':bcrypt:5$CCCCCCCCCCCCCCCCCCCCC.$E5YPO9kmyuRGyh0XouQYb4YMJKvyOeW', "" ],
+ ];
+ // phpcs:enable
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/password/EncryptedPasswordTest.php b/www/wiki/tests/phpunit/includes/password/EncryptedPasswordTest.php
new file mode 100644
index 00000000..6dfdea69
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/password/EncryptedPasswordTest.php
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * @covers EncryptedPassword
+ * @covers ParameterizedPassword
+ * @covers Password
+ */
+class EncryptedPasswordTest extends PasswordTestCase {
+ protected function getTypeConfigs() {
+ return [
+ 'both' => [
+ 'class' => EncryptedPassword::class,
+ 'underlying' => 'pbkdf2',
+ 'secrets' => [
+ md5( 'secret1' ),
+ md5( 'secret2' ),
+ ],
+ 'cipher' => 'aes-256-cbc',
+ ],
+ 'secret1' => [
+ 'class' => EncryptedPassword::class,
+ 'underlying' => 'pbkdf2',
+ 'secrets' => [
+ md5( 'secret1' ),
+ ],
+ 'cipher' => 'aes-256-cbc',
+ ],
+ 'secret2' => [
+ 'class' => EncryptedPassword::class,
+ 'underlying' => 'pbkdf2',
+ 'secrets' => [
+ md5( 'secret2' ),
+ ],
+ 'cipher' => 'aes-256-cbc',
+ ],
+ 'pbkdf2' => [
+ 'class' => Pbkdf2Password::class,
+ 'algo' => 'sha256',
+ 'cost' => '10',
+ 'length' => '64',
+ ],
+ ];
+ }
+
+ public static function providePasswordTests() {
+ // phpcs:disable Generic.Files.LineLength
+ return [
+ // Encrypted with secret1
+ [ true, ':both:aes-256-cbc:0:izBpxujqC1YbzpCB3qAzgg==:ZqHnitT1pL4YJqKqFES2KEevZYSy2LtlibW5+IMi4XKOGKGy6sE638BXyBbLQQsBtTSrt+JyzwOayKtwIfRbaQsBridx/O1JwBSai1TkGkOsYMBXnlu2Bu/EquCBj5QpjYh7p3Uq4rpiop1KQlin1BJMwnAa1PovhxjpxnYhlhkM4X5ALoGi3XM0bapN48vt', 'password' ],
+ [ true, ':secret1:aes-256-cbc:0:izBpxujqC1YbzpCB3qAzgg==:ZqHnitT1pL4YJqKqFES2KEevZYSy2LtlibW5+IMi4XKOGKGy6sE638BXyBbLQQsBtTSrt+JyzwOayKtwIfRbaQsBridx/O1JwBSai1TkGkOsYMBXnlu2Bu/EquCBj5QpjYh7p3Uq4rpiop1KQlin1BJMwnAa1PovhxjpxnYhlhkM4X5ALoGi3XM0bapN48vt', 'password' ],
+ [ false, ':secret1:aes-256-cbc:0:izBpxujqC1YbzpCB3qAzgg==:ZqHnitT1pL4YJqKqFES2KEevZYSy2LtlibW5+IMi4XKOGKGy6sE638BXyBbLQQsBtTSrt+JyzwOayKtwIfRbaQsBridx/O1JwBSai1TkGkOsYMBXnlu2Bu/EquCBj5QpjYh7p3Uq4rpiop1KQlin1BJMwnAa1PovhxjpxnYhlhkM4X5ALoGi3XM0bapN48vt', 'notpassword' ],
+
+ // Encrypted with secret2
+ [ true, ':both:aes-256-cbc:1:m1LCnQVIakfYBNlr9KEgQg==:5yPTctqrzsybdgaMEag18AZYbnL37pAtXVBqmWxkjXbnNmiDH+1bHoL8lsEVTH/sJntC82kNVgE7zeiD8xUVLYF2VUnvB5+sU+aysE45/zwsCu7a22TaischMAOWrsHZ/tIgS/TnZY2d+HNyxgsEeeYf/QoL+FhmqHquK02+4SRbA5lLuj9niYy1r5CoM9cQ', 'password' ],
+ [ true, ':secret2:aes-256-cbc:0:m1LCnQVIakfYBNlr9KEgQg==:5yPTctqrzsybdgaMEag18AZYbnL37pAtXVBqmWxkjXbnNmiDH+1bHoL8lsEVTH/sJntC82kNVgE7zeiD8xUVLYF2VUnvB5+sU+aysE45/zwsCu7a22TaischMAOWrsHZ/tIgS/TnZY2d+HNyxgsEeeYf/QoL+FhmqHquK02+4SRbA5lLuj9niYy1r5CoM9cQ', 'password' ],
+ ];
+ // phpcs:enable
+ }
+
+ /**
+ * Wrong encryption key selected
+ * @expectedException PasswordError
+ */
+ public function testDecryptionError() {
+ // phpcs:ignore Generic.Files.LineLength
+ $hash = ':secret1:aes-256-cbc:0:m1LCnQVIakfYBNlr9KEgQg==:5yPTctqrzsybdgaMEag18AZYbnL37pAtXVBqmWxkjXbnNmiDH+1bHoL8lsEVTH/sJntC82kNVgE7zeiD8xUVLYF2VUnvB5+sU+aysE45/zwsCu7a22TaischMAOWrsHZ/tIgS/TnZY2d+HNyxgsEeeYf/QoL+FhmqHquK02+4SRbA5lLuj9niYy1r5CoM9cQ';
+ $password = $this->passwordFactory->newFromCiphertext( $hash );
+ $password->crypt( 'password' );
+ }
+
+ public function testUpdate() {
+ // phpcs:ignore Generic.Files.LineLength
+ $hash = ':both:aes-256-cbc:0:izBpxujqC1YbzpCB3qAzgg==:ZqHnitT1pL4YJqKqFES2KEevZYSy2LtlibW5+IMi4XKOGKGy6sE638BXyBbLQQsBtTSrt+JyzwOayKtwIfRbaQsBridx/O1JwBSai1TkGkOsYMBXnlu2Bu/EquCBj5QpjYh7p3Uq4rpiop1KQlin1BJMwnAa1PovhxjpxnYhlhkM4X5ALoGi3XM0bapN48vt';
+ $fromHash = $this->passwordFactory->newFromCiphertext( $hash );
+ $fromPlaintext = $this->passwordFactory->newFromPlaintext( 'password', $fromHash );
+ $this->assertTrue( $fromHash->update() );
+
+ $serialized = $fromHash->toString();
+ $this->assertRegExp( '/^:both:aes-256-cbc:1:/', $serialized );
+ $fromNewHash = $this->passwordFactory->newFromCiphertext( $serialized );
+ $fromPlaintext = $this->passwordFactory->newFromPlaintext( 'password', $fromNewHash );
+ $this->assertTrue( $fromHash->equals( $fromPlaintext ) );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php b/www/wiki/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php
new file mode 100644
index 00000000..6a965a03
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/password/LayeredParameterizedPasswordTest.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * @covers LayeredParameterizedPassword
+ * @covers Password
+ */
+class LayeredParameterizedPasswordTest extends PasswordTestCase {
+ protected function getTypeConfigs() {
+ return [
+ 'testLargeLayeredTop' => [
+ 'class' => LayeredParameterizedPassword::class,
+ 'types' => [
+ 'testLargeLayeredBottom',
+ 'testLargeLayeredBottom',
+ 'testLargeLayeredBottom',
+ 'testLargeLayeredBottom',
+ 'testLargeLayeredFinal',
+ ],
+ ],
+ 'testLargeLayeredBottom' => [
+ 'class' => Pbkdf2Password::class,
+ 'algo' => 'sha512',
+ 'cost' => 1024,
+ 'length' => 512,
+ ],
+ 'testLargeLayeredFinal' => [
+ 'class' => BcryptPassword::class,
+ 'cost' => 5,
+ ]
+ ];
+ }
+
+ protected function getValidTypes() {
+ return [ 'testLargeLayeredFinal' ];
+ }
+
+ public static function providePasswordTests() {
+ // phpcs:disable Generic.Files.LineLength
+ return [
+ [
+ true,
+ ':testLargeLayeredTop:sha512:1024:512!sha512:1024:512!sha512:1024:512!sha512:1024:512!5!vnRy+2SrSA0fHt3dwhTP5g==!AVnwfZsAQjn+gULv7FSGjA==!xvHUX3WcpkeSn1lvjWcvBg==!It+OC/N9tu+d3ByHhuB0BQ==!Tb.gqUOiD.aWktVwHM.Q/O!7CcyMfXUPky5ptyATJsR2nq3vUqtnBC',
+ 'testPassword123'
+ ],
+ ];
+ // phpcs:enable
+ }
+
+ /**
+ * @covers LayeredParameterizedPassword::partialCrypt
+ */
+ public function testLargeLayeredPartialUpdate() {
+ /** @var ParameterizedPassword $partialPassword */
+ $partialPassword = $this->passwordFactory->newFromType( 'testLargeLayeredBottom' );
+ $partialPassword->crypt( 'testPassword123' );
+
+ /** @var LayeredParameterizedPassword $totalPassword */
+ $totalPassword = $this->passwordFactory->newFromType( 'testLargeLayeredTop' );
+ $totalPassword->partialCrypt( $partialPassword );
+
+ $this->assertTrue( $totalPassword->equals( 'testPassword123' ) );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/password/MWOldPasswordTest.php b/www/wiki/tests/phpunit/includes/password/MWOldPasswordTest.php
new file mode 100644
index 00000000..50100826
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/password/MWOldPasswordTest.php
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * @covers MWOldPassword
+ * @covers ParameterizedPassword
+ * @covers Password
+ */
+class MWOldPasswordTest extends PasswordTestCase {
+ protected function getTypeConfigs() {
+ return [ 'A' => [
+ 'class' => MWOldPassword::class,
+ ] ];
+ }
+
+ public static function providePasswordTests() {
+ return [
+ [ true, ':A:5f4dcc3b5aa765d61d8327deb882cf99', 'password' ],
+ // Type-B password with incorrect type name is accepted
+ [ true, ':A:salt:9842afc7cb949c440c51347ed809362f', 'password' ],
+ [ false, ':A:d529e941509eb9e9b9cfaeae1fe7ca23', 'password' ],
+ [ false, ':A:salt:d529e941509eb9e9b9cfaeae1fe7ca23', 'password' ],
+ ];
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/password/MWSaltedPasswordTest.php b/www/wiki/tests/phpunit/includes/password/MWSaltedPasswordTest.php
new file mode 100644
index 00000000..5616868d
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/password/MWSaltedPasswordTest.php
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * @covers MWSaltedPassword
+ * @covers ParameterizedPassword
+ * @covers Password
+ */
+class MWSaltedPasswordTest extends PasswordTestCase {
+ protected function getTypeConfigs() {
+ return [ 'B' => [
+ 'class' => MWSaltedPassword::class,
+ ] ];
+ }
+
+ public static function providePasswordTests() {
+ return [
+ [ true, ':B:salt:9842afc7cb949c440c51347ed809362f', 'password' ],
+ [ false, ':B:salt:d529e941509eb9e9b9cfaeae1fe7ca23', 'password' ],
+ ];
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/password/PasswordFactoryTest.php b/www/wiki/tests/phpunit/includes/password/PasswordFactoryTest.php
new file mode 100644
index 00000000..01b0de2c
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/password/PasswordFactoryTest.php
@@ -0,0 +1,110 @@
+<?php
+
+/**
+ * @covers PasswordFactory
+ */
+class PasswordFactoryTest extends MediaWikiTestCase {
+ public function testRegister() {
+ $pf = new PasswordFactory;
+ $pf->register( 'foo', [ 'class' => InvalidPassword::class ] );
+ $this->assertArrayHasKey( 'foo', $pf->getTypes() );
+ }
+
+ public function testSetDefaultType() {
+ $pf = new PasswordFactory;
+ $pf->register( '1', [ 'class' => InvalidPassword::class ] );
+ $pf->register( '2', [ 'class' => InvalidPassword::class ] );
+ $pf->setDefaultType( '1' );
+ $this->assertSame( '1', $pf->getDefaultType() );
+ $pf->setDefaultType( '2' );
+ $this->assertSame( '2', $pf->getDefaultType() );
+ }
+
+ /**
+ * @expectedException Exception
+ */
+ public function testSetDefaultTypeError() {
+ $pf = new PasswordFactory;
+ $pf->setDefaultType( 'bogus' );
+ }
+
+ public function testInit() {
+ $config = new HashConfig( [
+ 'PasswordConfig' => [
+ 'foo' => [ 'class' => InvalidPassword::class ],
+ ],
+ 'PasswordDefault' => 'foo'
+ ] );
+ $pf = new PasswordFactory;
+ $pf->init( $config );
+ $this->assertSame( 'foo', $pf->getDefaultType() );
+ $this->assertArrayHasKey( 'foo', $pf->getTypes() );
+ }
+
+ public function testNewFromCiphertext() {
+ $pf = new PasswordFactory;
+ $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
+ $pw = $pf->newFromCiphertext( ':B:salt:d529e941509eb9e9b9cfaeae1fe7ca23' );
+ $this->assertInstanceOf( MWSaltedPassword::class, $pw );
+ }
+
+ public function provideNewFromCiphertextErrors() {
+ return [ [ 'blah' ], [ ':blah:' ] ];
+ }
+
+ /**
+ * @dataProvider provideNewFromCiphertextErrors
+ * @expectedException PasswordError
+ */
+ public function testNewFromCiphertextErrors( $hash ) {
+ $pf = new PasswordFactory;
+ $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
+ $pf->newFromCiphertext( $hash );
+ }
+
+ public function testNewFromType() {
+ $pf = new PasswordFactory;
+ $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
+ $pw = $pf->newFromType( 'B' );
+ $this->assertInstanceOf( MWSaltedPassword::class, $pw );
+ }
+
+ /**
+ * @expectedException PasswordError
+ */
+ public function testNewFromTypeError() {
+ $pf = new PasswordFactory;
+ $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
+ $pf->newFromType( 'bogus' );
+ }
+
+ public function testNewFromPlaintext() {
+ $pf = new PasswordFactory;
+ $pf->register( 'A', [ 'class' => MWOldPassword::class ] );
+ $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
+ $pf->setDefaultType( 'A' );
+
+ $this->assertInstanceOf( InvalidPassword::class, $pf->newFromPlaintext( null ) );
+ $this->assertInstanceOf( MWOldPassword::class, $pf->newFromPlaintext( 'password' ) );
+ $this->assertInstanceOf( MWSaltedPassword::class,
+ $pf->newFromPlaintext( 'password', $pf->newFromType( 'B' ) ) );
+ }
+
+ public function testNeedsUpdate() {
+ $pf = new PasswordFactory;
+ $pf->register( 'A', [ 'class' => MWOldPassword::class ] );
+ $pf->register( 'B', [ 'class' => MWSaltedPassword::class ] );
+ $pf->setDefaultType( 'A' );
+
+ $this->assertFalse( $pf->needsUpdate( $pf->newFromType( 'A' ) ) );
+ $this->assertTrue( $pf->needsUpdate( $pf->newFromType( 'B' ) ) );
+ }
+
+ public function testGenerateRandomPasswordString() {
+ $this->assertSame( 13, strlen( PasswordFactory::generateRandomPasswordString( 13 ) ) );
+ }
+
+ public function testNewInvalidPassword() {
+ $this->assertInstanceOf( InvalidPassword::class, PasswordFactory::newInvalidPassword() );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/password/PasswordPolicyChecksTest.php b/www/wiki/tests/phpunit/includes/password/PasswordPolicyChecksTest.php
new file mode 100644
index 00000000..7dfb3cf5
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/password/PasswordPolicyChecksTest.php
@@ -0,0 +1,159 @@
+<?php
+/**
+ * Testing password-policy check functions
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+class PasswordPolicyChecksTest extends MediaWikiTestCase {
+
+ /**
+ * @covers PasswordPolicyChecks::checkMinimalPasswordLength
+ */
+ public function testCheckMinimalPasswordLength() {
+ $statusOK = PasswordPolicyChecks::checkMinimalPasswordLength(
+ 3, // policy value
+ User::newFromName( 'user' ), // User
+ 'password' // password
+ );
+ $this->assertTrue( $statusOK->isGood(), 'Password is longer than minimal policy' );
+ $statusShort = PasswordPolicyChecks::checkMinimalPasswordLength(
+ 10, // policy value
+ User::newFromName( 'user' ), // User
+ 'password' // password
+ );
+ $this->assertFalse(
+ $statusShort->isGood(),
+ 'Password is shorter than minimal policy'
+ );
+ $this->assertTrue(
+ $statusShort->isOK(),
+ 'Password is shorter than minimal policy, not fatal'
+ );
+ }
+
+ /**
+ * @covers PasswordPolicyChecks::checkMinimumPasswordLengthToLogin
+ */
+ public function testCheckMinimumPasswordLengthToLogin() {
+ $statusOK = PasswordPolicyChecks::checkMinimumPasswordLengthToLogin(
+ 3, // policy value
+ User::newFromName( 'user' ), // User
+ 'password' // password
+ );
+ $this->assertTrue( $statusOK->isGood(), 'Password is longer than minimal policy' );
+ $statusShort = PasswordPolicyChecks::checkMinimumPasswordLengthToLogin(
+ 10, // policy value
+ User::newFromName( 'user' ), // User
+ 'password' // password
+ );
+ $this->assertFalse(
+ $statusShort->isGood(),
+ 'Password is shorter than minimum login policy'
+ );
+ $this->assertFalse(
+ $statusShort->isOK(),
+ 'Password is shorter than minimum login policy, fatal'
+ );
+ }
+
+ /**
+ * @covers PasswordPolicyChecks::checkMaximalPasswordLength
+ */
+ public function testCheckMaximalPasswordLength() {
+ $statusOK = PasswordPolicyChecks::checkMaximalPasswordLength(
+ 100, // policy value
+ User::newFromName( 'user' ), // User
+ 'password' // password
+ );
+ $this->assertTrue( $statusOK->isGood(), 'Password is shorter than maximal policy' );
+ $statusLong = PasswordPolicyChecks::checkMaximalPasswordLength(
+ 4, // policy value
+ User::newFromName( 'user' ), // User
+ 'password' // password
+ );
+ $this->assertFalse( $statusLong->isGood(),
+ 'Password is longer than maximal policy'
+ );
+ $this->assertFalse( $statusLong->isOK(),
+ 'Password is longer than maximal policy, fatal'
+ );
+ }
+
+ /**
+ * @covers PasswordPolicyChecks::checkPasswordCannotMatchUsername
+ */
+ public function testCheckPasswordCannotMatchUsername() {
+ $statusOK = PasswordPolicyChecks::checkPasswordCannotMatchUsername(
+ 1, // policy value
+ User::newFromName( 'user' ), // User
+ 'password' // password
+ );
+ $this->assertTrue( $statusOK->isGood(), 'Password does not match username' );
+ $statusLong = PasswordPolicyChecks::checkPasswordCannotMatchUsername(
+ 1, // policy value
+ User::newFromName( 'user' ), // User
+ 'user' // password
+ );
+ $this->assertFalse( $statusLong->isGood(), 'Password matches username' );
+ $this->assertTrue( $statusLong->isOK(), 'Password matches username, not fatal' );
+ }
+
+ /**
+ * @covers PasswordPolicyChecks::checkPasswordCannotMatchBlacklist
+ */
+ public function testCheckPasswordCannotMatchBlacklist() {
+ $statusOK = PasswordPolicyChecks::checkPasswordCannotMatchBlacklist(
+ true, // policy value
+ User::newFromName( 'Username' ), // User
+ 'AUniquePassword' // password
+ );
+ $this->assertTrue( $statusOK->isGood(), 'Password is not on blacklist' );
+ $statusLong = PasswordPolicyChecks::checkPasswordCannotMatchBlacklist(
+ true, // policy value
+ User::newFromName( 'Useruser1' ), // User
+ 'Passpass1' // password
+ );
+ $this->assertFalse( $statusLong->isGood(), 'Password matches blacklist' );
+ $this->assertTrue( $statusLong->isOK(), 'Password matches blacklist, not fatal' );
+ }
+
+ public static function providePopularBlacklist() {
+ return [
+ [ false, 'sitename' ],
+ [ false, 'password' ],
+ [ false, '12345' ],
+ [ true, 'hqY98gCZ6qM8s8' ],
+ ];
+ }
+
+ /**
+ * @covers PasswordPolicyChecks::checkPopularPasswordBlacklist
+ * @dataProvider providePopularBlacklist
+ */
+ public function testCheckPopularPasswordBlacklist( $expected, $password ) {
+ global $IP;
+ $this->setMwGlobals( [
+ 'wgSitename' => 'sitename',
+ 'wgPopularPasswordFile' => "$IP/serialized/commonpasswords.cdb"
+ ] );
+ $user = User::newFromName( 'username' );
+ $status = PasswordPolicyChecks::checkPopularPasswordBlacklist( PHP_INT_MAX, $user, $password );
+ $this->assertSame( $expected, $status->isGood() );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/password/PasswordTest.php b/www/wiki/tests/phpunit/includes/password/PasswordTest.php
new file mode 100644
index 00000000..65c91993
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/password/PasswordTest.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ * Testing framework for the Password infrastructure
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @covers InvalidPassword
+ */
+class PasswordTest extends MediaWikiTestCase {
+ public function testInvalidUnequalInvalid() {
+ $passwordFactory = new PasswordFactory();
+ $invalid1 = $passwordFactory->newFromCiphertext( null );
+ $invalid2 = $passwordFactory->newFromCiphertext( null );
+
+ $this->assertFalse( $invalid1->equals( $invalid2 ) );
+ }
+
+ public function testInvalidPlaintext() {
+ $passwordFactory = new PasswordFactory();
+ $invalid = $passwordFactory->newFromPlaintext( null );
+
+ $this->assertInstanceOf( InvalidPassword::class, $invalid );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/password/PasswordTestCase.php b/www/wiki/tests/phpunit/includes/password/PasswordTestCase.php
new file mode 100644
index 00000000..80b9838d
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/password/PasswordTestCase.php
@@ -0,0 +1,112 @@
+<?php
+/**
+ * Testing framework for the password hashes
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @since 1.24
+ */
+abstract class PasswordTestCase extends MediaWikiTestCase {
+ /**
+ * @var PasswordFactory
+ */
+ protected $passwordFactory;
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->passwordFactory = new PasswordFactory();
+ foreach ( $this->getTypeConfigs() as $type => $config ) {
+ $this->passwordFactory->register( $type, $config );
+ }
+ }
+
+ /**
+ * Return an array of configs to be used for this class's password type.
+ *
+ * @return array[]
+ */
+ abstract protected function getTypeConfigs();
+
+ /**
+ * An array of tests in the form of (bool, string, string), where the first
+ * element is whether the second parameter (a password hash) and the third
+ * parameter (a password) should match.
+ * @return array
+ * @throws MWException
+ * @abstract
+ */
+ public static function providePasswordTests() {
+ throw new MWException( "Not implemented" );
+ }
+
+ /**
+ * @dataProvider providePasswordTests
+ */
+ public function testHashing( $shouldMatch, $hash, $password ) {
+ $hash = $this->passwordFactory->newFromCiphertext( $hash );
+ $password = $this->passwordFactory->newFromPlaintext( $password, $hash );
+ $this->assertSame( $shouldMatch, $hash->equals( $password ) );
+ }
+
+ /**
+ * @dataProvider providePasswordTests
+ */
+ public function testStringSerialization( $shouldMatch, $hash, $password ) {
+ $hashObj = $this->passwordFactory->newFromCiphertext( $hash );
+ $serialized = $hashObj->toString();
+ $unserialized = $this->passwordFactory->newFromCiphertext( $serialized );
+ $this->assertTrue( $hashObj->equals( $unserialized ) );
+ }
+
+ /**
+ * @dataProvider providePasswordTests
+ * @covers InvalidPassword
+ */
+ public function testInvalidUnequalNormal( $shouldMatch, $hash, $password ) {
+ $invalid = $this->passwordFactory->newFromCiphertext( null );
+ $normal = $this->passwordFactory->newFromCiphertext( $hash );
+
+ $this->assertFalse( $invalid->equals( $normal ) );
+ $this->assertFalse( $normal->equals( $invalid ) );
+ }
+
+ protected function getValidTypes() {
+ return array_keys( $this->getTypeConfigs() );
+ }
+
+ public function provideTypes( $type ) {
+ $params = [];
+ foreach ( $this->getValidTypes() as $type ) {
+ $params[] = [ $type ];
+ }
+ return $params;
+ }
+
+ /**
+ * @dataProvider provideTypes
+ */
+ public function testCrypt( $type ) {
+ $fromType = $this->passwordFactory->newFromType( $type );
+ $fromType->crypt( 'password' );
+ $fromPlaintext = $this->passwordFactory->newFromPlaintext( 'password', $fromType );
+ $this->assertTrue( $fromType->equals( $fromPlaintext ) );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/password/Pbkdf2PasswordFallbackTest.php b/www/wiki/tests/phpunit/includes/password/Pbkdf2PasswordFallbackTest.php
new file mode 100644
index 00000000..cf851c81
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/password/Pbkdf2PasswordFallbackTest.php
@@ -0,0 +1,29 @@
+<?php
+
+
+/**
+ * @group large
+ * @covers Pbkdf2Password
+ */
+class Pbkdf2PasswordFallbackTest extends PasswordTestCase {
+ protected function getTypeConfigs() {
+ return [
+ 'pbkdf2' => [
+ 'class' => Pbkdf2Password::class,
+ 'algo' => 'sha256',
+ 'cost' => '10000',
+ 'length' => '128',
+ 'use-hash-extension' => false,
+ ],
+ ];
+ }
+
+ public static function providePasswordTests() {
+ return [
+ [ true, ":pbkdf2:sha1:1:20:c2FsdA==:DGDID5YfDnHzqbUkr2ASBi/gN6Y=", 'password' ],
+ [ true, ":pbkdf2:sha1:2:20:c2FsdA==:6mwBTcctb4zNHtkqzh1B8NjeiVc=", 'password' ],
+ [ true, ":pbkdf2:sha1:4096:20:c2FsdA==:SwB5AbdlSJq+rUnZJvch0GWkKcE=", 'password' ],
+ [ true, ":pbkdf2:sha1:4096:16:c2EAbHQ=:Vvpqp1VICZ3MN9fwNCXgww==", "pass\x00word" ],
+ ];
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/password/Pbkdf2PasswordTest.php b/www/wiki/tests/phpunit/includes/password/Pbkdf2PasswordTest.php
new file mode 100644
index 00000000..7e97ab1a
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/password/Pbkdf2PasswordTest.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * @group large
+ * @covers Pbkdf2Password
+ * @covers Password
+ * @covers ParameterizedPassword
+ * @requires function hash_pbkdf2
+ */
+class Pbkdf2PasswordTest extends PasswordTestCase {
+ protected function getTypeConfigs() {
+ return [ 'pbkdf2' => [
+ 'class' => Pbkdf2Password::class,
+ 'algo' => 'sha256',
+ 'cost' => '10000',
+ 'length' => '128',
+ 'use-hash-extension' => true,
+ ] ];
+ }
+
+ public static function providePasswordTests() {
+ return [
+ [ true, ":pbkdf2:sha1:1:20:c2FsdA==:DGDID5YfDnHzqbUkr2ASBi/gN6Y=", 'password' ],
+ [ true, ":pbkdf2:sha1:2:20:c2FsdA==:6mwBTcctb4zNHtkqzh1B8NjeiVc=", 'password' ],
+ [ true, ":pbkdf2:sha1:4096:20:c2FsdA==:SwB5AbdlSJq+rUnZJvch0GWkKcE=", 'password' ],
+ [ true, ":pbkdf2:sha1:4096:16:c2EAbHQ=:Vvpqp1VICZ3MN9fwNCXgww==", "pass\x00word" ],
+ ];
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/password/UserPasswordPolicyTest.php b/www/wiki/tests/phpunit/includes/password/UserPasswordPolicyTest.php
new file mode 100644
index 00000000..78175fac
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/password/UserPasswordPolicyTest.php
@@ -0,0 +1,232 @@
+<?php
+/**
+ * Testing for password-policy enforcement, based on a user's groups.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * @group Database
+ * @covers UserPasswordPolicy
+ */
+class UserPasswordPolicyTest extends MediaWikiTestCase {
+
+ protected $tablesUsed = [ 'user', 'user_groups' ];
+
+ protected $policies = [
+ 'checkuser' => [
+ 'MinimalPasswordLength' => 10,
+ 'MinimumPasswordLengthToLogin' => 6,
+ 'PasswordCannotMatchUsername' => true,
+ ],
+ 'sysop' => [
+ 'MinimalPasswordLength' => 8,
+ 'MinimumPasswordLengthToLogin' => 1,
+ 'PasswordCannotMatchUsername' => true,
+ ],
+ 'default' => [
+ 'MinimalPasswordLength' => 4,
+ 'MinimumPasswordLengthToLogin' => 1,
+ 'PasswordCannotMatchBlacklist' => true,
+ 'MaximalPasswordLength' => 4096,
+ ],
+ ];
+
+ protected $checks = [
+ 'MinimalPasswordLength' => 'PasswordPolicyChecks::checkMinimalPasswordLength',
+ 'MinimumPasswordLengthToLogin' => 'PasswordPolicyChecks::checkMinimumPasswordLengthToLogin',
+ 'PasswordCannotMatchUsername' => 'PasswordPolicyChecks::checkPasswordCannotMatchUsername',
+ 'PasswordCannotMatchBlacklist' => 'PasswordPolicyChecks::checkPasswordCannotMatchBlacklist',
+ 'MaximalPasswordLength' => 'PasswordPolicyChecks::checkMaximalPasswordLength',
+ ];
+
+ private function getUserPasswordPolicy() {
+ return new UserPasswordPolicy( $this->policies, $this->checks );
+ }
+
+ public function testGetPoliciesForUser() {
+ $upp = $this->getUserPasswordPolicy();
+
+ $user = User::newFromName( 'TestUserPolicy' );
+ $user->addToDatabase();
+ $user->addGroup( 'sysop' );
+
+ $this->assertArrayEquals(
+ [
+ 'MinimalPasswordLength' => 8,
+ 'MinimumPasswordLengthToLogin' => 1,
+ 'PasswordCannotMatchUsername' => 1,
+ 'PasswordCannotMatchBlacklist' => true,
+ 'MaximalPasswordLength' => 4096,
+ ],
+ $upp->getPoliciesForUser( $user )
+ );
+ }
+
+ public function testGetPoliciesForGroups() {
+ $effective = UserPasswordPolicy::getPoliciesForGroups(
+ $this->policies,
+ [ 'user', 'checkuser' ],
+ $this->policies['default']
+ );
+
+ $this->assertArrayEquals(
+ [
+ 'MinimalPasswordLength' => 10,
+ 'MinimumPasswordLengthToLogin' => 6,
+ 'PasswordCannotMatchUsername' => true,
+ 'PasswordCannotMatchBlacklist' => true,
+ 'MaximalPasswordLength' => 4096,
+ ],
+ $effective
+ );
+ }
+
+ /**
+ * @dataProvider provideCheckUserPassword
+ */
+ public function testCheckUserPassword( $username, $groups, $password, $valid, $ok, $msg ) {
+ $upp = $this->getUserPasswordPolicy();
+
+ $user = User::newFromName( $username );
+ $user->addToDatabase();
+ foreach ( $groups as $group ) {
+ $user->addGroup( $group );
+ }
+
+ $status = $upp->checkUserPassword( $user, $password );
+ $this->assertSame( $valid, $status->isGood(), $msg . ' - password valid' );
+ $this->assertSame( $ok, $status->isOK(), $msg . ' - can login' );
+ }
+
+ public function provideCheckUserPassword() {
+ return [
+ [
+ 'PassPolicyUser',
+ [],
+ '',
+ false,
+ false,
+ 'No groups, default policy, password too short to login'
+ ],
+ [
+ 'PassPolicyUser',
+ [ 'user' ],
+ 'aaa',
+ false,
+ true,
+ 'Default policy, short password'
+ ],
+ [
+ 'PassPolicyUser',
+ [ 'sysop' ],
+ 'abcdabcdabcd',
+ true,
+ true,
+ 'Sysop with good password'
+ ],
+ [
+ 'PassPolicyUser',
+ [ 'sysop' ],
+ 'abcd',
+ false,
+ true,
+ 'Sysop with short password'
+ ],
+ [
+ 'PassPolicyUser',
+ [ 'sysop', 'checkuser' ],
+ 'abcdabcd',
+ false,
+ true,
+ 'Checkuser with short password'
+ ],
+ [
+ 'PassPolicyUser',
+ [ 'sysop', 'checkuser' ],
+ 'abcd',
+ false,
+ false,
+ 'Checkuser with too short password to login'
+ ],
+ [
+ 'Useruser',
+ [ 'user' ],
+ 'Passpass',
+ false,
+ true,
+ 'Username & password on blacklist'
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideMaxOfPolicies
+ */
+ public function testMaxOfPolicies( $p1, $p2, $max, $msg ) {
+ $this->assertArrayEquals(
+ $max,
+ UserPasswordPolicy::maxOfPolicies( $p1, $p2 ),
+ $msg
+ );
+ }
+
+ public function provideMaxOfPolicies() {
+ return [
+ [
+ [ 'MinimalPasswordLength' => 8 ], // p1
+ [ 'MinimalPasswordLength' => 2 ], // p2
+ [ 'MinimalPasswordLength' => 8 ], // max
+ 'Basic max in p1'
+ ],
+ [
+ [ 'MinimalPasswordLength' => 2 ], // p1
+ [ 'MinimalPasswordLength' => 8 ], // p2
+ [ 'MinimalPasswordLength' => 8 ], // max
+ 'Basic max in p2'
+ ],
+ [
+ [ 'MinimalPasswordLength' => 8 ], // p1
+ [
+ 'MinimalPasswordLength' => 2,
+ 'PasswordCannotMatchUsername' => 1,
+ ], // p2
+ [
+ 'MinimalPasswordLength' => 8,
+ 'PasswordCannotMatchUsername' => 1,
+ ], // max
+ 'Missing items in p1'
+ ],
+ [
+ [
+ 'MinimalPasswordLength' => 8,
+ 'PasswordCannotMatchUsername' => 1,
+ ], // p1
+ [
+ 'MinimalPasswordLength' => 2,
+ ], // p2
+ [
+ 'MinimalPasswordLength' => 8,
+ 'PasswordCannotMatchUsername' => 1,
+ ], // max
+ 'Missing items in p2'
+ ],
+ ];
+ }
+
+}