MediaWiki  1.23.2
MWCryptRand.php
Go to the documentation of this file.
1 <?php
27 class MWCryptRand {
31  const MIN_ITERATIONS = 1000;
32 
39  const MSEC_PER_BYTE = 0.5;
40 
44  protected static $singleton = null;
45 
49  protected $algo = null;
50 
54  protected $hashLength = null;
55 
60  protected $strong = null;
61 
65  protected function initialRandomState() {
66  // $_SERVER contains a variety of unstable user and system specific information
67  // It'll vary a little with each page, and vary even more with separate users
68  // It'll also vary slightly across different machines
69  $state = serialize( $_SERVER );
70 
71  // To try vary the system information of the state a bit more
72  // by including the system's hostname into the state
73  $state .= wfHostname();
74 
75  // Try to gather a little entropy from the different php rand sources
76  $state .= rand() . uniqid( mt_rand(), true );
77 
78  // Include some information about the filesystem's current state in the random state
79  $files = array();
80 
81  // We know this file is here so grab some info about ourselves
82  $files[] = __FILE__;
83 
84  // We must also have a parent folder, and with the usual file structure, a grandparent
85  $files[] = __DIR__;
86  $files[] = dirname( __DIR__ );
87 
88  // The config file is likely the most often edited file we know should
89  // be around so include its stat info into the state.
90  // The constant with its location will almost always be defined, as
91  // WebStart.php defines MW_CONFIG_FILE to $IP/LocalSettings.php unless
92  // being configured with MW_CONFIG_CALLBACK (e.g. the installer).
93  if ( defined( 'MW_CONFIG_FILE' ) ) {
94  $files[] = MW_CONFIG_FILE;
95  }
96 
97  foreach ( $files as $file ) {
99  $stat = stat( $file );
101  if ( $stat ) {
102  // stat() duplicates data into numeric and string keys so kill off all the numeric ones
103  foreach ( $stat as $k => $v ) {
104  if ( is_numeric( $k ) ) {
105  unset( $k );
106  }
107  }
108  // The absolute filename itself will differ from install to install so don't leave it out
109  if ( ( $path = realpath( $file ) ) !== false ) {
110  $state .= $path;
111  } else {
112  $state .= $file;
113  }
114  $state .= implode( '', $stat );
115  } else {
116  // The fact that the file isn't there is worth at least a
117  // minuscule amount of entropy.
118  $state .= '0';
119  }
120  }
121 
122  // Try and make this a little more unstable by including the varying process
123  // id of the php process we are running inside of if we are able to access it
124  if ( function_exists( 'getmypid' ) ) {
125  $state .= getmypid();
126  }
127 
128  // If available try to increase the instability of the data by throwing in
129  // the precise amount of memory that we happen to be using at the moment.
130  if ( function_exists( 'memory_get_usage' ) ) {
131  $state .= memory_get_usage( true );
132  }
133 
134  // It's mostly worthless but throw the wiki's id into the data for a little more variance
135  $state .= wfWikiID();
136 
137  // If we have a secret key or proxy key set then throw it into the state as well
138  global $wgSecretKey, $wgProxyKey;
139  if ( $wgSecretKey ) {
140  $state .= $wgSecretKey;
141  } elseif ( $wgProxyKey ) {
142  $state .= $wgProxyKey;
143  }
144 
145  return $state;
146  }
147 
155  protected function driftHash( $data ) {
156  // Minimum number of iterations (to avoid slow operations causing the
157  // loop to gather little entropy)
158  $minIterations = self::MIN_ITERATIONS;
159  // Duration of time to spend doing calculations (in seconds)
160  $duration = ( self::MSEC_PER_BYTE / 1000 ) * $this->hashLength();
161  // Create a buffer to use to trigger memory operations
162  $bufLength = 10000000;
163  $buffer = str_repeat( ' ', $bufLength );
164  $bufPos = 0;
165 
166  // Iterate for $duration seconds or at least $minIterations number of iterations
167  $iterations = 0;
168  $startTime = microtime( true );
169  $currentTime = $startTime;
170  while ( $iterations < $minIterations || $currentTime - $startTime < $duration ) {
171  // Trigger some memory writing to trigger some bus activity
172  // This may create variance in the time between iterations
173  $bufPos = ( $bufPos + 13 ) % $bufLength;
174  $buffer[$bufPos] = ' ';
175  // Add the drift between this iteration and the last in as entropy
176  $nextTime = microtime( true );
177  $delta = (int)( ( $nextTime - $currentTime ) * 1000000 );
178  $data .= $delta;
179  // Every 100 iterations hash the data and entropy
180  if ( $iterations % 100 === 0 ) {
181  $data = sha1( $data );
182  }
183  $currentTime = $nextTime;
184  $iterations++;
185  }
186  $timeTaken = $currentTime - $startTime;
187  $data = $this->hash( $data );
188 
189  wfDebug( __METHOD__ . ": Clock drift calculation " .
190  "(time-taken=" . ( $timeTaken * 1000 ) . "ms, " .
191  "iterations=$iterations, " .
192  "time-per-iteration=" . ( $timeTaken / $iterations * 1e6 ) . "us)\n" );
193 
194  return $data;
195  }
196 
201  protected function randomState() {
202  static $state = null;
203  if ( is_null( $state ) ) {
204  // Initialize the state with whatever unstable data we can find
205  // It's important that this data is hashed right afterwards to prevent
206  // it from being leaked into the output stream
207  $state = $this->hash( $this->initialRandomState() );
208  }
209  // Generate a new random state based on the initial random state or previous
210  // random state by combining it with clock drift
211  $state = $this->driftHash( $state );
212 
213  return $state;
214  }
215 
221  protected function hashAlgo() {
222  if ( !is_null( $this->algo ) ) {
223  return $this->algo;
224  }
225 
226  $algos = hash_algos();
227  $preference = array( 'whirlpool', 'sha256', 'sha1', 'md5' );
228 
229  foreach ( $preference as $algorithm ) {
230  if ( in_array( $algorithm, $algos ) ) {
231  $this->algo = $algorithm;
232  wfDebug( __METHOD__ . ": Using the {$this->algo} hash algorithm.\n" );
233 
234  return $this->algo;
235  }
236  }
237 
238  // We only reach here if no acceptable hash is found in the list, this should
239  // be a technical impossibility since most of php's hash list is fixed and
240  // some of the ones we list are available as their own native functions
241  // But since we already require at least 5.2 and hash() was default in
242  // 5.1.2 we don't bother falling back to methods like sha1 and md5.
243  throw new MWException( "Could not find an acceptable hashing function in hash_algos()" );
244  }
245 
252  protected function hashLength() {
253  if ( is_null( $this->hashLength ) ) {
254  $this->hashLength = strlen( $this->hash( '' ) );
255  }
256 
257  return $this->hashLength;
258  }
259 
267  protected function hash( $data ) {
268  return hash( $this->hashAlgo(), $data, true );
269  }
270 
279  protected function hmac( $data, $key ) {
280  return hash_hmac( $this->hashAlgo(), $data, $key, true );
281  }
282 
286  public function realWasStrong() {
287  if ( is_null( $this->strong ) ) {
288  throw new MWException( __METHOD__ . ' called before generation of random data' );
289  }
290 
291  return $this->strong;
292  }
293 
297  public function realGenerate( $bytes, $forceStrong = false ) {
298  wfProfileIn( __METHOD__ );
299 
300  wfDebug( __METHOD__ . ": Generating cryptographic random bytes for " .
301  wfGetAllCallers( 5 ) . "\n" );
302 
303  $bytes = floor( $bytes );
304  static $buffer = '';
305  if ( is_null( $this->strong ) ) {
306  // Set strength to false initially until we know what source data is coming from
307  $this->strong = true;
308  }
309 
310  if ( strlen( $buffer ) < $bytes ) {
311  // If available make use of mcrypt_create_iv URANDOM source to generate randomness
312  // On unix-like systems this reads from /dev/urandom but does it without any buffering
313  // and bypasses openbasedir restrictions, so it's preferable to reading directly
314  // On Windows starting in PHP 5.3.0 Windows' native CryptGenRandom is used to generate
315  // entropy so this is also preferable to just trying to read urandom because it may work
316  // on Windows systems as well.
317  if ( function_exists( 'mcrypt_create_iv' ) ) {
318  wfProfileIn( __METHOD__ . '-mcrypt' );
319  $rem = $bytes - strlen( $buffer );
320  $iv = mcrypt_create_iv( $rem, MCRYPT_DEV_URANDOM );
321  if ( $iv === false ) {
322  wfDebug( __METHOD__ . ": mcrypt_create_iv returned false.\n" );
323  } else {
324  $buffer .= $iv;
325  wfDebug( __METHOD__ . ": mcrypt_create_iv generated " . strlen( $iv ) .
326  " bytes of randomness.\n" );
327  }
328  wfProfileOut( __METHOD__ . '-mcrypt' );
329  }
330  }
331 
332  if ( strlen( $buffer ) < $bytes ) {
333  // If available make use of openssl's random_pseudo_bytes method to
334  // attempt to generate randomness. However don't do this on Windows
335  // with PHP < 5.3.4 due to a bug:
336  // http://stackoverflow.com/questions/1940168/openssl-random-pseudo-bytes-is-slow-php
337  // http://git.php.net/?p=php-src.git;a=commitdiff;h=cd62a70863c261b07f6dadedad9464f7e213cad5
338  if ( function_exists( 'openssl_random_pseudo_bytes' )
339  && ( !wfIsWindows() || version_compare( PHP_VERSION, '5.3.4', '>=' ) )
340  ) {
341  wfProfileIn( __METHOD__ . '-openssl' );
342  $rem = $bytes - strlen( $buffer );
343  $openssl_bytes = openssl_random_pseudo_bytes( $rem, $openssl_strong );
344  if ( $openssl_bytes === false ) {
345  wfDebug( __METHOD__ . ": openssl_random_pseudo_bytes returned false.\n" );
346  } else {
347  $buffer .= $openssl_bytes;
348  wfDebug( __METHOD__ . ": openssl_random_pseudo_bytes generated " .
349  strlen( $openssl_bytes ) . " bytes of " .
350  ( $openssl_strong ? "strong" : "weak" ) . " randomness.\n" );
351  }
352  if ( strlen( $buffer ) >= $bytes ) {
353  // openssl tells us if the random source was strong, if some of our data was generated
354  // using it use it's say on whether the randomness is strong
355  $this->strong = !!$openssl_strong;
356  }
357  wfProfileOut( __METHOD__ . '-openssl' );
358  }
359  }
360 
361  // Only read from urandom if we can control the buffer size or were passed forceStrong
362  if ( strlen( $buffer ) < $bytes &&
363  ( function_exists( 'stream_set_read_buffer' ) || $forceStrong )
364  ) {
365  wfProfileIn( __METHOD__ . '-fopen-urandom' );
366  $rem = $bytes - strlen( $buffer );
367  if ( !function_exists( 'stream_set_read_buffer' ) && $forceStrong ) {
368  wfDebug( __METHOD__ . ": Was forced to read from /dev/urandom " .
369  "without control over the buffer size.\n" );
370  }
371  // /dev/urandom is generally considered the best possible commonly
372  // available random source, and is available on most *nix systems.
374  $urandom = fopen( "/dev/urandom", "rb" );
376 
377  // Attempt to read all our random data from urandom
378  // php's fread always does buffered reads based on the stream's chunk_size
379  // so in reality it will usually read more than the amount of data we're
380  // asked for and not storing that risks depleting the system's random pool.
381  // If stream_set_read_buffer is available set the chunk_size to the amount
382  // of data we need. Otherwise read 8k, php's default chunk_size.
383  if ( $urandom ) {
384  // php's default chunk_size is 8k
385  $chunk_size = 1024 * 8;
386  if ( function_exists( 'stream_set_read_buffer' ) ) {
387  // If possible set the chunk_size to the amount of data we need
388  stream_set_read_buffer( $urandom, $rem );
389  $chunk_size = $rem;
390  }
391  $random_bytes = fread( $urandom, max( $chunk_size, $rem ) );
392  $buffer .= $random_bytes;
393  fclose( $urandom );
394  wfDebug( __METHOD__ . ": /dev/urandom generated " . strlen( $random_bytes ) .
395  " bytes of randomness.\n" );
396 
397  if ( strlen( $buffer ) >= $bytes ) {
398  // urandom is always strong, set to true if all our data was generated using it
399  $this->strong = true;
400  }
401  } else {
402  wfDebug( __METHOD__ . ": /dev/urandom could not be opened.\n" );
403  }
404  wfProfileOut( __METHOD__ . '-fopen-urandom' );
405  }
406 
407  // If we cannot use or generate enough data from a secure source
408  // use this loop to generate a good set of pseudo random data.
409  // This works by initializing a random state using a pile of unstable data
410  // and continually shoving it through a hash along with a variable salt.
411  // We hash the random state with more salt to avoid the state from leaking
412  // out and being used to predict the /randomness/ that follows.
413  if ( strlen( $buffer ) < $bytes ) {
414  wfDebug( __METHOD__ .
415  ": Falling back to using a pseudo random state to generate randomness.\n" );
416  }
417  while ( strlen( $buffer ) < $bytes ) {
418  wfProfileIn( __METHOD__ . '-fallback' );
419  $buffer .= $this->hmac( $this->randomState(), mt_rand() );
420  // This code is never really cryptographically strong, if we use it
421  // at all, then set strong to false.
422  $this->strong = false;
423  wfProfileOut( __METHOD__ . '-fallback' );
424  }
425 
426  // Once the buffer has been filled up with enough random data to fulfill
427  // the request shift off enough data to handle the request and leave the
428  // unused portion left inside the buffer for the next request for random data
429  $generated = substr( $buffer, 0, $bytes );
430  $buffer = substr( $buffer, $bytes );
431 
432  wfDebug( __METHOD__ . ": " . strlen( $buffer ) .
433  " bytes of randomness leftover in the buffer.\n" );
434 
435  wfProfileOut( __METHOD__ );
436 
437  return $generated;
438  }
439 
443  public function realGenerateHex( $chars, $forceStrong = false ) {
444  // hex strings are 2x the length of raw binary so we divide the length in half
445  // odd numbers will result in a .5 that leads the generate() being 1 character
446  // short, so we use ceil() to ensure that we always have enough bytes
447  $bytes = ceil( $chars / 2 );
448  // Generate the data and then convert it to a hex string
449  $hex = bin2hex( $this->generate( $bytes, $forceStrong ) );
450 
451  // A bit of paranoia here, the caller asked for a specific length of string
452  // here, and it's possible (eg when given an odd number) that we may actually
453  // have at least 1 char more than they asked for. Just in case they made this
454  // call intending to insert it into a database that does truncation we don't
455  // want to give them too much and end up with their database and their live
456  // code having two different values because part of what we gave them is truncated
457  // hence, we strip out any run of characters longer than what we were asked for.
458  return substr( $hex, 0, $chars );
459  }
460 
467  protected static function singleton() {
468  if ( is_null( self::$singleton ) ) {
469  self::$singleton = new self;
470  }
471 
472  return self::$singleton;
473  }
474 
482  public static function wasStrong() {
483  return self::singleton()->realWasStrong();
484  }
485 
498  public static function generate( $bytes, $forceStrong = false ) {
499  return self::singleton()->realGenerate( $bytes, $forceStrong );
500  }
501 
514  public static function generateHex( $chars, $forceStrong = false ) {
515  return self::singleton()->realGenerateHex( $chars, $forceStrong );
516  }
517 }
MWCryptRand\wasStrong
static wasStrong()
Return a boolean indicating whether or not the source used for cryptographic random bytes generation ...
Definition: MWCryptRand.php:482
php
skin txt MediaWiki includes four core it has been set as the default in MediaWiki since the replacing Monobook it had been been the default skin since before being replaced by Vector largely rewritten in while keeping its appearance Several legacy skins were removed in the as the burden of supporting them became too heavy to bear Those in etc for skin dependent CSS etc for skin dependent JavaScript These can also be customised on a per user by etc This feature has led to a wide variety of user styles becoming that gallery is a good place to ending in php
Definition: skin.txt:62
$files
$files
Definition: importImages.php:67
MWCryptRand\hmac
hmac( $data, $key)
Generate an acceptably unstable one-way-hmac of some text making use of the best hash algorithm that ...
Definition: MWCryptRand.php:279
MWCryptRand\driftHash
driftHash( $data)
Randomly hash data while mixing in clock drift data for randomness.
Definition: MWCryptRand.php:155
MWCryptRand\realGenerateHex
realGenerateHex( $chars, $forceStrong=false)
Definition: MWCryptRand.php:443
wfProfileIn
wfProfileIn( $functionname)
Begin profiling of a function.
Definition: Profiler.php:33
wfSuppressWarnings
wfSuppressWarnings( $end=false)
Reference-counted warning suppression.
Definition: GlobalFunctions.php:2387
wfHostname
wfHostname()
Fetch server name for use in error reporting etc.
Definition: GlobalFunctions.php:1786
MWCryptRand\generateHex
static generateHex( $chars, $forceStrong=false)
Generate a run of (ideally) cryptographically random data and return it in hexadecimal string format.
Definition: MWCryptRand.php:514
MWCryptRand\randomState
randomState()
Return a rolling random state initially build using data from unstable sources.
Definition: MWCryptRand.php:201
MWCryptRand\MIN_ITERATIONS
const MIN_ITERATIONS
Minimum number of iterations we want to make in our drift calculations.
Definition: MWCryptRand.php:31
MWCryptRand\generate
static generate( $bytes, $forceStrong=false)
Generate a run of (ideally) cryptographically random data and return it in raw binary form.
Definition: MWCryptRand.php:498
MWException
MediaWiki exception.
Definition: MWException.php:26
wfRestoreWarnings
wfRestoreWarnings()
Restore error level to previous value.
Definition: GlobalFunctions.php:2417
MWCryptRand\hashLength
hashLength()
Return the byte-length output of the hash algorithm we are using in self::hash and self::hmac.
Definition: MWCryptRand.php:252
wfProfileOut
wfProfileOut( $functionname='missing')
Stop profiling of a function.
Definition: Profiler.php:46
array
the array() calling protocol came about after MediaWiki 1.4rc1.
List of Api Query prop modules.
global
when a variable name is used in a it is silently declared as a new masking the global
Definition: design.txt:93
MWCryptRand\initialRandomState
initialRandomState()
Initialize an initial random state based off of whatever we can find.
Definition: MWCryptRand.php:65
MWCryptRand\$hashLength
$hashLength
The number of bytes outputted by the hash algorithm.
Definition: MWCryptRand.php:54
wfDebug
wfDebug( $text, $dest='all')
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:933
MWCryptRand\$strong
$strong
A boolean indicating whether the previous random generation was done using cryptographically strong r...
Definition: MWCryptRand.php:60
wfWikiID
wfWikiID()
Get an ASCII string identifying this wiki This is used as a prefix in memcached keys.
Definition: GlobalFunctions.php:3604
wfIsWindows
wfIsWindows()
Check if the operating system is Windows.
Definition: GlobalFunctions.php:2524
MWCryptRand
Definition: MWCryptRand.php:27
wfGetAllCallers
wfGetAllCallers( $limit=3)
Return a string consisting of callers in the stack.
Definition: GlobalFunctions.php:1958
$file
if(PHP_SAPI !='cli') $file
Definition: UtfNormalTest2.php:30
MWCryptRand\$algo
$algo
The hash algorithm being used.
Definition: MWCryptRand.php:49
MWCryptRand\singleton
static singleton()
Publicly exposed static methods.
Definition: MWCryptRand.php:467
MWCryptRand\MSEC_PER_BYTE
const MSEC_PER_BYTE
Number of milliseconds we want to spend generating each separate byte of the final generated bytes.
Definition: MWCryptRand.php:39
$path
$path
Definition: NoLocalSettings.php:35
as
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
MWCryptRand\hashAlgo
hashAlgo()
Decide on the best acceptable hash algorithm we have available for hash()
Definition: MWCryptRand.php:221
MWCryptRand\$singleton
static $singleton
Singleton instance for public use.
Definition: MWCryptRand.php:44
MWCryptRand\hash
hash( $data)
Generate an acceptably unstable one-way-hash of some text making use of the best hash algorithm that ...
Definition: MWCryptRand.php:267
MWCryptRand\realWasStrong
realWasStrong()
Definition: MWCryptRand.php:286
MWCryptRand\realGenerate
realGenerate( $bytes, $forceStrong=false)
Definition: MWCryptRand.php:297