Index: trunk/phase3/includes/CryptRand.php |
— | — | @@ -0,0 +1,476 @@ |
| 2 | +<?php |
| 3 | +/** |
| 4 | + * A cryptographic random generator class used for generating secret keys |
| 5 | + * |
| 6 | + * This is based in part on Drupal code as well as what we used in our own code |
| 7 | + * prior to introduction of this class. |
| 8 | + * |
| 9 | + * @author Daniel Friesen |
| 10 | + * @file |
| 11 | + */ |
| 12 | + |
| 13 | +class MWCryptRand { |
| 14 | + |
| 15 | + /** |
| 16 | + * Minimum number of iterations we want to make in our drift calculations. |
| 17 | + */ |
| 18 | + const MIN_ITERATIONS = 1000; |
| 19 | + |
| 20 | + /** |
| 21 | + * Number of milliseconds we want to spend generating each separate byte |
| 22 | + * of the final generated bytes. |
| 23 | + * This is used in combination with the hash length to determine the duration |
| 24 | + * we should spend doing drift calculations. |
| 25 | + */ |
| 26 | + const MSEC_PER_BYTE = 0.5; |
| 27 | + |
| 28 | + /** |
| 29 | + * Singleton instance for public use |
| 30 | + */ |
| 31 | + protected static $singleton = null; |
| 32 | + |
| 33 | + /** |
| 34 | + * The hash algorithm being used |
| 35 | + */ |
| 36 | + protected $algo = null; |
| 37 | + |
| 38 | + /** |
| 39 | + * The number of bytes outputted by the hash algorithm |
| 40 | + */ |
| 41 | + protected $hashLength = null; |
| 42 | + |
| 43 | + /** |
| 44 | + * A boolean indicating whether the previous random generation was done using |
| 45 | + * cryptographically strong random number generator or not. |
| 46 | + */ |
| 47 | + protected $strong = null; |
| 48 | + |
| 49 | + /** |
| 50 | + * Initialize an initial random state based off of whatever we can find |
| 51 | + */ |
| 52 | + protected function initialRandomState() { |
| 53 | + // $_SERVER contains a variety of unstable user and system specific information |
| 54 | + // It'll vary a little with each page, and vary even more with separate users |
| 55 | + // It'll also vary slightly across different machines |
| 56 | + $state = serialize( $_SERVER ); |
| 57 | + |
| 58 | + // To try and vary the system information of the state a bit more |
| 59 | + // by including the system's hostname into the state |
| 60 | + $state .= wfHostname(); |
| 61 | + |
| 62 | + // Try to gather a little entropy from the different php rand sources |
| 63 | + $state .= rand() . uniqid( mt_rand(), true ); |
| 64 | + |
| 65 | + // Include some information about the filesystem's current state in the random state |
| 66 | + $files = array(); |
| 67 | + // We know this file is here so grab some info about ourself |
| 68 | + $files[] = __FILE__; |
| 69 | + // The config file is likely the most often edited file we know should be around |
| 70 | + // so if the constant with it's location is defined include it's stat info into the state |
| 71 | + if ( defined( 'MW_CONFIG_FILE' ) ) { |
| 72 | + $files[] = MW_CONFIG_FILE; |
| 73 | + } |
| 74 | + foreach ( $files as $file ) { |
| 75 | + wfSuppressWarnings(); |
| 76 | + $stat = stat( $file ); |
| 77 | + wfRestoreWarnings(); |
| 78 | + if ( $stat ) { |
| 79 | + // stat() duplicates data into numeric and string keys so kill off all the numeric ones |
| 80 | + foreach ( $stat as $k => $v ) { |
| 81 | + if ( is_numeric( $k ) ) { |
| 82 | + unset( $k ); |
| 83 | + } |
| 84 | + } |
| 85 | + // The absolute filename itself will differ from install to install so don't leave it out |
| 86 | + $state .= realpath( $file ); |
| 87 | + $state .= implode( '', $stat ); |
| 88 | + } else { |
| 89 | + // The fact that the file isn't there is worth at least a |
| 90 | + // minuscule amount of entropy. |
| 91 | + $state .= '0'; |
| 92 | + } |
| 93 | + } |
| 94 | + |
| 95 | + // Try and make this a little more unstable by including the varying process |
| 96 | + // id of the php process we are running inside of if we are able to access it |
| 97 | + if ( function_exists( 'getmypid' ) ) { |
| 98 | + $state .= getmypid(); |
| 99 | + } |
| 100 | + |
| 101 | + // If available try to increase the instability of the data by throwing in |
| 102 | + // the precise amount of memory that we happen to be using at the moment. |
| 103 | + if ( function_exists( 'memory_get_usage' ) ) { |
| 104 | + $state .= memory_get_usage( true ); |
| 105 | + } |
| 106 | + |
| 107 | + // It's mostly worthless but throw the wiki's id into the data for a little more variance |
| 108 | + $state .= wfWikiID(); |
| 109 | + |
| 110 | + // If we have a secret key or proxy key set then throw it into the state as well |
| 111 | + global $wgSecretKey, $wgProxyKey; |
| 112 | + if ( $wgSecretKey ) { |
| 113 | + $state .= $wgSecretKey; |
| 114 | + } elseif ( $wgProxyKey ) { |
| 115 | + $state .= $wgProxyKey; |
| 116 | + } |
| 117 | + |
| 118 | + return $state; |
| 119 | + } |
| 120 | + |
| 121 | + /** |
| 122 | + * Randomly hash data while mixing in clock drift data for randomness |
| 123 | + * |
| 124 | + * @param $data The data to randomly hash. |
| 125 | + * @return String The hashed bytes |
| 126 | + * @author Tim Starling |
| 127 | + */ |
| 128 | + protected function driftHash( $data ) { |
| 129 | + // Minimum number of iterations (to avoid slow operations causing the loop to gather little entropy) |
| 130 | + $minIterations = self::MIN_ITERATIONS; |
| 131 | + // Duration of time to spend doing calculations (in seconds) |
| 132 | + $duration = ( self::MSEC_PER_BYTE / 1000 ) * $this->hashLength(); |
| 133 | + // Create a buffer to use to trigger memory operations |
| 134 | + $bufLength = 10000000; |
| 135 | + $buffer = str_repeat( ' ', $bufLength ); |
| 136 | + $bufPos = 0; |
| 137 | + |
| 138 | + // Iterate for $duration seconds or at least $minIerations number of iterations |
| 139 | + $iterations = 0; |
| 140 | + $startTime = microtime( true ); |
| 141 | + $currentTime = $startTime; |
| 142 | + while ( $iterations < $minIterations || $currentTime - $startTime < $duration ) { |
| 143 | + // Trigger some memory writing to trigger some bus activity |
| 144 | + // This may create variance in the time between iterations |
| 145 | + $bufPos = ( $bufPos + 13 ) % $bufLength; |
| 146 | + $buffer[$bufPos] = ' '; |
| 147 | + // Add the drift between this iteration and the last in as entropy |
| 148 | + $nextTime = microtime( true ); |
| 149 | + $delta = (int)( ( $nextTime - $currentTime ) * 1000000 ); |
| 150 | + $data .= $delta; |
| 151 | + // Every 100 iterations hash the data and entropy |
| 152 | + if ( $iterations % 100 === 0 ) { |
| 153 | + $data = sha1( $data ); |
| 154 | + } |
| 155 | + $currentTime = $nextTime; |
| 156 | + $iterations++; |
| 157 | + } |
| 158 | + $timeTaken = $currentTime - $startTime; |
| 159 | + $data = $this->hash( $data ); |
| 160 | + |
| 161 | + wfDebug( __METHOD__ . ": Clock drift calculation " . |
| 162 | + "(time-taken=" . ( $timeTaken * 1000 ) . "ms, " . |
| 163 | + "iterations=$iterations, " . |
| 164 | + "time-per-iteration=" . ( $timeTaken / $iterations * 1e6 ) . "us)\n" ); |
| 165 | + return $data; |
| 166 | + } |
| 167 | + |
| 168 | + /** |
| 169 | + * Return a rolling random state initially build using data from unstable sources |
| 170 | + * @return A new weak random state |
| 171 | + */ |
| 172 | + protected function randomState() { |
| 173 | + static $state = null; |
| 174 | + if ( is_null( $state ) ) { |
| 175 | + // Initialize the state with whatever unstable data we can find |
| 176 | + // It's important that this data is hashed right afterwards to prevent |
| 177 | + // it from being leaked into the output stream |
| 178 | + $state = $this->hash( $this->initialRandomState() ); |
| 179 | + } |
| 180 | + // Generate a new random state based on the initial random state or previous |
| 181 | + // random state by combining it with clock drift |
| 182 | + $state = $this->driftHash( $state ); |
| 183 | + return $state; |
| 184 | + } |
| 185 | + |
| 186 | + /** |
| 187 | + * Decide on the best acceptable hash algorithm we have available for hash() |
| 188 | + * @return String A hash algorithm |
| 189 | + */ |
| 190 | + protected function hashAlgo() { |
| 191 | + if ( !is_null( $algo ) ) { |
| 192 | + return $algo; |
| 193 | + } |
| 194 | + |
| 195 | + $algos = hash_algos(); |
| 196 | + $preference = array( 'whirlpool', 'sha256', 'sha1', 'md5' ); |
| 197 | + |
| 198 | + foreach ( $preference as $algorithm ) { |
| 199 | + if ( in_array( $algorithm, $algos ) ) { |
| 200 | + $algo = $algorithm; # assign to static |
| 201 | + wfDebug( __METHOD__ . ": Using the $algo hash algorithm.\n" ); |
| 202 | + return $algo; |
| 203 | + } |
| 204 | + } |
| 205 | + |
| 206 | + // We only reach here if no acceptable hash is found in the list, this should |
| 207 | + // be a technical impossibility since most of php's hash list is fixed and |
| 208 | + // some of the ones we list are available as their own native functions |
| 209 | + // But since we already require at least 5.2 and hash() was default in |
| 210 | + // 5.1.2 we don't bother falling back to methods like sha1 and md5. |
| 211 | + throw new MWException( "Could not find an acceptable hashing function in hash_algos()" ); |
| 212 | + } |
| 213 | + |
| 214 | + /** |
| 215 | + * Return the byte-length output of the hash algorithm we are |
| 216 | + * using in self::hash and self::hmac. |
| 217 | + * |
| 218 | + * @return int Number of bytes the hash outputs |
| 219 | + */ |
| 220 | + protected function hashLength() { |
| 221 | + if ( is_null( $hashLength ) ) { |
| 222 | + $hashLength = strlen( $this->hash( '' ) ); |
| 223 | + } |
| 224 | + return $hashLength; |
| 225 | + } |
| 226 | + |
| 227 | + /** |
| 228 | + * Generate an acceptably unstable one-way-hash of some text |
| 229 | + * making use of the best hash algorithm that we have available. |
| 230 | + * |
| 231 | + * @return String A raw hash of the data |
| 232 | + */ |
| 233 | + protected function hash( $data ) { |
| 234 | + return hash( $this->hashAlgo(), $data, true ); |
| 235 | + } |
| 236 | + |
| 237 | + /** |
| 238 | + * Generate an acceptably unstable one-way-hmac of some text |
| 239 | + * making use of the best hash algorithm that we have available. |
| 240 | + * |
| 241 | + * @return String A raw hash of the data |
| 242 | + */ |
| 243 | + protected function hmac( $data, $key ) { |
| 244 | + return hash_hmac( $this->hashAlgo(), $data, $key, true ); |
| 245 | + } |
| 246 | + |
| 247 | + /** |
| 248 | + * @see self::wasStrong() |
| 249 | + */ |
| 250 | + public function realWasStrong() { |
| 251 | + if ( is_null( $this->strong ) ) { |
| 252 | + throw new MWException( __METHOD__ . ' called before generation of random data' ); |
| 253 | + } |
| 254 | + return $this->strong; |
| 255 | + } |
| 256 | + |
| 257 | + /** |
| 258 | + * @see self::generate() |
| 259 | + */ |
| 260 | + public function realGenerate( $bytes, $forceStrong = false, $method = null ) { |
| 261 | + wfProfileIn( __METHOD__ ); |
| 262 | + if ( is_string( $forceStrong ) && is_null( $method ) ) { |
| 263 | + // If $forceStrong is a string then it's really $method |
| 264 | + $method = $forceStrong; |
| 265 | + $forceStrong = false; |
| 266 | + } |
| 267 | + |
| 268 | + if ( !is_null( $method ) ) { |
| 269 | + wfDebug( __METHOD__ . ": Generating cryptographic random bytes for $method\n" ); |
| 270 | + } |
| 271 | + |
| 272 | + $bytes = floor( $bytes ); |
| 273 | + static $buffer = ''; |
| 274 | + if ( is_null( $this->strong ) ) { |
| 275 | + // Set strength to false initially until we know what source data is coming from |
| 276 | + $this->strong = true; |
| 277 | + } |
| 278 | + |
| 279 | + if ( strlen( $buffer ) < $bytes ) { |
| 280 | + // If available make use of mcrypt_create_iv URANDOM source to generate randomness |
| 281 | + // On unix-like systems this reads from /dev/urandom but does it without any buffering |
| 282 | + // and bypasses openbasdir restrictions so it's preferable to reading directly |
| 283 | + // On Windows starting in PHP 5.3.0 Windows' native CryptGenRandom is used to generate |
| 284 | + // entropy so this is also preferable to just trying to read urandom because it may work |
| 285 | + // on Windows systems as well. |
| 286 | + if ( function_exists( 'mcrypt_create_iv' ) ) { |
| 287 | + wfProfileIn( __METHOD__ . '-mcrypt' ); |
| 288 | + $rem = $bytes - strlen( $buffer ); |
| 289 | + wfDebug( __METHOD__ . ": Trying to generate $rem bytes of randomness using mcrypt_create_iv.\n" ); |
| 290 | + $iv = mcrypt_create_iv( $rem, MCRYPT_DEV_URANDOM ); |
| 291 | + if ( $iv === false ) { |
| 292 | + wfDebug( __METHOD__ . ": mcrypt_create_iv returned false.\n" ); |
| 293 | + } else { |
| 294 | + $bytes .= $iv; |
| 295 | + wfDebug( __METHOD__ . ": mcrypt_create_iv generated " . strlen( $iv ) . " bytes of randomness.\n" ); |
| 296 | + } |
| 297 | + wfProfileOut( __METHOD__ . '-mcrypt' ); |
| 298 | + } |
| 299 | + } |
| 300 | + |
| 301 | + if ( strlen( $buffer ) < $bytes ) { |
| 302 | + // If available make use of openssl's random_pesudo_bytes method to attempt to generate randomness. |
| 303 | + // However don't do this on Windows with PHP < 5.3.4 due to a bug: |
| 304 | + // http://stackoverflow.com/questions/1940168/openssl-random-pseudo-bytes-is-slow-php |
| 305 | + if ( function_exists( 'openssl_random_pseudo_bytes' ) |
| 306 | + && ( !wfIsWindows() || version_compare( PHP_VERSION, '5.3.4', '>=' ) ) |
| 307 | + ) { |
| 308 | + wfProfileIn( __METHOD__ . '-openssl' ); |
| 309 | + $rem = $bytes - strlen( $buffer ); |
| 310 | + wfDebug( __METHOD__ . ": Trying to generate $rem bytes of randomness using openssl_random_pseudo_bytes.\n" ); |
| 311 | + $openssl_bytes = openssl_random_pseudo_bytes( $rem, $openssl_strong ); |
| 312 | + if ( $openssl_bytes === false ) { |
| 313 | + wfDebug( __METHOD__ . ": openssl_random_pseudo_bytes returned false.\n" ); |
| 314 | + } else { |
| 315 | + $buffer .= $openssl_bytes; |
| 316 | + wfDebug( __METHOD__ . ": openssl_random_pseudo_bytes generated " . strlen( $openssl_bytes ) . " bytes of " . ( $openssl_strong ? "strong" : "weak" ) . " randomness.\n" ); |
| 317 | + } |
| 318 | + if ( strlen( $buffer ) >= $bytes ) { |
| 319 | + // openssl tells us if the random source was strong, if some of our data was generated |
| 320 | + // using it use it's say on whether the randomness is strong |
| 321 | + $this->strong = !!$openssl_strong; |
| 322 | + } |
| 323 | + wfProfileOut( __METHOD__ . '-openssl' ); |
| 324 | + } |
| 325 | + } |
| 326 | + |
| 327 | + // Only read from urandom if we can control the buffer size or were passed forceStrong |
| 328 | + if ( strlen( $buffer ) < $bytes && ( function_exists( 'stream_set_read_buffer' ) || $forceStrong ) ) { |
| 329 | + wfProfileIn( __METHOD__ . '-fopen-urandom' ); |
| 330 | + $rem = $bytes - strlen( $buffer ); |
| 331 | + wfDebug( __METHOD__ . ": Trying to generate $rem bytes of randomness using /dev/urandom.\n" ); |
| 332 | + if ( !function_exists( 'stream_set_read_buffer' ) && $forceStrong ) { |
| 333 | + wfDebug( __METHOD__ . ": Was forced to read from /dev/urandom without control over the buffer size.\n" ); |
| 334 | + } |
| 335 | + // /dev/urandom is generally considered the best possible commonly |
| 336 | + // available random source, and is available on most *nix systems. |
| 337 | + wfSuppressWarnings(); |
| 338 | + $urandom = fopen( "/dev/urandom", "rb" ); |
| 339 | + wfRestoreWarnings(); |
| 340 | + |
| 341 | + // Attempt to read all our random data from urandom |
| 342 | + // php's fread always does buffered reads based on the stream's chunk_size |
| 343 | + // so in reality it will usually read more than the amount of data we're |
| 344 | + // asked for and not storing that risks depleting the system's random pool. |
| 345 | + // If stream_set_read_buffer is available set the chunk_size to the amount |
| 346 | + // of data we need. Otherwise read 8k, php's default chunk_size. |
| 347 | + if ( $urandom ) { |
| 348 | + // php's default chunk_size is 8k |
| 349 | + $chunk_size = 1024 * 8; |
| 350 | + if ( function_exists( 'stream_set_read_buffer' ) ) { |
| 351 | + // If possible set the chunk_size to the amount of data we need |
| 352 | + stream_set_read_buffer( $urandom, $rem ); |
| 353 | + $chunk_size = $rem; |
| 354 | + } |
| 355 | + wfDebug( __METHOD__ . ": Reading from /dev/urandom with a buffer size of $chunk_size.\n" ); |
| 356 | + $random_bytes = fread( $urandom, max( $chunk_size, $rem ) ); |
| 357 | + $buffer .= $random_bytes; |
| 358 | + fclose( $urandom ); |
| 359 | + wfDebug( __METHOD__ . ": /dev/urandom generated " . strlen( $random_bytes ) . " bytes of randomness.\n" ); |
| 360 | + if ( strlen( $buffer ) >= $bytes ) { |
| 361 | + // urandom is always strong, set to true if all our data was generated using it |
| 362 | + $this->strong = true; |
| 363 | + } |
| 364 | + } else { |
| 365 | + wfDebug( __METHOD__ . ": /dev/urandom could not be opened.\n" ); |
| 366 | + } |
| 367 | + wfProfileOut( __METHOD__ . '-fopen-urandom' ); |
| 368 | + } |
| 369 | + |
| 370 | + // If we cannot use or generate enough data from a secure source |
| 371 | + // use this loop to generate a good set of pseudo random data. |
| 372 | + // This works by initializing a random state using a pile of unstable data |
| 373 | + // and continually shoving it through a hash along with a variable salt. |
| 374 | + // We hash the random state with more salt to avoid the state from leaking |
| 375 | + // out and being used to predict the /randomness/ that follows. |
| 376 | + if ( strlen( $buffer ) < $bytes ) { |
| 377 | + wfDebug( __METHOD__ . ": Falling back to using a pseudo random state to generate randomness.\n" ); |
| 378 | + } |
| 379 | + while ( strlen( $buffer ) < $bytes ) { |
| 380 | + wfProfileIn( __METHOD__ . '-fallback' ); |
| 381 | + $buffer .= $this->hmac( $this->randomState(), mt_rand() ); |
| 382 | + // This code is never really cryptographically strong, if we use it |
| 383 | + // at all, then set strong to false. |
| 384 | + $this->strong = false; |
| 385 | + wfProfileOut( __METHOD__ . '-fallback' ); |
| 386 | + } |
| 387 | + |
| 388 | + // Once the buffer has been filled up with enough random data to fulfill |
| 389 | + // the request shift off enough data to handle the request and leave the |
| 390 | + // unused portion left inside the buffer for the next request for random data |
| 391 | + $generated = substr( $buffer, 0, $bytes ); |
| 392 | + $buffer = substr( $buffer, $bytes ); |
| 393 | + |
| 394 | + wfDebug( __METHOD__ . ": " . strlen( $buffer ) . " bytes of randomness leftover in the buffer.\n" ); |
| 395 | + |
| 396 | + wfProfileOut( __METHOD__ ); |
| 397 | + return $generated; |
| 398 | + } |
| 399 | + |
| 400 | + /** |
| 401 | + * @see self::generateHex() |
| 402 | + */ |
| 403 | + public function realGenerateHex( $chars, $forceStrong = false, $method = null ) { |
| 404 | + // hex strings are 2x the length of raw binary so we divide the length in half |
| 405 | + // odd numbers will result in a .5 that leads the generate() being 1 character |
| 406 | + // short, so we use ceil() to ensure that we always have enough bytes |
| 407 | + $bytes = ceil( $chars / 2 ); |
| 408 | + // Generate the data and then convert it to a hex string |
| 409 | + $hex = bin2hex( $this->generate( $bytes, $forceStrong, $method ) ); |
| 410 | + // A bit of paranoia here, the caller asked for a specific length of string |
| 411 | + // here, and it's possible (eg when given an odd number) that we may actually |
| 412 | + // have at least 1 char more than they asked for. Just in case they made this |
| 413 | + // call intending to insert it into a database that does truncation we don't |
| 414 | + // want to give them too much and end up with their database and their live |
| 415 | + // code having two different values because part of what we gave them is truncated |
| 416 | + // hence, we strip out any run of characters longer than what we were asked for. |
| 417 | + return substr( $hex, 0, $chars ); |
| 418 | + } |
| 419 | + |
| 420 | + /** Publicly exposed static methods **/ |
| 421 | + |
| 422 | + /** |
| 423 | + * Return a singleton instance of MWCryptRand |
| 424 | + */ |
| 425 | + protected static function singleton() { |
| 426 | + if ( is_null( self::$singleton ) ) { |
| 427 | + self::$singleton = new self; |
| 428 | + } |
| 429 | + return self::$singleton; |
| 430 | + } |
| 431 | + |
| 432 | + /** |
| 433 | + * Return a boolean indicating whether or not the source used for cryptographic |
| 434 | + * random bytes generation in the previously run generate* call |
| 435 | + * was cryptographically strong. |
| 436 | + * |
| 437 | + * @return bool Returns true if the source was strong, false if not. |
| 438 | + */ |
| 439 | + public static function wasStrong() { |
| 440 | + return self::singleton()->realWasStrong(); |
| 441 | + } |
| 442 | + |
| 443 | + /** |
| 444 | + * Generate a run of (ideally) cryptographically random data and return |
| 445 | + * it in raw binary form. |
| 446 | + * You can use MWCryptRand::wasStrong() if you wish to know if the source used |
| 447 | + * was cryptographically strong. |
| 448 | + * |
| 449 | + * @param $bytes int the number of bytes of random data to generate |
| 450 | + * @param $forceStrong bool Pass true if you want generate to prefer cryptographically |
| 451 | + * strong sources of entropy even if reading from them may steal |
| 452 | + * more entropy from the system than optimal. |
| 453 | + * @param $method The calling method, for debug info. May be the second argument if you are not using forceStrong |
| 454 | + * @return String Raw binary random data |
| 455 | + */ |
| 456 | + public static function generate( $bytes, $forceStrong = false, $method = null ) { |
| 457 | + return self::singleton()->realGenerate( $bytes, $forceStrong, $method ); |
| 458 | + } |
| 459 | + |
| 460 | + /** |
| 461 | + * Generate a run of (ideally) cryptographically random data and return |
| 462 | + * it in hexadecimal string format. |
| 463 | + * You can use MWCryptRand::wasStrong() if you wish to know if the source used |
| 464 | + * was cryptographically strong. |
| 465 | + * |
| 466 | + * @param $chars int the number of hex chars of random data to generate |
| 467 | + * @param $forceStrong bool Pass true if you want generate to prefer cryptographically |
| 468 | + * strong sources of entropy even if reading from them may steal |
| 469 | + * more entropy from the system than optimal. |
| 470 | + * @param $method The calling method, for debug info. May be the second argument if you are not using forceStrong |
| 471 | + * @return String Hexadecimal random data |
| 472 | + */ |
| 473 | + public static function generateHex( $chars, $forceStrong = false, $method = null ) { |
| 474 | + return self::singleton()->realGenerateHex( $chars, $forceStrong, $method ); |
| 475 | + } |
| 476 | + |
| 477 | +} |
Index: trunk/phase3/includes/User.php |
— | — | @@ -836,23 +836,20 @@ |
837 | 837 | } |
838 | 838 | |
839 | 839 | /** |
840 | | - * Return a random password. Sourced from mt_rand, so it's not particularly secure. |
841 | | - * @todo hash random numbers to improve security, like generateToken() |
| 840 | + * Return a random password. |
842 | 841 | * |
843 | 842 | * @return String new random password |
844 | 843 | */ |
845 | 844 | public static function randomPassword() { |
846 | 845 | global $wgMinimalPasswordLength; |
847 | | - $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz'; |
848 | | - $l = strlen( $pwchars ) - 1; |
849 | | - |
850 | | - $pwlength = max( 7, $wgMinimalPasswordLength ); |
851 | | - $digit = mt_rand( 0, $pwlength - 1 ); |
852 | | - $np = ''; |
853 | | - for ( $i = 0; $i < $pwlength; $i++ ) { |
854 | | - $np .= $i == $digit ? chr( mt_rand( 48, 57 ) ) : $pwchars[ mt_rand( 0, $l ) ]; |
855 | | - } |
856 | | - return $np; |
| 846 | + // Decide the final password length based on our min password length, stopping at a minimum of 10 chars |
| 847 | + $length = max( 10, $wgMinimalPasswordLength ); |
| 848 | + // Multiply by 1.25 to get the number of hex characters we need |
| 849 | + $length = $length * 1.25; |
| 850 | + // Generate random hex chars |
| 851 | + $hex = MWCryptRand::generateHex( $length, __METHOD__ ); |
| 852 | + // Convert from base 16 to base 32 to get a proper password like string |
| 853 | + return wfBaseConvert( $hex, 16, 32 ); |
857 | 854 | } |
858 | 855 | |
859 | 856 | /** |
— | — | @@ -882,7 +879,7 @@ |
883 | 880 | $this->mTouched = '0'; # Allow any pages to be cached |
884 | 881 | } |
885 | 882 | |
886 | | - $this->setToken(); # Random |
| 883 | + $this->mToken = null; // Don't run cryptographic functions till we need a token |
887 | 884 | $this->mEmailAuthenticated = null; |
888 | 885 | $this->mEmailToken = ''; |
889 | 886 | $this->mEmailTokenExpires = null; |
— | — | @@ -989,11 +986,11 @@ |
990 | 987 | return false; |
991 | 988 | } |
992 | 989 | |
993 | | - if ( $request->getSessionData( 'wsToken' ) !== null ) { |
994 | | - $passwordCorrect = $proposedUser->getToken() === $request->getSessionData( 'wsToken' ); |
| 990 | + if ( $request->getSessionData( 'wsToken' ) ) { |
| 991 | + $passwordCorrect = $proposedUser->getToken( false ) === $request->getSessionData( 'wsToken' ); |
995 | 992 | $from = 'session'; |
996 | | - } elseif ( $request->getCookie( 'Token' ) !== null ) { |
997 | | - $passwordCorrect = $proposedUser->getToken() === $request->getCookie( 'Token' ); |
| 993 | + } elseif ( $request->getCookie( 'Token' ) ) { |
| 994 | + $passwordCorrect = $proposedUser->getToken( false ) === $request->getCookie( 'Token' ); |
998 | 995 | $from = 'cookie'; |
999 | 996 | } else { |
1000 | 997 | # No session or persistent login cookie |
— | — | @@ -1098,6 +1095,9 @@ |
1099 | 1096 | } |
1100 | 1097 | $this->mTouched = wfTimestamp( TS_MW, $row->user_touched ); |
1101 | 1098 | $this->mToken = $row->user_token; |
| 1099 | + if ( $this->mToken == '' ) { |
| 1100 | + $this->mToken = null; |
| 1101 | + } |
1102 | 1102 | $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, $row->user_email_authenticated ); |
1103 | 1103 | $this->mEmailToken = $row->user_email_token; |
1104 | 1104 | $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, $row->user_email_token_expires ); |
— | — | @@ -2023,10 +2023,14 @@ |
2024 | 2024 | |
2025 | 2025 | /** |
2026 | 2026 | * Get the user's current token. |
| 2027 | + * @param $forceCreation Force the generation of a new token if the user doesn't have one (default=true for backwards compatibility) |
2027 | 2028 | * @return String Token |
2028 | 2029 | */ |
2029 | | - public function getToken() { |
| 2030 | + public function getToken( $forceCreation = true ) { |
2030 | 2031 | $this->load(); |
| 2032 | + if ( !$this->mToken && $forceCreation ) { |
| 2033 | + $this->setToken(); |
| 2034 | + } |
2031 | 2035 | return $this->mToken; |
2032 | 2036 | } |
2033 | 2037 | |
— | — | @@ -2040,14 +2044,7 @@ |
2041 | 2045 | global $wgSecretKey, $wgProxyKey; |
2042 | 2046 | $this->load(); |
2043 | 2047 | if ( !$token ) { |
2044 | | - if ( $wgSecretKey ) { |
2045 | | - $key = $wgSecretKey; |
2046 | | - } elseif ( $wgProxyKey ) { |
2047 | | - $key = $wgProxyKey; |
2048 | | - } else { |
2049 | | - $key = microtime(); |
2050 | | - } |
2051 | | - $this->mToken = md5( $key . mt_rand( 0, 0x7fffffff ) . wfWikiID() . $this->mId ); |
| 2048 | + $this->mToken = MWCryptRand::generateHex( USER_TOKEN_LENGTH, __METHOD__ ); |
2052 | 2049 | } else { |
2053 | 2050 | $this->mToken = $token; |
2054 | 2051 | } |
— | — | @@ -2814,7 +2811,7 @@ |
2815 | 2812 | 'user_email' => $this->mEmail, |
2816 | 2813 | 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), |
2817 | 2814 | 'user_touched' => $dbw->timestamp( $this->mTouched ), |
2818 | | - 'user_token' => $this->mToken, |
| 2815 | + 'user_token' => strval( $this->mToken ), |
2819 | 2816 | 'user_email_token' => $this->mEmailToken, |
2820 | 2817 | 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ), |
2821 | 2818 | ), array( /* WHERE */ |
— | — | @@ -2880,7 +2877,7 @@ |
2881 | 2878 | 'user_email' => $user->mEmail, |
2882 | 2879 | 'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ), |
2883 | 2880 | 'user_real_name' => $user->mRealName, |
2884 | | - 'user_token' => $user->mToken, |
| 2881 | + 'user_token' => strval( $user->mToken ), |
2885 | 2882 | 'user_registration' => $dbw->timestamp( $user->mRegistration ), |
2886 | 2883 | 'user_editcount' => 0, |
2887 | 2884 | 'user_touched' => $dbw->timestamp( self::newTouchedTimestamp() ), |
— | — | @@ -2917,7 +2914,7 @@ |
2918 | 2915 | 'user_email' => $this->mEmail, |
2919 | 2916 | 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), |
2920 | 2917 | 'user_real_name' => $this->mRealName, |
2921 | | - 'user_token' => $this->mToken, |
| 2918 | + 'user_token' => strval( $this->mToken ), |
2922 | 2919 | 'user_registration' => $dbw->timestamp( $this->mRegistration ), |
2923 | 2920 | 'user_editcount' => 0, |
2924 | 2921 | 'user_touched' => $dbw->timestamp( $this->mTouched ), |
— | — | @@ -3182,7 +3179,7 @@ |
3183 | 3180 | } else { |
3184 | 3181 | $token = $request->getSessionData( 'wsEditToken' ); |
3185 | 3182 | if ( $token === null ) { |
3186 | | - $token = self::generateToken(); |
| 3183 | + $token = MWCryptRand::generateHex( 32, __METHOD__ ); |
3187 | 3184 | $request->setSessionData( 'wsEditToken', $token ); |
3188 | 3185 | } |
3189 | 3186 | if( is_array( $salt ) ) { |
— | — | @@ -3197,10 +3194,10 @@ |
3198 | 3195 | * |
3199 | 3196 | * @param $salt String Optional salt value |
3200 | 3197 | * @return String The new random token |
| 3198 | + * @deprecated since 1.20; Use MWCryptRand for secure purposes or wfRandomString for pesudo-randomness |
3201 | 3199 | */ |
3202 | 3200 | public static function generateToken( $salt = '' ) { |
3203 | | - $token = dechex( mt_rand() ) . dechex( mt_rand() ); |
3204 | | - return md5( $token . $salt ); |
| 3201 | + return MWCryptRand::generateHex( 32, __METHOD__ ); |
3205 | 3202 | } |
3206 | 3203 | |
3207 | 3204 | /** |
— | — | @@ -3306,12 +3303,11 @@ |
3307 | 3304 | global $wgUserEmailConfirmationTokenExpiry; |
3308 | 3305 | $now = time(); |
3309 | 3306 | $expires = $now + $wgUserEmailConfirmationTokenExpiry; |
3310 | | - $expiration = wfTimestamp( TS_MW, $expires ); |
3311 | | - $token = self::generateToken( $this->mId . $this->mEmail . $expires ); |
3312 | | - $hash = md5( $token ); |
3313 | 3307 | $this->load(); |
| 3308 | + $token = MWCryptRand::generateHex( 32, __METHOD__ ); |
| 3309 | + $hash = md5( $token ); |
3314 | 3310 | $this->mEmailToken = $hash; |
3315 | | - $this->mEmailTokenExpires = $expiration; |
| 3311 | + $this->mEmailTokenExpires = wfTimestamp( TS_MW, $expires ); |
3316 | 3312 | return $token; |
3317 | 3313 | } |
3318 | 3314 | |
— | — | @@ -3860,7 +3856,7 @@ |
3861 | 3857 | |
3862 | 3858 | if( $wgPasswordSalt ) { |
3863 | 3859 | if ( $salt === false ) { |
3864 | | - $salt = substr( wfGenerateToken(), 0, 8 ); |
| 3860 | + $salt = MWCryptRand::generateHex( 8, __METHOD__ ); |
3865 | 3861 | } |
3866 | 3862 | return ':B:' . $salt . ':' . md5( $salt . '-' . md5( $password ) ); |
3867 | 3863 | } else { |
Index: trunk/phase3/includes/GlobalFunctions.php |
— | — | @@ -295,6 +295,24 @@ |
296 | 296 | } |
297 | 297 | |
298 | 298 | /** |
| 299 | + * Get a random string containing a number of pesudo-random hex |
| 300 | + * characters. |
| 301 | + * @note This is not secure, if you are trying to generate some sort |
| 302 | + * of token please use MWCryptRand instead. |
| 303 | + * |
| 304 | + * @param $length int The length of the string to generate |
| 305 | + * @return String |
| 306 | + * @since 1.20 |
| 307 | + */ |
| 308 | +function wfRandomString( $length = 32 ) { |
| 309 | + $str = ''; |
| 310 | + while ( strlen( $str ) < $length ) { |
| 311 | + $str .= dechex( mt_rand() ); |
| 312 | + } |
| 313 | + return substr( $str, 0, $length ); |
| 314 | +} |
| 315 | + |
| 316 | +/** |
299 | 317 | * We want some things to be included as literal characters in our title URLs |
300 | 318 | * for prettiness, which urlencode encodes by default. According to RFC 1738, |
301 | 319 | * all of the following should be safe: |
— | — | @@ -3323,6 +3341,33 @@ |
3324 | 3342 | } |
3325 | 3343 | |
3326 | 3344 | /** |
| 3345 | + * Override session_id before session startup if php's built-in |
| 3346 | + * session generation code is not secure. |
| 3347 | + */ |
| 3348 | +function wfFixSessionID() { |
| 3349 | + // If the cookie or session id is already set we already have a session and should abort |
| 3350 | + if ( isset( $_COOKIE[ session_name() ] ) || session_id() ) { |
| 3351 | + return; |
| 3352 | + } |
| 3353 | + |
| 3354 | + // PHP's built-in session entropy is enabled if: |
| 3355 | + // - entropy_file is set or you're on Windows with php 5.3.3+ |
| 3356 | + // - AND entropy_length is > 0 |
| 3357 | + // We treat it as disabled if it doesn't have an entropy length of at least 32 |
| 3358 | + $entropyEnabled = ( |
| 3359 | + ( wfIsWindows() && version_compare( PHP_VERSION, '5.3.3', '>=' ) ) |
| 3360 | + || ini_get( 'session.entropy_file' ) |
| 3361 | + ) |
| 3362 | + && intval( ini_get( 'session.entropy_length' ) ) >= 32; |
| 3363 | + |
| 3364 | + // If built-in entropy is not enabled or not sufficient override php's built in session id generation code |
| 3365 | + if ( !$entropyEnabled ) { |
| 3366 | + wfDebug( __METHOD__ . ": PHP's built in entropy is disabled or not sufficient, overriding session id generation using our cryptrand source.\n" ); |
| 3367 | + session_id( MWCryptRand::generateHex( 32, __METHOD__ ) ); |
| 3368 | + } |
| 3369 | +} |
| 3370 | + |
| 3371 | +/** |
3327 | 3372 | * Initialise php session |
3328 | 3373 | * |
3329 | 3374 | * @param $sessionId Bool |
— | — | @@ -3361,6 +3406,8 @@ |
3362 | 3407 | session_cache_limiter( 'private, must-revalidate' ); |
3363 | 3408 | if ( $sessionId ) { |
3364 | 3409 | session_id( $sessionId ); |
| 3410 | + } else { |
| 3411 | + wfFixSessionID(); |
3365 | 3412 | } |
3366 | 3413 | wfSuppressWarnings(); |
3367 | 3414 | session_start(); |
— | — | @@ -3681,8 +3728,11 @@ |
3682 | 3729 | * characters before hashing. |
3683 | 3730 | * @return string |
3684 | 3731 | * @codeCoverageIgnore |
| 3732 | + * @deprecated since 1.20; Please use MWCryptRand for security purposes and wfRandomString for pesudo-random strings |
| 3733 | + * @warning This method is NOT secure. Additionally it has many callers that use it for pesudo-random purposes. |
3685 | 3734 | */ |
3686 | 3735 | function wfGenerateToken( $salt = '' ) { |
| 3736 | + wfDeprecated( __METHOD__, '1.20' ); |
3687 | 3737 | $salt = serialize( $salt ); |
3688 | 3738 | return md5( mt_rand( 0, 0x7fffffff ) . $salt ); |
3689 | 3739 | } |
Index: trunk/phase3/includes/parser/Parser.php |
— | — | @@ -561,7 +561,7 @@ |
562 | 562 | * @return string |
563 | 563 | */ |
564 | 564 | static public function getRandomString() { |
565 | | - return dechex( mt_rand( 0, 0x7fffffff ) ) . dechex( mt_rand( 0, 0x7fffffff ) ); |
| 565 | + return wfRandomString( 16 ); |
566 | 566 | } |
567 | 567 | |
568 | 568 | /** |
Index: trunk/phase3/includes/installer/Installer.php |
— | — | @@ -1446,8 +1446,7 @@ |
1447 | 1447 | } |
1448 | 1448 | |
1449 | 1449 | /** |
1450 | | - * Generate $wgSecretKey. Will warn if we had to use mt_rand() instead of |
1451 | | - * /dev/urandom |
| 1450 | + * Generate $wgSecretKey. Will warn if we had to use an insecure random source. |
1452 | 1451 | * |
1453 | 1452 | * @return Status |
1454 | 1453 | */ |
— | — | @@ -1460,8 +1459,8 @@ |
1461 | 1460 | } |
1462 | 1461 | |
1463 | 1462 | /** |
1464 | | - * Generate a secret value for variables using either |
1465 | | - * /dev/urandom or mt_rand(). Produce a warning in the later case. |
| 1463 | + * Generate a secret value for variables using our CryptRand generator. |
| 1464 | + * Produce a warning if the random source was insecure. |
1466 | 1465 | * |
1467 | 1466 | * @param $keys Array |
1468 | 1467 | * @return Status |
— | — | @@ -1469,28 +1468,18 @@ |
1470 | 1469 | protected function doGenerateKeys( $keys ) { |
1471 | 1470 | $status = Status::newGood(); |
1472 | 1471 | |
1473 | | - wfSuppressWarnings(); |
1474 | | - $file = fopen( "/dev/urandom", "r" ); |
1475 | | - wfRestoreWarnings(); |
1476 | | - |
| 1472 | + $strong = true; |
1477 | 1473 | foreach ( $keys as $name => $length ) { |
1478 | | - if ( $file ) { |
1479 | | - $secretKey = bin2hex( fread( $file, $length / 2 ) ); |
1480 | | - } else { |
1481 | | - $secretKey = ''; |
1482 | | - |
1483 | | - for ( $i = 0; $i < $length / 8; $i++ ) { |
1484 | | - $secretKey .= dechex( mt_rand( 0, 0x7fffffff ) ); |
1485 | | - } |
| 1474 | + $secretKey = MWCryptRand::generateHex( $length, true ); |
| 1475 | + if ( !MWCryptRand::wasStrong() ) { |
| 1476 | + $strong = false; |
1486 | 1477 | } |
1487 | 1478 | |
1488 | 1479 | $this->setVar( $name, $secretKey ); |
1489 | 1480 | } |
1490 | 1481 | |
1491 | | - if ( $file ) { |
1492 | | - fclose( $file ); |
1493 | | - } else { |
1494 | | - $names = array_keys ( $keys ); |
| 1482 | + if ( !$strong ) { |
| 1483 | + $names = array_keys( $keys ); |
1495 | 1484 | $names = preg_replace( '/^(.*)$/', '\$$1', $names ); |
1496 | 1485 | global $wgLang; |
1497 | 1486 | $status->warning( 'config-insecure-keys', $wgLang->listToText( $names ), count( $names ) ); |
Index: trunk/phase3/includes/AutoLoader.php |
— | — | @@ -50,6 +50,7 @@ |
51 | 51 | 'ConfEditorToken' => 'includes/ConfEditor.php', |
52 | 52 | 'Cookie' => 'includes/Cookie.php', |
53 | 53 | 'CookieJar' => 'includes/Cookie.php', |
| 54 | + 'MWCryptRand' => 'includes/CryptRand.php', |
54 | 55 | 'CurlHttpRequest' => 'includes/HttpFunctions.php', |
55 | 56 | // 'DBDataObject' => 'includes/DBDataObject.php', |
56 | 57 | // 'DBTable' => 'includes/DBTable.php', |
Index: trunk/phase3/includes/specials/SpecialUserlogin.php |
— | — | @@ -1150,9 +1150,9 @@ |
1151 | 1151 | */ |
1152 | 1152 | public static function setLoginToken() { |
1153 | 1153 | global $wgRequest; |
1154 | | - // Use User::generateToken() instead of $user->editToken() |
| 1154 | + // Generate a token directly instead of using $user->editToken() |
1155 | 1155 | // because the latter reuses $_SESSION['wsEditToken'] |
1156 | | - $wgRequest->setSessionData( 'wsLoginToken', User::generateToken() ); |
| 1156 | + $wgRequest->setSessionData( 'wsLoginToken', MWCryptRand::generateHex( 32, __METHOD__ ) ); |
1157 | 1157 | } |
1158 | 1158 | |
1159 | 1159 | /** |
— | — | @@ -1177,7 +1177,7 @@ |
1178 | 1178 | */ |
1179 | 1179 | public static function setCreateaccountToken() { |
1180 | 1180 | global $wgRequest; |
1181 | | - $wgRequest->setSessionData( 'wsCreateaccountToken', User::generateToken() ); |
| 1181 | + $wgRequest->setSessionData( 'wsCreateaccountToken', MWCryptRand::generateHex( 32, __METHOD__ ) ); |
1182 | 1182 | } |
1183 | 1183 | |
1184 | 1184 | /** |
Index: trunk/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: trunk/phase3/includes/Preferences.php |
— | — | @@ -916,6 +916,7 @@ |
917 | 917 | |
918 | 918 | if ( $wgEnableAPI ) { |
919 | 919 | # Some random gibberish as a proposed default |
| 920 | + // @fixme This should use CryptRand but we may not want to read urandom on every view |
920 | 921 | $hash = sha1( mt_rand() . microtime( true ) ); |
921 | 922 | |
922 | 923 | $defaultPreferences['watchlisttoken'] = array( |
Index: trunk/phase3/includes/Import.php |
— | — | @@ -829,7 +829,7 @@ |
830 | 830 | * @return string |
831 | 831 | */ |
832 | 832 | static function registerSource( $source ) { |
833 | | - $id = wfGenerateToken(); |
| 833 | + $id = wfRandomString(); |
834 | 834 | |
835 | 835 | self::$sourceRegistrations[$id] = $source; |
836 | 836 | |
Index: trunk/phase3/RELEASE-NOTES-1.20 |
— | — | @@ -22,6 +22,8 @@ |
23 | 23 | * (bug 34302) Add CSS classes to email fields in user preferences |
24 | 24 | * Introduced $wgDebugDBTransactions to trace transaction status (currently PostgreSQL only) |
25 | 25 | * (bug 23795) Add parser itself to ParserMakeImageParams hook. |
| 26 | +* Introduce a cryptographic random number generator source api for use when |
| 27 | + generating various tokens. |
26 | 28 | |
27 | 29 | === Bug fixes in 1.20 === |
28 | 30 | * (bug 30245) Use the correct way to construct a log page title. |
— | — | @@ -61,6 +63,9 @@ |
62 | 64 | * Mizo (lus) added |
63 | 65 | |
64 | 66 | === Other changes in 1.20 === |
| 67 | +* The user_token field is now left empty until a user attempts to login and |
| 68 | + cookies need to be set. It is also now possible to reset every user's |
| 69 | + user_token simply by clearing the values in the user_token column. |
65 | 70 | |
66 | 71 | == Compatibility == |
67 | 72 | |