r114233 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r114232‎ | r114233 | r114234 >
Date:05:17, 20 March 2012
Author:dantman
Status:deferred (Comments)
Tags:tstarling 
Comment:
Commit the cryptrand project worked on in git:
- MWCryptRand: A new api for generating cryptographic randomness for security tokens. Uses whatever cryptographic source is available and if not falls back to using random state and clock drift.
- wfRandomString - A simple non-cryptographic pesudo-random string generation function to replace wfGenerateToken which was written pretending to be secure when it's really not.
- Core updates to use MWCryptRand in various places:
-- user_token generation (to do this we stop generating user_token implicitly and only generate it when needed to avoid depleting the system's entropy pool by reading random data we'll never use)
-- email confirmation token generation
-- password salt generation
-- temporary password generation
-- Generation of the automatic watchlist token
-- login and create user tokens
-- session ids when php's entropy sources are not set
-- the installer when generating wgSecretKey and the upgrade key
Modified paths:
  • /trunk/phase3/RELEASE-NOTES-1.20 (modified) (history)
  • /trunk/phase3/includes/AutoLoader.php (modified) (history)
  • /trunk/phase3/includes/CryptRand.php (added) (history)
  • /trunk/phase3/includes/GlobalFunctions.php (modified) (history)
  • /trunk/phase3/includes/Import.php (modified) (history)
  • /trunk/phase3/includes/Preferences.php (modified) (history)
  • /trunk/phase3/includes/User.php (modified) (history)
  • /trunk/phase3/includes/installer/Installer.php (modified) (history)
  • /trunk/phase3/includes/parser/Parser.php (modified) (history)
  • /trunk/phase3/includes/specials/SpecialUserlogin.php (modified) (history)
  • /trunk/phase3/includes/specials/SpecialWatchlist.php (modified) (history)

Diff [purge]

