r114241 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r114240‎ | r114241 | r114242 >
Date:09:39, 20 March 2012
Author:dantman
Status:ok
Tags:
Comment:
Backport CryptRand from r110825 and r114233 to REL1_17, REL1_18, and REL1_19 so all our supported versions of MediaWiki support proper cryptographic random sources for our tokens.
Modified paths:
  • /branches/REL1_17/phase3/includes/AutoLoader.php (modified) (history)
  • /branches/REL1_17/phase3/includes/CryptRand.php (added) (history)
  • /branches/REL1_17/phase3/includes/GlobalFunctions.php (modified) (history)
  • /branches/REL1_17/phase3/includes/User.php (modified) (history)
  • /branches/REL1_17/phase3/includes/installer/Installer.php (modified) (history)
  • /branches/REL1_17/phase3/includes/specials/SpecialUserlogin.php (modified) (history)
  • /branches/REL1_18/phase3/includes/AutoLoader.php (modified) (history)
  • /branches/REL1_18/phase3/includes/CryptRand.php (added) (history)
  • /branches/REL1_18/phase3/includes/GlobalFunctions.php (modified) (history)
  • /branches/REL1_18/phase3/includes/User.php (modified) (history)
  • /branches/REL1_18/phase3/includes/installer/Installer.php (modified) (history)
  • /branches/REL1_18/phase3/includes/specials/SpecialUserlogin.php (modified) (history)
  • /branches/REL1_18/phase3/includes/specials/SpecialWatchlist.php (modified) (history)
  • /branches/REL1_19/phase3/includes/AutoLoader.php (modified) (history)
  • /branches/REL1_19/phase3/includes/CryptRand.php (added) (history)
  • /branches/REL1_19/phase3/includes/GlobalFunctions.php (modified) (history)
  • /branches/REL1_19/phase3/includes/User.php (modified) (history)
  • /branches/REL1_19/phase3/includes/installer/Installer.php (modified) (history)
  • /branches/REL1_19/phase3/includes/specials/SpecialUserlogin.php (modified) (history)
  • /branches/REL1_19/phase3/includes/specials/SpecialWatchlist.php (modified) (history)

Diff [purge]

Index: branches/REL1_17/phase3/includes/CryptRand.php
@@ -0,0 +1,476 @@
 2+<?php
 3+/**
 4+ * A cryptographic random generator class used for generating secret keys
 5+ *
 6+ * This is based in part on Drupal code as well as what we used in our own code
 7+ * prior to introduction of this class.
 8+ *
 9+ * @author Daniel Friesen
 10+ * @file
 11+ */
 12+
 13+class MWCryptRand {
 14+
 15+ /**
 16+ * Minimum number of iterations we want to make in our drift calculations.
 17+ */
 18+ const MIN_ITERATIONS = 1000;
 19+
 20+ /**
 21+ * Number of milliseconds we want to spend generating each separate byte
 22+ * of the final generated bytes.
 23+ * This is used in combination with the hash length to determine the duration
 24+ * we should spend doing drift calculations.
 25+ */
 26+ const MSEC_PER_BYTE = 0.5;
 27+
 28+ /**
 29+ * Singleton instance for public use
 30+ */
 31+ protected static $singleton = null;
 32+
 33+ /**
 34+ * The hash algorithm being used
 35+ */
 36+ protected $algo = null;
 37+
 38+ /**
 39+ * The number of bytes outputted by the hash algorithm
 40+ */
 41+ protected $hashLength = null;
 42+
 43+ /**
 44+ * A boolean indicating whether the previous random generation was done using
 45+ * cryptographically strong random number generator or not.
 46+ */
 47+ protected $strong = null;
 48+
 49+ /**
 50+ * Initialize an initial random state based off of whatever we can find
 51+ */
 52+ protected function initialRandomState() {
 53+ // $_SERVER contains a variety of unstable user and system specific information
 54+ // It'll vary a little with each page, and vary even more with separate users
 55+ // It'll also vary slightly across different machines
 56+ $state = serialize( $_SERVER );
 57+
 58+ // To try and vary the system information of the state a bit more
 59+ // by including the system's hostname into the state
 60+ $state .= wfHostname();
 61+
 62+ // Try to gather a little entropy from the different php rand sources
 63+ $state .= rand() . uniqid( mt_rand(), true );
 64+
 65+ // Include some information about the filesystem's current state in the random state
 66+ $files = array();
 67+ // We know this file is here so grab some info about ourself
 68+ $files[] = __FILE__;
 69+ // The config file is likely the most often edited file we know should be around
 70+ // so if the constant with it's location is defined include it's stat info into the state
 71+ if ( defined( 'MW_CONFIG_FILE' ) ) {
 72+ $files[] = MW_CONFIG_FILE;
 73+ }
 74+ foreach ( $files as $file ) {
 75+ wfSuppressWarnings();
 76+ $stat = stat( $file );
 77+ wfRestoreWarnings();
 78+ if ( $stat ) {
 79+ // stat() duplicates data into numeric and string keys so kill off all the numeric ones
 80+ foreach ( $stat as $k => $v ) {
 81+ if ( is_numeric( $k ) ) {
 82+ unset( $k );
 83+ }
 84+ }
 85+ // The absolute filename itself will differ from install to install so don't leave it out
 86+ $state .= realpath( $file );
 87+ $state .= implode( '', $stat );
 88+ } else {
 89+ // The fact that the file isn't there is worth at least a
 90+ // minuscule amount of entropy.
 91+ $state .= '0';
 92+ }
 93+ }
 94+
 95+ // Try and make this a little more unstable by including the varying process
 96+ // id of the php process we are running inside of if we are able to access it
 97+ if ( function_exists( 'getmypid' ) ) {
 98+ $state .= getmypid();
 99+ }
 100+
 101+ // If available try to increase the instability of the data by throwing in
 102+ // the precise amount of memory that we happen to be using at the moment.
 103+ if ( function_exists( 'memory_get_usage' ) ) {
 104+ $state .= memory_get_usage( true );
 105+ }
 106+
 107+ // It's mostly worthless but throw the wiki's id into the data for a little more variance
 108+ $state .= wfWikiID();
 109+
 110+ // If we have a secret key or proxy key set then throw it into the state as well
 111+ global $wgSecretKey, $wgProxyKey;
 112+ if ( $wgSecretKey ) {
 113+ $state .= $wgSecretKey;
 114+ } elseif ( $wgProxyKey ) {
 115+ $state .= $wgProxyKey;
 116+ }
 117+
 118+ return $state;
 119+ }
 120+
 121+ /**
 122+ * Randomly hash data while mixing in clock drift data for randomness
 123+ *
 124+ * @param $data The data to randomly hash.
 125+ * @return String The hashed bytes
 126+ * @author Tim Starling
 127+ */
 128+ protected function driftHash( $data ) {
 129+ // Minimum number of iterations (to avoid slow operations causing the loop to gather little entropy)
 130+ $minIterations = self::MIN_ITERATIONS;
 131+ // Duration of time to spend doing calculations (in seconds)
 132+ $duration = ( self::MSEC_PER_BYTE / 1000 ) * $this->hashLength();
 133+ // Create a buffer to use to trigger memory operations
 134+ $bufLength = 10000000;
 135+ $buffer = str_repeat( ' ', $bufLength );
 136+ $bufPos = 0;
 137+
 138+ // Iterate for $duration seconds or at least $minIerations number of iterations
 139+ $iterations = 0;
 140+ $startTime = microtime( true );
 141+ $currentTime = $startTime;
 142+ while ( $iterations < $minIterations || $currentTime - $startTime < $duration ) {
 143+ // Trigger some memory writing to trigger some bus activity
 144+ // This may create variance in the time between iterations
 145+ $bufPos = ( $bufPos + 13 ) % $bufLength;
 146+ $buffer[$bufPos] = ' ';
 147+ // Add the drift between this iteration and the last in as entropy
 148+ $nextTime = microtime( true );
 149+ $delta = (int)( ( $nextTime - $currentTime ) * 1000000 );
 150+ $data .= $delta;
 151+ // Every 100 iterations hash the data and entropy
 152+ if ( $iterations % 100 === 0 ) {
 153+ $data = sha1( $data );
 154+ }
 155+ $currentTime = $nextTime;
 156+ $iterations++;
 157+ }
 158+ $timeTaken = $currentTime - $startTime;
 159+ $data = $this->hash( $data );
 160+
 161+ wfDebug( __METHOD__ . ": Clock drift calculation " .
 162+ "(time-taken=" . ( $timeTaken * 1000 ) . "ms, " .
 163+ "iterations=$iterations, " .
 164+ "time-per-iteration=" . ( $timeTaken / $iterations * 1e6 ) . "us)\n" );
 165+ return $data;
 166+ }
 167+
 168+ /**
 169+ * Return a rolling random state initially build using data from unstable sources
 170+ * @return A new weak random state
 171+ */
 172+ protected function randomState() {
 173+ static $state = null;
 174+ if ( is_null( $state ) ) {
 175+ // Initialize the state with whatever unstable data we can find
 176+ // It's important that this data is hashed right afterwards to prevent
 177+ // it from being leaked into the output stream
 178+ $state = $this->hash( $this->initialRandomState() );
 179+ }
 180+ // Generate a new random state based on the initial random state or previous
 181+ // random state by combining it with clock drift
 182+ $state = $this->driftHash( $state );
 183+ return $state;
 184+ }
 185+
 186+ /**
 187+ * Decide on the best acceptable hash algorithm we have available for hash()
 188+ * @return String A hash algorithm
 189+ */
 190+ protected function hashAlgo() {
 191+ if ( !is_null( $algo ) ) {
 192+ return $algo;
 193+ }
 194+
 195+ $algos = hash_algos();
 196+ $preference = array( 'whirlpool', 'sha256', 'sha1', 'md5' );
 197+
 198+ foreach ( $preference as $algorithm ) {
 199+ if ( in_array( $algorithm, $algos ) ) {
 200+ $algo = $algorithm; # assign to static
 201+ wfDebug( __METHOD__ . ": Using the $algo hash algorithm.\n" );
 202+ return $algo;
 203+ }
 204+ }
 205+
 206+ // We only reach here if no acceptable hash is found in the list, this should
 207+ // be a technical impossibility since most of php's hash list is fixed and
 208+ // some of the ones we list are available as their own native functions
 209+ // But since we already require at least 5.2 and hash() was default in
 210+ // 5.1.2 we don't bother falling back to methods like sha1 and md5.
 211+ throw new MWException( "Could not find an acceptable hashing function in hash_algos()" );
 212+ }
 213+
 214+ /**
 215+ * Return the byte-length output of the hash algorithm we are
 216+ * using in self::hash and self::hmac.
 217+ *
 218+ * @return int Number of bytes the hash outputs
 219+ */
 220+ protected function hashLength() {
 221+ if ( is_null( $hashLength ) ) {
 222+ $hashLength = strlen( $this->hash( '' ) );
 223+ }
 224+ return $hashLength;
 225+ }
 226+
 227+ /**
 228+ * Generate an acceptably unstable one-way-hash of some text
 229+ * making use of the best hash algorithm that we have available.
 230+ *
 231+ * @return String A raw hash of the data
 232+ */
 233+ protected function hash( $data ) {
 234+ return hash( $this->hashAlgo(), $data, true );
 235+ }
 236+
 237+ /**
 238+ * Generate an acceptably unstable one-way-hmac of some text
 239+ * making use of the best hash algorithm that we have available.
 240+ *
 241+ * @return String A raw hash of the data
 242+ */
 243+ protected function hmac( $data, $key ) {
 244+ return hash_hmac( $this->hashAlgo(), $data, $key, true );
 245+ }
 246+
 247+ /**
 248+ * @see self::wasStrong()
 249+ */
 250+ public function realWasStrong() {
 251+ if ( is_null( $this->strong ) ) {
 252+ throw new MWException( __METHOD__ . ' called before generation of random data' );
 253+ }
 254+ return $this->strong;
 255+ }
 256+
 257+ /**
 258+ * @see self::generate()
 259+ */
 260+ public function realGenerate( $bytes, $forceStrong = false, $method = null ) {
 261+ wfProfileIn( __METHOD__ );
 262+ if ( is_string( $forceStrong ) && is_null( $method ) ) {
 263+ // If $forceStrong is a string then it's really $method
 264+ $method = $forceStrong;
 265+ $forceStrong = false;
 266+ }
 267+
 268+ if ( !is_null( $method ) ) {
 269+ wfDebug( __METHOD__ . ": Generating cryptographic random bytes for $method\n" );
 270+ }
 271+
 272+ $bytes = floor( $bytes );
 273+ static $buffer = '';
 274+ if ( is_null( $this->strong ) ) {
 275+ // Set strength to false initially until we know what source data is coming from
 276+ $this->strong = true;
 277+ }
 278+
 279+ if ( strlen( $buffer ) < $bytes ) {
 280+ // If available make use of mcrypt_create_iv URANDOM source to generate randomness
 281+ // On unix-like systems this reads from /dev/urandom but does it without any buffering
 282+ // and bypasses openbasdir restrictions so it's preferable to reading directly
 283+ // On Windows starting in PHP 5.3.0 Windows' native CryptGenRandom is used to generate
 284+ // entropy so this is also preferable to just trying to read urandom because it may work
 285+ // on Windows systems as well.
 286+ if ( function_exists( 'mcrypt_create_iv' ) ) {
 287+ wfProfileIn( __METHOD__ . '-mcrypt' );
 288+ $rem = $bytes - strlen( $buffer );
 289+ wfDebug( __METHOD__ . ": Trying to generate $rem bytes of randomness using mcrypt_create_iv.\n" );
 290+ $iv = mcrypt_create_iv( $rem, MCRYPT_DEV_URANDOM );
 291+ if ( $iv === false ) {
 292+ wfDebug( __METHOD__ . ": mcrypt_create_iv returned false.\n" );
 293+ } else {
 294+ $bytes .= $iv;
 295+ wfDebug( __METHOD__ . ": mcrypt_create_iv generated " . strlen( $iv ) . " bytes of randomness.\n" );
 296+ }
 297+ wfProfileOut( __METHOD__ . '-mcrypt' );
 298+ }
 299+ }
 300+
 301+ if ( strlen( $buffer ) < $bytes ) {
 302+ // If available make use of openssl's random_pesudo_bytes method to attempt to generate randomness.
 303+ // However don't do this on Windows with PHP < 5.3.4 due to a bug:
 304+ // http://stackoverflow.com/questions/1940168/openssl-random-pseudo-bytes-is-slow-php
 305+ if ( function_exists( 'openssl_random_pseudo_bytes' )
 306+ && ( !wfIsWindows() || version_compare( PHP_VERSION, '5.3.4', '>=' ) )
 307+ ) {
 308+ wfProfileIn( __METHOD__ . '-openssl' );
 309+ $rem = $bytes - strlen( $buffer );
 310+ wfDebug( __METHOD__ . ": Trying to generate $rem bytes of randomness using openssl_random_pseudo_bytes.\n" );
 311+ $openssl_bytes = openssl_random_pseudo_bytes( $rem, $openssl_strong );
 312+ if ( $openssl_bytes === false ) {
 313+ wfDebug( __METHOD__ . ": openssl_random_pseudo_bytes returned false.\n" );
 314+ } else {
 315+ $buffer .= $openssl_bytes;
 316+ wfDebug( __METHOD__ . ": openssl_random_pseudo_bytes generated " . strlen( $openssl_bytes ) . " bytes of " . ( $openssl_strong ? "strong" : "weak" ) . " randomness.\n" );
 317+ }
 318+ if ( strlen( $buffer ) >= $bytes ) {
 319+ // openssl tells us if the random source was strong, if some of our data was generated
 320+ // using it use it's say on whether the randomness is strong
 321+ $this->strong = !!$openssl_strong;
 322+ }
 323+ wfProfileOut( __METHOD__ . '-openssl' );
 324+ }
 325+ }
 326+
 327+ // Only read from urandom if we can control the buffer size or were passed forceStrong
 328+ if ( strlen( $buffer ) < $bytes && ( function_exists( 'stream_set_read_buffer' ) || $forceStrong ) ) {
 329+ wfProfileIn( __METHOD__ . '-fopen-urandom' );
 330+ $rem = $bytes - strlen( $buffer );
 331+ wfDebug( __METHOD__ . ": Trying to generate $rem bytes of randomness using /dev/urandom.\n" );
 332+ if ( !function_exists( 'stream_set_read_buffer' ) && $forceStrong ) {
 333+ wfDebug( __METHOD__ . ": Was forced to read from /dev/urandom without control over the buffer size.\n" );
 334+ }
 335+ // /dev/urandom is generally considered the best possible commonly
 336+ // available random source, and is available on most *nix systems.
 337+ wfSuppressWarnings();
 338+ $urandom = fopen( "/dev/urandom", "rb" );
 339+ wfRestoreWarnings();
 340+
 341+ // Attempt to read all our random data from urandom
 342+ // php's fread always does buffered reads based on the stream's chunk_size
 343+ // so in reality it will usually read more than the amount of data we're
 344+ // asked for and not storing that risks depleting the system's random pool.
 345+ // If stream_set_read_buffer is available set the chunk_size to the amount
 346+ // of data we need. Otherwise read 8k, php's default chunk_size.
 347+ if ( $urandom ) {
 348+ // php's default chunk_size is 8k
 349+ $chunk_size = 1024 * 8;
 350+ if ( function_exists( 'stream_set_read_buffer' ) ) {
 351+ // If possible set the chunk_size to the amount of data we need
 352+ stream_set_read_buffer( $urandom, $rem );
 353+ $chunk_size = $rem;
 354+ }
 355+ wfDebug( __METHOD__ . ": Reading from /dev/urandom with a buffer size of $chunk_size.\n" );
 356+ $random_bytes = fread( $urandom, max( $chunk_size, $rem ) );
 357+ $buffer .= $random_bytes;
 358+ fclose( $urandom );
 359+ wfDebug( __METHOD__ . ": /dev/urandom generated " . strlen( $random_bytes ) . " bytes of randomness.\n" );
 360+ if ( strlen( $buffer ) >= $bytes ) {
 361+ // urandom is always strong, set to true if all our data was generated using it
 362+ $this->strong = true;
 363+ }
 364+ } else {
 365+ wfDebug( __METHOD__ . ": /dev/urandom could not be opened.\n" );
 366+ }
 367+ wfProfileOut( __METHOD__ . '-fopen-urandom' );
 368+ }
 369+
 370+ // If we cannot use or generate enough data from a secure source
 371+ // use this loop to generate a good set of pseudo random data.
 372+ // This works by initializing a random state using a pile of unstable data
 373+ // and continually shoving it through a hash along with a variable salt.
 374+ // We hash the random state with more salt to avoid the state from leaking
 375+ // out and being used to predict the /randomness/ that follows.
 376+ if ( strlen( $buffer ) < $bytes ) {
 377+ wfDebug( __METHOD__ . ": Falling back to using a pseudo random state to generate randomness.\n" );
 378+ }
 379+ while ( strlen( $buffer ) < $bytes ) {
 380+ wfProfileIn( __METHOD__ . '-fallback' );
 381+ $buffer .= $this->hmac( $this->randomState(), mt_rand() );
 382+ // This code is never really cryptographically strong, if we use it
 383+ // at all, then set strong to false.
 384+ $this->strong = false;
 385+ wfProfileOut( __METHOD__ . '-fallback' );
 386+ }
 387+
 388+ // Once the buffer has been filled up with enough random data to fulfill
 389+ // the request shift off enough data to handle the request and leave the
 390+ // unused portion left inside the buffer for the next request for random data
 391+ $generated = substr( $buffer, 0, $bytes );
 392+ $buffer = substr( $buffer, $bytes );
 393+
 394+ wfDebug( __METHOD__ . ": " . strlen( $buffer ) . " bytes of randomness leftover in the buffer.\n" );
 395+
 396+ wfProfileOut( __METHOD__ );
 397+ return $generated;
 398+ }
 399+
 400+ /**
 401+ * @see self::generateHex()
 402+ */
 403+ public function realGenerateHex( $chars, $forceStrong = false, $method = null ) {
 404+ // hex strings are 2x the length of raw binary so we divide the length in half
 405+ // odd numbers will result in a .5 that leads the generate() being 1 character
 406+ // short, so we use ceil() to ensure that we always have enough bytes
 407+ $bytes = ceil( $chars / 2 );
 408+ // Generate the data and then convert it to a hex string
 409+ $hex = bin2hex( $this->generate( $bytes, $forceStrong, $method ) );
 410+ // A bit of paranoia here, the caller asked for a specific length of string
 411+ // here, and it's possible (eg when given an odd number) that we may actually
 412+ // have at least 1 char more than they asked for. Just in case they made this
 413+ // call intending to insert it into a database that does truncation we don't
 414+ // want to give them too much and end up with their database and their live
 415+ // code having two different values because part of what we gave them is truncated
 416+ // hence, we strip out any run of characters longer than what we were asked for.
 417+ return substr( $hex, 0, $chars );
 418+ }
 419+
 420+ /** Publicly exposed static methods **/
 421+
 422+ /**
 423+ * Return a singleton instance of MWCryptRand
 424+ */
 425+ protected static function singleton() {
 426+ if ( is_null( self::$singleton ) ) {
 427+ self::$singleton = new self;
 428+ }
 429+ return self::$singleton;
 430+ }
 431+
 432+ /**
 433+ * Return a boolean indicating whether or not the source used for cryptographic
 434+ * random bytes generation in the previously run generate* call
 435+ * was cryptographically strong.
 436+ *
 437+ * @return bool Returns true if the source was strong, false if not.
 438+ */
 439+ public static function wasStrong() {
 440+ return self::singleton()->realWasStrong();
 441+ }
 442+
 443+ /**
 444+ * Generate a run of (ideally) cryptographically random data and return
 445+ * it in raw binary form.
 446+ * You can use MWCryptRand::wasStrong() if you wish to know if the source used
 447+ * was cryptographically strong.
 448+ *
 449+ * @param $bytes int the number of bytes of random data to generate
 450+ * @param $forceStrong bool Pass true if you want generate to prefer cryptographically
 451+ * strong sources of entropy even if reading from them may steal
 452+ * more entropy from the system than optimal.
 453+ * @param $method The calling method, for debug info. May be the second argument if you are not using forceStrong
 454+ * @return String Raw binary random data
 455+ */
 456+ public static function generate( $bytes, $forceStrong = false, $method = null ) {
 457+ return self::singleton()->realGenerate( $bytes, $forceStrong, $method );
 458+ }
 459+
 460+ /**
 461+ * Generate a run of (ideally) cryptographically random data and return
 462+ * it in hexadecimal string format.
 463+ * You can use MWCryptRand::wasStrong() if you wish to know if the source used
 464+ * was cryptographically strong.
 465+ *
 466+ * @param $chars int the number of hex chars of random data to generate
 467+ * @param $forceStrong bool Pass true if you want generate to prefer cryptographically
 468+ * strong sources of entropy even if reading from them may steal
 469+ * more entropy from the system than optimal.
 470+ * @param $method The calling method, for debug info. May be the second argument if you are not using forceStrong
 471+ * @return String Hexadecimal random data
 472+ */
 473+ public static function generateHex( $chars, $forceStrong = false, $method = null ) {
 474+ return self::singleton()->realGenerateHex( $chars, $forceStrong, $method );
 475+ }
 476+
 477+}
