Merge "Add support for blacklisting common passwords"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 4 Dec 2015 20:17:45 +0000 (20:17 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 4 Dec 2015 20:17:45 +0000 (20:17 +0000)
includes/DefaultSettings.php
includes/password/PasswordPolicyChecks.php
languages/i18n/en.json
languages/i18n/qqq.json
maintenance/createCommonPasswordCdb.php [new file with mode: 0644]
serialized/commonpasswords.cdb [new file with mode: 0644]

index 9d93797..e76b627 100644 (file)
@@ -4365,6 +4365,10 @@ $wgCentralIdLookupProvider = 'local';
  *     - PasswordCannotMatchUsername - Password cannot match username to
  *     - PasswordCannotMatchBlacklist - Username/password combination cannot
  *             match a specific, hardcoded blacklist.
+ *     - PasswordCannotBePopular - Blacklist passwords which are known to be
+ *             commonly chosen. Set to integer n to ban the top n passwords.
+ *             If you want to ban all common passwords on file, use the
+ *             PHP_INT_MAX constant.
  * @since 1.26
  */
 $wgPasswordPolicy = array(
@@ -4373,11 +4377,13 @@ $wgPasswordPolicy = array(
                        'MinimalPasswordLength' => 8,
                        'MinimumPasswordLengthToLogin' => 1,
                        'PasswordCannotMatchUsername' => true,
+                       'PasswordCannotBePopular' => 25,
                ),
                'sysop' => array(
                        'MinimalPasswordLength' => 8,
                        'MinimumPasswordLengthToLogin' => 1,
                        'PasswordCannotMatchUsername' => true,
+                       'PasswordCannotBePopular' => 25,
                ),
                'bot' => array(
                        'MinimalPasswordLength' => 8,
@@ -4397,6 +4403,7 @@ $wgPasswordPolicy = array(
                'PasswordCannotMatchUsername' => 'PasswordPolicyChecks::checkPasswordCannotMatchUsername',
                'PasswordCannotMatchBlacklist' => 'PasswordPolicyChecks::checkPasswordCannotMatchBlacklist',
                'MaximalPasswordLength' => 'PasswordPolicyChecks::checkMaximalPasswordLength',
+               'PasswordCannotBePopular' => 'PasswordPolicyChecks::checkPopularPasswordBlacklist'
        ),
 );
 
@@ -7791,6 +7798,19 @@ $wgVirtualRestConfig = array(
  */
 $wgSearchRunSuggestedQuery = true;
 
+/**
+ * Where popular password file is located.
+ *
+ * Default in core contains 50,000 most popular. This config
+ * allows you to change which file, in case you want to generate
+ * a password file with > 50000 entries in it.
+ *
+ * @see maintenance/createCommonPasswordCdb.php
+ * @since 1.27
+ * @var string path to file
+ */
+$wgPopularPasswordFile = __DIR__ . '/../serialized/commonpasswords.cdb';
+
 /**
  * For really cool vim folding this needs to be at the end:
  * vim: foldmarker=@{,@} foldmethod=marker
index eb4a958..b1098f5 100644 (file)
@@ -20,6 +20,8 @@
  * @file
  */
 
+use \Cdb\Reader as CdbReader;
+
 /**
  * Functions to check passwords against a policy requirement
  * @since 1.26
@@ -112,4 +114,50 @@ class PasswordPolicyChecks {
                return $status;
        }
 
+       /**
+        * Ensure that password isn't in top X most popular passwords
+        *
+        * @param $policyVal int Cut off to use. Will automatically shrink to the max
+        *   supported for error messages if set to more than max number of passwords on file,
+        *   so you can use the PHP_INT_MAX constant here safely.
+        * @param $user User
+        * @param $password String
+        * @since 1.27
+        * @return Status
+        */
+       public static function checkPopularPasswordBlacklist( $policyVal, User $user, $password ) {
+               global $wgPopularPasswordFile, $wgSitename;
+               $status = Status::newGood();
+               if ( $policyVal > 0 ) {
+                       $langEn = Language::factory( 'en' );
+                       $passwordKey = $langEn->lc( trim( $password ) );
+
+                       // People often use the name of the current site, which won't be
+                       // in the common password file. Also check '' for people who use
+                       // just whitespace.
+                       $sitename = $langEn->lc( trim( $wgSitename ) );
+                       $hardcodedCommonPasswords = array( '', 'wiki', 'mediawiki', $sitename );
+                       if ( in_array( $passwordKey, $hardcodedCommonPasswords ) ) {
+                               $status->error( 'passwordtoopopular' );
+                               return $status;
+                       }
+
+                       // This could throw an exception, but there's not a good way
+                       // of failing gracefully, if say the file is missing, so just
+                       // let the exception fall through.
+                       // Format of cdb file is mapping password => popularity rank.
+                       // See maintenance/createCommonPasswordCdb.php
+                       $db = CdbReader::open( $wgPopularPasswordFile );
+
+                       $res = $db->get( $passwordKey );
+                       if ( $res && (int)$res <= $policyVal ) {
+                               // Note: If you want to find the true number of common
+                               // passwords stored (for reporting the error), you have to take
+                               // the max of the policyVal and $db->get( '_TOTALENTRIES' ).
+                               $status->error( 'passwordtoopopular' );
+                       }
+               }
+               return $status;
+       }
+
 }
index a7f0923..32e9b00 100644 (file)
        "wrongpasswordempty": "Password entered was blank.\nPlease try again.",
        "passwordtooshort": "Passwords must be at least {{PLURAL:$1|1 character|$1 characters}}.",
        "passwordtoolong": "Passwords cannot be longer than {{PLURAL:$1|1 character|$1 characters}}.",
+       "passwordtoopopular": "Commonly chosen passwords cannot be used. Please choose a more unique password.",
        "password-name-match": "Your password must be different from your username.",
        "password-login-forbidden": "The use of this username and password has been forbidden.",
        "mailmypassword": "Reset password",
index 2319663..15538e6 100644 (file)
        "wrongpasswordempty": "Error message displayed when entering a blank password.\n{{Identical|Please try again}}",
        "passwordtooshort": "This message is shown in [[Special:Preferences]] and [[Special:CreateAccount]].\n\nParameters:\n* $1 - the minimum number of characters in the password",
        "passwordtoolong": "This message is shown in [[Special:Preferences]], [[Special:CreateAccount]], and [[Special:Userlogin]].\n\nParameters:\n* $1 - the maximum number of characters in the password",
+       "passwordtoopopular": "Shown if the user chooses a really popular password.",
        "password-name-match": "Used as error message when password validity check failed.",
        "password-login-forbidden": "Error message shown when the user has tried to log in using one of the special username/password combinations used for MediaWiki testing. (See [[mwr:75589]], [[mwr:75605]].)",
        "mailmypassword": "Used as label for Submit button in [[Special:PasswordReset]].\n{{Identical|Reset password}}",
diff --git a/maintenance/createCommonPasswordCdb.php b/maintenance/createCommonPasswordCdb.php
new file mode 100644 (file)
index 0000000..c678712
--- /dev/null
@@ -0,0 +1,117 @@
+<?php
+/**
+ * Create serialized/commonpasswords.cdb
+ *
+ * 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
+ * @ingroup Maintenance
+ */
+
+require_once __DIR__ . '/Maintenance.php';
+
+/**
+ * Maintenance script to create common password cdb database.
+ *
+ * Meant to take a file like
+ * https://github.com/danielmiessler/SecLists/blob/master/Passwords/rockyou.txt?raw=true
+ * as input.
+ * @see serialized/commonpasswords.cdb and PasswordPolicyChecks::checkPopularPasswordBlacklist
+ * @since 1.27
+ * @ingroup Maintenance
+ */
+class GenerateCommonPassword extends Maintenance {
+       public function __construct() {
+               global $IP;
+               parent::__construct();
+               $this->mDescription = "Generate CDB file of common passwords";
+               $this->addOption( 'limit', "Max number of passwords to write", false, true, 'l' );
+               $this->addArg( 'inputfile', 'List of passwords (one per line) to use or - for stdin', true );
+               $this->addArg(
+                       'output',
+                       "Location to write CDB file to (Try $IP/serialized/commonpasswords.cdb)",
+                       true
+               );
+       }
+
+       public function execute() {
+               $limit = (int)$this->getOption( 'limit', PHP_INT_MAX );
+               $langEn = Language::factory( 'en' );
+
+               $infile = $this->getArg( 0 );
+               if ( $infile === '-' ) {
+                       $infile = 'php://stdin';
+               }
+               $outfile = $this->getArg( 1 );
+
+               if ( !is_readable( $infile ) && $infile !== 'php://stdin' ) {
+                       $this->error( "Cannot open input file $infile for reading", 1 );
+               }
+
+               $file = fopen( $infile, 'r' );
+               if ( $file === false ) {
+                       $this->error( "Cannot read input file $infile", 1 );
+               }
+
+               try {
+                       $db = \Cdb\Writer::open( $outfile );
+
+                       $alreadyWritten = array();
+                       $skipped = 0;
+                       for ( $i = 0; ( $i - $skipped ) < $limit; $i++ ) {
+                               if ( feof( $file ) ) {
+                                       break;
+                               }
+                               $rawLine = fgets( $file );
+
+                               if ( $rawLine === false ) {
+                                       $this->error( "Error reading input file" );
+                                       break;
+                               }
+                               if ( substr( $rawLine, -1 ) !== "\n" && !feof( $file ) ) {
+                                       // We're assuming that this just won't happen.
+                                       $this->error( "fgets did not return whole line at $i??" );
+                               }
+                               $line = $langEn->lc( trim( $rawLine ) );
+                               if ( $line === '' ) {
+                                       $this->error( "Line number " . ( $i + 1 ) . " is blank?" );
+                                       $skipped++;
+                                       continue;
+                               }
+                               if ( isset( $alreadyWritten[$line] ) ) {
+                                       $this->output( "Password '$line' already written (line " . ( $i + 1 ) .")\n" );
+                                       $skipped++;
+                                       continue;
+                               }
+                               $alreadyWritten[$line] = true;
+                               $db->set( $line, $i + 1 - $skipped );
+                       }
+                       // All caps, so cannot conflict with potential password
+                       $db->set( '_TOTALENTRIES', $i - $skipped );
+                       $db->close();
+
+                       $this->output( "Successfully wrote " . ( $i - $skipped ) .
+                               " (out of $i) passwords to $outfile\n"
+                       );
+               } catch ( \Cdb\Exception $e ) {
+                       $this->error( "Error writing cdb file: " . $e->getMessage(), 2 );
+               }
+
+       }
+}
+
+$maintClass = "GenerateCommonPassword";
+require_once RUN_MAINTENANCE_IF_MAIN;
diff --git a/serialized/commonpasswords.cdb b/serialized/commonpasswords.cdb
new file mode 100644 (file)
index 0000000..7b7b043
Binary files /dev/null and b/serialized/commonpasswords.cdb differ