Index: trunk/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: trunk/phase3/includes/User.php
@@ -836,23 +836,20 @@
837837 }
838838
839839 /**
840 - * Return a random password. Sourced from mt_rand, so it's not particularly secure.
841 - * @todo hash random numbers to improve security, like generateToken()
 840+ * Return a random password.
842841 *
843842 * @return String new random password
844843 */
845844 public static function randomPassword() {
846845 global $wgMinimalPasswordLength;
847 - $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
848 - $l = strlen( $pwchars ) - 1;
849 -
850 - $pwlength = max( 7, $wgMinimalPasswordLength );
851 - $digit = mt_rand( 0, $pwlength - 1 );
852 - $np = '';
853 - for ( $i = 0; $i < $pwlength; $i++ ) {
854 - $np .= $i == $digit ? chr( mt_rand( 48, 57 ) ) : $pwchars[ mt_rand( 0, $l ) ];
855 - }
856 - return $np;
 846+ // Decide the final password length based on our min password length, stopping at a minimum of 10 chars
 847+ $length = max( 10, $wgMinimalPasswordLength );
 848+ // Multiply by 1.25 to get the number of hex characters we need
 849+ $length = $length * 1.25;
 850+ // Generate random hex chars
 851+ $hex = MWCryptRand::generateHex( $length, __METHOD__ );
 852+ // Convert from base 16 to base 32 to get a proper password like string
 853+ return wfBaseConvert( $hex, 16, 32 );
857854 }
858855
859856 /**
@@ -882,7 +879,7 @@
883880 $this->mTouched = '0'; # Allow any pages to be cached
884881 }
885882
886 - $this->setToken(); # Random
 883+ $this->mToken = null; // Don't run cryptographic functions till we need a token
887884 $this->mEmailAuthenticated = null;
888885 $this->mEmailToken = '';
889886 $this->mEmailTokenExpires = null;
@@ -989,11 +986,11 @@
990987 return false;
991988 }
992989
993 - if ( $request->getSessionData( 'wsToken' ) !== null ) {
994 - $passwordCorrect = $proposedUser->getToken() === $request->getSessionData( 'wsToken' );
 990+ if ( $request->getSessionData( 'wsToken' ) ) {
 991+ $passwordCorrect = $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' );
995992 $from = 'session';
996 - } elseif ( $request->getCookie( 'Token' ) !== null ) {
997 - $passwordCorrect = $proposedUser->getToken() === $request->getCookie( 'Token' );
 993+ } elseif ( $request->getCookie( 'Token' ) ) {
 994+ $passwordCorrect = $proposedUser->getToken( false ) === $request->getCookie( 'Token' );
998995 $from = 'cookie';
999996 } else {
1000997 # No session or persistent login cookie
@@ -1098,6 +1095,9 @@
10991096 }
11001097 $this->mTouched = wfTimestamp( TS_MW, $row->user_touched );
11011098 $this->mToken = $row->user_token;
 1099+ if ( $this->mToken == '' ) {
 1100+ $this->mToken = null;
 1101+ }
11021102 $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated );
11031103 $this->mEmailToken = $row->user_email_token;
11041104 $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires );
@@ -2023,10 +2023,14 @@
20242024
20252025 /**
20262026 * Get the user's current token.
 2027+ * @param $forceCreation Force the generation of a new token if the user doesn't have one (default=true for backwards compatibility)
20272028 * @return String Token
20282029 */
2029 - public function getToken() {
 2030+ public function getToken( $forceCreation = true ) {
20302031 $this->load();
 2032+ if ( !$this->mToken && $forceCreation ) {
 2033+ $this->setToken();
 2034+ }
20312035 return $this->mToken;
20322036 }
20332037
@@ -2040,14 +2044,7 @@
20412045 global $wgSecretKey, $wgProxyKey;
20422046 $this->load();
20432047 if ( !$token ) {
2044 - if ( $wgSecretKey ) {
2045 - $key = $wgSecretKey;
2046 - } elseif ( $wgProxyKey ) {
2047 - $key = $wgProxyKey;
2048 - } else {
2049 - $key = microtime();
2050 - }
2051 - $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . wfWikiID() . $this->mId );
 2048+ $this->mToken = MWCryptRand::generateHex( USER_TOKEN_LENGTH, __METHOD__ );
20522049 } else {
20532050 $this->mToken = $token;
20542051 }
@@ -2814,7 +2811,7 @@
28152812 'user_email' => $this->mEmail,
28162813 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
28172814 'user_touched' => $dbw->timestamp( $this->mTouched ),
2818 - 'user_token' => $this->mToken,
 2815+ 'user_token' => strval( $this->mToken ),