Index: branches/REL1_17/phase3/includes/User.php
@@ -786,23 +786,20 @@
787787 }
788788
789789 /**
790 - * Return a random password. Sourced from mt_rand, so it's not particularly secure.
791 - * @todo hash random numbers to improve security, like generateToken()
 790+ * Return a random password.
792791 *
793792 * @return \string New random password
794793 */
795794 static function randomPassword() {
796 - global $wgMinimalPasswordLength;
797 - $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
798 - $l = strlen( $pwchars ) - 1;
799 -
800 - $pwlength = max( 7, $wgMinimalPasswordLength );
801 - $digit = mt_rand( 0, $pwlength - 1 );
802 - $np = '';
803 - for ( $i = 0; $i < $pwlength; $i++ ) {
804 - $np .= $i == $digit ? chr( mt_rand( 48, 57 ) ) : $pwchars{ mt_rand( 0, $l ) };
805 - }
806 - return $np;
 795+ global $wgMinimalPasswordLength;
 796+ // Decide the final password length based on our min password length, stopping at a minimum of 10 chars
 797+ $length = max( 10, $wgMinimalPasswordLength );
 798+ // Multiply by 1.25 to get the number of hex characters we need
 799+ $length = $length * 1.25;
 800+ // Generate random hex chars
 801+ $hex = MWCryptRand::generateHex( $length, __METHOD__ );
 802+ // Convert from base 16 to base 32 to get a proper password like string
 803+ return wfBaseConvert( $hex, 16, 32 );
807804 }
808805
809806 /**
@@ -832,7 +829,7 @@
833830 $this->mTouched = '0'; # Allow any pages to be cached
834831 }
835832
836 - $this->setToken(); # Random
 833+ $this->mToken = null; // Don't run cryptographic functions till we need a token
837834 $this->mEmailAuthenticated = null;
838835 $this->mEmailToken = '';
839836 $this->mEmailTokenExpires = null;
@@ -919,11 +916,11 @@
920917 return false;
921918 }
922919
923 - if ( isset( $_SESSION['wsToken'] ) ) {
924 - $passwordCorrect = $proposedUser->getToken() === $_SESSION['wsToken'];
 920+ if ( isset( $_SESSION['wsToken'] ) && $_SESSION['wsToken'] ) {
 921+ $passwordCorrect = $proposedUser->getToken( false ) === $_SESSION['wsToken'];
925922 $from = 'session';
926 - } else if ( $wgRequest->getCookie( 'Token' ) !== null ) {
927 - $passwordCorrect = $proposedUser->getToken() === $wgRequest->getCookie( 'Token' );
 923+ } elseif ( $wgRequest->getCookie( 'Token' ) ) {
 924+ $passwordCorrect = $proposedUser->getToken( false ) === $wgRequest->getCookie( 'Token' );
928925 $from = 'cookie';
929926 } else {
930927 # No session or persistent login cookie
@@ -1012,6 +1009,9 @@
10131010 $this->decodeOptions( $row->user_options );
10141011 $this->mTouched = wfTimestamp(TS_MW,$row->user_touched);
10151012 $this->mToken = $row->user_token;
 1013+ if ( $this->mToken == '' ) {
 1014+ $this->mToken = null;
 1015+ }
10161016 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
10171017 $this->mEmailToken = $row->user_email_token;
10181018 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
@@ -1841,10 +1841,14 @@
18421842
18431843 /**
18441844 * Get the user's current token.
 1845+ * @param $forceCreation Force the generation of a new token if the user doesn't have one (default=true for backwards compatibility)
18451846 * @return \string Token
18461847 */
1847 - function getToken() {
 1848+ function getToken( $forceCreation = true ) {
18481849 $this->load();
 1850+ if ( !$this->mToken && $forceCreation ) {
 1851+ $this->setToken();
 1852+ }
18491853 return $this->mToken;
18501854 }
18511855
@@ -1859,14 +1863,7 @@
18601864 global $wgSecretKey, $wgProxyKey;
18611865 $this->load();
18621866 if ( !$token ) {
1863 - if ( $wgSecretKey ) {
1864 - $key = $wgSecretKey;
1865 - } elseif ( $wgProxyKey ) {
1866 - $key = $wgProxyKey;
1867 - } else {
1868 - $key = microtime();
1869 - }
1870 - $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . wfWikiID() . $this->mId );
 1867+ $this->mToken = MWCryptRand::generateHex( USER_TOKEN_LENGTH, __METHOD__ );
18711868 } else {
18721869 $this->mToken = $token;
18731870 }
@@ -2477,6 +2474,14 @@
24782475 function setCookies() {
24792476 $this->load();
24802477 if ( 0 == $this->mId ) return;
 2478+ if ( !$this->mToken ) {
 2479+ // When token is empty or NULL generate a new one and then save it to the database
 2480+ // This allows a wiki to re-secure itself after a leak of it's user table or $wgSecretKey
 2481+ // Simply by setting every cell in the user_token column to NULL and letting them be
 2482+ // regenerated as users log back into the wiki.
 2483+ $this->setToken();
 2484+ $this->saveSettings();
 2485+ }
24812486 $session = array(
24822487 'wsUserID' => $this->mId,
24832488 'wsToken' => $this->mToken,
@@ -2555,7 +2560,7 @@
25562561 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
25572562 'user_options' => '',
25582563 'user_touched' => $dbw->timestamp( $this->mTouched ),
2559 - 'user_token' => $this->mToken,
 2564+ 'user_token' => strval( $this->mToken ),
25602565 'user_email_token' => $this->mEmailToken,
25612566 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
25622567 ), array( /* WHERE */
@@ -2621,7 +2626,7 @@
26222627 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
26232628 'user_real_name' => $user->mRealName,
26242629 'user_options' => '',
2625 - 'user_token' => $user->mToken,
 2630+ 'user_token' => strval( $user->mToken ),
26262631 'user_registration' => $dbw->timestamp( $user->mRegistration ),
26272632 'user_editcount' => 0,
26282633 );
@@ -2655,7 +2660,7 @@
26562661 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
26572662 'user_real_name' => $this->mRealName,
26582663 'user_options' => '',
2659 - 'user_token' => $this->mToken,
 2664+ 'user_token' => strval( $this->mToken ),
26602665 'user_registration' => $dbw->timestamp( $this->mRegistration ),
26612666 'user_editcount' => 0,
26622667 ), __METHOD__
@@ -2881,7 +2886,7 @@
28822887 return EDIT_TOKEN_SUFFIX;
28832888 } else {
28842889 if( !isset( $_SESSION['wsEditToken'] ) ) {
2885 - $token = self::generateToken();
 2890+ $token = MWCryptRand::generateHex( 32, __METHOD__ );
28862891 $_SESSION['wsEditToken'] = $token;
28872892 } else {
28882893 $token = $_SESSION['wsEditToken'];
@@ -2900,8 +2905,7 @@
29012906 * @return \string The new random token
29022907 */
29032908 public static function generateToken( $salt = '' ) {
2904 - $token = dechex( mt_rand() ) . dechex( mt_rand() );
2905 - return md5( $token . $salt );
 2909+ return MWCryptRand::generateHex( 32, __METHOD__ );
29062910 }
29072911
29082912 /**
@@ -3005,12 +3009,12 @@
30063010 function confirmationToken( &$expiration ) {
30073011 $now = time();
30083012 $expires = $now + 7 * 24 * 60 * 60;
3009 - $expiration = wfTimestamp( TS_MW, $expires );
3010 - $token = self::generateToken( $this->mId . $this->mEmail . $expires );
 3013+ $expiration =
 3014+ $token = MWCryptRand::generateHex( 32, __METHOD__ );
30113015 $hash = md5( $token );
30123016 $this->load();
30133017 $this->mEmailToken = $hash;
3014 - $this->mEmailTokenExpires = $expiration;
 3018+ $this->mEmailTokenExpires = wfTimestamp( TS_MW, $expires );
30153019 return $token;
30163020 }
30173021
@@ -3560,7 +3564,7 @@
35613565
35623566 if( $wgPasswordSalt ) {
35633567 if ( $salt === false ) {
3564 - $salt = substr( wfGenerateToken(), 0, 8 );
 3568+ $salt = MWCryptRand::generateHex( 8, __METHOD__ );
35653569 }
35663570 return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
35673571 } else {
Index: branches/REL1_17/phase3/includes/GlobalFunctions.php
@@ -3038,6 +3038,33 @@
30393039 }
30403040
30413041 /**
 3042+ * Override session_id before session startup if php's built-in
 3043+ * session generation code is not secure.
 3044+ */
 3045+function wfFixSessionID() {
 3046+ // If the cookie or session id is already set we already have a session and should abort
 3047+ if ( isset( $_COOKIE[ session_name() ] ) || session_id() ) {
 3048+ return;
 3049+ }
 3050+
 3051+ // PHP's built-in session entropy is enabled if:
 3052+ // - entropy_file is set or you're on Windows with php 5.3.3+
 3053+ // - AND entropy_length is > 0
 3054+ // We treat it as disabled if it doesn't have an entropy length of at least 32
 3055+ $entropyEnabled = (
 3056+ ( wfIsWindows() && version_compare( PHP_VERSION, '5.3.3', '>=' ) )
 3057+ || ini_get( 'session.entropy_file' )
 3058+ )
 3059+ && intval( ini_get( 'session.entropy_length' ) ) >= 32;
 3060+
 3061+ // If built-in entropy is not enabled or not sufficient override php's built in session id generation code
 3062+ if ( !$entropyEnabled ) {
 3063+ wfDebug( __METHOD__ . ": PHP's built in entropy is disabled or not sufficient, overriding session id generation using our cryptrand source.\n" );
 3064+ session_id( MWCryptRand::generateHex( 32, __METHOD__ ) );
 3065+ }
 3066+}
 3067+
 3068+/**
30423069 * Initialise php session
30433070 */
30443071 function wfSetupSession( $sessionId = false ) {
@@ -3068,6 +3095,8 @@
30693096 session_cache_limiter( 'private, must-revalidate' );
30703097 if ( $sessionId ) {
30713098 session_id( $sessionId );
 3099+ } else {
 3100+ wfFixSessionID();
30723101 }
30733102 wfSuppressWarnings();
30743103 session_start();
Index: branches/REL1_17/phase3/includes/installer/Installer.php
@@ -1333,8 +1333,7 @@
13341334 }
13351335
13361336 /**
1337 - * Generate $wgSecretKey. Will warn if we had to use mt_rand() instead of
1338 - * /dev/urandom
 1337+ * Generate $wgSecretKey. Will warn if we had to use an insecure random source.
13391338 *
13401339 * @return Status
13411340 */
@@ -1347,8 +1346,8 @@
13481347 }
13491348
13501349 /**
1351 - * Generate a secret value for variables using either
1352 - * /dev/urandom or mt_rand(). Produce a warning in the later case.
 1350+ * Generate a secret value for variables using our CryptRand generator.
 1351+ * Produce a warning if the random source was insecure.
13531352 *
13541353 * @param $keys Array
13551354 * @return Status
@@ -1356,28 +1355,18 @@
13571356 protected function doGenerateKeys( $keys ) {
13581357 $status = Status::newGood();
13591358
1360 - wfSuppressWarnings();
1361 - $file = fopen( "/dev/urandom", "r" );
1362 - wfRestoreWarnings();
1363 -
 1359+ $strong = true;
13641360 foreach ( $keys as $name => $length ) {
1365 - if ( $file ) {
1366 - $secretKey = bin2hex( fread( $file, $length / 2 ) );
1367 - } else {
1368 - $secretKey = '';
1369 -
1370 - for ( $i = 0; $i < $length / 8; $i++ ) {
1371 - $secretKey .= dechex( mt_rand( 0, 0x7fffffff ) );
1372 - }
 1361+ $secretKey = MWCryptRand::generateHex( $length, true );
 1362+ if ( !MWCryptRand::wasStrong() ) {
 1363+ $strong = false;
13731364 }
13741365
13751366 $this->setVar( $name, $secretKey );
13761367 }
13771368
1378 - if ( $file ) {
1379 - fclose( $file );
1380 - } else {
1381 - $names = array_keys ( $keys );
 1369+ if ( !$strong ) {
 1370+ $names = array_keys( $keys );
13821371 $names = preg_replace( '/^(.*)$/', '\$$1', $names );
13831372 global $wgLang;
13841373 $status->warning( 'config-insecure-keys', $wgLang->listToText( $names ), count( $names ) );
Index: branches/REL1_17/phase3/includes/AutoLoader.php
@@ -167,6 +167,7 @@
168168 'MessageBlobStore' => 'includes/MessageBlobStore.php',
169169 'MessageCache' => 'includes/MessageCache.php',
170170 'MimeMagic' => 'includes/MimeMagic.php',
 171+ 'MWCryptRand' => 'includes/CryptRand.php',
171172 'MWException' => 'includes/Exception.php',
172173 'MWHttpRequest' => 'includes/HttpFunctions.php',
173174 'MWMemcached' => 'includes/memcached-client.php',
Index: branches/REL1_17/phase3/includes/specials/SpecialUserlogin.php
@@ -1099,9 +1099,9 @@
11001100 */
11011101 public static function setLoginToken() {
11021102 global $wgRequest;
1103 - // Use User::generateToken() instead of $user->editToken()
 1103+ // Generate a token directly instead of using $user->editToken()
11041104 // because the latter reuses $_SESSION['wsEditToken']
1105 - $wgRequest->setSessionData( 'wsLoginToken', User::generateToken() );
 1105+ $wgRequest->setSessionData( 'wsLoginToken', MWCryptRand::generateHex( 32, __METHOD__ ) );
11061106 }
11071107
11081108 /**
@@ -1125,7 +1125,7 @@
11261126 */
11271127 public static function setCreateaccountToken() {
11281128 global $wgRequest;
1129 - $wgRequest->setSessionData( 'wsCreateaccountToken', User::generateToken() );
 1129+ $wgRequest->setSessionData( 'wsCreateaccountToken', MWCryptRand::generateHex( 32, __METHOD__ ) );
11301130 }
11311131
11321132 /**
Index: branches/REL1_18/phase3/includes/CryptRand.php
@@ -0,0 +1,476 @@
 2+<?php
 3+/**
 4+ * A cryptographic random generator class used for generating secret keys
 5+ *
 6+ * This is based in part on Drupal code as well as what we used in our own code
 7+ * prior to introduction of this class.
 8+ *
 9+ * @author Daniel Friesen
 10+ * @file
 11+ */
 12+
 13+class MWCryptRand {
 14+
 15+ /**
 16+ * Minimum number of iterations we want to make in our drift calculations.
 17+ */
 18+ const MIN_ITERATIONS = 1000;
 19+
 20+ /**
 21+ * Number of milliseconds we want to spend generating each separate byte
 22+ * of the final generated bytes.
 23+ * This is used in combination with the hash length to determine the duration
 24+ * we should spend doing drift calculations.
 25+ */
 26+ const MSEC_PER_BYTE = 0.5;
 27+
 28+ /**
 29+ * Singleton instance for public use
 30+ */
 31+ protected static $singleton = null;
 32+
 33+ /**
 34+ * The hash algorithm being used
 35+ */
 36+ protected $algo = null;
 37+
 38+ /**
 39+ * The number of bytes outputted by the hash algorithm
 40+ */
 41+ protected $hashLength = null;
 42+
 43+ /**
 44+ * A boolean indicating whether the previous random generation was done using
 45+ * cryptographically strong random number generator or not.
 46+ */
 47+ protected $strong = null;
 48+
 49+ /**
 50+ * Initialize an initial random state based off of whatever we can find
 51+ */
 52+ protected function initialRandomState() {
 53+ // $_SERVER contains a variety of unstable user and system specific information
 54+ // It'll vary a little with each page, and vary even more with separate users
 55+ // It'll also vary slightly across different machines
 56+ $state = serialize( $_SERVER );
 57+
 58+ // To try and vary the system information of the state a bit more
 59+ // by including the system's hostname into the state
 60+ $state .= wfHostname();
 61+
 62+ // Try to gather a little entropy from the different php rand sources
 63+ $state .= rand() . uniqid( mt_rand(), true );
 64+
 65+ // Include some information about the filesystem's current state in the random state
 66+ $files = array();
 67+ // We know this file is here so grab some info about ourself
 68+ $files[] = __FILE__;
 69+ // The config file is likely the most often edited file we know should be around
 70+ // so if the constant with it's location is defined include it's stat info into the state
 71+ if ( defined( 'MW_CONFIG_FILE' ) ) {
 72+ $files[] = MW_CONFIG_FILE;
 73+ }
 74+ foreach ( $files as $file ) {
 75+ wfSuppressWarnings();
 76+ $stat = stat( $file );
 77+ wfRestoreWarnings();
 78+ if ( $stat ) {
 79+ // stat() duplicates data into numeric and string keys so kill off all the numeric ones
 80+ foreach ( $stat as $k => $v ) {
 81+ if ( is_numeric( $k ) ) {
 82+ unset( $k );
 83+ }
 84+ }
 85+ // The absolute filename itself will differ from install to install so don't leave it out
 86+ $state .= realpath( $file );
 87+ $state .= implode( '', $stat );
 88+ } else {
 89+ // The fact that the file isn't there is worth at least a
 90+ // minuscule amount of entropy.
 91+ $state .= '0';
 92+ }
 93+ }
 94+
 95+ // Try and make this a little more unstable by including the varying process
 96+ // id of the php process we are running inside of if we are able to access it
 97+ if ( function_exists( 'getmypid' ) ) {
 98+ $state .= getmypid();
 99+ }
 100+
 101+ // If available try to increase the instability of the data by throwing in
 102+ // the precise amount of memory that we happen to be using at the moment.
 103+ if ( function_exists( 'memory_get_usage' ) ) {
 104+ $state .= memory_get_usage( true );
 105+ }
 106+
 107+ // It's mostly worthless but throw the wiki's id into the data for a little more variance
 108+ $state .= wfWikiID();
 109+
 110+ // If we have a secret key or proxy key set then throw it into the state as well
 111+ global $wgSecretKey, $wgProxyKey;
 112+ if ( $wgSecretKey ) {
 113+ $state .= $wgSecretKey;
 114+ } elseif ( $wgProxyKey ) {
 115+ $state .= $wgProxyKey;
 116+ }
 117+
 118+ return $state;
 119+ }
 120+
 121+ /**
 122+ * Randomly hash data while mixing in clock drift data for randomness
 123+ *
 124+ * @param $data The data to randomly hash.
 125+ * @return String The hashed bytes
 126+ * @author Tim Starling
 127+ */
 128+ protected function driftHash( $data ) {
 129+ // Minimum number of iterations (to avoid slow operations causing the loop to gather little entropy)
 130+ $minIterations = self::MIN_ITERATIONS;
 131+ // Duration of time to spend doing calculations (in seconds)
 132+ $duration = ( self::MSEC_PER_BYTE / 1000 ) * $this->hashLength();
 133+ // Create a buffer to use to trigger memory operations
 134+ $bufLength = 10000000;
 135+ $buffer = str_repeat( ' ', $bufLength );
 136+ $bufPos = 0;
 137+
 138+ // Iterate for $duration seconds or at least $minIerations number of iterations
 139+ $iterations = 0;
 140+ $startTime = microtime( true );
 141+ $currentTime = $startTime;
 142+ while ( $iterations < $minIterations || $currentTime - $startTime < $duration ) {
 143+ // Trigger some memory writing to trigger some bus activity
 144+ // This may create variance in the time between iterations
 145+ $bufPos = ( $bufPos + 13 ) % $bufLength;
 146+ $buffer[$bufPos] = ' ';
 147+ // Add the drift between this iteration and the last in as entropy
 148+ $nextTime = microtime( true );
 149+ $delta = (int)( ( $nextTime - $currentTime ) * 1000000 );
 150+ $data .= $delta;
 151+ // Every 100 iterations hash the data and entropy
 152+ if ( $iterations % 100 === 0 ) {
 153+ $data = sha1( $data );
 154+ }
 155+ $currentTime = $nextTime;
 156+ $iterations++;
 157+ }
 158+ $timeTaken = $currentTime - $startTime;
 159+ $data = $this->hash( $data );
 160+
 161+ wfDebug( __METHOD__ . ": Clock drift calculation " .
 162+ "(time-taken=" . ( $timeTaken * 1000 ) . "ms, " .
 163+ "iterations=$iterations, " .
 164+ "time-per-iteration=" . ( $timeTaken / $iterations * 1e6 ) . "us)\n" );
 165+ return $data;
 166+ }
 167+
 168+ /**
 169+ * Return a rolling random state initially build using data from unstable sources
 170+ * @return A new weak random state
 171+ */
 172+ protected function randomState() {
 173+ static $state = null;
 174+ if ( is_null( $state ) ) {
 175+ // Initialize the state with whatever unstable data we can find
 176+ // It's important that this data is hashed right afterwards to prevent
 177+ // it from being leaked into the output stream
 178+ $state = $this->hash( $this->initialRandomState() );
 179+ }
 180+ // Generate a new random state based on the initial random state or previous
 181+ // random state by combining it with clock drift
 182+ $state = $this->driftHash( $state );
 183+ return $state;
 184+ }
 185+
 186+ /**
 187+ * Decide on the best acceptable hash algorithm we have available for hash()
 188+ * @return String A hash algorithm
 189+ */
 190+ protected function hashAlgo() {
 191+ if ( !is_null( $algo ) ) {
 192+ return $algo;
 193+ }
 194+
 195+ $algos = hash_algos();
 196+ $preference = array( 'whirlpool', 'sha256', 'sha1', 'md5' );
 197+
 198+ foreach ( $preference as $algorithm ) {
 199+ if ( in_array( $algorithm, $algos ) ) {
 200+ $algo = $algorithm; # assign to static
 201+ wfDebug( __METHOD__ . ": Using the $algo hash algorithm.\n" );
 202+ return $algo;
 203+ }
 204+ }
 205+
 206+ // We only reach here if no acceptable hash is found in the list, this should
 207+ // be a technical impossibility since most of php's hash list is fixed and
 208+ // some of the ones we list are available as their own native functions
 209+ // But since we already require at least 5.2 and hash() was default in
 210+ // 5.1.2 we don't bother falling back to methods like sha1 and md5.
 211+ throw new MWException( "Could not find an acceptable hashing function in hash_algos()" );
 212+ }
 213+
 214+ /**
 215+ * Return the byte-length output of the hash algorithm we are
 216+ * using in self::hash and self::hmac.
 217+ *
 218+ * @return int Number of bytes the hash outputs
 219+ */
 220+ protected function hashLength() {
 221+ if ( is_null( $hashLength ) ) {
 222+ $hashLength = strlen( $this->hash( '' ) );
 223+ }
 224+ return $hashLength;
 225+ }
 226+
 227+ /**
 228+ * Generate an acceptably unstable one-way-hash of some text
 229+ * making use of the best hash algorithm that we have available.
 230+ *
 231+ * @return String A raw hash of the data
 232+ */
 233+ protected function hash( $data ) {
 234+ return hash( $this->hashAlgo(), $data, true );
 235+ }
 236+
 237+ /**
 238+ * Generate an acceptably unstable one-way-hmac of some text
 239+ * making use of the best hash algorithm that we have available.
 240+ *
 241+ * @return String A raw hash of the data
 242+ */
 243+ protected function hmac( $data, $key ) {
 244+ return hash_hmac( $this->hashAlgo(), $data, $key, true );
 245+ }
 246+
 247+ /**
 248+ * @see self::wasStrong()
 249+ */
 250+ public function realWasStrong() {
 251+ if ( is_null( $this->strong ) ) {
 252+ throw new MWException( __METHOD__ . ' called before generation of random data' );
 253+ }
 254+ return $this->strong;
 255+ }
 256+
 257+ /**
 258+ * @see self::generate()
 259+ */
 260+ public function realGenerate( $bytes, $forceStrong = false, $method = null ) {
 261+ wfProfileIn( __METHOD__ );
 262+ if ( is_string( $forceStrong ) && is_null( $method ) ) {
 263+ // If $forceStrong is a string then it's really $method
 264+ $method = $forceStrong;
 265+ $forceStrong = false;
 266+ }
 267+
 268+ if ( !is_null( $method ) ) {
 269+ wfDebug( __METHOD__ . ": Generating cryptographic random bytes for $method\n" );
 270+ }
 271+
 272+ $bytes = floor( $bytes );
 273+ static $buffer = '';
 274+ if ( is_null( $this->strong ) ) {
 275+ // Set strength to false initially until we know what source data is coming from
 276+ $this->strong = true;
 277+ }
 278+
 279+ if ( strlen( $buffer ) < $bytes ) {
 280+ // If available make use of mcrypt_create_iv URANDOM source to generate randomness
 281+ // On unix-like systems this reads from /dev/urandom but does it without any buffering
 282+ // and bypasses openbasdir restrictions so it's preferable to reading directly
 283+ // On Windows starting in PHP 5.3.0 Windows' native CryptGenRandom is used to generate
 284+ // entropy so this is also preferable to just trying to read urandom because it may work
 285+ // on Windows systems as well.
 286+ if ( function_exists( 'mcrypt_create_iv' ) ) {
 287+ wfProfileIn( __METHOD__ . '-mcrypt' );
 288+ $rem = $bytes - strlen( $buffer );
 289+ wfDebug( __METHOD__ . ": Trying to generate $rem bytes of randomness using mcrypt_create_iv.\n" );
 290+ $iv = mcrypt_create_iv( $rem, MCRYPT_DEV_URANDOM );
 291+ if ( $iv === false ) {
 292+ wfDebug( __METHOD__ . ": mcrypt_create_iv returned false.\n" );
 293+ } else {
 294+ $bytes .= $iv;
 295+ wfDebug( __METHOD__ . ": mcrypt_create_iv generated " . strlen( $iv ) . " bytes of randomness.\n" );
 296+ }
 297+ wfProfileOut( __METHOD__ . '-mcrypt' );
 298+ }
 299+ }
 300+
 301+ if ( strlen( $buffer ) < $bytes ) {
 302+ // If available make use of openssl's random_pesudo_bytes method to attempt to generate randomness.
 303+ // However don't do this on Windows with PHP < 5.3.4 due to a bug:
 304+ // http://stackoverflow.com/questions/1940168/openssl-random-pseudo-bytes-is-slow-php
 305+ if ( function_exists( 'openssl_random_pseudo_bytes' )
 306+ && ( !wfIsWindows() || version_compare( PHP_VERSION, '5.3.4', '>=' ) )
 307+ ) {
 308+ wfProfileIn( __METHOD__ . '-openssl' );
 309+ $rem = $bytes - strlen( $buffer );
 310+ wfDebug( __METHOD__ . ": Trying to generate $rem bytes of randomness using openssl_random_pseudo_bytes.\n" );
 311+ $openssl_bytes = openssl_random_pseudo_bytes( $rem, $openssl_strong );
 312+ if ( $openssl_bytes === false ) {
 313+ wfDebug( __METHOD__ . ": openssl_random_pseudo_bytes returned false.\n" );
 314+ } else {
 315+ $buffer .= $openssl_bytes;
 316+ wfDebug( __METHOD__ . ": openssl_random_pseudo_bytes generated " . strlen( $openssl_bytes ) . " bytes of " . ( $openssl_strong ? "strong" : "weak" ) . " randomness.\n" );
 317+ }
 318+ if ( strlen( $buffer ) >= $bytes ) {
 319+ // openssl tells us if the random source was strong, if some of our data was generated
 320+ // using it use it's say on whether the randomness is strong
 321+ $this->strong = !!$openssl_strong;
 322+ }
 323+ wfProfileOut( __METHOD__ . '-openssl' );
 324+ }
 325+ }
 326+
 327+ // Only read from urandom if we can control the buffer size or were passed forceStrong
 328+ if ( strlen( $buffer ) < $bytes && ( function_exists( 'stream_set_read_buffer' ) || $forceStrong ) ) {
 329+ wfProfileIn( __METHOD__ . '-fopen-urandom' );
 330+ $rem = $bytes - strlen( $buffer );
 331+ wfDebug( __METHOD__ . ": Trying to generate $rem bytes of randomness using /dev/urandom.\n" );
 332+ if ( !function_exists( 'stream_set_read_buffer' ) && $forceStrong ) {
 333+ wfDebug( __METHOD__ . ": Was forced to read from /dev/urandom without control over the buffer size.\n" );
 334+ }
 335+ // /dev/urandom is generally considered the best possible commonly
 336+ // available random source, and is available on most *nix systems.
 337+ wfSuppressWarnings();
 338+ $urandom = fopen( "/dev/urandom", "rb" );
 339+ wfRestoreWarnings();
 340+
 341+ // Attempt to read all our random data from urandom
 342+ // php's fread always does buffered reads based on the stream's chunk_size
 343+ // so in reality it will usually read more than the amount of data we're
 344+ // asked for and not storing that risks depleting the system's random pool.
 345+ // If stream_set_read_buffer is available set the chunk_size to the amount
 346+ // of data we need. Otherwise read 8k, php's default chunk_size.
 347+ if ( $urandom ) {
 348+ // php's default chunk_size is 8k
 349+ $chunk_size = 1024 * 8;
 350+ if ( function_exists( 'stream_set_read_buffer' ) ) {
 351+ // If possible set the chunk_size to the amount of data we need
 352+ stream_set_read_buffer( $urandom, $rem );
 353+ $chunk_size = $rem;
 354+ }
 355+ wfDebug( __METHOD__ . ": Reading from /dev/urandom with a buffer size of $chunk_size.\n" );
 356+ $random_bytes = fread( $urandom, max( $chunk_size, $rem ) );
 357+ $buffer .= $random_bytes;
 358+ fclose( $urandom );
 359+ wfDebug( __METHOD__ . ": /dev/urandom generated " . strlen( $random_bytes ) . " bytes of randomness.\n" );
 360+ if ( strlen( $buffer ) >= $bytes ) {
 361+ // urandom is always strong, set to true if all our data was generated using it
 362+ $this->strong = true;
 363+ }
 364+ } else {
 365+ wfDebug( __METHOD__ . ": /dev/urandom could not be opened.\n" );
 366+ }
 367+ wfProfileOut( __METHOD__ . '-fopen-urandom' );
 368+ }
 369+
 370+ // If we cannot use or generate enough data from a secure source
 371+ // use this loop to generate a good set of pseudo random data.
 372+ // This works by initializing a random state using a pile of unstable data
 373+ // and continually shoving it through a hash along with a variable salt.
 374+ // We hash the random state with more salt to avoid the state from leaking
 375+ // out and being used to predict the /randomness/ that follows.
 376+ if ( strlen( $buffer ) < $bytes ) {
 377+ wfDebug( __METHOD__ . ": Falling back to using a pseudo random state to generate randomness.\n" );
 378+ }
 379+ while ( strlen( $buffer ) < $bytes ) {
 380+ wfProfileIn( __METHOD__ . '-fallback' );
 381+ $buffer .= $this->hmac( $this->randomState(), mt_rand() );
 382+ // This code is never really cryptographically strong, if we use it
 383+ // at all, then set strong to false.
 384+ $this->strong = false;
 385+ wfProfileOut( __METHOD__ . '-fallback' );
 386+ }
 387+
 388+ // Once the buffer has been filled up with enough random data to fulfill
 389+ // the request shift off enough data to handle the request and leave the
 390+ // unused portion left inside the buffer for the next request for random data
 391+ $generated = substr( $buffer, 0, $bytes );
 392+ $buffer = substr( $buffer, $bytes );
 393+
 394+ wfDebug( __METHOD__ . ": " . strlen( $buffer ) . " bytes of randomness leftover in the buffer.\n" );
 395+
 396+ wfProfileOut( __METHOD__ );
 397+ return $generated;
 398+ }
 399+
 400+ /**
 401+ * @see self::generateHex()
 402+ */
 403+ public function realGenerateHex( $chars, $forceStrong = false, $method = null ) {
 404+ // hex strings are 2x the length of raw binary so we divide the length in half
 405+ // odd numbers will result in a .5 that leads the generate() being 1 character
 406+ // short, so we use ceil() to ensure that we always have enough bytes
 407+ $bytes = ceil( $chars / 2 );
 408+ // Generate the data and then convert it to a hex string
 409+ $hex = bin2hex( $this->generate( $bytes, $forceStrong, $method ) );
 410+ // A bit of paranoia here, the caller asked for a specific length of string
 411+ // here, and it's possible (eg when given an odd number) that we may actually
 412+ // have at least 1 char more than they asked for. Just in case they made this
 413+ // call intending to insert it into a database that does truncation we don't
 414+ // want to give them too much and end up with their database and their live
 415+ // code having two different values because part of what we gave them is truncated
 416+ // hence, we strip out any run of characters longer than what we were asked for.
 417+ return substr( $hex, 0, $chars );
 418+ }
 419+
 420+ /** Publicly exposed static methods **/
 421+
 422+ /**
 423+ * Return a singleton instance of MWCryptRand
 424+ */
 425+ protected static function singleton() {
 426+ if ( is_null( self::$singleton ) ) {
 427+ self::$singleton = new self;
 428+ }
 429+ return self::$singleton;
 430+ }
 431+
 432+ /**
 433+ * Return a boolean indicating whether or not the source used for cryptographic
 434+ * random bytes generation in the previously run generate* call
 435+ * was cryptographically strong.
 436+ *
 437+ * @return bool Returns true if the source was strong, false if not.
 438+ */
 439+ public static function wasStrong() {
 440+ return self::singleton()->realWasStrong();
 441+ }
 442+
 443+ /**
 444+ * Generate a run of (ideally) cryptographically random data and return
 445+ * it in raw binary form.
 446+ * You can use MWCryptRand::wasStrong() if you wish to know if the source used
 447+ * was cryptographically strong.
 448+ *
 449+ * @param $bytes int the number of bytes of random data to generate
 450+ * @param $forceStrong bool Pass true if you want generate to prefer cryptographically
 451+ * strong sources of entropy even if reading from them may steal
 452+ * more entropy from the system than optimal.
 453+ * @param $method The calling method, for debug info. May be the second argument if you are not using forceStrong
 454+ * @return String Raw binary random data
 455+ */
 456+ public static function generate( $bytes, $forceStrong = false, $method = null ) {
 457+ return self::singleton()->realGenerate( $bytes, $forceStrong, $method );
 458+ }
 459+
 460+ /**
 461+ * Generate a run of (ideally) cryptographically random data and return
 462+ * it in hexadecimal string format.
 463+ * You can use MWCryptRand::wasStrong() if you wish to know if the source used
 464+ * was cryptographically strong.
 465+ *
 466+ * @param $chars int the number of hex chars of random data to generate
 467+ * @param $forceStrong bool Pass true if you want generate to prefer cryptographically
 468+ * strong sources of entropy even if reading from them may steal
 469+ * more entropy from the system than optimal.
 470+ * @param $method The calling method, for debug info. May be the second argument if you are not using forceStrong
 471+ * @return String Hexadecimal random data
 472+ */
 473+ public static function generateHex( $chars, $forceStrong = false, $method = null ) {
 474+ return self::singleton()->realGenerateHex( $chars, $forceStrong, $method );
 475+ }
 476+
 477+}
Index: branches/REL1_18/phase3/includes/User.php
@@ -831,23 +831,20 @@
832832 }
833833
834834 /**
835 - * Return a random password. Sourced from mt_rand, so it's not particularly secure.
836 - * @todo hash random numbers to improve security, like generateToken()
 835+ * Return a random password.
837836 *
838837 * @return String new random password
839838 */
840839 public static function randomPassword() {
841840 global $wgMinimalPasswordLength;
842 - $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
843 - $l = strlen( $pwchars ) - 1;
844 -
845 - $pwlength = max( 7, $wgMinimalPasswordLength );
846 - $digit = mt_rand( 0, $pwlength - 1 );
847 - $np = '';
848 - for ( $i = 0; $i < $pwlength; $i++ ) {
849 - $np .= $i == $digit ? chr( mt_rand( 48, 57 ) ) : $pwchars[ mt_rand( 0, $l ) ];
850 - }
851 - return $np;
 841+ // Decide the final password length based on our min password length, stopping at a minimum of 10 chars
 842+ $length = max( 10, $wgMinimalPasswordLength );
 843+ // Multiply by 1.25 to get the number of hex characters we need
 844+ $length = $length * 1.25;
 845+ // Generate random hex chars
 846+ $hex = MWCryptRand::generateHex( $length, __METHOD__ );
 847+ // Convert from base 16 to base 32 to get a proper password like string
 848+ return wfBaseConvert( $hex, 16, 32 );
852849 }
853850
854851 /**
@@ -877,7 +874,7 @@
878875 $this->mTouched = '0'; # Allow any pages to be cached
879876 }
880877
881 - $this->setToken(); # Random
 878+ $this->mToken = null; // Don't run cryptographic functions till we need a token
882879 $this->mEmailAuthenticated = null;
883880 $this->mEmailToken = '';
884881 $this->mEmailTokenExpires = null;
@@ -984,11 +981,11 @@
985982 return false;
986983 }
987984
988 - if ( $request->getSessionData( 'wsToken' ) !== null ) {
989 - $passwordCorrect = $proposedUser->getToken() === $request->getSessionData( 'wsToken' );
 985+ if ( $request->getSessionData( 'wsToken' ) ) {
 986+ $passwordCorrect = $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' );
990987 $from = 'session';
991 - } elseif ( $request->getCookie( 'Token' ) !== null ) {
992 - $passwordCorrect = $proposedUser->getToken() === $request->getCookie( 'Token' );
 988+ } elseif ( $request->getCookie( 'Token' ) ) {
 989+ $passwordCorrect = $proposedUser->getToken( false ) === $request->getCookie( 'Token' );
993990 $from = 'cookie';
994991 } else {
995992 # No session or persistent login cookie
@@ -1083,6 +1080,9 @@
10841081 $this->decodeOptions( $row->user_options );
10851082 $this->mTouched = wfTimestamp(TS_MW,$row->user_touched);
10861083 $this->mToken = $row->user_token;
 1084+ if ( $this->mToken == '' ) {
 1085+ $this->mToken = null;
 1086+ }
10871087 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
10881088 $this->mEmailToken = $row->user_email_token;
10891089 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
@@ -1989,10 +1989,14 @@
19901990
19911991 /**
19921992 * Get the user's current token.
 1993+ * @param $forceCreation Force the generation of a new token if the user doesn't have one (default=true for backwards compatibility)
19931994 * @return String Token
19941995 */
1995 - public function getToken() {
 1996+ public function getToken( $forceCreation = true ) {
19961997 $this->load();
 1998+ if ( !$this->mToken && $forceCreation ) {
 1999+ $this->setToken();
 2000+ }
19972001 return $this->mToken;
19982002 }
19992003
@@ -2006,14 +2010,7 @@
20072011 global $wgSecretKey, $wgProxyKey;
20082012 $this->load();
20092013 if ( !$token ) {
2010 - if ( $wgSecretKey ) {
2011 - $key = $wgSecretKey;
2012 - } elseif ( $wgProxyKey ) {
2013 - $key = $wgProxyKey;
2014 - } else {
2015 - $key = microtime();
2016 - }
2017 - $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . wfWikiID() . $this->mId );
 2014+ $this->mToken = MWCryptRand::generateHex( USER_TOKEN_LENGTH, __METHOD__ );
20182015 } else {
20192016 $this->mToken = $token;
20202017 }
@@ -2718,6 +2715,14 @@
27192716
27202717 $this->load();
27212718 if ( 0 == $this->mId ) return;
 2719+ if ( !$this->mToken ) {
 2720+ // When token is empty or NULL generate a new one and then save it to the database
 2721+ // This allows a wiki to re-secure itself after a leak of it's user table or $wgSecretKey
 2722+ // Simply by setting every cell in the user_token column to NULL and letting them be
 2723+ // regenerated as users log back into the wiki.
 2724+ $this->setToken();
 2725+ $this->saveSettings();
 2726+ }
