diff options
Diffstat (limited to 'www/wiki/tests/phpunit/includes/password')
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' + ], + ]; + } + +} |