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 @@ |
787 | 787 | } |
788 | 788 | |
789 | 789 | /** |
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. |
792 | 791 | * |
793 | 792 | * @return \string New random password |
794 | 793 | */ |
795 | 794 | 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 ); |
807 | 804 | } |
808 | 805 | |
809 | 806 | /** |
— | — | @@ -832,7 +829,7 @@ |
833 | 830 | $this->mTouched = '0'; # Allow any pages to be cached |
834 | 831 | } |
835 | 832 | |
836 | | - $this->setToken(); # Random |
| 833 | + $this->mToken = null; // Don't run cryptographic functions till we need a token |
837 | 834 | $this->mEmailAuthenticated = null; |
838 | 835 | $this->mEmailToken = ''; |
839 | 836 | $this->mEmailTokenExpires = null; |
— | — | @@ -919,11 +916,11 @@ |
920 | 917 | return false; |
921 | 918 | } |
922 | 919 | |
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']; |
925 | 922 | $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' ); |
928 | 925 | $from = 'cookie'; |
929 | 926 | } else { |
930 | 927 | # No session or persistent login cookie |
— | — | @@ -1012,6 +1009,9 @@ |
1013 | 1010 | $this->decodeOptions( $row->user_options ); |
1014 | 1011 | $this->mTouched = wfTimestamp(TS_MW,$row->user_touched); |
1015 | 1012 | $this->mToken = $row->user_token; |
| 1013 | + if ( $this->mToken == '' ) { |
| 1014 | + $this->mToken = null; |
| 1015 | + } |
1016 | 1016 | $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated ); |
1017 | 1017 | $this->mEmailToken = $row->user_email_token; |
1018 | 1018 | $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires ); |
— | — | @@ -1841,10 +1841,14 @@ |
1842 | 1842 | |
1843 | 1843 | /** |
1844 | 1844 | * 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) |
1845 | 1846 | * @return \string Token |
1846 | 1847 | */ |
1847 | | - function getToken() { |
| 1848 | + function getToken( $forceCreation = true ) { |
1848 | 1849 | $this->load(); |
| 1850 | + if ( !$this->mToken && $forceCreation ) { |
| 1851 | + $this->setToken(); |
| 1852 | + } |
1849 | 1853 | return $this->mToken; |
1850 | 1854 | } |
1851 | 1855 | |
— | — | @@ -1859,14 +1863,7 @@ |
1860 | 1864 | global $wgSecretKey, $wgProxyKey; |
1861 | 1865 | $this->load(); |
1862 | 1866 | 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__ ); |
1871 | 1868 | } else { |
1872 | 1869 | $this->mToken = $token; |
1873 | 1870 | } |
— | — | @@ -2477,6 +2474,14 @@ |
2478 | 2475 | function setCookies() { |
2479 | 2476 | $this->load(); |
2480 | 2477 | 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 | + } |
2481 | 2486 | $session = array( |
2482 | 2487 | 'wsUserID' => $this->mId, |
2483 | 2488 | 'wsToken' => $this->mToken, |
— | — | @@ -2555,7 +2560,7 @@ |
2556 | 2561 | 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), |
2557 | 2562 | 'user_options' => '', |
2558 | 2563 | 'user_touched' => $dbw->timestamp( $this->mTouched ), |
2559 | | - 'user_token' => $this->mToken, |
| 2564 | + 'user_token' => strval( $this->mToken ), |
2560 | 2565 | 'user_email_token' => $this->mEmailToken, |
2561 | 2566 | 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ), |
2562 | 2567 | ), array( /* WHERE */ |
— | — | @@ -2621,7 +2626,7 @@ |
2622 | 2627 | 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ), |
2623 | 2628 | 'user_real_name' => $user->mRealName, |
2624 | 2629 | 'user_options' => '', |
2625 | | - 'user_token' => $user->mToken, |
| 2630 | + 'user_token' => strval( $user->mToken ), |
2626 | 2631 | 'user_registration' => $dbw->timestamp( $user->mRegistration ), |
2627 | 2632 | 'user_editcount' => 0, |
2628 | 2633 | ); |
— | — | @@ -2655,7 +2660,7 @@ |
2656 | 2661 | 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), |
2657 | 2662 | 'user_real_name' => $this->mRealName, |
2658 | 2663 | 'user_options' => '', |
2659 | | - 'user_token' => $this->mToken, |
| 2664 | + 'user_token' => strval( $this->mToken ), |
2660 | 2665 | 'user_registration' => $dbw->timestamp( $this->mRegistration ), |
2661 | 2666 | 'user_editcount' => 0, |
2662 | 2667 | ), __METHOD__ |
— | — | @@ -2881,7 +2886,7 @@ |
2882 | 2887 | return EDIT_TOKEN_SUFFIX; |
2883 | 2888 | } else { |
2884 | 2889 | if( !isset( $_SESSION['wsEditToken'] ) ) { |
2885 | | - $token = self::generateToken(); |
| 2890 | + $token = MWCryptRand::generateHex( 32, __METHOD__ ); |
2886 | 2891 | $_SESSION['wsEditToken'] = $token; |
2887 | 2892 | } else { |
2888 | 2893 | $token = $_SESSION['wsEditToken']; |
— | — | @@ -2900,8 +2905,7 @@ |
2901 | 2906 | * @return \string The new random token |
2902 | 2907 | */ |
2903 | 2908 | public static function generateToken( $salt = '' ) { |
2904 | | - $token = dechex( mt_rand() ) . dechex( mt_rand() ); |
2905 | | - return md5( $token . $salt ); |
| 2909 | + return MWCryptRand::generateHex( 32, __METHOD__ ); |
2906 | 2910 | } |
2907 | 2911 | |
2908 | 2912 | /** |
— | — | @@ -3005,12 +3009,12 @@ |
3006 | 3010 | function confirmationToken( &$expiration ) { |
3007 | 3011 | $now = time(); |
3008 | 3012 | $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__ ); |
3011 | 3015 | $hash = md5( $token ); |
3012 | 3016 | $this->load(); |
3013 | 3017 | $this->mEmailToken = $hash; |
3014 | | - $this->mEmailTokenExpires = $expiration; |
| 3018 | + $this->mEmailTokenExpires = wfTimestamp( TS_MW, $expires ); |
3015 | 3019 | return $token; |
3016 | 3020 | } |
3017 | 3021 | |
— | — | @@ -3560,7 +3564,7 @@ |
3561 | 3565 | |
3562 | 3566 | if( $wgPasswordSalt ) { |
3563 | 3567 | if ( $salt === false ) { |
3564 | | - $salt = substr( wfGenerateToken(), 0, 8 ); |
| 3568 | + $salt = MWCryptRand::generateHex( 8, __METHOD__ ); |
3565 | 3569 | } |
3566 | 3570 | return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) ); |
3567 | 3571 | } else { |
Index: branches/REL1_17/phase3/includes/GlobalFunctions.php |
— | — | @@ -3038,6 +3038,33 @@ |
3039 | 3039 | } |
3040 | 3040 | |
3041 | 3041 | /** |
| 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 | +/** |
3042 | 3069 | * Initialise php session |
3043 | 3070 | */ |
3044 | 3071 | function wfSetupSession( $sessionId = false ) { |
— | — | @@ -3068,6 +3095,8 @@ |
3069 | 3096 | session_cache_limiter( 'private, must-revalidate' ); |
3070 | 3097 | if ( $sessionId ) { |
3071 | 3098 | session_id( $sessionId ); |
| 3099 | + } else { |
| 3100 | + wfFixSessionID(); |
3072 | 3101 | } |
3073 | 3102 | wfSuppressWarnings(); |
3074 | 3103 | session_start(); |
Index: branches/REL1_17/phase3/includes/installer/Installer.php |
— | — | @@ -1333,8 +1333,7 @@ |
1334 | 1334 | } |
1335 | 1335 | |
1336 | 1336 | /** |
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. |
1339 | 1338 | * |
1340 | 1339 | * @return Status |
1341 | 1340 | */ |
— | — | @@ -1347,8 +1346,8 @@ |
1348 | 1347 | } |
1349 | 1348 | |
1350 | 1349 | /** |
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. |
1353 | 1352 | * |
1354 | 1353 | * @param $keys Array |
1355 | 1354 | * @return Status |
— | — | @@ -1356,28 +1355,18 @@ |
1357 | 1356 | protected function doGenerateKeys( $keys ) { |
1358 | 1357 | $status = Status::newGood(); |
1359 | 1358 | |
1360 | | - wfSuppressWarnings(); |
1361 | | - $file = fopen( "/dev/urandom", "r" ); |
1362 | | - wfRestoreWarnings(); |
1363 | | - |
| 1359 | + $strong = true; |
1364 | 1360 | 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; |
1373 | 1364 | } |
1374 | 1365 | |
1375 | 1366 | $this->setVar( $name, $secretKey ); |
1376 | 1367 | } |
1377 | 1368 | |
1378 | | - if ( $file ) { |
1379 | | - fclose( $file ); |
1380 | | - } else { |
1381 | | - $names = array_keys ( $keys ); |
| 1369 | + if ( !$strong ) { |
| 1370 | + $names = array_keys( $keys ); |
1382 | 1371 | $names = preg_replace( '/^(.*)$/', '\$$1', $names ); |
1383 | 1372 | global $wgLang; |
1384 | 1373 | $status->warning( 'config-insecure-keys', $wgLang->listToText( $names ), count( $names ) ); |
Index: branches/REL1_17/phase3/includes/AutoLoader.php |
— | — | @@ -167,6 +167,7 @@ |
168 | 168 | 'MessageBlobStore' => 'includes/MessageBlobStore.php', |
169 | 169 | 'MessageCache' => 'includes/MessageCache.php', |
170 | 170 | 'MimeMagic' => 'includes/MimeMagic.php', |
| 171 | + 'MWCryptRand' => 'includes/CryptRand.php', |
171 | 172 | 'MWException' => 'includes/Exception.php', |
172 | 173 | 'MWHttpRequest' => 'includes/HttpFunctions.php', |
173 | 174 | 'MWMemcached' => 'includes/memcached-client.php', |
Index: branches/REL1_17/phase3/includes/specials/SpecialUserlogin.php |
— | — | @@ -1099,9 +1099,9 @@ |
1100 | 1100 | */ |
1101 | 1101 | public static function setLoginToken() { |
1102 | 1102 | global $wgRequest; |
1103 | | - // Use User::generateToken() instead of $user->editToken() |
| 1103 | + // Generate a token directly instead of using $user->editToken() |
1104 | 1104 | // because the latter reuses $_SESSION['wsEditToken'] |
1105 | | - $wgRequest->setSessionData( 'wsLoginToken', User::generateToken() ); |
| 1105 | + $wgRequest->setSessionData( 'wsLoginToken', MWCryptRand::generateHex( 32, __METHOD__ ) ); |
1106 | 1106 | } |
1107 | 1107 | |
1108 | 1108 | /** |
— | — | @@ -1125,7 +1125,7 @@ |
1126 | 1126 | */ |
1127 | 1127 | public static function setCreateaccountToken() { |
1128 | 1128 | global $wgRequest; |
1129 | | - $wgRequest->setSessionData( 'wsCreateaccountToken', User::generateToken() ); |
| 1129 | + $wgRequest->setSessionData( 'wsCreateaccountToken', MWCryptRand::generateHex( 32, __METHOD__ ) ); |
1130 | 1130 | } |
1131 | 1131 | |
1132 | 1132 | /** |
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 @@ |
832 | 832 | } |
833 | 833 | |
834 | 834 | /** |
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. |
837 | 836 | * |
838 | 837 | * @return String new random password |
839 | 838 | */ |
840 | 839 | public static function randomPassword() { |
841 | 840 | 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 ); |
852 | 849 | } |
853 | 850 | |
854 | 851 | /** |
— | — | @@ -877,7 +874,7 @@ |
878 | 875 | $this->mTouched = '0'; # Allow any pages to be cached |
879 | 876 | } |
880 | 877 | |
881 | | - $this->setToken(); # Random |
| 878 | + $this->mToken = null; // Don't run cryptographic functions till we need a token |
882 | 879 | $this->mEmailAuthenticated = null; |
883 | 880 | $this->mEmailToken = ''; |
884 | 881 | $this->mEmailTokenExpires = null; |
— | — | @@ -984,11 +981,11 @@ |
985 | 982 | return false; |
986 | 983 | } |
987 | 984 | |
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' ); |
990 | 987 | $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' ); |
993 | 990 | $from = 'cookie'; |
994 | 991 | } else { |
995 | 992 | # No session or persistent login cookie |
— | — | @@ -1083,6 +1080,9 @@ |
1084 | 1081 | $this->decodeOptions( $row->user_options ); |
1085 | 1082 | $this->mTouched = wfTimestamp(TS_MW,$row->user_touched); |
1086 | 1083 | $this->mToken = $row->user_token; |
| 1084 | + if ( $this->mToken == '' ) { |
| 1085 | + $this->mToken = null; |
| 1086 | + } |
1087 | 1087 | $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated ); |
1088 | 1088 | $this->mEmailToken = $row->user_email_token; |
1089 | 1089 | $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires ); |
— | — | @@ -1989,10 +1989,14 @@ |
1990 | 1990 | |
1991 | 1991 | /** |
1992 | 1992 | * 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) |
1993 | 1994 | * @return String Token |
1994 | 1995 | */ |
1995 | | - public function getToken() { |
| 1996 | + public function getToken( $forceCreation = true ) { |
1996 | 1997 | $this->load(); |
| 1998 | + if ( !$this->mToken && $forceCreation ) { |
| 1999 | + $this->setToken(); |
| 2000 | + } |
1997 | 2001 | return $this->mToken; |
1998 | 2002 | } |
1999 | 2003 | |
— | — | @@ -2006,14 +2010,7 @@ |
2007 | 2011 | global $wgSecretKey, $wgProxyKey; |
2008 | 2012 | $this->load(); |
2009 | 2013 | 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__ ); |
2018 | 2015 | } else { |
2019 | 2016 | $this->mToken = $token; |
2020 | 2017 | } |
— | — | @@ -2718,6 +2715,14 @@ |
2719 | 2716 | |
2720 | 2717 | $this->load(); |
2721 | 2718 | 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 | + } |
2722 | 2727 | $session = array( |
2723 | 2728 | 'wsUserID' => $this->mId, |
2724 | 2729 | 'wsToken' => $this->mToken, |
— | — | @@ -2795,7 +2800,7 @@ |
2796 | 2801 | 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), |
2797 | 2802 | 'user_options' => '', |
2798 | 2803 | 'user_touched' => $dbw->timestamp( $this->mTouched ), |
2799 | | - 'user_token' => $this->mToken, |
| 2804 | + 'user_token' => strval( $this->mToken ), |
2800 | 2805 | 'user_email_token' => $this->mEmailToken, |
2801 | 2806 | 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ), |
2802 | 2807 | ), array( /* WHERE */ |
— | — | @@ -2862,7 +2867,7 @@ |
2863 | 2868 | 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ), |
2864 | 2869 | 'user_real_name' => $user->mRealName, |
2865 | 2870 | 'user_options' => '', |
2866 | | - 'user_token' => $user->mToken, |
| 2871 | + 'user_token' => strval( $user->mToken ), |
2867 | 2872 | 'user_registration' => $dbw->timestamp( $user->mRegistration ), |
2868 | 2873 | 'user_editcount' => 0, |
2869 | 2874 | ); |
— | — | @@ -2896,7 +2901,7 @@ |
2897 | 2902 | 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), |
2898 | 2903 | 'user_real_name' => $this->mRealName, |
2899 | 2904 | 'user_options' => '', |
2900 | | - 'user_token' => $this->mToken, |
| 2905 | + 'user_token' => strval( $this->mToken ), |
2901 | 2906 | 'user_registration' => $dbw->timestamp( $this->mRegistration ), |
2902 | 2907 | 'user_editcount' => 0, |
2903 | 2908 | ), __METHOD__ |
— | — | @@ -3144,7 +3149,7 @@ |
3145 | 3150 | } else { |
3146 | 3151 | $token = $request->getSessionData( 'wsEditToken' ); |
3147 | 3152 | if ( $token === null ) { |
3148 | | - $token = self::generateToken(); |
| 3153 | + $token = MWCryptRand::generateHex( 32, __METHOD__ ); |
3149 | 3154 | $request->setSessionData( 'wsEditToken', $token ); |
3150 | 3155 | } |
3151 | 3156 | if( is_array( $salt ) ) { |
— | — | @@ -3161,8 +3166,7 @@ |
3162 | 3167 | * @return String The new random token |
3163 | 3168 | */ |
3164 | 3169 | public static function generateToken( $salt = '' ) { |
3165 | | - $token = dechex( mt_rand() ) . dechex( mt_rand() ); |
3166 | | - return md5( $token . $salt ); |
| 3170 | + return MWCryptRand::generateHex( 32, __METHOD__ ); |
3167 | 3171 | } |
3168 | 3172 | |
3169 | 3173 | /** |
— | — | @@ -3268,12 +3272,11 @@ |
3269 | 3273 | global $wgUserEmailConfirmationTokenExpiry; |
3270 | 3274 | $now = time(); |
3271 | 3275 | $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__ ); |
3274 | 3278 | $hash = md5( $token ); |
3275 | | - $this->load(); |
3276 | 3279 | $this->mEmailToken = $hash; |
3277 | | - $this->mEmailTokenExpires = $expiration; |
| 3280 | + $this->mEmailTokenExpires = wfTimestamp( TS_MW, $expires ); |
3278 | 3281 | return $token; |
3279 | 3282 | } |
3280 | 3283 | |
— | — | @@ -3828,7 +3831,7 @@ |
3829 | 3832 | |
3830 | 3833 | if( $wgPasswordSalt ) { |
3831 | 3834 | if ( $salt === false ) { |
3832 | | - $salt = substr( wfGenerateToken(), 0, 8 ); |
| 3835 | + $salt = MWCryptRand::generateHex( 8, __METHOD__ ); |
3833 | 3836 | } |
3834 | 3837 | return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) ); |
3835 | 3838 | } else { |
Index: branches/REL1_18/phase3/includes/GlobalFunctions.php |
— | — | @@ -3065,6 +3065,33 @@ |
3066 | 3066 | } |
3067 | 3067 | |
3068 | 3068 | /** |
| 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 | +/** |
3069 | 3096 | * Initialise php session |
3070 | 3097 | * |
3071 | 3098 | * @param $sessionId Bool |
— | — | @@ -3103,6 +3130,8 @@ |
3104 | 3131 | session_cache_limiter( 'private, must-revalidate' ); |
3105 | 3132 | if ( $sessionId ) { |
3106 | 3133 | session_id( $sessionId ); |
| 3134 | + } else { |
| 3135 | + wfFixSessionID(); |
3107 | 3136 | } |
3108 | 3137 | wfSuppressWarnings(); |
3109 | 3138 | session_start(); |
Index: branches/REL1_18/phase3/includes/installer/Installer.php |
— | — | @@ -1347,8 +1347,7 @@ |
1348 | 1348 | } |
1349 | 1349 | |
1350 | 1350 | /** |
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. |
1353 | 1352 | * |
1354 | 1353 | * @return Status |
1355 | 1354 | */ |
— | — | @@ -1361,8 +1360,8 @@ |
1362 | 1361 | } |
1363 | 1362 | |
1364 | 1363 | /** |
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. |
1367 | 1366 | * |
1368 | 1367 | * @param $keys Array |
1369 | 1368 | * @return Status |
— | — | @@ -1370,28 +1369,18 @@ |
1371 | 1370 | protected function doGenerateKeys( $keys ) { |
1372 | 1371 | $status = Status::newGood(); |
1373 | 1372 | |
1374 | | - wfSuppressWarnings(); |
1375 | | - $file = fopen( "/dev/urandom", "r" ); |
1376 | | - wfRestoreWarnings(); |
1377 | | - |
| 1373 | + $strong = true; |
1378 | 1374 | 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; |
1387 | 1378 | } |
1388 | 1379 | |
1389 | 1380 | $this->setVar( $name, $secretKey ); |
1390 | 1381 | } |
1391 | 1382 | |
1392 | | - if ( $file ) { |
1393 | | - fclose( $file ); |
1394 | | - } else { |
1395 | | - $names = array_keys ( $keys ); |
| 1383 | + if ( !$strong ) { |
| 1384 | + $names = array_keys( $keys ); |
1396 | 1385 | $names = preg_replace( '/^(.*)$/', '\$$1', $names ); |
1397 | 1386 | global $wgLang; |
1398 | 1387 | $status->warning( 'config-insecure-keys', $wgLang->listToText( $names ), count( $names ) ); |
Index: branches/REL1_18/phase3/includes/AutoLoader.php |
— | — | @@ -150,6 +150,7 @@ |
151 | 151 | 'Message' => 'includes/Message.php', |
152 | 152 | 'MessageBlobStore' => 'includes/MessageBlobStore.php', |
153 | 153 | 'MimeMagic' => 'includes/MimeMagic.php', |
| 154 | + 'MWCryptRand' => 'includes/CryptRand.php', |
154 | 155 | 'MWException' => 'includes/Exception.php', |
155 | 156 | 'MWExceptionHandler' => 'includes/Exception.php', |
156 | 157 | 'MWFunction' => 'includes/MWFunction.php', |
Index: branches/REL1_18/phase3/includes/specials/SpecialUserlogin.php |
— | — | @@ -1114,9 +1114,9 @@ |
1115 | 1115 | */ |
1116 | 1116 | public static function setLoginToken() { |
1117 | 1117 | global $wgRequest; |
1118 | | - // Use User::generateToken() instead of $user->editToken() |
| 1118 | + // Generate a token directly instead of using $user->editToken() |
1119 | 1119 | // because the latter reuses $_SESSION['wsEditToken'] |
1120 | | - $wgRequest->setSessionData( 'wsLoginToken', User::generateToken() ); |
| 1120 | + $wgRequest->setSessionData( 'wsLoginToken', MWCryptRand::generateHex( 32, __METHOD__ ) ); |
1121 | 1121 | } |
1122 | 1122 | |
1123 | 1123 | /** |
— | — | @@ -1140,7 +1140,7 @@ |
1141 | 1141 | */ |
1142 | 1142 | public static function setCreateaccountToken() { |
1143 | 1143 | global $wgRequest; |
1144 | | - $wgRequest->setSessionData( 'wsCreateaccountToken', User::generateToken() ); |
| 1144 | + $wgRequest->setSessionData( 'wsCreateaccountToken', MWCryptRand::generateHex( 32, __METHOD__ ) ); |
1145 | 1145 | } |
1146 | 1146 | |
1147 | 1147 | /** |
Index: branches/REL1_18/phase3/includes/specials/SpecialWatchlist.php |
— | — | @@ -43,7 +43,7 @@ |
44 | 44 | // Add feed links |
45 | 45 | $wlToken = $user->getOption( 'watchlisttoken' ); |
46 | 46 | if ( !$wlToken ) { |
47 | | - $wlToken = sha1( mt_rand() . microtime( true ) ); |
| 47 | + $wlToken = MWCryptRand::generateHex( 40 ); |
48 | 48 | $user->setOption( 'watchlisttoken', $wlToken ); |
49 | 49 | $user->saveSettings(); |
50 | 50 | } |
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 @@ |
832 | 832 | } |
833 | 833 | |
834 | 834 | /** |
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. |
837 | 836 | * |
838 | 837 | * @return String new random password |
839 | 838 | */ |
840 | 839 | public static function randomPassword() { |
841 | 840 | 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 ); |
852 | 849 | } |
853 | 850 | |
854 | 851 | /** |
— | — | @@ -877,7 +874,7 @@ |
878 | 875 | $this->mTouched = '0'; # Allow any pages to be cached |
879 | 876 | } |
880 | 877 | |
881 | | - $this->setToken(); # Random |
| 878 | + $this->mToken = null; // Don't run cryptographic functions till we need a token |
882 | 879 | $this->mEmailAuthenticated = null; |
883 | 880 | $this->mEmailToken = ''; |
884 | 881 | $this->mEmailTokenExpires = null; |
— | — | @@ -984,11 +981,11 @@ |
985 | 982 | return false; |
986 | 983 | } |
987 | 984 | |
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' ); |
990 | 987 | $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' ); |
993 | 990 | $from = 'cookie'; |
994 | 991 | } else { |
995 | 992 | # No session or persistent login cookie |
— | — | @@ -1093,6 +1090,9 @@ |
1094 | 1091 | } |
1095 | 1092 | $this->mTouched = wfTimestamp( TS_MW, $row->user_touched ); |
1096 | 1093 | $this->mToken = $row->user_token; |
| 1094 | + if ( $this->mToken == '' ) { |
| 1095 | + $this->mToken = null; |
| 1096 | + } |
1097 | 1097 | $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated ); |
1098 | 1098 | $this->mEmailToken = $row->user_email_token; |
1099 | 1099 | $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires ); |
— | — | @@ -2018,10 +2018,14 @@ |
2019 | 2019 | |
2020 | 2020 | /** |
2021 | 2021 | * 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) |
2022 | 2023 | * @return String Token |
2023 | 2024 | */ |
2024 | | - public function getToken() { |
| 2025 | + public function getToken( $forceCreation = true ) { |
2025 | 2026 | $this->load(); |
| 2027 | + if ( !$this->mToken && $forceCreation ) { |
| 2028 | + $this->setToken(); |
| 2029 | + } |
2026 | 2030 | return $this->mToken; |
2027 | 2031 | } |
2028 | 2032 | |
— | — | @@ -2035,14 +2039,7 @@ |
2036 | 2040 | global $wgSecretKey, $wgProxyKey; |
2037 | 2041 | $this->load(); |
2038 | 2042 | 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__ ); |
2047 | 2044 | } else { |
2048 | 2045 | $this->mToken = $token; |
2049 | 2046 | } |
— | — | @@ -2748,6 +2745,14 @@ |
2749 | 2746 | |
2750 | 2747 | $this->load(); |
2751 | 2748 | 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 | + } |
2752 | 2757 | $session = array( |
2753 | 2758 | 'wsUserID' => $this->mId, |
2754 | 2759 | 'wsToken' => $this->mToken, |
— | — | @@ -2824,7 +2829,7 @@ |
2825 | 2830 | 'user_email' => $this->mEmail, |
2826 | 2831 | 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), |
2827 | 2832 | 'user_touched' => $dbw->timestamp( $this->mTouched ), |
2828 | | - 'user_token' => $this->mToken, |
| 2833 | + 'user_token' => strval( $this->mToken ), |
2829 | 2834 | 'user_email_token' => $this->mEmailToken, |
2830 | 2835 | 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ), |
2831 | 2836 | ), array( /* WHERE */ |
— | — | @@ -2890,7 +2895,7 @@ |
2891 | 2896 | 'user_email' => $user->mEmail, |
2892 | 2897 | 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ), |
2893 | 2898 | 'user_real_name' => $user->mRealName, |
2894 | | - 'user_token' => $user->mToken, |
| 2899 | + 'user_token' => strval( $user->mToken ), |
2895 | 2900 | 'user_registration' => $dbw->timestamp( $user->mRegistration ), |
2896 | 2901 | 'user_editcount' => 0, |
2897 | 2902 | ); |
— | — | @@ -2923,7 +2928,7 @@ |
2924 | 2929 | 'user_email' => $this->mEmail, |
2925 | 2930 | 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), |
2926 | 2931 | 'user_real_name' => $this->mRealName, |
2927 | | - 'user_token' => $this->mToken, |
| 2932 | + 'user_token' => strval( $this->mToken ), |
2928 | 2933 | 'user_registration' => $dbw->timestamp( $this->mRegistration ), |
2929 | 2934 | 'user_editcount' => 0, |
2930 | 2935 | ), __METHOD__ |
— | — | @@ -3187,7 +3192,7 @@ |
3188 | 3193 | } else { |
3189 | 3194 | $token = $request->getSessionData( 'wsEditToken' ); |
3190 | 3195 | if ( $token === null ) { |
3191 | | - $token = self::generateToken(); |
| 3196 | + $token = MWCryptRand::generateHex( 32, __METHOD__ ); |
3192 | 3197 | $request->setSessionData( 'wsEditToken', $token ); |
3193 | 3198 | } |
3194 | 3199 | if( is_array( $salt ) ) { |
— | — | @@ -3204,8 +3209,7 @@ |
3205 | 3210 | * @return String The new random token |
3206 | 3211 | */ |
3207 | 3212 | public static function generateToken( $salt = '' ) { |
3208 | | - $token = dechex( mt_rand() ) . dechex( mt_rand() ); |
3209 | | - return md5( $token . $salt ); |
| 3213 | + return MWCryptRand::generateHex( 32, __METHOD__ ); |
3210 | 3214 | } |
3211 | 3215 | |
3212 | 3216 | /** |
— | — | @@ -3311,12 +3315,11 @@ |
3312 | 3316 | global $wgUserEmailConfirmationTokenExpiry; |
3313 | 3317 | $now = time(); |
3314 | 3318 | $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__ ); |
3317 | 3321 | $hash = md5( $token ); |
3318 | | - $this->load(); |
3319 | 3322 | $this->mEmailToken = $hash; |
3320 | | - $this->mEmailTokenExpires = $expiration; |
| 3323 | + $this->mEmailTokenExpires = wfTimestamp( TS_MW, $expires ); |
3321 | 3324 | return $token; |
3322 | 3325 | } |
3323 | 3326 | |
— | — | @@ -3865,7 +3868,7 @@ |
3866 | 3869 | |
3867 | 3870 | if( $wgPasswordSalt ) { |
3868 | 3871 | if ( $salt === false ) { |
3869 | | - $salt = substr( wfGenerateToken(), 0, 8 ); |
| 3872 | + $salt = MWCryptRand::generateHex( 8, __METHOD__ ); |
3870 | 3873 | } |
3871 | 3874 | return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) ); |
3872 | 3875 | } else { |
Index: branches/REL1_19/phase3/includes/GlobalFunctions.php |
— | — | @@ -3292,6 +3292,33 @@ |
3293 | 3293 | } |
3294 | 3294 | |
3295 | 3295 | /** |
| 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 | +/** |
3296 | 3323 | * Initialise php session |
3297 | 3324 | * |
3298 | 3325 | * @param $sessionId Bool |
— | — | @@ -3330,6 +3357,8 @@ |
3331 | 3358 | session_cache_limiter( 'private, must-revalidate' ); |
3332 | 3359 | if ( $sessionId ) { |
3333 | 3360 | session_id( $sessionId ); |
| 3361 | + } else { |
| 3362 | + wfFixSessionID(); |
3334 | 3363 | } |
3335 | 3364 | wfSuppressWarnings(); |
3336 | 3365 | session_start(); |
Index: branches/REL1_19/phase3/includes/installer/Installer.php |
— | — | @@ -1405,8 +1405,7 @@ |
1406 | 1406 | } |
1407 | 1407 | |
1408 | 1408 | /** |
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. |
1411 | 1410 | * |
1412 | 1411 | * @return Status |
1413 | 1412 | */ |
— | — | @@ -1419,8 +1418,8 @@ |
1420 | 1419 | } |
1421 | 1420 | |
1422 | 1421 | /** |
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. |
1425 | 1424 | * |
1426 | 1425 | * @param $keys Array |
1427 | 1426 | * @return Status |
— | — | @@ -1428,28 +1427,18 @@ |
1429 | 1428 | protected function doGenerateKeys( $keys ) { |
1430 | 1429 | $status = Status::newGood(); |
1431 | 1430 | |
1432 | | - wfSuppressWarnings(); |
1433 | | - $file = fopen( "/dev/urandom", "r" ); |
1434 | | - wfRestoreWarnings(); |
1435 | | - |
| 1431 | + $strong = true; |
1436 | 1432 | 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; |
1445 | 1436 | } |
1446 | 1437 | |
1447 | 1438 | $this->setVar( $name, $secretKey ); |
1448 | 1439 | } |
1449 | 1440 | |
1450 | | - if ( $file ) { |
1451 | | - fclose( $file ); |
1452 | | - } else { |
1453 | | - $names = array_keys ( $keys ); |
| 1441 | + if ( !$strong ) { |
| 1442 | + $names = array_keys( $keys ); |
1454 | 1443 | $names = preg_replace( '/^(.*)$/', '\$$1', $names ); |
1455 | 1444 | global $wgLang; |
1456 | 1445 | $status->warning( 'config-insecure-keys', $wgLang->listToText( $names ), count( $names ) ); |
Index: branches/REL1_19/phase3/includes/AutoLoader.php |
— | — | @@ -49,6 +49,7 @@ |
50 | 50 | 'ConfEditorToken' => 'includes/ConfEditor.php', |
51 | 51 | 'Cookie' => 'includes/Cookie.php', |
52 | 52 | 'CookieJar' => 'includes/Cookie.php', |
| 53 | + 'MWCryptRand' => 'includes/CryptRand.php', |
53 | 54 | 'CurlHttpRequest' => 'includes/HttpFunctions.php', |
54 | 55 | 'DeferrableUpdate' => 'includes/DeferredUpdates.php', |
55 | 56 | 'DeferredUpdates' => 'includes/DeferredUpdates.php', |
Index: branches/REL1_19/phase3/includes/specials/SpecialUserlogin.php |
— | — | @@ -1136,9 +1136,9 @@ |
1137 | 1137 | */ |
1138 | 1138 | public static function setLoginToken() { |
1139 | 1139 | global $wgRequest; |
1140 | | - // Use User::generateToken() instead of $user->editToken() |
| 1140 | + // Generate a token directly instead of using $user->editToken() |
1141 | 1141 | // because the latter reuses $_SESSION['wsEditToken'] |
1142 | | - $wgRequest->setSessionData( 'wsLoginToken', User::generateToken() ); |
| 1142 | + $wgRequest->setSessionData( 'wsLoginToken', MWCryptRand::generateHex( 32, __METHOD__ ) ); |
1143 | 1143 | } |
1144 | 1144 | |
1145 | 1145 | /** |
— | — | @@ -1162,7 +1162,7 @@ |
1163 | 1163 | */ |
1164 | 1164 | public static function setCreateaccountToken() { |
1165 | 1165 | global $wgRequest; |
1166 | | - $wgRequest->setSessionData( 'wsCreateaccountToken', User::generateToken() ); |
| 1166 | + $wgRequest->setSessionData( 'wsCreateaccountToken', MWCryptRand::generateHex( 32, __METHOD__ ) ); |
1167 | 1167 | } |
1168 | 1168 | |
1169 | 1169 | /** |
Index: branches/REL1_19/phase3/includes/specials/SpecialWatchlist.php |
— | — | @@ -43,7 +43,7 @@ |
44 | 44 | // Add feed links |
45 | 45 | $wlToken = $user->getOption( 'watchlisttoken' ); |
46 | 46 | if ( !$wlToken ) { |
47 | | - $wlToken = sha1( mt_rand() . microtime( true ) ); |
| 47 | + $wlToken = MWCryptRand::generateHex( 40 ); |
48 | 48 | $user->setOption( 'watchlisttoken', $wlToken ); |
49 | 49 | $user->saveSettings(); |
50 | 50 | } |