Completed
Branch master (9259dd)
by
unknown
27:26
created

MWCryptRand::realGenerate()   F

Complexity

Conditions 20
Paths 2112

Size

Total Lines 124
Code Lines 64

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 20
eloc 64
nc 2112
nop 2
dl 0
loc 124
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * A cryptographic random generator class used for generating secret keys
4
 *
5
 * This is based in part on Drupal code as well as what we used in our own code
6
 * prior to introduction of this class.
7
 *
8
 * This program is free software; you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation; either version 2 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
 * GNU General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License along
19
 * with this program; if not, write to the Free Software Foundation, Inc.,
20
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21
 * http://www.gnu.org/copyleft/gpl.html
22
 *
23
 * @author Daniel Friesen
24
 * @file
25
 */
26
27
class MWCryptRand {
28
	/**
29
	 * Minimum number of iterations we want to make in our drift calculations.
30
	 */
31
	const MIN_ITERATIONS = 1000;
32
33
	/**
34
	 * Number of milliseconds we want to spend generating each separate byte
35
	 * of the final generated bytes.
36
	 * This is used in combination with the hash length to determine the duration
37
	 * we should spend doing drift calculations.
38
	 */
39
	const MSEC_PER_BYTE = 0.5;
40
41
	/**
42
	 * Singleton instance for public use
43
	 */
44
	protected static $singleton = null;
45
46
	/**
47
	 * A boolean indicating whether the previous random generation was done using
48
	 * cryptographically strong random number generator or not.
49
	 */
50
	protected $strong = null;
51
52
	/**
53
	 * Initialize an initial random state based off of whatever we can find
54
	 * @return string
55
	 */
56
	protected function initialRandomState() {
0 ignored issues
show
Coding Style introduced by
initialRandomState uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
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
138
	/**
139
	 * Randomly hash data while mixing in clock drift data for randomness
140
	 *
141
	 * @param string $data The data to randomly hash.
142
	 * @return string The hashed bytes
143
	 * @author Tim Starling
144
	 */
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
187
	/**
188
	 * Return a rolling random state initially build using data from unstable sources
189
	 * @return string A new weak random state
190
	 */
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
206
	/**
207
	 * @see self::wasStrong()
208
	 */
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
217
	/**
218
	 * @see self::generate()
219
	 */
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(), 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
345
	/**
346
	 * @see self::generateHex()
347
	 */
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
366
	/** Publicly exposed static methods **/
367
368
	/**
369
	 * Return a singleton instance of MWCryptRand
370
	 * @return MWCryptRand
371
	 */
372
	protected static function singleton() {
373
		if ( is_null( self::$singleton ) ) {
374
			self::$singleton = new self;
375
		}
376
377
		return self::$singleton;
378
	}
379
380
	/**
381
	 * Return a boolean indicating whether or not the source used for cryptographic
382
	 * random bytes generation in the previously run generate* call
383
	 * was cryptographically strong.
384
	 *
385
	 * @return bool Returns true if the source was strong, false if not.
386
	 */
387
	public static function wasStrong() {
388
		return self::singleton()->realWasStrong();
389
	}
390
391
	/**
392
	 * Generate a run of (ideally) cryptographically random data and return
393
	 * it in raw binary form.
394
	 * You can use MWCryptRand::wasStrong() if you wish to know if the source used
395
	 * was cryptographically strong.
396
	 *
397
	 * @param int $bytes The number of bytes of random data to generate
398
	 * @param bool $forceStrong Pass true if you want generate to prefer cryptographically
399
	 *                          strong sources of entropy even if reading from them may steal
400
	 *                          more entropy from the system than optimal.
401
	 * @return string Raw binary random data
402
	 */
403
	public static function generate( $bytes, $forceStrong = false ) {
404
		return self::singleton()->realGenerate( $bytes, $forceStrong );
405
	}
406
407
	/**
408
	 * Generate a run of (ideally) cryptographically random data and return
409
	 * it in hexadecimal string format.
410
	 * You can use MWCryptRand::wasStrong() if you wish to know if the source used
411
	 * was cryptographically strong.
412
	 *
413
	 * @param int $chars The number of hex chars of random data to generate
414
	 * @param bool $forceStrong Pass true if you want generate to prefer cryptographically
415
	 *                          strong sources of entropy even if reading from them may steal
416
	 *                          more entropy from the system than optimal.
417
	 * @return string Hexadecimal random data
418
	 */
419
	public static function generateHex( $chars, $forceStrong = false ) {
420
		return self::singleton()->realGenerateHex( $chars, $forceStrong );
421
	}
422
}
423