MediaWiki  1.27.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 
50  protected $strong = null;
51 
56  protected function initialRandomState() {
57  // $_SERVER contains a variety of unstable user and system specific information
58  // It'll vary a little with each page, and vary even more with separate users
59  // It'll also vary slightly across different machines
60  $state = serialize( $_SERVER );
61 
62  // To try vary the system information of the state a bit more
63  // by including the system's hostname into the state
64  $state .= wfHostname();
65 
66  // Try to gather a little entropy from the different php rand sources
67  $state .= rand() . uniqid( mt_rand(), true );
68 
69  // Include some information about the filesystem's current state in the random state
70  $files = [];
71 
72  // We know this file is here so grab some info about ourselves
73  $files[] = __FILE__;
74 
75  // We must also have a parent folder, and with the usual file structure, a grandparent
76  $files[] = __DIR__;
77  $files[] = dirname( __DIR__ );
78 
79  // The config file is likely the most often edited file we know should
80  // be around so include its stat info into the state.
81  // The constant with its location will almost always be defined, as
82  // WebStart.php defines MW_CONFIG_FILE to $IP/LocalSettings.php unless
83  // being configured with MW_CONFIG_CALLBACK (e.g. the installer).
84  if ( defined( 'MW_CONFIG_FILE' ) ) {
85  $files[] = MW_CONFIG_FILE;
86  }
87 
88  foreach ( $files as $file ) {
89  MediaWiki\suppressWarnings();
90  $stat = stat( $file );
91  MediaWiki\restoreWarnings();
92  if ( $stat ) {
93  // stat() duplicates data into numeric and string keys so kill off all the numeric ones
94  foreach ( $stat as $k => $v ) {
95  if ( is_numeric( $k ) ) {
96  unset( $k );
97  }
98  }
99  // The absolute filename itself will differ from install to install so don't leave it out
100  $path = realpath( $file );
101  if ( $path !== false ) {
102  $state .= $path;
103  } else {
104  $state .= $file;
105  }
106  $state .= implode( '', $stat );
107  } else {
108  // The fact that the file isn't there is worth at least a
109  // minuscule amount of entropy.
110  $state .= '0';
111  }
112  }
113 
114  // Try and make this a little more unstable by including the varying process
115  // id of the php process we are running inside of if we are able to access it
116  if ( function_exists( 'getmypid' ) ) {
117  $state .= getmypid();
118  }
119 
120  // If available try to increase the instability of the data by throwing in
121  // the precise amount of memory that we happen to be using at the moment.
122  if ( function_exists( 'memory_get_usage' ) ) {
123  $state .= memory_get_usage( true );
124  }
125 
126  // It's mostly worthless but throw the wiki's id into the data for a little more variance
127  $state .= wfWikiID();
128 
129  // If we have a secret key set then throw it into the state as well
130  global $wgSecretKey;
131  if ( $wgSecretKey ) {
132  $state .= $wgSecretKey;
133  }
134 
135  return $state;
136  }
137 
145  protected function driftHash( $data ) {
146  // Minimum number of iterations (to avoid slow operations causing the
147  // loop to gather little entropy)
148  $minIterations = self::MIN_ITERATIONS;
149  // Duration of time to spend doing calculations (in seconds)
150  $duration = ( self::MSEC_PER_BYTE / 1000 ) * MWCryptHash::hashLength();
151  // Create a buffer to use to trigger memory operations
152  $bufLength = 10000000;
153  $buffer = str_repeat( ' ', $bufLength );
154  $bufPos = 0;
155 
156  // Iterate for $duration seconds or at least $minIterations number of iterations
157  $iterations = 0;
158  $startTime = microtime( true );
159  $currentTime = $startTime;
160  while ( $iterations < $minIterations || $currentTime - $startTime < $duration ) {
161  // Trigger some memory writing to trigger some bus activity
162  // This may create variance in the time between iterations
163  $bufPos = ( $bufPos + 13 ) % $bufLength;
164  $buffer[$bufPos] = ' ';
165  // Add the drift between this iteration and the last in as entropy
166  $nextTime = microtime( true );
167  $delta = (int)( ( $nextTime - $currentTime ) * 1000000 );
168  $data .= $delta;
169  // Every 100 iterations hash the data and entropy
170  if ( $iterations % 100 === 0 ) {
171  $data = sha1( $data );
172  }
173  $currentTime = $nextTime;
174  $iterations++;
175  }
176  $timeTaken = $currentTime - $startTime;
177  $data = MWCryptHash::hash( $data );
178 
179  wfDebug( __METHOD__ . ": Clock drift calculation " .
180  "(time-taken=" . ( $timeTaken * 1000 ) . "ms, " .
181  "iterations=$iterations, " .
182  "time-per-iteration=" . ( $timeTaken / $iterations * 1e6 ) . "us)\n" );
183 
184  return $data;
185  }
186 
191  protected function randomState() {
192  static $state = null;
193  if ( is_null( $state ) ) {
194  // Initialize the state with whatever unstable data we can find
195  // It's important that this data is hashed right afterwards to prevent
196  // it from being leaked into the output stream
197  $state = MWCryptHash::hash( $this->initialRandomState() );
198  }
199  // Generate a new random state based on the initial random state or previous
200  // random state by combining it with clock drift
201  $state = $this->driftHash( $state );
202 
203  return $state;
204  }
205 
209  public function realWasStrong() {
210  if ( is_null( $this->strong ) ) {
211  throw new MWException( __METHOD__ . ' called before generation of random data' );
212  }
213 
214  return $this->strong;
215  }
216 
220  public function realGenerate( $bytes, $forceStrong = false ) {
221 
222  wfDebug( __METHOD__ . ": Generating cryptographic random bytes for " .
223  wfGetAllCallers( 5 ) . "\n" );
224 
225  $bytes = floor( $bytes );
226  static $buffer = '';
227  if ( is_null( $this->strong ) ) {
228  // Set strength to false initially until we know what source data is coming from
229  $this->strong = true;
230  }
231 
232  if ( strlen( $buffer ) < $bytes ) {
233  // If available make use of mcrypt_create_iv URANDOM source to generate randomness
234  // On unix-like systems this reads from /dev/urandom but does it without any buffering
235  // and bypasses openbasedir restrictions, so it's preferable to reading directly
236  // On Windows starting in PHP 5.3.0 Windows' native CryptGenRandom is used to generate
237  // entropy so this is also preferable to just trying to read urandom because it may work
238  // on Windows systems as well.
239  if ( function_exists( 'mcrypt_create_iv' ) ) {
240  $rem = $bytes - strlen( $buffer );
241  $iv = mcrypt_create_iv( $rem, MCRYPT_DEV_URANDOM );
242  if ( $iv === false ) {
243  wfDebug( __METHOD__ . ": mcrypt_create_iv returned false.\n" );
244  } else {
245  $buffer .= $iv;
246  wfDebug( __METHOD__ . ": mcrypt_create_iv generated " . strlen( $iv ) .
247  " bytes of randomness.\n" );
248  }
249  }
250  }
251 
252  if ( strlen( $buffer ) < $bytes ) {
253  if ( function_exists( 'openssl_random_pseudo_bytes' ) ) {
254  $rem = $bytes - strlen( $buffer );
255  $openssl_bytes = openssl_random_pseudo_bytes( $rem, $openssl_strong );
256  if ( $openssl_bytes === false ) {
257  wfDebug( __METHOD__ . ": openssl_random_pseudo_bytes returned false.\n" );
258  } else {
259  $buffer .= $openssl_bytes;
260  wfDebug( __METHOD__ . ": openssl_random_pseudo_bytes generated " .
261  strlen( $openssl_bytes ) . " bytes of " .
262  ( $openssl_strong ? "strong" : "weak" ) . " randomness.\n" );
263  }
264  if ( strlen( $buffer ) >= $bytes ) {
265  // openssl tells us if the random source was strong, if some of our data was generated
266  // using it use it's say on whether the randomness is strong
267  $this->strong = !!$openssl_strong;
268  }
269  }
270  }
271 
272  // Only read from urandom if we can control the buffer size or were passed forceStrong
273  if ( strlen( $buffer ) < $bytes &&
274  ( function_exists( 'stream_set_read_buffer' ) || $forceStrong )
275  ) {
276  $rem = $bytes - strlen( $buffer );
277  if ( !function_exists( 'stream_set_read_buffer' ) && $forceStrong ) {
278  wfDebug( __METHOD__ . ": Was forced to read from /dev/urandom " .
279  "without control over the buffer size.\n" );
280  }
281  // /dev/urandom is generally considered the best possible commonly
282  // available random source, and is available on most *nix systems.
283  MediaWiki\suppressWarnings();
284  $urandom = fopen( "/dev/urandom", "rb" );
285  MediaWiki\restoreWarnings();
286 
287  // Attempt to read all our random data from urandom
288  // php's fread always does buffered reads based on the stream's chunk_size
289  // so in reality it will usually read more than the amount of data we're
290  // asked for and not storing that risks depleting the system's random pool.
291  // If stream_set_read_buffer is available set the chunk_size to the amount
292  // of data we need. Otherwise read 8k, php's default chunk_size.
293  if ( $urandom ) {
294  // php's default chunk_size is 8k
295  $chunk_size = 1024 * 8;
296  if ( function_exists( 'stream_set_read_buffer' ) ) {
297  // If possible set the chunk_size to the amount of data we need
298  stream_set_read_buffer( $urandom, $rem );
299  $chunk_size = $rem;
300  }
301  $random_bytes = fread( $urandom, max( $chunk_size, $rem ) );
302  $buffer .= $random_bytes;
303  fclose( $urandom );
304  wfDebug( __METHOD__ . ": /dev/urandom generated " . strlen( $random_bytes ) .
305  " bytes of randomness.\n" );
306 
307  if ( strlen( $buffer ) >= $bytes ) {
308  // urandom is always strong, set to true if all our data was generated using it
309  $this->strong = true;
310  }
311  } else {
312  wfDebug( __METHOD__ . ": /dev/urandom could not be opened.\n" );
313  }
314  }
315 
316  // If we cannot use or generate enough data from a secure source
317  // use this loop to generate a good set of pseudo random data.
318  // This works by initializing a random state using a pile of unstable data
319  // and continually shoving it through a hash along with a variable salt.
320  // We hash the random state with more salt to avoid the state from leaking
321  // out and being used to predict the /randomness/ that follows.
322  if ( strlen( $buffer ) < $bytes ) {
323  wfDebug( __METHOD__ .
324  ": Falling back to using a pseudo random state to generate randomness.\n" );
325  }
326  while ( strlen( $buffer ) < $bytes ) {
327  $buffer .= MWCryptHash::hmac( $this->randomState(), strval( mt_rand() ) );
328  // This code is never really cryptographically strong, if we use it
329  // at all, then set strong to false.
330  $this->strong = false;
331  }
332 
333  // Once the buffer has been filled up with enough random data to fulfill
334  // the request shift off enough data to handle the request and leave the
335  // unused portion left inside the buffer for the next request for random data
336  $generated = substr( $buffer, 0, $bytes );
337  $buffer = substr( $buffer, $bytes );
338 
339  wfDebug( __METHOD__ . ": " . strlen( $buffer ) .
340  " bytes of randomness leftover in the buffer.\n" );
341 
342  return $generated;
343  }
344 
348  public function realGenerateHex( $chars, $forceStrong = false ) {
349  // hex strings are 2x the length of raw binary so we divide the length in half
350  // odd numbers will result in a .5 that leads the generate() being 1 character
351  // short, so we use ceil() to ensure that we always have enough bytes
352  $bytes = ceil( $chars / 2 );
353  // Generate the data and then convert it to a hex string
354  $hex = bin2hex( $this->generate( $bytes, $forceStrong ) );
355 
356  // A bit of paranoia here, the caller asked for a specific length of string
357  // here, and it's possible (eg when given an odd number) that we may actually
358  // have at least 1 char more than they asked for. Just in case they made this
359  // call intending to insert it into a database that does truncation we don't
360  // want to give them too much and end up with their database and their live
361  // code having two different values because part of what we gave them is truncated
362  // hence, we strip out any run of characters longer than what we were asked for.
363  return substr( $hex, 0, $chars );
364  }
365 
372  protected static function singleton() {
373  if ( is_null( self::$singleton ) ) {
374  self::$singleton = new self;
375  }
376 
377  return self::$singleton;
378  }
379 
387  public static function wasStrong() {
388  return self::singleton()->realWasStrong();
389  }
390 
403  public static function generate( $bytes, $forceStrong = false ) {
404  return self::singleton()->realGenerate( $bytes, $forceStrong );
405  }
406 
419  public static function generateHex( $chars, $forceStrong = false ) {
420  return self::singleton()->realGenerateHex( $chars, $forceStrong );
421  }
422 }
const MIN_ITERATIONS
Minimum number of iterations we want to make in our drift calculations.
Definition: MWCryptRand.php:31
wfHostname()
Fetch server name for use in error reporting etc.
static wasStrong()
Return a boolean indicating whether or not the source used for cryptographic random bytes generation ...
$files
when a variable name is used in a it is silently declared as a new local masking the global
Definition: design.txt:93
static hash($data, $raw=true)
Generate an acceptably unstable one-way-hash of some text making use of the best hash algorithm that ...
Definition: MWCryptHash.php:94
wfDebug($text, $dest= 'all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
$strong
A boolean indicating whether the previous random generation was done using cryptographically strong r...
Definition: MWCryptRand.php:50
initialRandomState()
Initialize an initial random state based off of whatever we can find.
Definition: MWCryptRand.php:56
randomState()
Return a rolling random state initially build using data from unstable sources.
static singleton()
Publicly exposed static methods.
realGenerate($bytes, $forceStrong=false)
realGenerateHex($chars, $forceStrong=false)
const MSEC_PER_BYTE
Number of milliseconds we want to spend generating each separate byte of the final generated bytes...
Definition: MWCryptRand.php:39
$buffer
wfWikiID()
Get an ASCII string identifying this wiki This is used as a prefix in memcached keys.
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
static hashLength($raw=true)
Return the byte-length output of the hash algorithm we are using in self::hash and self::hmac...
Definition: MWCryptHash.php:77
wfGetAllCallers($limit=3)
Return a string consisting of callers in the stack.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
static hmac($data, $key, $raw=true)
Generate an acceptably unstable one-way-hmac of some text making use of the best hash algorithm that ...
static $singleton
Singleton instance for public use.
Definition: MWCryptRand.php:44
static generateHex($chars, $forceStrong=false)
Generate a run of (ideally) cryptographically random data and return it in hexadecimal string format...
driftHash($data)
Randomly hash data while mixing in clock drift data for randomness.
serialize()
Definition: ApiMessage.php:94
static generate($bytes, $forceStrong=false)
Generate a run of (ideally) cryptographically random data and return it in raw binary form...