MediaWiki  1.29.1
CryptRand.php
Go to the documentation of this file.
1 <?php
26 use Psr\Log\LoggerInterface;
27 
28 class CryptRand {
32  const MIN_ITERATIONS = 1000;
33 
40  const MSEC_PER_BYTE = 0.5;
41 
46  protected $strong = null;
47 
53  protected $randomFuncs = [];
54 
60  protected $randomFiles = [];
61 
65  protected $logger;
66 
67  public function __construct( array $randomFuncs, array $randomFiles, LoggerInterface $logger ) {
68  $this->randomFuncs = $randomFuncs;
69  $this->randomFiles = $randomFiles;
70  $this->logger = $logger;
71  }
72 
77  protected function initialRandomState() {
78  // $_SERVER contains a variety of unstable user and system specific information
79  // It'll vary a little with each page, and vary even more with separate users
80  // It'll also vary slightly across different machines
81  $state = serialize( $_SERVER );
82 
83  // Try to gather a little entropy from the different php rand sources
84  $state .= rand() . uniqid( mt_rand(), true );
85 
86  // Include some information about the filesystem's current state in the random state
87  $files = $this->randomFiles;
88 
89  // We know this file is here so grab some info about ourselves
90  $files[] = __FILE__;
91 
92  // We must also have a parent folder, and with the usual file structure, a grandparent
93  $files[] = __DIR__;
94  $files[] = dirname( __DIR__ );
95 
96  foreach ( $files as $file ) {
97  MediaWiki\suppressWarnings();
98  $stat = stat( $file );
99  MediaWiki\restoreWarnings();
100  if ( $stat ) {
101  // stat() duplicates data into numeric and string keys so kill off all the numeric ones
102  foreach ( $stat as $k => $v ) {
103  if ( is_numeric( $k ) ) {
104  unset( $k );
105  }
106  }
107  // The absolute filename itself will differ from install to install so don't leave it out
108  $path = realpath( $file );
109  if ( $path !== 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  foreach ( $this->randomFuncs as $randomFunc ) {
135  $state .= call_user_func( $randomFunc );
136  }
137 
138  return $state;
139  }
140 
148  protected function driftHash( $data ) {
149  // Minimum number of iterations (to avoid slow operations causing the
150  // loop to gather little entropy)
151  $minIterations = self::MIN_ITERATIONS;
152  // Duration of time to spend doing calculations (in seconds)
153  $duration = ( self::MSEC_PER_BYTE / 1000 ) * MWCryptHash::hashLength();
154  // Create a buffer to use to trigger memory operations
155  $bufLength = 10000000;
156  $buffer = str_repeat( ' ', $bufLength );
157  $bufPos = 0;
158 
159  // Iterate for $duration seconds or at least $minIterations number of iterations
160  $iterations = 0;
161  $startTime = microtime( true );
162  $currentTime = $startTime;
163  while ( $iterations < $minIterations || $currentTime - $startTime < $duration ) {
164  // Trigger some memory writing to trigger some bus activity
165  // This may create variance in the time between iterations
166  $bufPos = ( $bufPos + 13 ) % $bufLength;
167  $buffer[$bufPos] = ' ';
168  // Add the drift between this iteration and the last in as entropy
169  $nextTime = microtime( true );
170  $delta = (int)( ( $nextTime - $currentTime ) * 1000000 );
171  $data .= $delta;
172  // Every 100 iterations hash the data and entropy
173  if ( $iterations % 100 === 0 ) {
174  $data = sha1( $data );
175  }
176  $currentTime = $nextTime;
177  $iterations++;
178  }
179  $timeTaken = $currentTime - $startTime;
180  $data = MWCryptHash::hash( $data );
181 
182  $this->logger->debug( "Clock drift calculation " .
183  "(time-taken=" . ( $timeTaken * 1000 ) . "ms, " .
184  "iterations=$iterations, " .
185  "time-per-iteration=" . ( $timeTaken / $iterations * 1e6 ) . "us)" );
186 
187  return $data;
188  }
189 
194  protected function randomState() {
195  static $state = null;
196  if ( is_null( $state ) ) {
197  // Initialize the state with whatever unstable data we can find
198  // It's important that this data is hashed right afterwards to prevent
199  // it from being leaked into the output stream
200  $state = MWCryptHash::hash( $this->initialRandomState() );
201  }
202  // Generate a new random state based on the initial random state or previous
203  // random state by combining it with clock drift
204  $state = $this->driftHash( $state );
205 
206  return $state;
207  }
208 
216  public function wasStrong() {
217  if ( is_null( $this->strong ) ) {
218  throw new RuntimeException( __METHOD__ . ' called before generation of random data' );
219  }
220 
221  return $this->strong;
222  }
223 
236  public function generate( $bytes, $forceStrong = false ) {
237 
238  $bytes = floor( $bytes );
239  static $buffer = '';
240  if ( is_null( $this->strong ) ) {
241  // Set strength to false initially until we know what source data is coming from
242  $this->strong = true;
243  }
244 
245  if ( strlen( $buffer ) < $bytes ) {
246  // If available make use of PHP 7's random_bytes
247  // On Linux, getrandom syscall will be used if available.
248  // On Windows CryptGenRandom will always be used
249  // On other platforms, /dev/urandom will be used.
250  // Avoids polyfills from before php 7.0
251  // All error situations will throw Exceptions and or Errors
252  if ( PHP_VERSION_ID >= 70000
253  || ( defined( 'HHVM_VERSION_ID' ) && HHVM_VERSION_ID >= 31101 )
254  ) {
255  $rem = $bytes - strlen( $buffer );
256  $buffer .= random_bytes( $rem );
257  }
258  if ( strlen( $buffer ) >= $bytes ) {
259  $this->strong = true;
260  }
261  }
262 
263  if ( strlen( $buffer ) < $bytes ) {
264  // If available make use of mcrypt_create_iv URANDOM source to generate randomness
265  // On unix-like systems this reads from /dev/urandom but does it without any buffering
266  // and bypasses openbasedir restrictions, so it's preferable to reading directly
267  // On Windows starting in PHP 5.3.0 Windows' native CryptGenRandom is used to generate
268  // entropy so this is also preferable to just trying to read urandom because it may work
269  // on Windows systems as well.
270  if ( function_exists( 'mcrypt_create_iv' ) ) {
271  $rem = $bytes - strlen( $buffer );
272  $iv = mcrypt_create_iv( $rem, MCRYPT_DEV_URANDOM );
273  if ( $iv === false ) {
274  $this->logger->debug( "mcrypt_create_iv returned false." );
275  } else {
276  $buffer .= $iv;
277  $this->logger->debug( "mcrypt_create_iv generated " . strlen( $iv ) .
278  " bytes of randomness." );
279  }
280  }
281  }
282 
283  if ( strlen( $buffer ) < $bytes ) {
284  if ( function_exists( 'openssl_random_pseudo_bytes' ) ) {
285  $rem = $bytes - strlen( $buffer );
286  $openssl_bytes = openssl_random_pseudo_bytes( $rem, $openssl_strong );
287  if ( $openssl_bytes === false ) {
288  $this->logger->debug( "openssl_random_pseudo_bytes returned false." );
289  } else {
290  $buffer .= $openssl_bytes;
291  $this->logger->debug( "openssl_random_pseudo_bytes generated " .
292  strlen( $openssl_bytes ) . " bytes of " .
293  ( $openssl_strong ? "strong" : "weak" ) . " randomness." );
294  }
295  if ( strlen( $buffer ) >= $bytes ) {
296  // openssl tells us if the random source was strong, if some of our data was generated
297  // using it use it's say on whether the randomness is strong
298  $this->strong = !!$openssl_strong;
299  }
300  }
301  }
302 
303  // Only read from urandom if we can control the buffer size or were passed forceStrong
304  if ( strlen( $buffer ) < $bytes &&
305  ( function_exists( 'stream_set_read_buffer' ) || $forceStrong )
306  ) {
307  $rem = $bytes - strlen( $buffer );
308  if ( !function_exists( 'stream_set_read_buffer' ) && $forceStrong ) {
309  $this->logger->debug( "Was forced to read from /dev/urandom " .
310  "without control over the buffer size." );
311  }
312  // /dev/urandom is generally considered the best possible commonly
313  // available random source, and is available on most *nix systems.
314  MediaWiki\suppressWarnings();
315  $urandom = fopen( "/dev/urandom", "rb" );
316  MediaWiki\restoreWarnings();
317 
318  // Attempt to read all our random data from urandom
319  // php's fread always does buffered reads based on the stream's chunk_size
320  // so in reality it will usually read more than the amount of data we're
321  // asked for and not storing that risks depleting the system's random pool.
322  // If stream_set_read_buffer is available set the chunk_size to the amount
323  // of data we need. Otherwise read 8k, php's default chunk_size.
324  if ( $urandom ) {
325  // php's default chunk_size is 8k
326  $chunk_size = 1024 * 8;
327  if ( function_exists( 'stream_set_read_buffer' ) ) {
328  // If possible set the chunk_size to the amount of data we need
329  stream_set_read_buffer( $urandom, $rem );
330  $chunk_size = $rem;
331  }
332  $random_bytes = fread( $urandom, max( $chunk_size, $rem ) );
333  $buffer .= $random_bytes;
334  fclose( $urandom );
335  $this->logger->debug( "/dev/urandom generated " . strlen( $random_bytes ) .
336  " bytes of randomness." );
337 
338  if ( strlen( $buffer ) >= $bytes ) {
339  // urandom is always strong, set to true if all our data was generated using it
340  $this->strong = true;
341  }
342  } else {
343  $this->logger->debug( "/dev/urandom could not be opened." );
344  }
345  }
346 
347  // If we cannot use or generate enough data from a secure source
348  // use this loop to generate a good set of pseudo random data.
349  // This works by initializing a random state using a pile of unstable data
350  // and continually shoving it through a hash along with a variable salt.
351  // We hash the random state with more salt to avoid the state from leaking
352  // out and being used to predict the /randomness/ that follows.
353  if ( strlen( $buffer ) < $bytes ) {
354  $this->logger->debug( __METHOD__ .
355  ": Falling back to using a pseudo random state to generate randomness." );
356  }
357  while ( strlen( $buffer ) < $bytes ) {
358  $buffer .= MWCryptHash::hmac( $this->randomState(), strval( mt_rand() ) );
359  // This code is never really cryptographically strong, if we use it
360  // at all, then set strong to false.
361  $this->strong = false;
362  }
363 
364  // Once the buffer has been filled up with enough random data to fulfill
365  // the request shift off enough data to handle the request and leave the
366  // unused portion left inside the buffer for the next request for random data
367  $generated = substr( $buffer, 0, $bytes );
368  $buffer = substr( $buffer, $bytes );
369 
370  $this->logger->debug( strlen( $buffer ) .
371  " bytes of randomness leftover in the buffer." );
372 
373  return $generated;
374  }
375 
388  public function generateHex( $chars, $forceStrong = false ) {
389  // hex strings are 2x the length of raw binary so we divide the length in half
390  // odd numbers will result in a .5 that leads the generate() being 1 character
391  // short, so we use ceil() to ensure that we always have enough bytes
392  $bytes = ceil( $chars / 2 );
393  // Generate the data and then convert it to a hex string
394  $hex = bin2hex( $this->generate( $bytes, $forceStrong ) );
395 
396  // A bit of paranoia here, the caller asked for a specific length of string
397  // here, and it's possible (eg when given an odd number) that we may actually
398  // have at least 1 char more than they asked for. Just in case they made this
399  // call intending to insert it into a database that does truncation we don't
400  // want to give them too much and end up with their database and their live
401  // code having two different values because part of what we gave them is truncated
402  // hence, we strip out any run of characters longer than what we were asked for.
403  return substr( $hex, 0, $chars );
404  }
405 }
CryptRand\MSEC_PER_BYTE
const MSEC_PER_BYTE
Number of milliseconds we want to spend generating each separate byte of the final generated bytes.
Definition: CryptRand.php:40
MWCryptHash\hmac
static hmac( $data, $key, $raw=true)
Generate an acceptably unstable one-way-hmac of some text making use of the best hash algorithm that ...
Definition: MWCryptHash.php:106
use
as see the revision history and available at free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to use
Definition: MIT-LICENSE.txt:10
CryptRand\$logger
LoggerInterface $logger
Definition: CryptRand.php:65
MWCryptHash\hash
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:93
serialize
serialize()
Definition: ApiMessage.php:177
CryptRand\$strong
$strong
A boolean indicating whether the previous random generation was done using cryptographically strong r...
Definition: CryptRand.php:46
MWCryptHash\hashLength
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:76
php
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
CryptRand\driftHash
driftHash( $data)
Randomly hash data while mixing in clock drift data for randomness.
Definition: CryptRand.php:148
CryptRand\$randomFuncs
callable[] $randomFuncs
List of functions to call to generate some random state.
Definition: CryptRand.php:53
CryptRand\$randomFiles
string[] $randomFiles
List of files to generate some random state from.
Definition: CryptRand.php:60
CryptRand\initialRandomState
initialRandomState()
Initialize an initial random state based off of whatever we can find.
Definition: CryptRand.php:77
CryptRand\randomState
randomState()
Return a rolling random state initially build using data from unstable sources.
Definition: CryptRand.php:194
CryptRand\generateHex
generateHex( $chars, $forceStrong=false)
Generate a run of (ideally) cryptographically random data and return it in hexadecimal string format.
Definition: CryptRand.php:388
CryptRand\wasStrong
wasStrong()
Return a boolean indicating whether or not the source used for cryptographic random bytes generation ...
Definition: CryptRand.php:216
CryptRand\generate
generate( $bytes, $forceStrong=false)
Generate a run of (ideally) cryptographically random data and return it in raw binary form.
Definition: CryptRand.php:236
CryptRand\MIN_ITERATIONS
const MIN_ITERATIONS
Minimum number of iterations we want to make in our drift calculations.
Definition: CryptRand.php:32
$path
$path
Definition: NoLocalSettings.php:26
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
CryptRand\__construct
__construct(array $randomFuncs, array $randomFiles, LoggerInterface $logger)
Definition: CryptRand.php:67
CryptRand
Definition: CryptRand.php:28
$buffer
$buffer
Definition: mwdoc-filter.php:48
array
the array() calling protocol came about after MediaWiki 1.4rc1.