27222727 $session = array(
27232728 'wsUserID' => $this->mId,
27242729 'wsToken' => $this->mToken,
@@ -2795,7 +2800,7 @@
27962801 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
27972802 'user_options' => '',
27982803 'user_touched' => $dbw->timestamp( $this->mTouched ),
2799 - 'user_token' => $this->mToken,
 2804+ 'user_token' => strval( $this->mToken ),
28002805 'user_email_token' => $this->mEmailToken,
28012806 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
28022807 ), array( /* WHERE */
@@ -2862,7 +2867,7 @@
28632868 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
28642869 'user_real_name' => $user->mRealName,
28652870 'user_options' => '',
2866 - 'user_token' => $user->mToken,
 2871+ 'user_token' => strval( $user->mToken ),
28672872 'user_registration' => $dbw->timestamp( $user->mRegistration ),
28682873 'user_editcount' => 0,
28692874 );
@@ -2896,7 +2901,7 @@
28972902 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
28982903 'user_real_name' => $this->mRealName,
28992904 'user_options' => '',
2900 - 'user_token' => $this->mToken,
 2905+ 'user_token' => strval( $this->mToken ),
29012906 'user_registration' => $dbw->timestamp( $this->mRegistration ),
29022907 'user_editcount' => 0,
29032908 ), __METHOD__
@@ -3144,7 +3149,7 @@
31453150 } else {
31463151 $token = $request->getSessionData( 'wsEditToken' );
31473152 if ( $token === null ) {
3148 - $token = self::generateToken();
 3153+ $token = MWCryptRand::generateHex( 32, __METHOD__ );
31493154 $request->setSessionData( 'wsEditToken', $token );
31503155 }
31513156 if( is_array( $salt ) ) {
@@ -3161,8 +3166,7 @@
31623167 * @return String The new random token
31633168 */
31643169 public static function generateToken( $salt = '' ) {
3165 - $token = dechex( mt_rand() ) . dechex( mt_rand() );
3166 - return md5( $token . $salt );
 3170+ return MWCryptRand::generateHex( 32, __METHOD__ );
31673171 }
31683172
31693173 /**
@@ -3268,12 +3272,11 @@
32693273 global $wgUserEmailConfirmationTokenExpiry;
32703274 $now = time();
32713275 $expires = $now + $wgUserEmailConfirmationTokenExpiry;
3272 - $expiration = wfTimestamp( TS_MW, $expires );
3273 - $token = self::generateToken( $this->mId . $this->mEmail . $expires );
 3276+ $this->load();
 3277+ $token = MWCryptRand::generateHex( 32, __METHOD__ );
32743278 $hash = md5( $token );
3275 - $this->load();
32763279 $this->mEmailToken = $hash;
3277 - $this->mEmailTokenExpires = $expiration;
 3280+ $this->mEmailTokenExpires = wfTimestamp( TS_MW, $expires );
32783281 return $token;
32793282 }
32803283
@@ -3828,7 +3831,7 @@
38293832
38303833 if( $wgPasswordSalt ) {
38313834 if ( $salt === false ) {
3832 - $salt = substr( wfGenerateToken(), 0, 8 );
 3835+ $salt = MWCryptRand::generateHex( 8, __METHOD__ );
38333836 }
38343837 return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
38353838 } else {
Index: branches/REL1_18/phase3/includes/GlobalFunctions.php
@@ -3065,6 +3065,33 @@
30663066 }
30673067
30683068 /**
 3069+ * Override session_id before session startup if php's built-in
 3070+ * session generation code is not secure.
 3071+ */
 3072+function wfFixSessionID() {
 3073+ // If the cookie or session id is already set we already have a session and should abort
 3074+ if ( isset( $_COOKIE[ session_name() ] ) || session_id() ) {
 3075+ return;
 3076+ }
 3077+
 3078+ // PHP's built-in session entropy is enabled if:
 3079+ // - entropy_file is set or you're on Windows with php 5.3.3+
 3080+ // - AND entropy_length is > 0
 3081+ // We treat it as disabled if it doesn't have an entropy length of at least 32
 3082+ $entropyEnabled = (
 3083+ ( wfIsWindows() && version_compare( PHP_VERSION, '5.3.3', '>=' ) )
 3084+ || ini_get( 'session.entropy_file' )
 3085+ )
 3086+ && intval( ini_get( 'session.entropy_length' ) ) >= 32;
 3087+
 3088+ // If built-in entropy is not enabled or not sufficient override php's built in session id generation code
 3089+ if ( !$entropyEnabled ) {
 3090+ wfDebug( __METHOD__ . ": PHP's built in entropy is disabled or not sufficient, overriding session id generation using our cryptrand source.\n" );
 3091+ session_id( MWCryptRand::generateHex( 32, __METHOD__ ) );
 3092+ }
 3093+}
 3094+
 3095+/**
30693096 * Initialise php session
30703097 *
30713098 * @param $sessionId Bool
@@ -3103,6 +3130,8 @@
31043131 session_cache_limiter( 'private, must-revalidate' );
31053132 if ( $sessionId ) {
31063133 session_id( $sessionId );
 3134+ } else {
 3135+ wfFixSessionID();
31073136 }
31083137 wfSuppressWarnings();
31093138 session_start();
Index: branches/REL1_18/phase3/includes/installer/Installer.php
@@ -1347,8 +1347,7 @@
13481348 }
13491349
13501350 /**
1351 - * Generate $wgSecretKey. Will warn if we had to use mt_rand() instead of
1352 - * /dev/urandom
 1351+ * Generate $wgSecretKey. Will warn if we had to use an insecure random source.
13531352 *
13541353 * @return Status
13551354 */
@@ -1361,8 +1360,8 @@
13621361 }
13631362
13641363 /**
1365 - * Generate a secret value for variables using either
1366 - * /dev/urandom or mt_rand(). Produce a warning in the later case.
 1364+ * Generate a secret value for variables using our CryptRand generator.
 1365+ * Produce a warning if the random source was insecure.
13671366 *
13681367 * @param $keys Array
13691368 * @return Status
@@ -1370,28 +1369,18 @@
13711370 protected function doGenerateKeys( $keys ) {
13721371 $status = Status::newGood();
13731372
1374 - wfSuppressWarnings();
1375 - $file = fopen( "/dev/urandom", "r" );
1376 - wfRestoreWarnings();
1377 -
 1373+ $strong = true;
13781374 foreach ( $keys as $name => $length ) {
1379 - if ( $file ) {
1380 - $secretKey = bin2hex( fread( $file, $length / 2 ) );
1381 - } else {
1382 - $secretKey = '';
1383 -
1384 - for ( $i = 0; $i < $length / 8; $i++ ) {
1385 - $secretKey .= dechex( mt_rand( 0, 0x7fffffff ) );
1386 - }
 1375+ $secretKey = MWCryptRand::generateHex( $length, true );
 1376+ if ( !MWCryptRand::wasStrong() ) {
 1377+ $strong = false;
13871378 }
13881379
13891380 $this->setVar( $name, $secretKey );
13901381 }
13911382
1392 - if ( $file ) {
1393 - fclose( $file );
1394 - } else {
1395 - $names = array_keys ( $keys );
 1383+ if ( !$strong ) {
 1384+ $names = array_keys( $keys );
13961385 $names = preg_replace( '/^(.*)$/', '\$$1', $names );
13971386 global $wgLang;
13981387 $status->warning( 'config-insecure-keys', $wgLang->listToText( $names ), count( $names ) );
Index: branches/REL1_18/phase3/includes/AutoLoader.php
@@ -150,6 +150,7 @@
151151 'Message' => 'includes/Message.php',
152152 'MessageBlobStore' => 'includes/MessageBlobStore.php',
153153 'MimeMagic' => 'includes/MimeMagic.php',
 154+ 'MWCryptRand' => 'includes/CryptRand.php',
154155 'MWException' => 'includes/Exception.php',
155156 'MWExceptionHandler' => 'includes/Exception.php',
156157 'MWFunction' => 'includes/MWFunction.php',
Index: branches/REL1_18/phase3/includes/specials/SpecialUserlogin.php
@@ -1114,9 +1114,9 @@
11151115 */
11161116 public static function setLoginToken() {
11171117 global $wgRequest;
1118 - // Use User::generateToken() instead of $user->editToken()
 1118+ // Generate a token directly instead of using $user->editToken()
11191119 // because the latter reuses $_SESSION['wsEditToken']
1120 - $wgRequest->setSessionData( 'wsLoginToken', User::generateToken() );
 1120+ $wgRequest->setSessionData( 'wsLoginToken', MWCryptRand::generateHex( 32, __METHOD__ ) );
11211121 }
11221122
11231123 /**
@@ -1140,7 +1140,7 @@
11411141 */
11421142 public static function setCreateaccountToken() {
11431143 global $wgRequest;
1144 - $wgRequest->setSessionData( 'wsCreateaccountToken', User::generateToken() );
 1144+ $wgRequest->setSessionData( 'wsCreateaccountToken', MWCryptRand::generateHex( 32, __METHOD__ ) );
11451145 }
11461146
11471147 /**
Index: branches/REL1_18/phase3/includes/specials/SpecialWatchlist.php
@@ -43,7 +43,7 @@
4444 // Add feed links
4545 $wlToken = $user->getOption( 'watchlisttoken' );
4646 if ( !$wlToken ) {
47 - $wlToken = sha1( mt_rand() . microtime( true ) );
 47+ $wlToken = MWCryptRand::generateHex( 40 );
4848 $user->setOption( 'watchlisttoken', $wlToken );
4949 $user->saveSettings();
5050 }
Index: branches/REL1_19/phase3/includes/CryptRand.php
@@ -0,0 +1,476 @@
 2+<?php
 3+/**
 4+ * A cryptographic random generator class used for generating secret keys
 5+ *
 6+ * This is based in part on Drupal code as well as what we used in our own code
 7+ * prior to introduction of this class.
 8+ *
 9+ * @author Daniel Friesen
 10+ * @file
 11+ */
 12+
 13+class MWCryptRand {
 14+
 15+ /**
 16+ * Minimum number of iterations we want to make in our drift calculations.
 17+ */
 18+ const MIN_ITERATIONS = 1000;
 19+
 20+ /**
 21+ * Number of milliseconds we want to spend generating each separate byte
 22+ * of the final generated bytes.
 23+ * This is used in combination with the hash length to determine the duration
 24+ * we should spend doing drift calculations.
 25+ */
 26+ const MSEC_PER_BYTE = 0.5;
 27+
 28+ /**
 29+ * Singleton instance for public use
 30+ */
 31+ protected static $singleton = null;
 32+
 33+ /**
 34+ * The hash algorithm being used
 35+ */
 36+ protected $algo = null;
 37+
 38+ /**
 39+ * The number of bytes outputted by the hash algorithm
 40+ */
 41+ protected $hashLength = null;
 42+
 43+ /**
 44+ * A boolean indicating whether the previous random generation was done using
 45+ * cryptographically strong random number generator or not.
 46+ */
 47+ protected $strong = null;
 48+
 49+ /**
 50+ * Initialize an initial random state based off of whatever we can find
 51+ */
 52+ protected function initialRandomState() {
 53+ // $_SERVER contains a variety of unstable user and system specific information
 54+ // It'll vary a little with each page, and vary even more with separate users
 55+ // It'll also vary slightly across different machines
 56+ $state = serialize( $_SERVER );
 57+
 58+ // To try and vary the system information of the state a bit more
 59+ // by including the system's hostname into the state
 60+ $state .= wfHostname();
 61+
 62+ // Try to gather a little entropy from the different php rand sources
 63+ $state .= rand() . uniqid( mt_rand(), true );
 64+
 65+ // Include some information about the filesystem's current state in the random state
 66+ $files = array();
 67+ // We know this file is here so grab some info about ourself
 68+ $files[] = __FILE__;
 69+ // The config file is likely the most often edited file we know should be around
 70+ // so if the constant with it's location is defined include it's stat info into the state
 71+ if ( defined( 'MW_CONFIG_FILE' ) ) {
 72+ $files[] = MW_CONFIG_FILE;
 73+ }
 74+ foreach ( $files as $file ) {
 75+ wfSuppressWarnings();
 76+ $stat = stat( $file );
 77+ wfRestoreWarnings();
 78+ if ( $stat ) {
 79+ // stat() duplicates data into numeric and string keys so kill off all the numeric ones
 80+ foreach ( $stat as $k => $v ) {
 81+ if ( is_numeric( $k ) ) {
 82+ unset( $k );
 83+ }
 84+ }
 85+ // The absolute filename itself will differ from install to install so don't leave it out
 86+ $state .= realpath( $file );
 87+ $state .= implode( '', $stat );
 88+ } else {
 89+ // The fact that the file isn't there is worth at least a
 90+ // minuscule amount of entropy.
 91+ $state .= '0';
 92+ }
 93+ }
 94+
 95+ // Try and make this a little more unstable by including the varying process
 96+ // id of the php process we are running inside of if we are able to access it
 97+ if ( function_exists( 'getmypid' ) ) {
 98+ $state .= getmypid();
 99+ }
 100+
 101+ // If available try to increase the instability of the data by throwing in
 102+ // the precise amount of memory that we happen to be using at the moment.
 103+ if ( function_exists( 'memory_get_usage' ) ) {
 104+ $state .= memory_get_usage( true );
 105+ }
 106+
 107+ // It's mostly worthless but throw the wiki's id into the data for a little more variance
 108+ $state .= wfWikiID();
 109+
 110+ // If we have a secret key or proxy key set then throw it into the state as well
 111+ global $wgSecretKey, $wgProxyKey;
 112+ if ( $wgSecretKey ) {
 113+ $state .= $wgSecretKey;
 114+ } elseif ( $wgProxyKey ) {
 115+ $state .= $wgProxyKey;
 116+ }
 117+
 118+ return $state;
 119+ }
 120+
 121+ /**
 122+ * Randomly hash data while mixing in clock drift data for randomness
 123+ *
 124+ * @param $data The data to randomly hash.
 125+ * @return String The hashed bytes
 126+ * @author Tim Starling
 127+ */
 128+ protected function driftHash( $data ) {
 129+ // Minimum number of iterations (to avoid slow operations causing the loop to gather little entropy)
 130+ $minIterations = self::MIN_ITERATIONS;
 131+ // Duration of time to spend doing calculations (in seconds)
 132+ $duration = ( self::MSEC_PER_BYTE / 1000 ) * $this->hashLength();
 133+ // Create a buffer to use to trigger memory operations
 134+ $bufLength = 10000000;
 135+ $buffer = str_repeat( ' ', $bufLength );
 136+ $bufPos = 0;
 137+
 138+ // Iterate for $duration seconds or at least $minIerations number of iterations
 139+ $iterations = 0;
 140+ $startTime = microtime( true );
 141+ $currentTime = $startTime;
 142+ while ( $iterations < $minIterations || $currentTime - $startTime < $duration ) {
 143+ // Trigger some memory writing to trigger some bus activity
 144+ // This may create variance in the time between iterations
 145+ $bufPos = ( $bufPos + 13 ) % $bufLength;
 146+ $buffer[$bufPos] = ' ';
 147+ // Add the drift between this iteration and the last in as entropy
 148+ $nextTime = microtime( true );
 149+ $delta = (int)( ( $nextTime - $currentTime ) * 1000000 );
 150+ $data .= $delta;
 151+ // Every 100 iterations hash the data and entropy
 152+ if ( $iterations % 100 === 0 ) {
 153+ $data = sha1( $data );
 154+ }
 155+ $currentTime = $nextTime;
 156+ $iterations++;
 157+ }
 158+ $timeTaken = $currentTime - $startTime;
 159+ $data = $this->hash( $data );
 160+
 161+ wfDebug( __METHOD__ . ": Clock drift calculation " .
 162+ "(time-taken=" . ( $timeTaken * 1000 ) . "ms, " .
 163+ "iterations=$iterations, " .
 164+ "time-per-iteration=" . ( $timeTaken / $iterations * 1e6 ) . "us)\n" );
 165+ return $data;
 166+ }
 167+
 168+ /**
 169+ * Return a rolling random state initially build using data from unstable sources
 170+ * @return A new weak random state
 171+ */
 172+ protected function randomState() {
 173+ static $state = null;
 174+ if ( is_null( $state ) ) {
 175+ // Initialize the state with whatever unstable data we can find
 176+ // It's important that this data is hashed right afterwards to prevent
 177+ // it from being leaked into the output stream
 178+ $state = $this->hash( $this->initialRandomState() );
 179+ }
 180+ // Generate a new random state based on the initial random state or previous
 181+ // random state by combining it with clock drift
 182+ $state = $this->driftHash( $state );
 183+ return $state;
 184+ }
 185+
 186+ /**
 187+ * Decide on the best acceptable hash algorithm we have available for hash()
 188+ * @return String A hash algorithm
 189+ */
 190+ protected function hashAlgo() {
 191+ if ( !is_null( $algo ) ) {
 192+ return $algo;
 193+ }
 194+
 195+ $algos = hash_algos();
 196+ $preference = array( 'whirlpool', 'sha256', 'sha1', 'md5' );
 197+
 198+ foreach ( $preference as $algorithm ) {
 199+ if ( in_array( $algorithm, $algos ) ) {
 200+ $algo = $algorithm; # assign to static
 201+ wfDebug( __METHOD__ . ": Using the $algo hash algorithm.\n" );
 202+ return $algo;
 203+ }
 204+ }
 205+
 206+ // We only reach here if no acceptable hash is found in the list, this should
 207+ // be a technical impossibility since most of php's hash list is fixed and
 208+ // some of the ones we list are available as their own native functions
 209+ // But since we already require at least 5.2 and hash() was default in
 210+ // 5.1.2 we don't bother falling back to methods like sha1 and md5.
 211+ throw new MWException( "Could not find an acceptable hashing function in hash_algos()" );
 212+ }
 213+
 214+ /**
 215+ * Return the byte-length output of the hash algorithm we are
 216+ * using in self::hash and self::hmac.
 217+ *
 218+ * @return int Number of bytes the hash outputs
 219+ */
 220+ protected function hashLength() {
 221+ if ( is_null( $hashLength ) ) {
 222+ $hashLength = strlen( $this->hash( '' ) );
 223+ }
 224+ return $hashLength;
 225+ }
 226+
 227+ /**
 228+ * Generate an acceptably unstable one-way-hash of some text
 229+ * making use of the best hash algorithm that we have available.
 230+ *
 231+ * @return String A raw hash of the data
 232+ */
 233+ protected function hash( $data ) {
 234+ return hash( $this->hashAlgo(), $data, true );
 235+ }
 236+
 237+ /**
 238+ * Generate an acceptably unstable one-way-hmac of some text
 239+ * making use of the best hash algorithm that we have available.
 240+ *
 241+ * @return String A raw hash of the data
 242+ */
 243+ protected function hmac( $data, $key ) {
 244+ return hash_hmac( $this->hashAlgo(), $data, $key, true );
 245+ }
 246+
 247+ /**
 248+ * @see self::wasStrong()
 249+ */
 250+ public function realWasStrong() {
 251+ if ( is_null( $this->strong ) ) {
 252+ throw new MWException( __METHOD__ . ' called before generation of random data' );
 253+ }
 254+ return $this->strong;
 255+ }
 256+
 257+ /**
 258+ * @see self::generate()
 259+ */
 260+ public function realGenerate( $bytes, $forceStrong = false, $method = null ) {
 261+ wfProfileIn( __METHOD__ );
 262+ if ( is_string( $forceStrong ) && is_null( $method ) ) {
 263+ // If $forceStrong is a string then it's really $method
 264+ $method = $forceStrong;
 265+ $forceStrong = false;
 266+ }
 267+
 268+ if ( !is_null( $method ) ) {
 269+ wfDebug( __METHOD__ . ": Generating cryptographic random bytes for $method\n" );
 270+ }
 271+
 272+ $bytes = floor( $bytes );
 273+ static $buffer = '';
 274+ if ( is_null( $this->strong ) ) {
 275+ // Set strength to false initially until we know what source data is coming from
 276+ $this->strong = true;
 277+ }
 278+
 279+ if ( strlen( $buffer ) < $bytes ) {
 280+ // If available make use of mcrypt_create_iv URANDOM source to generate randomness
 281+ // On unix-like systems this reads from /dev/urandom but does it without any buffering
 282+ // and bypasses openbasdir restrictions so it's preferable to reading directly
 283+ // On Windows starting in PHP 5.3.0 Windows' native CryptGenRandom is used to generate
 284+ // entropy so this is also preferable to just trying to read urandom because it may work
 285+ // on Windows systems as well.
 286+ if ( function_exists( 'mcrypt_create_iv' ) ) {
 287+ wfProfileIn( __METHOD__ . '-mcrypt' );
 288+ $rem = $bytes - strlen( $buffer );
 289+ wfDebug( __METHOD__ . ": Trying to generate $rem bytes of randomness using mcrypt_create_iv.\n" );
 290+ $iv = mcrypt_create_iv( $rem, MCRYPT_DEV_URANDOM );
 291+ if ( $iv === false ) {
 292+ wfDebug( __METHOD__ . ": mcrypt_create_iv returned false.\n" );
 293+ } else {
 294+ $bytes .= $iv;
 295+ wfDebug( __METHOD__ . ": mcrypt_create_iv generated " . strlen( $iv ) . " bytes of randomness.\n" );
 296+ }
 297+ wfProfileOut( __METHOD__ . '-mcrypt' );
 298+ }
 299+ }
 300+
 301+ if ( strlen( $buffer ) < $bytes ) {
 302+ // If available make use of openssl's random_pesudo_bytes method to attempt to generate randomness.
 303+ // However don't do this on Windows with PHP < 5.3.4 due to a bug:
 304+ // http://stackoverflow.com/questions/1940168/openssl-random-pseudo-bytes-is-slow-php
 305+ if ( function_exists( 'openssl_random_pseudo_bytes' )
 306+ && ( !wfIsWindows() || version_compare( PHP_VERSION, '5.3.4', '>=' ) )
 307+ ) {
 308+ wfProfileIn( __METHOD__ . '-openssl' );
 309+ $rem = $bytes - strlen( $buffer );
 310+ wfDebug( __METHOD__ . ": Trying to generate $rem bytes of randomness using openssl_random_pseudo_bytes.\n" );
 311+ $openssl_bytes = openssl_random_pseudo_bytes( $rem, $openssl_strong );
 312+ if ( $openssl_bytes === false ) {
 313+ wfDebug( __METHOD__ . ": openssl_random_pseudo_bytes returned false.\n" );
 314+ } else {
 315+ $buffer .= $openssl_bytes;
 316+ wfDebug( __METHOD__ . ": openssl_random_pseudo_bytes generated " . strlen( $openssl_bytes ) . " bytes of " . ( $openssl_strong ? "strong" : "weak" ) . " randomness.\n" );
 317+ }
 318+ if ( strlen( $buffer ) >= $bytes ) {
 319+ // openssl tells us if the random source was strong, if some of our data was generated
 320+ // using it use it's say on whether the randomness is strong
 321+ $this->strong = !!$openssl_strong;
 322+ }
 323+ wfProfileOut( __METHOD__ . '-openssl' );
 324+ }
 325+ }
 326+
 327+ // Only read from urandom if we can control the buffer size or were passed forceStrong
 328+ if ( strlen( $buffer ) < $bytes && ( function_exists( 'stream_set_read_buffer' ) || $forceStrong ) ) {
 329+ wfProfileIn( __METHOD__ . '-fopen-urandom' );
 330+ $rem = $bytes - strlen( $buffer );
 331+ wfDebug( __METHOD__ . ": Trying to generate $rem bytes of randomness using /dev/urandom.\n" );
 332+ if ( !function_exists( 'stream_set_read_buffer' ) && $forceStrong ) {
 333+ wfDebug( __METHOD__ . ": Was forced to read from /dev/urandom without control over the buffer size.\n" );
 334+ }
 335+ // /dev/urandom is generally considered the best possible commonly
 336+ // available random source, and is available on most *nix systems.
 337+ wfSuppressWarnings();
 338+ $urandom = fopen( "/dev/urandom", "rb" );
 339+ wfRestoreWarnings();
 340+
 341+ // Attempt to read all our random data from urandom
 342+ // php's fread always does buffered reads based on the stream's chunk_size
 343+ // so in reality it will usually read more than the amount of data we're
 344+ // asked for and not storing that risks depleting the system's random pool.
 345+ // If stream_set_read_buffer is available set the chunk_size to the amount
 346+ // of data we need. Otherwise read 8k, php's default chunk_size.
 347+ if ( $urandom ) {
 348+ // php's default chunk_size is 8k
 349+ $chunk_size = 1024 * 8;
 350+ if ( function_exists( 'stream_set_read_buffer' ) ) {
 351+ // If possible set the chunk_size to the amount of data we need
 352+ stream_set_read_buffer( $urandom, $rem );
 353+ $chunk_size = $rem;
 354+ }
 355+ wfDebug( __METHOD__ . ": Reading from /dev/urandom with a buffer size of $chunk_size.\n" );
 356+ $random_bytes = fread( $urandom, max( $chunk_size, $rem ) );
 357+ $buffer .= $random_bytes;
 358+ fclose( $urandom );
 359+ wfDebug( __METHOD__ . ": /dev/urandom generated " . strlen( $random_bytes ) . " bytes of randomness.\n" );
 360+ if ( strlen( $buffer ) >= $bytes ) {
 361+ // urandom is always strong, set to true if all our data was generated using it
 362+ $this->strong = true;
 363+ }
 364+ } else {
 365+ wfDebug( __METHOD__ . ": /dev/urandom could not be opened.\n" );
 366+ }
 367+ wfProfileOut( __METHOD__ . '-fopen-urandom' );
 368+ }
 369+
 370+ // If we cannot use or generate enough data from a secure source
 371+ // use this loop to generate a good set of pseudo random data.
 372+ // This works by initializing a random state using a pile of unstable data
 373+ // and continually shoving it through a hash along with a variable salt.
 374+ // We hash the random state with more salt to avoid the state from leaking
 375+ // out and being used to predict the /randomness/ that follows.
 376+ if ( strlen( $buffer ) < $bytes ) {
 377+ wfDebug( __METHOD__ . ": Falling back to using a pseudo random state to generate randomness.\n" );
 378+ }
 379+ while ( strlen( $buffer ) < $bytes ) {
 380+ wfProfileIn( __METHOD__ . '-fallback' );
 381+ $buffer .= $this->hmac( $this->randomState(), mt_rand() );
 382+ // This code is never really cryptographically strong, if we use it
 383+ // at all, then set strong to false.
 384+ $this->strong = false;
 385+ wfProfileOut( __METHOD__ . '-fallback' );
 386+ }
 387+
 388+ // Once the buffer has been filled up with enough random data to fulfill
 389+ // the request shift off enough data to handle the request and leave the
 390+ // unused portion left inside the buffer for the next request for random data
 391+ $generated = substr( $buffer, 0, $bytes );
 392+ $buffer = substr( $buffer, $bytes );
 393+
 394+ wfDebug( __METHOD__ . ": " . strlen( $buffer ) . " bytes of randomness leftover in the buffer.\n" );
 395+
 396+ wfProfileOut( __METHOD__ );
 397+ return $generated;
 398+ }
 399+
 400+ /**
 401+ * @see self::generateHex()
 402+ */
 403+ public function realGenerateHex( $chars, $forceStrong = false, $method = null ) {
 404+ // hex strings are 2x the length of raw binary so we divide the length in half
 405+ // odd numbers will result in a .5 that leads the generate() being 1 character
 406+ // short, so we use ceil() to ensure that we always have enough bytes
 407+ $bytes = ceil( $chars / 2 );
 408+ // Generate the data and then convert it to a hex string
 409+ $hex = bin2hex( $this->generate( $bytes, $forceStrong, $method ) );
 410+ // A bit of paranoia here, the caller asked for a specific length of string
 411+ // here, and it's possible (eg when given an odd number) that we may actually
 412+ // have at least 1 char more than they asked for. Just in case they made this
 413+ // call intending to insert it into a database that does truncation we don't
 414+ // want to give them too much and end up with their database and their live
 415+ // code having two different values because part of what we gave them is truncated
 416+ // hence, we strip out any run of characters longer than what we were asked for.
 417+ return substr( $hex, 0, $chars );
 418+ }
 419+
 420+ /** Publicly exposed static methods **/
 421+
 422+ /**
 423+ * Return a singleton instance of MWCryptRand
 424+ */
 425+ protected static function singleton() {
 426+ if ( is_null( self::$singleton ) ) {
 427+ self::$singleton = new self;
 428+ }
 429+ return self::$singleton;
 430+ }
 431+
 432+ /**
 433+ * Return a boolean indicating whether or not the source used for cryptographic
 434+ * random bytes generation in the previously run generate* call
 435+ * was cryptographically strong.
 436+ *
 437+ * @return bool Returns true if the source was strong, false if not.
 438+ */
 439+ public static function wasStrong() {
 440+ return self::singleton()->realWasStrong();
 441+ }
 442+
 443+ /**
 444+ * Generate a run of (ideally) cryptographically random data and return
 445+ * it in raw binary form.
 446+ * You can use MWCryptRand::wasStrong() if you wish to know if the source used
 447+ * was cryptographically strong.
 448+ *
 449+ * @param $bytes int the number of bytes of random data to generate
 450+ * @param $forceStrong bool Pass true if you want generate to prefer cryptographically
 451+ * strong sources of entropy even if reading from them may steal
 452+ * more entropy from the system than optimal.
 453+ * @param $method The calling method, for debug info. May be the second argument if you are not using forceStrong
 454+ * @return String Raw binary random data
 455+ */
 456+ public static function generate( $bytes, $forceStrong = false, $method = null ) {
 457+ return self::singleton()->realGenerate( $bytes, $forceStrong, $method );
 458+ }
 459+
 460+ /**
 461+ * Generate a run of (ideally) cryptographically random data and return
 462+ * it in hexadecimal string format.
 463+ * You can use MWCryptRand::wasStrong() if you wish to know if the source used
 464+ * was cryptographically strong.
 465+ *
 466+ * @param $chars int the number of hex chars of random data to generate
 467+ * @param $forceStrong bool Pass true if you want generate to prefer cryptographically
 468+ * strong sources of entropy even if reading from them may steal
 469+ * more entropy from the system than optimal.
 470+ * @param $method The calling method, for debug info. May be the second argument if you are not using forceStrong
 471+ * @return String Hexadecimal random data
 472+ */
 473+ public static function generateHex( $chars, $forceStrong = false, $method = null ) {
 474+ return self::singleton()->realGenerateHex( $chars, $forceStrong, $method );
 475+ }
 476+
 477+}
Index: branches/REL1_19/phase3/includes/User.php
@@ -831,23 +831,20 @@
832832 }
833833
834834 /**
835 - * Return a random password. Sourced from mt_rand, so it's not particularly secure.
836 - * @todo hash random numbers to improve security, like generateToken()
 835+ * Return a random password.
837836 *
838837 * @return String new random password
839838 */
840839 public static function randomPassword() {
841840 global $wgMinimalPasswordLength;
842 - $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
843 - $l = strlen( $pwchars ) - 1;
844 -
845 - $pwlength = max( 7, $wgMinimalPasswordLength );
846 - $digit = mt_rand( 0, $pwlength - 1 );
847 - $np = '';
848 - for ( $i = 0; $i < $pwlength; $i++ ) {
849 - $np .= $i == $digit ? chr( mt_rand( 48, 57 ) ) : $pwchars[ mt_rand( 0, $l ) ];
850 - }
851 - return $np;
 841+ // Decide the final password length based on our min password length, stopping at a minimum of 10 chars
 842+ $length = max( 10, $wgMinimalPasswordLength );
 843+ // Multiply by 1.25 to get the number of hex characters we need
 844+ $length = $length * 1.25;
 845+ // Generate random hex chars
 846+ $hex = MWCryptRand::generateHex( $length, __METHOD__ );
 847+ // Convert from base 16 to base 32 to get a proper password like string
 848+ return wfBaseConvert( $hex, 16, 32 );
852849 }
853850
854851 /**
@@ -877,7 +874,7 @@
878875 $this->mTouched = '0'; # Allow any pages to be cached
879876 }
880877
881 - $this->setToken(); # Random
 878+ $this->mToken = null; // Don't run cryptographic functions till we need a token
882879 $this->mEmailAuthenticated = null;
883880 $this->mEmailToken = '';
884881 $this->mEmailTokenExpires = null;
@@ -984,11 +981,11 @@
985982 return false;
986983 }
987984
988 - if ( $request->getSessionData( 'wsToken' ) !== null ) {
989 - $passwordCorrect = $proposedUser->getToken() === $request->getSessionData( 'wsToken' );
 985+ if ( $request->getSessionData( 'wsToken' ) ) {
 986+ $passwordCorrect = $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' );
990987 $from = 'session';
991 - } elseif ( $request->getCookie( 'Token' ) !== null ) {
992 - $passwordCorrect = $proposedUser->getToken() === $request->getCookie( 'Token' );
 988+ } elseif ( $request->getCookie( 'Token' ) ) {
 989+ $passwordCorrect = $proposedUser->getToken( false ) === $request->getCookie( 'Token' );
993990 $from = 'cookie';
994991 } else {
995992 # No session or persistent login cookie
@@ -1093,6 +1090,9 @@
10941091 }
10951092 $this->mTouched = wfTimestamp( TS_MW, $row->user_touched );
10961093 $this->mToken = $row->user_token;
 1094+ if ( $this->mToken == '' ) {
 1095+ $this->mToken = null;
 1096+ }
10971097 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
10981098 $this->mEmailToken = $row->user_email_token;
10991099 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
@@ -2018,10 +2018,14 @@
20192019
20202020 /**
20212021 * Get the user's current token.
 2022+ * @param $forceCreation Force the generation of a new token if the user doesn't have one (default=true for backwards compatibility)
20222023 * @return String Token
20232024 */
2024 - public function getToken() {
 2025+ public function getToken( $forceCreation = true ) {
20252026 $this->load();
 2027+ if ( !$this->mToken && $forceCreation ) {
 2028+ $this->setToken();
 2029+ }
20262030 return $this->mToken;
20272031 }
20282032
@@ -2035,14 +2039,7 @@
20362040 global $wgSecretKey, $wgProxyKey;
20372041 $this->load();
20382042 if ( !$token ) {
2039 - if ( $wgSecretKey ) {
2040 - $key = $wgSecretKey;
2041 - } elseif ( $wgProxyKey ) {
2042 - $key = $wgProxyKey;
2043 - } else {
2044 - $key = microtime();
2045 - }
2046 - $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . wfWikiID() . $this->mId );
 2043+ $this->mToken = MWCryptRand::generateHex( USER_TOKEN_LENGTH, __METHOD__ );
20472044 } else {
20482045 $this->mToken = $token;
20492046 }
@@ -2748,6 +2745,14 @@
27492746
27502747 $this->load();
27512748 if ( 0 == $this->mId ) return;
 2749+ if ( !$this->mToken ) {
 2750+ // When token is empty or NULL generate a new one and then save it to the database
 2751+ // This allows a wiki to re-secure itself after a leak of it's user table or $wgSecretKey
 2752+ // Simply by setting every cell in the user_token column to NULL and letting them be
 2753+ // regenerated as users log back into the wiki.
 2754+ $this->setToken();
 2755+ $this->saveSettings();
 2756+ }
