Passed
Pull Request — release-2.1 (#5077)
by Mathias
05:16
created

sha1_smf()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
c 0
b 0
f 0
nop 1
dl 0
loc 15
rs 10
nc 3
1
<?php
2
3
/**
4
 * This file provides compatibility functions and code for older versions of
5
 * PHP, such as the sha1() function, missing extensions, or 64-bit vs 32-bit
6
 * systems. It is only included for those older versions or when the respective
7
 * extension or function cannot be found.
8
 *
9
 * Simple Machines Forum (SMF)
10
 *
11
 * @package SMF
12
 * @author Simple Machines http://www.simplemachines.org
13
 * @copyright 2018 Simple Machines and individual contributors
14
 * @license http://www.simplemachines.org/about/smf/license.php BSD
15
 *
16
 * @version 2.1 Beta 4
17
 */
18
19
if (!defined('SMF'))
20
	die('No direct access...');
21
22
23
/**
24
 * Define the old SMF sha1 function. Uses mhash if available
25
 * @param string $str The string
26
 * @return string The sha1 hashed version of $str
27
 */
28
function sha1_smf($str)
29
{
30
	// If we have mhash loaded in, use it instead!
31
	if (function_exists('mhash') && defined('MHASH_SHA1'))
32
		return bin2hex(mhash(MHASH_SHA1, $str));
33
34
	$nblk = (strlen($str) + 8 >> 6) + 1;
35
	$blks = array_pad(array(), $nblk * 16, 0);
36
37
	for ($i = 0; $i < strlen($str); $i++)
38
		$blks[$i >> 2] |= ord($str{$i}) << (24 - ($i % 4) * 8);
39
40
	$blks[$i >> 2] |= 0x80 << (24 - ($i % 4) * 8);
41
42
	return sha1_core($blks, strlen($str) * 8);
43
}
44
45
/**
46
 * This is the core SHA-1 calculation routine, used by sha1().
47
 * @param string $x
48
 * @param int $len
49
 * @return string
50
 */
51
function sha1_core($x, $len)
52
{
53
	@$x[$len >> 5] |= 0x80 << (24 - $len % 32);
54
	$x[(($len + 64 >> 9) << 4) + 15] = $len;
55
56
	$w = array();
57
	$a = 1732584193;
58
	$b = -271733879;
59
	$c = -1732584194;
60
	$d = 271733878;
61
	$e = -1009589776;
62
63
	for ($i = 0, $n = count($x); $i < $n; $i += 16)
64
	{
65
		$olda = $a;
66
		$oldb = $b;
67
		$oldc = $c;
68
		$oldd = $d;
69
		$olde = $e;
70
71
		for ($j = 0; $j < 80; $j++)
72
		{
73
			if ($j < 16)
74
				$w[$j] = isset($x[$i + $j]) ? $x[$i + $j] : 0;
75
			else
76
				$w[$j] = sha1_rol($w[$j - 3] ^ $w[$j - 8] ^ $w[$j - 14] ^ $w[$j - 16], 1);
77
78
			$t = sha1_rol($a, 5) + sha1_ft($j, $b, $c, $d) + $e + $w[$j] + sha1_kt($j);
79
			$e = $d;
80
			$d = $c;
81
			$c = sha1_rol($b, 30);
82
			$b = $a;
83
			$a = $t;
84
		}
85
86
		$a += $olda;
87
		$b += $oldb;
88
		$c += $oldc;
89
		$d += $oldd;
90
		$e += $olde;
91
	}
92
93
	return sprintf('%08x%08x%08x%08x%08x', $a, $b, $c, $d, $e);
94
}
95
96
/**
97
 * Helper function for the core SHA-1 calculation
98
 * @param int $t
99
 * @param int $b
100
 * @param int $c
101
 * @param int $d
102
 * @return int
103
 */
104
function sha1_ft($t, $b, $c, $d)
105
{
106
	if ($t < 20)
107
		return ($b & $c) | ((~$b) & $d);
108
	if ($t < 40)
109
		return $b ^ $c ^ $d;
110
	if ($t < 60)
111
		return ($b & $c) | ($b & $d) | ($c & $d);
112
113
	return $b ^ $c ^ $d;
114
}
115
116
/**
117
 * Helper function for the core SHA-1 calculation
118
 * @param int $t
119
 * @return int 1518500249, 1859775393, -1894007588 or -899497514 depending on the value of $t
120
 */
121
function sha1_kt($t)
122
{
123
	return $t < 20 ? 1518500249 : ($t < 40 ? 1859775393 : ($t < 60 ? -1894007588 : -899497514));
124
}
125
126
/**
127
 * Helper function for the core SHA-1 calculation
128
 * @param int $num
129
 * @param int $cnt
130
 * @return int
131
 */
132
function sha1_rol($num, $cnt)
133
{
134
	// Unfortunately, PHP uses unsigned 32-bit longs only.  So we have to kludge it a bit.
135
	if ($num & 0x80000000)
136
		$a = ($num >> 1 & 0x7fffffff) >> (31 - $cnt);
137
	else
138
		$a = $num >> (32 - $cnt);
139
140
	return ($num << $cnt) | $a;
141
}
142
143
/**
144
 * Available since: (PHP 5)
145
 * If the optional raw_output is set to TRUE, then the sha1 digest is instead returned in raw binary format with a length of 20,
146
 * otherwise the returned value is a 40-character hexadecimal number.
147
 * @param string $text The text to hash
148
 * @return string The sha1 hash of $text
149
 */
150
function sha1_raw($text)
151
{
152
	return sha1($text, true);
153
}
154
155
/**
156
 * Compatibility function.
157
 * crc32 doesn't work as expected on 64-bit functions - make our own.
158
 * https://php.net/crc32#79567
159
 * @param string $number
160
 * @return string The crc32 polynomial of $number
161
 */
162
if (!function_exists('smf_crc32'))
163
{
164
	function smf_crc32($number)
165
	{
166
		$crc = crc32($number);
167
168
		if ($crc & 0x80000000)
169
		{
170
			$crc ^= 0xffffffff;
171
			$crc += 1;
172
			$crc = -$crc;
173
		}
174
175
		return $crc;
176
	}
177
}
178
179
/**
180
 * Random_* Compatibility Library
181
 * for using the new PHP 7 random_* API in PHP 5 projects
182
 *
183
 * @version 2.0.17
184
 * @released 2018-07-04
185
 *
186
 * The MIT License (MIT)
187
 *
188
 * Copyright (c) 2015 - 2018 Paragon Initiative Enterprises
189
 *
190
 * Permission is hereby granted, free of charge, to any person obtaining a copy
191
 * of this software and associated documentation files (the "Software"), to deal
192
 * in the Software without restriction, including without limitation the rights
193
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
194
 * copies of the Software, and to permit persons to whom the Software is
195
 * furnished to do so, subject to the following conditions:
196
 *
197
 * The above copyright notice and this permission notice shall be included in
198
 * all copies or substantial portions of the Software.
199
 *
200
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
201
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
202
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
203
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
204
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
205
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
206
 * SOFTWARE.
207
 */
208
if (!is_callable('random_bytes') || !is_callable('random_int')
209
{
210
	if (!is_callable('RandomCompat_strlen'))
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected T_IF on line 210 at column 1
Loading history...
211
	{
212
		if (defined('MB_OVERLOAD_STRING') && ((int) ini_get('mbstring.func_overload')) & MB_OVERLOAD_STRING)
213
		{
214
			/**
215
			 * strlen() implementation that isn't brittle to mbstring.func_overload
216
			 *
217
			 * This version uses mb_strlen() in '8bit' mode to treat strings as raw
218
			 * binary rather than UTF-8, ISO-8859-1, etc
219
			 *
220
			 * @param string $binary_string
221
			 *
222
			 * @throws TypeError
223
			 *
224
			 * @return int
225
			 */
226
			function RandomCompat_strlen($binary_string)
227
			{
228
				if (!is_string($binary_string))
229
				{
230
					throw new TypeError(
231
						'RandomCompat_strlen() expects a string'
232
					);
233
				}
234
235
				return (int) mb_strlen($binary_string, '8bit');
236
			}
237
		}
238
		else
239
		{
240
			/**
241
			 * strlen() implementation that isn't brittle to mbstring.func_overload
242
			 *
243
			 * This version just used the default strlen()
244
			 *
245
			 * @param string $binary_string
246
			 *
247
			 * @throws TypeError
248
			 *
249
			 * @return int
250
			 */
251
			function RandomCompat_strlen($binary_string)
252
			{
253
				if (!is_string($binary_string))
254
				{
255
					throw new TypeError(
256
						'RandomCompat_strlen() expects a string'
257
					);
258
				}
259
				return (int) strlen($binary_string);
260
			}
261
		}
262
	}
263
264
	if (!is_callable('RandomCompat_substr'))
265
	{
266
		if (defined('MB_OVERLOAD_STRING') && ((int) ini_get('mbstring.func_overload')) & MB_OVERLOAD_STRING)
267
		{
268
			/**
269
			 * substr() implementation that isn't brittle to mbstring.func_overload
270
			 *
271
			 * This version uses mb_substr() in '8bit' mode to treat strings as raw
272
			 * binary rather than UTF-8, ISO-8859-1, etc
273
			 *
274
			 * @param string $binary_string
275
			 * @param int $start
276
			 * @param int|null $length (optional)
277
			 *
278
			 * @throws TypeError
279
			 *
280
			 * @return string
281
			 */
282
			function RandomCompat_substr($binary_string, $start, $length = null)
283
			{
284
				if (!is_string($binary_string))
285
				{
286
					throw new TypeError(
287
						'RandomCompat_substr(): First argument should be a string'
288
					);
289
				}
290
291
				if (!is_int($start))
292
				{
293
					throw new TypeError(
294
						'RandomCompat_substr(): Second argument should be an integer'
295
					);
296
				}
297
298
				if ($length === null)
299
				{
300
					/**
301
					 * mb_substr($str, 0, NULL, '8bit') returns an empty string on
302
					 * PHP 5.3, so we have to find the length ourselves.
303
					 */
304
					/** @var int $length */
305
					$length = RandomCompat_strlen($binary_string) - $start;
306
				}
307
				elseif (!is_int($length))
308
				{
309
					throw new TypeError(
310
						'RandomCompat_substr(): Third argument should be an integer, or omitted'
311
					);
312
				}
313
314
				// Consistency with PHP's behavior
315
				if ($start === RandomCompat_strlen($binary_string) && $length === 0)
316
				{
317
					return '';
318
				}
319
320
				if ($start > RandomCompat_strlen($binary_string))
321
				{
322
					return '';
323
				}
324
325
				return (string) mb_substr(
326
					(string) $binary_string,
327
					(int) $start,
328
					(int) $length,
329
					'8bit'
330
				);
331
			}
332
		}
333
		else
334
		{
335
			/**
336
			 * substr() implementation that isn't brittle to mbstring.func_overload
337
			 *
338
			 * This version just uses the default substr()
339
			 *
340
			 * @param string $binary_string
341
			 * @param int $start
342
			 * @param int|null $length (optional)
343
			 *
344
			 * @throws TypeError
345
			 *
346
			 * @return string
347
			 */
348
			function RandomCompat_substr($binary_string, $start, $length = null)
349
			{
350
				if (!is_string($binary_string))
351
				{
352
					throw new TypeError(
353
						'RandomCompat_substr(): First argument should be a string'
354
					);
355
				}
356
357
				if (!is_int($start))
358
				{
359
					throw new TypeError(
360
						'RandomCompat_substr(): Second argument should be an integer'
361
					);
362
				}
363
364
				if ($length !== null)
365
				{
366
					if (!is_int($length))
367
					{
368
						throw new TypeError(
369
							'RandomCompat_substr(): Third argument should be an integer, or omitted'
370
						);
371
					}
372
373
					return (string) substr(
374
						(string )$binary_string,
375
						(int) $start,
376
						(int) $length
377
					);
378
				}
379
380
				return (string) substr(
381
					(string) $binary_string,
382
					(int) $start
383
				);
384
			}
385
		}
386
	}
387
388
	if (!is_callable('RandomCompat_intval'))
389
	{
390
		/**
391
		 * Cast to an integer if we can, safely.
392
		 * 
393
		 * If you pass it a float in the range (~PHP_INT_MAX, PHP_INT_MAX)
394
		 * (non-inclusive), it will sanely cast it to an int. If you it's equal to
395
		 * ~PHP_INT_MAX or PHP_INT_MAX, we let it fail as not an integer. Floats 
396
		 * lose precision, so the <= and => operators might accidentally let a float
397
		 * through.
398
		 * 
399
		 * @param int|float $number    The number we want to convert to an int
400
		 * @param bool      $fail_open Set to true to not throw an exception
401
		 * 
402
		 * @return float|int
403
		 * @psalm-suppress InvalidReturnType
404
		 *
405
		 * @throws TypeError
406
		 */
407
		function RandomCompat_intval($number, $fail_open = false)
408
		{
409
			if (is_int($number) || is_float($number))
410
			{
411
				$number += 0;
412
			}
413
			elseif (is_numeric($number))
414
			{
415
				/** @psalm-suppress InvalidOperand */
416
				$number += 0;
417
			}
418
			/** @var int|float $number */
419
420
			if (is_float($number) && $number > ~PHP_INT_MAX && $number < PHP_INT_MAX)
421
			{
422
				$number = (int) $number;
423
			}
424
425
			if (is_int($number))
426
			{
427
				return (int) $number;
428
			}
429
			elseif (!$fail_open)
430
			{
431
				throw new TypeError(
432
					'Expected an integer.'
433
				);
434
			}
435
436
			return $number;
437
		}
438
	}
439
}
440
441
if (!is_callable('random_bytes'))
442
{
443
	/**
444
	 * Reading directly from /dev/urandom:
445
	 */
446
	if (DIRECTORY_SEPARATOR === '/')
447
	{
448
		// DIRECTORY_SEPARATOR === '/' on Unix-like OSes -- this is a fast
449
		// way to exclude Windows.
450
		/**
451
		 * Unless open_basedir is enabled, use /dev/urandom for
452
		 * random numbers in accordance with best practices
453
		 *
454
		 * Why we use /dev/urandom and not /dev/random
455
		 * @ref http://sockpuppet.org/blog/2014/02/25/safely-generate-random-numbers
456
		 *
457
		 * @param int $bytes
458
		 *
459
		 * @return string
460
		 */
461
		function random_bytes($bytes)
462
		{
463
			/** @var resource $fp */
464
			static $fp = null;
465
466
			/**
467
			 * This block should only be run once
468
			 */
469
			if (empty($fp))
470
			{
471
				/**
472
				 * We use /dev/urandom if it is a char device.
473
				 * We never fall back to /dev/random
474
				 */
475
				/** @var resource|bool $fp */
476
				$fp = fopen('/dev/urandom', 'rb');
477
				if (is_resource($fp))
478
				{
479
					/** @var array<string, int> $st */
480
					$st = fstat($fp);
481
					if (($st['mode'] & 0170000) !== 020000)
482
					{
483
						fclose($fp);
484
						$fp = false;
485
					}
486
				}
487
488
				if (is_resource($fp))
489
				{
490
					/**
491
					 * stream_set_read_buffer() does not exist in HHVM
492
					 *
493
					 * If we don't set the stream's read buffer to 0, PHP will
494
					 * internally buffer 8192 bytes, which can waste entropy
495
					 *
496
					 * stream_set_read_buffer returns 0 on success
497
					 */
498
					if (is_callable('stream_set_read_buffer'))
499
						stream_set_read_buffer($fp, RANDOM_COMPAT_READ_BUFFER);
500
501
					if (is_callable('stream_set_chunk_size'))
502
						stream_set_chunk_size($fp, RANDOM_COMPAT_READ_BUFFER);
503
				}
504
			}
505
506
			try
507
			{
508
				/** @var int $bytes */
509
				$bytes = RandomCompat_intval($bytes);
510
			}
511
			catch (TypeError $ex)
512
			{
513
				log_error('random_bytes(): $bytes must be an integer');
514
			}
515
516
			if ($bytes < 1)
517
			{
518
				log_error('Length must be greater than 0');
519
			}
520
521
			/**
522
			 * This if() block only runs if we managed to open a file handle
523
			 *
524
			 * It does not belong in an else {} block, because the above
525
			 * if (empty($fp)) line is logic that should only be run once per
526
			 * page load.
527
			 */
528
			if (is_resource($fp))
529
			{
530
				/**
531
				 * @var int
532
				 */
533
				$remaining = $bytes;
534
535
				/**
536
				 * @var string|bool
537
				 */
538
				$buf = '';
539
540
				/**
541
				 * We use fread() in a loop to protect against partial reads
542
				 */
543
				do
544
				{
545
					/**
546
					 * @var string|bool
547
					 */
548
					$read = fread($fp, $remaining);
549
					if (!is_string($read))
550
					{
551
						if ($read === false)
552
						{
553
							/**
554
							 * We cannot safely read from the file. Exit the
555
							 * do-while loop and trigger the exception condition
556
							 *
557
							 * @var string|bool
558
							 */
559
							$buf = false;
560
							break;
561
						}
562
					}
563
					/**
564
					 * Decrease the number of bytes returned from remaining
565
					 */
566
					$remaining -= RandomCompat_strlen($read);
567
					
568
					/**
569
					 * @var string|bool
570
					 */
571
					$buf = $buf . $read;
572
573
				} while ($remaining > 0);
574
575
				/**
576
				 * Is our result valid?
577
				 */
578
				if (is_string($buf))
579
				{
580
					if (RandomCompat_strlen($buf) === $bytes)
581
					{
582
						/**
583
						 * Return our random entropy buffer here:
584
						 */
585
						return $buf;
586
					}
587
				}
588
			}
589
590
			/**
591
			 * If we reach here, PHP has failed us.
592
			 */
593
			log_error('Error reading from source device');
594
		}
595
	}
596
597
	/**
598
	 * mcrypt_create_iv()
599
	 *
600
	 * We only want to use mcypt_create_iv() if:
601
	 *
602
	 * - random_bytes() hasn't already been defined
603
	 * - the mcrypt extensions is loaded
604
	 * - One of these two conditions is true:
605
	 *   - We're on Windows (DIRECTORY_SEPARATOR !== '/')
606
	 *   - We're not on Windows and /dev/urandom is readabale
607
	 *     (i.e. we're not in a chroot jail)
608
	 * - Special case:
609
	 *   - If we're not on Windows, but the PHP version is between
610
	 *     5.6.10 and 5.6.12, we don't want to use mcrypt. It will
611
	 *     hang indefinitely. This is bad.
612
	 *   - If we're on Windows, we want to use PHP >= 5.3.7 or else
613
	 *     we get insufficient entropy errors.
614
	 */
615
	if (!is_callable('random_bytes') &&
616
		// Windows on PHP < 5.3.7 is broken, but non-Windows is not known to be.
617
		(DIRECTORY_SEPARATOR === '/' || PHP_VERSION_ID >= 50307)
618
		&&
619
		// Prevent this code from hanging indefinitely on non-Windows;
620
		// see https://bugs.php.net/bug.php?id=69833
621
		(
622
			DIRECTORY_SEPARATOR !== '/' ||
623
			(PHP_VERSION_ID <= 50609 || PHP_VERSION_ID >= 50613)
624
		)
625
		&& extension_loaded('mcrypt'))
626
	{
627
		/**
628
		 * Powered by ext/mcrypt (and thankfully NOT libmcrypt)
629
		 *
630
		 * @ref https://bugs.php.net/bug.php?id=55169
631
		 * @ref https://github.com/php/php-src/blob/c568ffe5171d942161fc8dda066bce844bdef676/ext/mcrypt/mcrypt.c#L1321-L1386
632
		 *
633
		 * @param int $bytes
634
		 *
635
		 * @throws Exception
636
		 *
637
		 * @return string
638
		 */
639
		function random_bytes($bytes)
640
		{
641
			try
642
			{
643
				/** @var int $bytes */
644
				$bytes = RandomCompat_intval($bytes);
645
			}
646
			catch (TypeError $ex)
647
				log_error('random_bytes(): $bytes must be an integer');
648
			
649
			if ($bytes < 1)
650
				log_error('Length must be greater than 0');
651
652
			/** @var string|bool $buf */
653
			$buf = @mcrypt_create_iv((int) $bytes, (int) MCRYPT_DEV_URANDOM);
654
655
			if (is_string($buf) && RandomCompat_strlen($buf) === $bytes)
656
			{
657
				/**
658
				 * Return our random entropy buffer here:
659
				 */
660
				return $buf;
661
			}
662
663
			/**
664
			 * If we reach here, PHP has failed us.
665
			 */
666
			log_error('Could not gather sufficient random data');
667
		}
668
	}
669
670
	/**
671
	 * This is a Windows-specific fallback, for when the mcrypt extension
672
	 * isn't loaded.
673
	 */
674
	if (!is_callable('random_bytes') && extension_loaded('com_dotnet') && class_exists('COM'))
675
	{
676
		$RandomCompat_disabled_classes = preg_split(
677
			'#\s*,\s*#',
678
			strtolower(ini_get('disable_classes'))
679
		);
680
681
		if (!in_array('com', $RandomCompat_disabled_classes))
682
		{
683
			try
684
			{
685
				$RandomCompatCOMtest = new COM('CAPICOM.Utilities.1');
686
				if (method_exists($RandomCompatCOMtest, 'GetRandom'))
687
				{
688
					/**
689
					 * Windows with PHP < 5.3.0 will not have the function
690
					 * openssl_random_pseudo_bytes() available, so let's use
691
					 * CAPICOM to work around this deficiency.
692
					 *
693
					 * @param int $bytes
694
					 *
695
					 * @return string
696
					 */
697
					function random_bytes($bytes)
698
					{
699
						try
700
						{
701
							/** @var int $bytes */
702
							$bytes = RandomCompat_intval($bytes);
703
						}
704
						catch (TypeError $ex)
705
							log_error('random_bytes(): $bytes must be an integer');
706
707
						if ($bytes < 1)
708
							log_error('Length must be greater than 0');
709
710
						/** @var string $buf */
711
						$buf = '';
712
						if (!class_exists('COM'))
713
							log_error('COM does not exist');
714
715
						/** @var COM $util */
716
						$util = new COM('CAPICOM.Utilities.1');
717
						$execCount = 0;
718
719
						/**
720
						 * Let's not let it loop forever. If we run N times and fail to
721
						 * get N bytes of random data, then CAPICOM has failed us.
722
						 */
723
						do
724
						{
725
							$buf .= base64_decode((string) $util->GetRandom($bytes, 0));
726
							if (RandomCompat_strlen($buf) >= $bytes)
727
							{
728
								/**
729
								 * Return our random entropy buffer here:
730
								 */
731
								return (string) RandomCompat_substr($buf, 0, $bytes);
732
							}
733
							++$execCount;
734
735
						} while ($execCount < $bytes);
736
737
						/**
738
						 * If we reach here, PHP has failed us.
739
						 */
740
						log_error('Could not gather sufficient random data');
741
					}
742
				}
743
			}
744
			catch (com_exception $e)
745
			{
746
				// Don't try to use it.
747
			}
748
		}
749
		$RandomCompat_disabled_classes = null;
750
		$RandomCompatCOMtest = null;
751
	}
752
753
	/**
754
	 * throw new Exception
755
	 */
756
	if (!is_callable('random_bytes'))
757
	{
758
		/**
759
		 * We don't have any more options, so let's throw an exception right now
760
		 * and hope the developer won't let it fail silently.
761
		 *
762
		 * @param mixed $length
763
		 * @psalm-suppress InvalidReturnType
764
		 * @throws Exception
765
		 * @return string
766
		 */
767
		function random_bytes($length)
768
		{
769
			unset($length); // Suppress "variable not used" warnings.
770
			log_error('There is no suitable CSPRNG installed on your system');
771
			return '';
772
		}
773
	}
774
}
775
776
if (!is_callable('random_int'))
777
{
778
	 /**
779
	 * Fetch a random integer between $min and $max inclusive
780
	 *
781
	 * @param int $min
782
	 * @param int $max
783
	 *
784
	 * @throws Exception
785
	 *
786
	 * @return int
787
	 */
788
	function random_int($min, $max)
789
	{
790
		/**
791
		 * Type and input logic checks
792
		 *
793
		 * If you pass it a float in the range (~PHP_INT_MAX, PHP_INT_MAX)
794
		 * (non-inclusive), it will sanely cast it to an int. If you it's equal to
795
		 * ~PHP_INT_MAX or PHP_INT_MAX, we let it fail as not an integer. Floats
796
		 * lose precision, so the <= and => operators might accidentally let a float
797
		 * through.
798
		 */
799
800
		try
801
		{
802
			/** @var int $min */
803
			$min = RandomCompat_intval($min);
804
		}
805
		catch (TypeError $ex)
806
			log_error('random_int(): $min must be an integer');
807
808
		try
809
		{
810
			/** @var int $max */
811
			$max = RandomCompat_intval($max);
812
		}
813
		catch (TypeError $ex)
814
			log_error('random_int(): $max must be an integer');
815
816
		/**
817
		 * Now that we've verified our weak typing system has given us an integer,
818
		 * let's validate the logic then we can move forward with generating random
819
		 * integers along a given range.
820
		 */
821
		if ($min > $max)
822
			log_error('Minimum value must be less than or equal to the maximum value');
823
824
		if ($max === $min)
825
			return (int) $min;
826
827
		/**
828
		 * Initialize variables to 0
829
		 *
830
		 * We want to store:
831
		 * $bytes => the number of random bytes we need
832
		 * $mask => an integer bitmask (for use with the &) operator
833
		 *          so we can minimize the number of discards
834
		 */
835
		$attempts = $bits = $bytes = $mask = $valueShift = 0;
836
		/** @var int $attempts */
837
		/** @var int $bits */
838
		/** @var int $bytes */
839
		/** @var int $mask */
840
		/** @var int $valueShift */
841
842
		/**
843
		 * At this point, $range is a positive number greater than 0. It might
844
		 * overflow, however, if $max - $min > PHP_INT_MAX. PHP will cast it to
845
		 * a float and we will lose some precision.
846
		 *
847
		 * @var int|float $range
848
		 */
849
		$range = $max - $min;
850
851
		/**
852
		 * Test for integer overflow:
853
		 */
854
		if (!is_int($range))
855
		{
856
857
			/**
858
			 * Still safely calculate wider ranges.
859
			 * Provided by @CodesInChaos, @oittaa
860
			 *
861
			 * @ref https://gist.github.com/CodesInChaos/03f9ea0b58e8b2b8d435
862
			 *
863
			 * We use ~0 as a mask in this case because it generates all 1s
864
			 *
865
			 * @ref https://eval.in/400356 (32-bit)
866
			 * @ref http://3v4l.org/XX9r5  (64-bit)
867
			 */
868
			$bytes = PHP_INT_SIZE;
869
			/** @var int $mask */
870
			$mask = ~0;
871
872
		}
873
		else
874
		{
875
876
			/**
877
			 * $bits is effectively ceil(log($range, 2)) without dealing with
878
			 * type juggling
879
			 */
880
			while ($range > 0)
881
			{
882
				if ($bits % 8 === 0)
883
				{
884
					++$bytes;
885
				}
886
				++$bits;
887
				$range >>= 1;
888
				/** @var int $mask */
889
				$mask = $mask << 1 | 1;
890
			}
891
			$valueShift = $min;
892
		}
893
894
		/** @var int $val */
895
		$val = 0;
896
		/**
897
		 * Now that we have our parameters set up, let's begin generating
898
		 * random integers until one falls between $min and $max
899
		 */
900
		/** @psalm-suppress RedundantCondition */
901
		do
902
		{
903
			/**
904
			 * The rejection probability is at most 0.5, so this corresponds
905
			 * to a failure probability of 2^-128 for a working RNG
906
			 */
907
			if ($attempts > 128)
908
				log_error('random_int: RNG is broken - too many rejections');
909
910
			/**
911
			 * Let's grab the necessary number of random bytes
912
			 */
913
			$randomByteString = random_bytes($bytes);
914
915
			/**
916
			 * Let's turn $randomByteString into an integer
917
			 *
918
			 * This uses bitwise operators (<< and |) to build an integer
919
			 * out of the values extracted from ord()
920
			 *
921
			 * Example: [9F] | [6D] | [32] | [0C] =>
922
			 *   159 + 27904 + 3276800 + 201326592 =>
923
			 *   204631455
924
			 */
925
			$val &= 0;
926
			for ($i = 0; $i < $bytes; ++$i)
927
				$val |= ord($randomByteString[$i]) << ($i * 8);
928
929
			/** @var int $val */
930
931
			/**
932
			 * Apply mask
933
			 */
934
			$val &= $mask;
935
			$val += $valueShift;
936
937
			++$attempts;
938
			/**
939
			 * If $val overflows to a floating point number,
940
			 * ... or is larger than $max,
941
			 * ... or smaller than $min,
942
			 * then try again.
943
			 */
944
945
		} while (!is_int($val) || $val > $max || $val < $min);
946
947
		return (int) $val;
948
	}
949
}
950
951
952
?>