28192816 'user_email_token' => $this->mEmailToken,
28202817 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
28212818 ), array( /* WHERE */
@@ -2880,7 +2877,7 @@
28812878 'user_email' => $user->mEmail,
28822879 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
28832880 'user_real_name' => $user->mRealName,
2884 - 'user_token' => $user->mToken,
 2881+ 'user_token' => strval( $user->mToken ),
28852882 'user_registration' => $dbw->timestamp( $user->mRegistration ),
28862883 'user_editcount' => 0,
28872884 'user_touched' => $dbw->timestamp( self::newTouchedTimestamp() ),
@@ -2917,7 +2914,7 @@
29182915 'user_email' => $this->mEmail,
29192916 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
29202917 'user_real_name' => $this->mRealName,
2921 - 'user_token' => $this->mToken,
 2918+ 'user_token' => strval( $this->mToken ),
29222919 'user_registration' => $dbw->timestamp( $this->mRegistration ),
29232920 'user_editcount' => 0,
29242921 'user_touched' => $dbw->timestamp( $this->mTouched ),
@@ -3182,7 +3179,7 @@
31833180 } else {
31843181 $token = $request->getSessionData( 'wsEditToken' );
31853182 if ( $token === null ) {
3186 - $token = self::generateToken();
 3183+ $token = MWCryptRand::generateHex( 32, __METHOD__ );
31873184 $request->setSessionData( 'wsEditToken', $token );
31883185 }
31893186 if( is_array( $salt ) ) {
@@ -3197,10 +3194,10 @@
31983195 *
31993196 * @param $salt String Optional salt value
32003197 * @return String The new random token
 3198+ * @deprecated since 1.20; Use MWCryptRand for secure purposes or wfRandomString for pesudo-randomness
32013199 */
32023200 public static function generateToken( $salt = '' ) {
3203 - $token = dechex( mt_rand() ) . dechex( mt_rand() );
3204 - return md5( $token . $salt );
 3201+ return MWCryptRand::generateHex( 32, __METHOD__ );
32053202 }
32063203
32073204 /**
@@ -3306,12 +3303,11 @@
33073304 global $wgUserEmailConfirmationTokenExpiry;
33083305 $now = time();
33093306 $expires = $now + $wgUserEmailConfirmationTokenExpiry;
3310 - $expiration = wfTimestamp( TS_MW, $expires );
3311 - $token = self::generateToken( $this->mId . $this->mEmail . $expires );
3312 - $hash = md5( $token );
33133307 $this->load();
 3308+ $token = MWCryptRand::generateHex( 32, __METHOD__ );
 3309+ $hash = md5( $token );
33143310 $this->mEmailToken = $hash;
3315 - $this->mEmailTokenExpires = $expiration;
 3311+ $this->mEmailTokenExpires = wfTimestamp( TS_MW, $expires );
33163312 return $token;
33173313 }
33183314
@@ -3860,7 +3856,7 @@
38613857
38623858 if( $wgPasswordSalt ) {
38633859 if ( $salt === false ) {
3864 - $salt = substr( wfGenerateToken(), 0, 8 );
 3860+ $salt = MWCryptRand::generateHex( 8, __METHOD__ );
38653861 }
38663862 return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) );
38673863 } else {
Index: trunk/phase3/includes/GlobalFunctions.php
@@ -295,6 +295,24 @@
296296 }
297297
298298 /**
 299+ * Get a random string containing a number of pesudo-random hex
 300+ * characters.
 301+ * @note This is not secure, if you are trying to generate some sort
 302+ * of token please use MWCryptRand instead.
 303+ *
 304+ * @param $length int The length of the string to generate
 305+ * @return String
 306+ * @since 1.20
 307+ */
 308+function wfRandomString( $length = 32 ) {
 309+ $str = '';
 310+ while ( strlen( $str ) < $length ) {
 311+ $str .= dechex( mt_rand() );
 312+ }
 313+ return substr( $str, 0, $length );
 314+}
 315+
 316+/**
299317 * We want some things to be included as literal characters in our title URLs
300318 * for prettiness, which urlencode encodes by default. According to RFC 1738,
301319 * all of the following should be safe:
@@ -3323,6 +3341,33 @@
33243342 }
33253343
33263344 /**
 3345+ * Override session_id before session startup if php's built-in
 3346+ * session generation code is not secure.
 3347+ */
 3348+function wfFixSessionID() {
 3349+ // If the cookie or session id is already set we already have a session and should abort
 3350+ if ( isset( $_COOKIE[ session_name() ] ) || session_id() ) {
 3351+ return;
 3352+ }
 3353+
 3354+ // PHP's built-in session entropy is enabled if:
 3355+ // - entropy_file is set or you're on Windows with php 5.3.3+
 3356+ // - AND entropy_length is > 0
 3357+ // We treat it as disabled if it doesn't have an entropy length of at least 32
 3358+ $entropyEnabled = (
 3359+ ( wfIsWindows() && version_compare( PHP_VERSION, '5.3.3', '>=' ) )
 3360+ || ini_get( 'session.entropy_file' )
 3361+ )
 3362+ && intval( ini_get( 'session.entropy_length' ) ) >= 32;
 3363+
 3364+ // If built-in entropy is not enabled or not sufficient override php's built in session id generation code
 3365+ if ( !$entropyEnabled ) {
 3366+ wfDebug( __METHOD__ . ": PHP's built in entropy is disabled or not sufficient, overriding session id generation using our cryptrand source.\n" );
 3367+ session_id( MWCryptRand::generateHex( 32, __METHOD__ ) );
 3368+ }
 3369+}
 3370+
 3371+/**
33273372 * Initialise php session
33283373 *
33293374 * @param $sessionId Bool
@@ -3361,6 +3406,8 @@
33623407 session_cache_limiter( 'private, must-revalidate' );
33633408 if ( $sessionId ) {
33643409 session_id( $sessionId );
 3410+ } else {
 3411+ wfFixSessionID();
33653412 }
33663413 wfSuppressWarnings();
33673414 session_start();
@@ -3681,8 +3728,11 @@
36823729 * characters before hashing.
36833730 * @return string
36843731 * @codeCoverageIgnore
 3732+ * @deprecated since 1.20; Please use MWCryptRand for security purposes and wfRandomString for pesudo-random strings
 3733+ * @warning This method is NOT secure. Additionally it has many callers that use it for pesudo-random purposes.
36853734 */
36863735 function wfGenerateToken( $salt = '' ) {
 3736+ wfDeprecated( __METHOD__, '1.20' );
36873737 $salt = serialize( $salt );
36883738 return md5( mt_rand( 0, 0x7fffffff ) . $salt );
36893739 }
Index: trunk/phase3/includes/parser/Parser.php
@@ -561,7 +561,7 @@
562562 * @return string
563563 */
564564 static public function getRandomString() {
565 - return dechex( mt_rand( 0, 0x7fffffff ) ) . dechex( mt_rand( 0, 0x7fffffff ) );
 565+ return wfRandomString( 16 );
566566 }
567567
568568 /**
Index: trunk/phase3/includes/installer/Installer.php
@@ -1446,8 +1446,7 @@
14471447 }
14481448
14491449 /**
1450 - * Generate $wgSecretKey. Will warn if we had to use mt_rand() instead of
1451 - * /dev/urandom
 1450+ * Generate $wgSecretKey. Will warn if we had to use an insecure random source.
14521451 *
14531452 * @return Status
14541453 */
@@ -1460,8 +1459,8 @@
14611460 }
14621461
14631462 /**
1464 - * Generate a secret value for variables using either
1465 - * /dev/urandom or mt_rand(). Produce a warning in the later case.
 1463+ * Generate a secret value for variables using our CryptRand generator.
 1464+ * Produce a warning if the random source was insecure.
14661465 *
14671466 * @param $keys Array
14681467 * @return Status
@@ -1469,28 +1468,18 @@
14701469 protected function doGenerateKeys( $keys ) {
14711470 $status = Status::newGood();
14721471
1473 - wfSuppressWarnings();
1474 - $file = fopen( "/dev/urandom", "r" );
1475 - wfRestoreWarnings();
1476 -
 1472+ $strong = true;
14771473 foreach ( $keys as $name => $length ) {
1478 - if ( $file ) {
1479 - $secretKey = bin2hex( fread( $file, $length / 2 ) );
1480 - } else {
1481 - $secretKey = '';
1482 -
1483 - for ( $i = 0; $i < $length / 8; $i++ ) {
1484 - $secretKey .= dechex( mt_rand( 0, 0x7fffffff ) );
1485 - }
 1474+ $secretKey = MWCryptRand::generateHex( $length, true );
 1475+ if ( !MWCryptRand::wasStrong() ) {
 1476+ $strong = false;
14861477 }
14871478
14881479 $this->setVar( $name, $secretKey );
14891480 }
14901481
1491 - if ( $file ) {
1492 - fclose( $file );
1493 - } else {
1494 - $names = array_keys ( $keys );
 1482+ if ( !$strong ) {
 1483+ $names = array_keys( $keys );
14951484 $names = preg_replace( '/^(.*)$/', '\$$1', $names );
14961485 global $wgLang;
14971486 $status->warning( 'config-insecure-keys', $wgLang->listToText( $names ), count( $names ) );
Index: trunk/phase3/includes/AutoLoader.php
@@ -50,6 +50,7 @@
5151 'ConfEditorToken' => 'includes/ConfEditor.php',
5252 'Cookie' => 'includes/Cookie.php',
5353 'CookieJar' => 'includes/Cookie.php',
 54+ 'MWCryptRand' => 'includes/CryptRand.php',
5455 'CurlHttpRequest' => 'includes/HttpFunctions.php',
5556 // 'DBDataObject' => 'includes/DBDataObject.php',
5657 // 'DBTable' => 'includes/DBTable.php',
Index: trunk/phase3/includes/specials/SpecialUserlogin.php
@@ -1150,9 +1150,9 @@
11511151 */
11521152 public static function setLoginToken() {
11531153 global $wgRequest;
1154 - // Use User::generateToken() instead of $user->editToken()
 1154+ // Generate a token directly instead of using $user->editToken()
11551155 // because the latter reuses $_SESSION['wsEditToken']
1156 - $wgRequest->setSessionData( 'wsLoginToken', User::generateToken() );
 1156+ $wgRequest->setSessionData( 'wsLoginToken', MWCryptRand::generateHex( 32, __METHOD__ ) );
11571157 }
11581158
11591159 /**
@@ -1177,7 +1177,7 @@
11781178 */
11791179 public static function setCreateaccountToken() {
11801180 global $wgRequest;
1181 - $wgRequest->setSessionData( 'wsCreateaccountToken', User::generateToken() );
 1181+ $wgRequest->setSessionData( 'wsCreateaccountToken', MWCryptRand::generateHex( 32, __METHOD__ ) );
11821182 }
11831183
11841184 /**
Index: trunk/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: trunk/phase3/includes/Preferences.php
@@ -916,6 +916,7 @@
917917
918918 if ( $wgEnableAPI ) {
919919 # Some random gibberish as a proposed default
 920+ // @fixme This should use CryptRand but we may not want to read urandom on every view
920921 $hash = sha1( mt_rand() . microtime( true ) );
921922
922923 $defaultPreferences['watchlisttoken'] = array(
Index: trunk/phase3/includes/Import.php
@@ -829,7 +829,7 @@
830830 * @return string
831831 */
832832 static function registerSource( $source ) {
833 - $id = wfGenerateToken();
 833+ $id = wfRandomString();
834834
835835 self::$sourceRegistrations[$id] = $source;
836836
Index: trunk/phase3/RELEASE-NOTES-1.20
@@ -22,6 +22,8 @@
2323 * (bug 34302) Add CSS classes to email fields in user preferences
2424 * Introduced $wgDebugDBTransactions to trace transaction status (currently PostgreSQL only)
2525 * (bug 23795) Add parser itself to ParserMakeImageParams hook.
 26+* Introduce a cryptographic random number generator source api for use when
 27+ generating various tokens.
2628
2729 === Bug fixes in 1.20 ===
2830 * (bug 30245) Use the correct way to construct a log page title.
@@ -61,6 +63,9 @@
6264 * Mizo (lus) added
6365
6466 === Other changes in 1.20 ===
 67+* The user_token field is now left empty until a user attempts to login and
 68+ cookies need to be set. It is also now possible to reset every user's
 69+ user_token simply by clearing the values in the user_token column.
6570
6671 == Compatibility ==
6772

Follow-up revisions

RevisionCommit summaryAuthorDate
r114241Backport CryptRand from r110825 and r114233 to REL1_17, REL1_18, and REL1_19 ...dantman09:39, 20 March 2012
r114270Followup r114233, define the method static variables to be usedreedy14:16, 20 March 2012
r114272Followup r114270 (essentially reverts it), and r114233, use class member vari...reedy14:21, 20 March 2012
r114283Backport r114272, Reedy's fix for r114233 to REL1_17, REL1_18, and REL1_19.dantman17:22, 20 March 2012

Comments

#Comment by Reedy (talk | contribs)   14:03, 20 March 2012

seeing on trunk:



Notice: A non well formed numeric value encountered in /home/reedy/mediawiki/trunk/phase3/includes/CryptRand.php on line 390

Call Stack:
    0.0003     651296   1. {main}() /home/reedy/mediawiki/trunk/phase3/index.php:0
    0.2429   15999288   2. MediaWiki->run() /home/reedy/mediawiki/trunk/phase3/index.php:58
    0.2430   15999288   3. MediaWiki->main() /home/reedy/mediawiki/trunk/phase3/includes/Wiki.php:502
    1.2842   39032608   4. MediaWiki->finalCleanup() /home/reedy/mediawiki/trunk/phase3/includes/Wiki.php:593
    1.2843   39032688   5. OutputPage->output() /home/reedy/mediawiki/trunk/phase3/includes/Wiki.php:405
    1.2938   39721992   6. SkinTemplate->outputPage() /home/reedy/mediawiki/trunk/phase3/includes/OutputPage.php:1982
    1.4869   41792816   7. SkinTemplate->buildContentNavigationUrls() /home/reedy/mediawiki/trunk/phase3/includes/SkinTemplate.php:451
    1.5072   41931248   8. WatchAction::getWatchToken() /home/reedy/mediawiki/trunk/phase3/includes/SkinTemplate.php:969
    1.5072   41931936   9. User->getEditToken() /home/reedy/mediawiki/trunk/phase3/includes/actions/WatchAction.php:122
    1.5090   42101528  10. MWCryptRand::generateHex() /home/reedy/mediawiki/trunk/phase3/includes/User.php:3182
    1.5090   42102312  11. MWCryptRand->realGenerateHex() /home/reedy/mediawiki/trunk/phase3/includes/CryptRand.php:473
    1.5090   42102392  12. MWCryptRand::generate() /home/reedy/mediawiki/trunk/phase3/includes/CryptRand.php:408
    1.5091   42102392  13. MWCryptRand->realGenerate() /home/reedy/mediawiki/trunk/phase3/includes/CryptRand.php:456
    1.5104   42103960  14. substr() /home/reedy/mediawiki/trunk/phase3/includes/CryptRand.php:390


Notice: A non well formed numeric value encountered in /home/reedy/mediawiki/trunk/phase3/includes/CryptRand.php on line 391

Call Stack:
    0.0003     651296   1. {main}() /home/reedy/mediawiki/trunk/phase3/index.php:0
    0.2429   15999288   2. MediaWiki->run() /home/reedy/mediawiki/trunk/phase3/index.php:58
    0.2430   15999288   3. MediaWiki->main() /home/reedy/mediawiki/trunk/phase3/includes/Wiki.php:502
    1.2842   39032608   4. MediaWiki->finalCleanup() /home/reedy/mediawiki/trunk/phase3/includes/Wiki.php:593
    1.2843   39032688   5. OutputPage->output() /home/reedy/mediawiki/trunk/phase3/includes/Wiki.php:405
    1.2938   39721992   6. SkinTemplate->outputPage() /home/reedy/mediawiki/trunk/phase3/includes/OutputPage.php:1982
    1.4869   41792816   7. SkinTemplate->buildContentNavigationUrls() /home/reedy/mediawiki/trunk/phase3/includes/SkinTemplate.php:451
    1.5072   41931248   8. WatchAction::getWatchToken() /home/reedy/mediawiki/trunk/phase3/includes/SkinTemplate.php:969
    1.5072   41931936   9. User->getEditToken() /home/reedy/mediawiki/trunk/phase3/includes/actions/WatchAction.php:122
    1.5090   42101528  10. MWCryptRand::generateHex() /home/reedy/mediawiki/trunk/phase3/includes/User.php:3182
    1.5090   42102312  11. MWCryptRand->realGenerateHex() /home/reedy/mediawiki/trunk/phase3/includes/CryptRand.php:473
    1.5090   42102392  12. MWCryptRand::generate() /home/reedy/mediawiki/trunk/phase3/includes/CryptRand.php:408
    1.5091   42102392  13. MWCryptRand->realGenerate() /home/reedy/mediawiki/trunk/phase3/includes/CryptRand.php:456
    1.5108   42104032  14. substr() /home/reedy/mediawiki/trunk/phase3/includes/CryptRand.php:391
#Comment by Dantman (talk | contribs)   15:52, 20 March 2012

WTF. I test, and test, and test these changes... and still I miss stuff when I'm asked to refactor code.

Status & tagging log