27522757 $session = array(
27532758 'wsUserID' => $this->mId,
27542759 'wsToken' => $this->mToken,
@@ -2824,7 +2829,7 @@
28252830 'user_email' => $this->mEmail,
28262831 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
28272832 'user_touched' => $dbw->timestamp( $this->mTouched ),
2828 - 'user_token' => $this->mToken,
 2833+ 'user_token' => strval( $this->mToken ),
28292834 'user_email_token' => $this->mEmailToken,
28302835 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
28312836 ), array( /* WHERE */
@@ -2890,7 +2895,7 @@
28912896 'user_email' => $user->mEmail,
28922897 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
28932898 'user_real_name' => $user->mRealName,
2894 - 'user_token' => $user->mToken,
 2899+ 'user_token' => strval( $user->mToken ),
28952900 'user_registration' => $dbw->timestamp( $user->mRegistration ),
28962901 'user_editcount' => 0,
28972902 );
@@ -2923,7 +2928,7 @@
29242929 'user_email' => $this->mEmail,
29252930 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
29262931 'user_real_name' => $this->mRealName,
2927 - 'user_token' => $this->mToken,
 2932+ 'user_token' => strval( $this->mToken ),
29282933 'user_registration' => $dbw->timestamp( $this->mRegistration ),
29292934 'user_editcount' => 0,
29302935 ), __METHOD__
@@ -3187,7 +3192,7 @@
31883193 } else {
31893194 $token = $request->getSessionData( 'wsEditToken' );
31903195 if ( $token === null ) {
3191 - $token = self::generateToken();
 3196+ $token = MWCryptRand::generateHex( 32, __METHOD__ );
31923197 $request->setSessionData( 'wsEditToken', $token );
31933198 }
31943199 if( is_array( $salt ) ) {
@@ -3204,8 +3209,7 @@
32053210 * @return String The new random token
32063211 */
32073212 public static function generateToken( $salt = '' ) {
3208 - $token = dechex( mt_rand() ) . dechex( mt_rand() );
3209 - return md5( $token . $salt );
 3213+ return MWCryptRand::generateHex( 32, __METHOD__ );
32103214 }
32113215
32123216 /**
@@ -3311,12 +3315,11 @@
33123316 global $wgUserEmailConfirmationTokenExpiry;
33133317 $now = time();
33143318 $expires = $now + $wgUserEmailConfirmationTokenExpiry;
3315 - $expiration = wfTimestamp( TS_MW, $expires );
3316 - $token = self::generateToken( $this->mId . $this->mEmail . $expires );
 3319+ $this->load();
 3320+ $token = MWCryptRand::generateHex( 32, __METHOD__ );
33173321 $hash = md5( $token );
3318 - $this->load();
33193322 $this->mEmailToken = $hash;
3320 - $this->mEmailTokenExpires = $expiration;
 3323+ $this->mEmailTokenExpires = wfTimestamp( TS_MW, $expires );
33213324 return $token;
33223325 }
33233326
@@ -3865,7 +3868,7 @@
38663869
38673870 if( $wgPasswordSalt ) {
38683871 if ( $salt === false ) {
3869 - $salt = substr( wfGenerateToken(), 0, 8 );
 3872+ $salt = MWCryptRand::generateHex( 8, __METHOD__ );
38703873 }
38713874 return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
38723875 } else {
Index: branches/REL1_19/phase3/includes/GlobalFunctions.php
@@ -3292,6 +3292,33 @@
32933293 }
32943294
32953295 /**
 3296+ * Override session_id before session startup if php's built-in
 3297+ * session generation code is not secure.
 3298+ */
 3299+function wfFixSessionID() {
 3300+ // If the cookie or session id is already set we already have a session and should abort
 3301+ if ( isset( $_COOKIE[ session_name() ] ) || session_id() ) {
 3302+ return;
 3303+ }
 3304+
 3305+ // PHP's built-in session entropy is enabled if:
 3306+ // - entropy_file is set or you're on Windows with php 5.3.3+
 3307+ // - AND entropy_length is > 0
 3308+ // We treat it as disabled if it doesn't have an entropy length of at least 32
 3309+ $entropyEnabled = (
 3310+ ( wfIsWindows() && version_compare( PHP_VERSION, '5.3.3', '>=' ) )
 3311+ || ini_get( 'session.entropy_file' )
 3312+ )
 3313+ && intval( ini_get( 'session.entropy_length' ) ) >= 32;
 3314+
 3315+ // If built-in entropy is not enabled or not sufficient override php's built in session id generation code
 3316+ if ( !$entropyEnabled ) {
 3317+ wfDebug( __METHOD__ . ": PHP's built in entropy is disabled or not sufficient, overriding session id generation using our cryptrand source.\n" );
 3318+ session_id( MWCryptRand::generateHex( 32, __METHOD__ ) );
 3319+ }
 3320+}
 3321+
 3322+/**
32963323 * Initialise php session
32973324 *
32983325 * @param $sessionId Bool
@@ -3330,6 +3357,8 @@
33313358 session_cache_limiter( 'private, must-revalidate' );
33323359 if ( $sessionId ) {
33333360 session_id( $sessionId );
 3361+ } else {
 3362+ wfFixSessionID();
33343363 }
33353364 wfSuppressWarnings();
33363365 session_start();
Index: branches/REL1_19/phase3/includes/installer/Installer.php
@@ -1405,8 +1405,7 @@
14061406 }
14071407
14081408 /**
1409 - * Generate $wgSecretKey. Will warn if we had to use mt_rand() instead of
1410 - * /dev/urandom
 1409+ * Generate $wgSecretKey. Will warn if we had to use an insecure random source.
14111410 *
14121411 * @return Status
14131412 */
@@ -1419,8 +1418,8 @@
14201419 }
14211420
14221421 /**
1423 - * Generate a secret value for variables using either
1424 - * /dev/urandom or mt_rand(). Produce a warning in the later case.
 1422+ * Generate a secret value for variables using our CryptRand generator.
 1423+ * Produce a warning if the random source was insecure.
14251424 *
14261425 * @param $keys Array
14271426 * @return Status
@@ -1428,28 +1427,18 @@
14291428 protected function doGenerateKeys( $keys ) {
14301429 $status = Status::newGood();
14311430
1432 - wfSuppressWarnings();
1433 - $file = fopen( "/dev/urandom", "r" );
1434 - wfRestoreWarnings();
1435 -
 1431+ $strong = true;
14361432 foreach ( $keys as $name => $length ) {
1437 - if ( $file ) {
1438 - $secretKey = bin2hex( fread( $file, $length / 2 ) );
1439 - } else {
1440 - $secretKey = '';
1441 -
1442 - for ( $i = 0; $i < $length / 8; $i++ ) {
1443 - $secretKey .= dechex( mt_rand( 0, 0x7fffffff ) );
1444 - }
 1433+ $secretKey = MWCryptRand::generateHex( $length, true );
 1434+ if ( !MWCryptRand::wasStrong() ) {
 1435+ $strong = false;
14451436 }
14461437
14471438 $this->setVar( $name, $secretKey );
14481439 }
14491440
1450 - if ( $file ) {
1451 - fclose( $file );
1452 - } else {
1453 - $names = array_keys ( $keys );
 1441+ if ( !$strong ) {
 1442+ $names = array_keys( $keys );
14541443 $names = preg_replace( '/^(.*)$/', '\$$1', $names );
14551444 global $wgLang;
14561445 $status->warning( 'config-insecure-keys', $wgLang->listToText( $names ), count( $names ) );
Index: branches/REL1_19/phase3/includes/AutoLoader.php
@@ -49,6 +49,7 @@
5050 'ConfEditorToken' => 'includes/ConfEditor.php',
5151 'Cookie' => 'includes/Cookie.php',
5252 'CookieJar' => 'includes/Cookie.php',
 53+ 'MWCryptRand' => 'includes/CryptRand.php',
5354 'CurlHttpRequest' => 'includes/HttpFunctions.php',
5455 'DeferrableUpdate' => 'includes/DeferredUpdates.php',
5556 'DeferredUpdates' => 'includes/DeferredUpdates.php',
Index: branches/REL1_19/phase3/includes/specials/SpecialUserlogin.php
@@ -1136,9 +1136,9 @@
11371137 */
11381138 public static function setLoginToken() {
11391139 global $wgRequest;
1140 - // Use User::generateToken() instead of $user->editToken()
 1140+ // Generate a token directly instead of using $user->editToken()
11411141 // because the latter reuses $_SESSION['wsEditToken']
1142 - $wgRequest->setSessionData( 'wsLoginToken', User::generateToken() );
 1142+ $wgRequest->setSessionData( 'wsLoginToken', MWCryptRand::generateHex( 32, __METHOD__ ) );
11431143 }
11441144
11451145 /**
@@ -1162,7 +1162,7 @@
11631163 */
11641164 public static function setCreateaccountToken() {
11651165 global $wgRequest;
1166 - $wgRequest->setSessionData( 'wsCreateaccountToken', User::generateToken() );
 1166+ $wgRequest->setSessionData( 'wsCreateaccountToken', MWCryptRand::generateHex( 32, __METHOD__ ) );
11671167 }
11681168
11691169 /**
Index: branches/REL1_19/phase3/includes/specials/SpecialWatchlist.php
@@ -43,7 +43,7 @@
4444 // Add feed links
4545 $wlToken = $user->getOption( 'watchlisttoken' );
4646 if ( !$wlToken ) {
47 - $wlToken = sha1( mt_rand() . microtime( true ) );
 47+ $wlToken = MWCryptRand::generateHex( 40 );
4848 $user->setOption( 'watchlisttoken', $wlToken );
4949 $user->saveSettings();
5050 }

Past revisions this follows-up on

RevisionCommit summaryAuthorDate
r110825(bug 34237) Regenerate an empty user_token and save to the database when we t...dantman08:07, 7 February 2012
r114233Commit the cryptrand project worked on in git:...dantman05:17, 20 March 2012

Status & tagging log