r44816 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r44815‎ | r44816 | r44817 >
Date:23:33, 19 December 2008
Author:skizzerz
Status:deferred
Tags:
Comment:
* adding extension SecurePasswords -- creates more secure password hashes in the database as well as a password strength checker
Modified paths:
  • /trunk/extensions/SecurePasswords (added) (history)
  • /trunk/extensions/SecurePasswords/SecurePasswords.php (added) (history)
  • /trunk/extensions/SecurePasswords/securepasswords.sql (added) (history)

Diff [purge]

Index: trunk/extensions/SecurePasswords/SecurePasswords.php
@@ -0,0 +1,292 @@
 2+<?php
 3+
 4+/**
 5+ * SecurePasswords extension by Ryan Schmidt (Skizzerz)
 6+ * See http://www.mediawiki.org/wiki/Extension:SecurePasswords for details
 7+ * Code is released under the Public Domain -- just please give credit where credit is due :)
 8+ */
 9+
 10+if( !defined( 'MEDIAWIKI' ) ) {
 11+ echo "Not an entry point!\n";
 12+ die( 1 );
 13+}
 14+
 15+$wgValidPasswords = array(
 16+ 'minlength' => $wgMinimalPasswordLength, #Minimum password length, should be at least 8
 17+ 'maxlength' => 30, #Maximum password length, set to something lower if the hashes are being truncated in the database
 18+ 'lowercase' => true, #Should we require at least one lowercase letter?
 19+ 'uppercase' => true, #Should we require at least one uppercase letter?
 20+ 'digit' => true, #Should we require at least one digit?
 21+ 'special' => false, #Should we require at least one special character (punctuation, etc.)?
 22+ 'usercheck' => true, #Should we disallow passwords that are the same as the username?
 23+ 'wordcheck' => function_exists( 'pspell_check' ), #Should we check the password against a dictionary to make sure that it is not a word?
 24+);
 25+
 26+$wgExtensionCredits['other'][] = array(
 27+ 'name' => 'SecurePasswords',
 28+ 'author' => 'Ryan Schmidt',
 29+ 'url' => 'http://www.mediawiki.org/wiki/Extension:SecurePasswords',
 30+ 'version' => '1.0',
 31+);
 32+
 33+$wgHooks['UserCryptPassword'][] = 'efSecurePasswordsCrypt'; //used to encrypt passwords
 34+$wgHooks['UserComparePasswords'][] = 'efSecurePasswordsCompare'; //used to compare a password with an encrypted password
 35+$wgHooks['isValidPassword'][] = 'efSecurePasswordsValidate'; //used to enforce password strength
 36+
 37+function efSecurePasswordsCrypt( &$password, &$salt, &$wgPasswordSalt, &$hash ) {
 38+ $hash = 'SP:';
 39+
 40+ if( $salt === false ) {
 41+ $salt = substr( wfGenerateToken(), 0, 8 );
 42+ }
 43+
 44+ $h = function_exists( 'hash' );
 45+ $m = function_exists( 'mcrypt_encrypt' );
 46+ $g = function_exists( 'gzcompress' );
 47+
 48+ if( $h ) {
 49+ $hash_algos = hash_algos();
 50+ $algos = array();
 51+ foreach( $hash_algos as $algo ) {
 52+ switch( $algo ) {
 53+ case 'md5':
 54+ $algos[] = array( 'A', 'md5' );
 55+ break;
 56+ case 'sha1':
 57+ $algos[] = array( 'B', 'sha1' );
 58+ break;
 59+ case 'sha512':
 60+ $algos[] = array( 'C', 'sha512' );
 61+ break;
 62+ case 'ripemd160':
 63+ $algos[] = array( 'D', 'ripemd160' );
 64+ break;
 65+ case 'ripemd320':
 66+ $algos[] = array( 'E', 'ripemd320' );
 67+ break;
 68+ case 'whirlpool':
 69+ $algos[] = array( 'F', 'whirlpool' );
 70+ break;
 71+ case 'tiger192,4':
 72+ $algos[] = array( 'G', 'tiger192,4' );
 73+ break;
 74+ case 'snefru':
 75+ $algos[] = array( 'H', 'snefru' );
 76+ break;
 77+ case 'gost':
 78+ $algos[] = array( 'I', 'gost' );
 79+ break;
 80+ case 'haval256,5':
 81+ $algos[] = array( 'J', 'haval256,5' );
 82+ break;
 83+ }
 84+ }
 85+
 86+ $r1 = rand( 0, count( $algos ) - 1 );
 87+ $r2 = rand( 0, count( $algos ) - 1 );
 88+ } else {
 89+ $algos = array( array( 'A', 'md5' ), array( 'B', 'sha1' ) );
 90+ $r1 = rand( 0, 1 );
 91+ $r2 = rand( 0, 1 );
 92+ }
 93+
 94+ $type = $algos[$r1][0] . $algos[$r2][0];
 95+
 96+ if( $h ) {
 97+ $pw1 = hash( $algos[$r1][1], $password );
 98+ $pw2 = hash( $algos[$r2][1], $salt . '-' . $pw1 );
 99+ } else {
 100+ if( $r1 === 0 ) {
 101+ $pw1 = md5( $password );
 102+ } else {
 103+ $pw1 = sha1( $password );
 104+ }
 105+
 106+ if( $r2 === 0 ) {
 107+ $pw2 = md5( $salt . '-' . $pw1 );
 108+ } else {
 109+ $pw2 = sha1( $salt . '-' . $pw1 );
 110+ }
 111+ }
 112+
 113+ if( $m ) {
 114+ global $wgSecretKey;
 115+ $size = mcrypt_get_iv_size( MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC );
 116+ $ksize = mcrypt_get_key_size( MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC );
 117+ $iv = mcrypt_create_iv( $size, MCRYPT_RAND );
 118+ $key = substr( $wgSecretKey, 0, $ksize - 1 );
 119+ $pw3 = mcrypt_encrypt( MCRYPT_RIJNDAEL_256, $key, $pw2, MCRYPT_MODE_CBC, $iv );
 120+ $pw4 = base64_encode( $pw3 ) . '|' . base64_encode( $iv );
 121+ } else {
 122+ $pw4 = $pw2;
 123+ }
 124+
 125+ if( $g ) {
 126+ $pwf = base64_encode( gzcompress( $pw4 ) );
 127+ } else {
 128+ $pwf = base64_encode( $pw4 );
 129+ }
 130+
 131+ $hash .= $type . ':' . $salt . ':' . $pwf;
 132+
 133+ // sometimes the mcrypt is invalid, so we need to do a quick check to make sure that comparing will work in the future
 134+ // otherwise the password won't work... and that would suck
 135+ $hash = efSecurePasswordsRecursiveCheck( $hash, $password, $salt );
 136+
 137+ return false;
 138+}
 139+
 140+function efSecurePasswordsCompare( &$hash, &$password, &$userId, &$result ) {
 141+ $bits = explode( ':', $hash, 4 );
 142+
 143+ if( $bits[0] != 'SP' || count( $bits ) != 4 ) {
 144+ return true;
 145+ }
 146+
 147+ $type1 = substr( $bits[1], 0, 1 );
 148+ $type2 = substr( $bits[1], 1, 1 );
 149+ $salt = $bits[2];
 150+ $hash1 = $bits[3];
 151+
 152+ $h = function_exists( 'hash' );
 153+ $m = function_exists( 'mcrypt_encrypt' );
 154+ $g = function_exists( 'gzcompress' );
 155+
 156+ $algos = array(
 157+ 'A' => 'md5',
 158+ 'B' => 'sha1',
 159+ 'C' => 'sha512',
 160+ 'D' => 'ripemd160',
 161+ 'E' => 'ripemd320',
 162+ 'F' => 'whirlpool',
 163+ 'G' => 'tiger192,4',
 164+ 'H' => 'snefru',
 165+ 'I' => 'gost',
 166+ 'J' => 'haval256,5',
 167+ );
 168+
 169+ if( $h ) {
 170+ $pw1 = hash( $algos[$type1], $password );
 171+ $pwf = hash( $algos[$type2], $salt . '-' . $pw1 );
 172+ } else {
 173+ if( $r1 === 0 ) {
 174+ $pw1 = md5( $password );
 175+ } else {
 176+ $pw1 = sha1( $password );
 177+ }
 178+
 179+ if( $r2 === 0 ) {
 180+ $pwf = md5( $salt . '-' . $pw1 );
 181+ } else {
 182+ $pwf = sha1( $salt . '-' . $pw1 );
 183+ }
 184+ }
 185+
 186+ if( $g ) {
 187+ $h1 = gzuncompress( base64_decode( $hash1 ) );
 188+ } else {
 189+ $h1 = base64_decode( $hash1 );
 190+ }
 191+
 192+ if( $m ) {
 193+ global $wgSecretKey;
 194+ $ksize = mcrypt_get_key_size( MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC );
 195+ $key = substr( $wgSecretKey, 0, $ksize - 1 );
 196+ $bits = explode( '|', $h1 );
 197+ $iv = base64_decode( $bits[1] );
 198+ $h2 = base64_decode( $bits[0] );
 199+ $hf = mcrypt_decrypt( MCRYPT_RIJNDAEL_256, $key, $h2, MCRYPT_MODE_CBC, $iv );
 200+ } else {
 201+ $hf = $h1;
 202+ }
 203+
 204+ $result = ($hf === $pwf);
 205+ return false;
 206+}
 207+
 208+function efSecurePasswordsRecursiveCheck( $hash, $password, $salt ) {
 209+ $result = false;
 210+ $x = 1; // unused, but necessary since everything is by reference
 211+ efSecurePasswordsCompare( $hash, $password, $x, $result );
 212+ if(!$result) {
 213+ efSecurePasswordsCrypt( $password, $salt, $x, $hash );
 214+ return efSecurePasswordsRecursiveCheck( $hash, $password, $salt );
 215+ }
 216+ return $hash;
 217+}
 218+
 219+function efSecurePasswordsValidate( $password, &$result, $user ) {
 220+ // if the password matches the user's current password, then don't check for validity
 221+ // this way users with passwords that don't fit the criteria can still log in :)
 222+ if( ( $id = $user->getId() ) !== 0 ) {
 223+ $dbr = wfGetDB( DB_SLAVE );
 224+ $hash = $dbr->selectField( 'user', 'user_password', 'user_id=' . $id );
 225+ if( User::comparePasswords( $hash, $password, $id ) ) {
 226+ $result = true;
 227+ return false;
 228+ }
 229+ }
 230+
 231+ global $wgValidPasswords, $wgContLang;
 232+ $lang = $wgContLang->getPreferredVariant( false );
 233+
 234+ // check password length
 235+ if( strlen( $password ) < $wgValidPasswords['minlength'] || strlen( $password ) > $wgValidPasswords['maxlength'] ) {
 236+ $result = false;
 237+ return false;
 238+ }
 239+
 240+ // check for a lowercase letter, if needed
 241+ if( $wgValidPasswords['lowercase'] && !preg_match( '/[a-z]/', $password ) ) {
 242+ $result = false;
 243+ return false;
 244+ }
 245+
 246+ // check for an uppercase letter, if needed
 247+ if( $wgValidPasswords['uppercase'] && !preg_match( '/[A-Z]/', $password ) ) {
 248+ $result = false;
 249+ return false;
 250+ }
 251+
 252+ // check for a digit, if needed
 253+ if( $wgValidPasswords['digit'] && !preg_match( '/[0-9]/', $password ) ) {
 254+ $result = false;
 255+ return false;
 256+ }
 257+
 258+ // check for a special character, if needed
 259+ $special = '.|\/!@#$%^&*()-_=+[]{}\\\\`~,<>?\'";: ';
 260+ if( $wgValidPasswords['special'] && !preg_match( '/[' . $special . ']/', $password ) ) {
 261+ $result = false;
 262+ return false;
 263+ }
 264+
 265+ // check for the username, if needed
 266+ if( $wgValidPasswords['usercheck'] && $wgContLang->lc( $password ) == $wgContLang->lc( $user->getName() ) ) {
 267+ $result = false;
 268+ return false;
 269+ }
 270+
 271+ // check for words, if needed
 272+ if( $wgValidPasswords['wordcheck'] && function_exists( 'pspell_check' ) ) {
 273+ $link = pspell_new( $lang );
 274+ if( $link ) {
 275+ if( pspell_check( $link, $password ) ) {
 276+ $result = false;
 277+ return false;
 278+ }
 279+ }
 280+ if( $lang != 'en' ) {
 281+ $link = pspell_new( 'en' );
 282+ if( $link ) {
 283+ if( pspell_check( $link, $password ) ) {
 284+ $result = false;
 285+ return false;
 286+ }
 287+ }
 288+ }
 289+ }
 290+
 291+ $result = true;
 292+ return false;
 293+}
\ No newline at end of file
Property changes on: trunk/extensions/SecurePasswords/SecurePasswords.php
___________________________________________________________________
Added: svn:eol-style
1294 + native
Index: trunk/extensions/SecurePasswords/securepasswords.sql
@@ -0,0 +1,6 @@
 2+--- Modify the user table for the SecurePasswords extension
 3+--- tinyblob doesn't cut it anymore for the password hashing used by this extension
 4+--- so this will change it to mediumblob
 5+
 6+ALTER TABLE /*$wgDBprefix*/user MODIFY user_password MEDIUMBLOB NOT NULL;
 7+ALTER TABLE /*$wgDBprefix*/user MODIFY user_newpassword MEDIUMBLOB NOT NULL;
\ No newline at end of file
Property changes on: trunk/extensions/SecurePasswords/securepasswords.sql
___________________________________________________________________
Added: svn:eol-style
18 + native

Status & tagging log