1<?php
2
3if (class_exists('ParagonIE_Sodium_Core_Util', false)) {
4 return;
5}
6
7/**
8 * Class ParagonIE_Sodium_Core_Util
9 */
10abstract class ParagonIE_Sodium_Core_Util
11{
12 const U32_MAX = 0xFFFFFFFF;
13
14 /**
15 * @param int $integer
16 * @param int $size (16, 32, 64)
17 * @return int
18 */
19 public static function abs($integer, $size = 0)
20 {
21 /** @var int $realSize */
22 $realSize = (PHP_INT_SIZE << 3) - 1;
23 if ($size) {
24 --$size;
25 } else {
26 /** @var int $size */
27 $size = $realSize;
28 }
29
30 $negative = -(($integer >> $size) & 1);
31 return (int) (
32 ($integer ^ $negative)
33 +
34 (($negative >> $realSize) & 1)
35 );
36 }
37
38 /**
39 * @param string $a
40 * @param string $b
41 * @return string
42 * @throws SodiumException
43 */
44 public static function andStrings($a, $b)
45 {
46 /* Type checks: */
47 if (!is_string($a)) {
48 throw new TypeError('Argument 1 must be a string');
49 }
50 if (!is_string($b)) {
51 throw new TypeError('Argument 2 must be a string');
52 }
53 $len = self::strlen($a);
54 if (self::strlen($b) !== $len) {
55 throw new SodiumException('Both strings must be of equal length to combine with bitwise AND');
56 }
57 return $a & $b;
58 }
59
60 /**
61 * Convert a binary string into a hexadecimal string without cache-timing
62 * leaks
63 *
64 * @internal You should not use this directly from another application
65 *
66 * @param string $binaryString (raw binary)
67 * @return string
68 * @throws TypeError
69 */
70 public static function bin2hex($binaryString)
71 {
72 /* Type checks: */
73 if (!is_string($binaryString)) {
74 throw new TypeError('Argument 1 must be a string, ' . gettype($binaryString) . ' given.');
75 }
76
77 $hex = '';
78 $len = self::strlen($binaryString);
79 for ($i = 0; $i < $len; ++$i) {
80 /** @var array<int, int> $chunk */
81 $chunk = unpack('C', $binaryString[$i]);
82 /** @var int $c */
83 $c = $chunk[1] & 0xf;
84 /** @var int $b */
85 $b = $chunk[1] >> 4;
86 $hex .= pack(
87 'CC',
88 (87 + $b + ((($b - 10) >> 8) & ~38)),
89 (87 + $c + ((($c - 10) >> 8) & ~38))
90 );
91 }
92 return $hex;
93 }
94
95 /**
96 * Convert a binary string into a hexadecimal string without cache-timing
97 * leaks, returning uppercase letters (as per RFC 4648)
98 *
99 * @internal You should not use this directly from another application
100 *
101 * @param string $bin_string (raw binary)
102 * @return string
103 * @throws TypeError
104 */
105 public static function bin2hexUpper($bin_string)
106 {
107 $hex = '';
108 $len = self::strlen($bin_string);
109 for ($i = 0; $i < $len; ++$i) {
110 /** @var array<int, int> $chunk */
111 $chunk = unpack('C', $bin_string[$i]);
112 /**
113 * Lower 16 bits
114 *
115 * @var int $c
116 */
117 $c = $chunk[1] & 0xf;
118
119 /**
120 * Upper 16 bits
121 * @var int $b
122 */
123 $b = $chunk[1] >> 4;
124
125 /**
126 * Use pack() and binary operators to turn the two integers
127 * into hexadecimal characters. We don't use chr() here, because
128 * it uses a lookup table internally and we want to avoid
129 * cache-timing side-channels.
130 */
131 $hex .= pack(
132 'CC',
133 (55 + $b + ((($b - 10) >> 8) & ~6)),
134 (55 + $c + ((($c - 10) >> 8) & ~6))
135 );
136 }
137 return $hex;
138 }
139
140 /**
141 * Cache-timing-safe variant of ord()
142 *
143 * @internal You should not use this directly from another application
144 *
145 * @param string $chr
146 * @return int
147 * @throws SodiumException
148 * @throws TypeError
149 */
150 public static function chrToInt($chr)
151 {
152 /* Type checks: */
153 if (!is_string($chr)) {
154 throw new TypeError('Argument 1 must be a string, ' . gettype($chr) . ' given.');
155 }
156 if (self::strlen($chr) !== 1) {
157 throw new SodiumException('chrToInt() expects a string that is exactly 1 character long');
158 }
159 /** @var array<int, int> $chunk */
160 $chunk = unpack('C', $chr);
161 return (int) ($chunk[1]);
162 }
163
164 /**
165 * Compares two strings.
166 *
167 * @internal You should not use this directly from another application
168 *
169 * @param string $left
170 * @param string $right
171 * @param int $len
172 * @return int
173 * @throws SodiumException
174 * @throws TypeError
175 */
176 public static function compare($left, $right, $len = null)
177 {
178 $leftLen = self::strlen($left);
179 $rightLen = self::strlen($right);
180 if ($len === null) {
181 $len = max($leftLen, $rightLen);
182 $left = str_pad($left, $len, "\x00", STR_PAD_RIGHT);
183 $right = str_pad($right, $len, "\x00", STR_PAD_RIGHT);
184 } elseif ($leftLen !== $rightLen) {
185 throw new SodiumException("Argument #1 and argument #2 must have the same length");
186 }
187
188 $gt = 0;
189 $eq = 1;
190 $i = $len;
191 while ($i !== 0) {
192 --$i;
193 $gt |= ((self::chrToInt($right[$i]) - self::chrToInt($left[$i])) >> 8) & $eq;
194 $eq &= ((self::chrToInt($right[$i]) ^ self::chrToInt($left[$i])) - 1) >> 8;
195 }
196 return ($gt + $gt + $eq) - 1;
197 }
198
199 /**
200 * If a variable does not match a given type, throw a TypeError.
201 *
202 * @param mixed $mixedVar
203 * @param string $type
204 * @param int $argumentIndex
205 * @throws TypeError
206 * @throws SodiumException
207 * @return void
208 */
209 public static function declareScalarType(&$mixedVar = null, $type = 'void', $argumentIndex = 0)
210 {
211 if (func_num_args() === 0) {
212 /* Tautology, by default */
213 return;
214 }
215 if (func_num_args() === 1) {
216 throw new TypeError('Declared void, but passed a variable');
217 }
218 $realType = strtolower(gettype($mixedVar));
219 $type = strtolower($type);
220 switch ($type) {
221 case 'null':
222 if ($mixedVar !== null) {
223 throw new TypeError('Argument ' . $argumentIndex . ' must be null, ' . $realType . ' given.');
224 }
225 break;
226 case 'integer':
227 case 'int':
228 $allow = array('int', 'integer');
229 if (!in_array($type, $allow)) {
230 throw new TypeError('Argument ' . $argumentIndex . ' must be an integer, ' . $realType . ' given.');
231 }
232 $mixedVar = (int) $mixedVar;
233 break;
234 case 'boolean':
235 case 'bool':
236 $allow = array('bool', 'boolean');
237 if (!in_array($type, $allow)) {
238 throw new TypeError('Argument ' . $argumentIndex . ' must be a boolean, ' . $realType . ' given.');
239 }
240 $mixedVar = (bool) $mixedVar;
241 break;
242 case 'string':
243 if (!is_string($mixedVar)) {
244 throw new TypeError('Argument ' . $argumentIndex . ' must be a string, ' . $realType . ' given.');
245 }
246 $mixedVar = (string) $mixedVar;
247 break;
248 case 'decimal':
249 case 'double':
250 case 'float':
251 $allow = array('decimal', 'double', 'float');
252 if (!in_array($type, $allow)) {
253 throw new TypeError('Argument ' . $argumentIndex . ' must be a float, ' . $realType . ' given.');
254 }
255 $mixedVar = (float) $mixedVar;
256 break;
257 case 'object':
258 if (!is_object($mixedVar)) {
259 throw new TypeError('Argument ' . $argumentIndex . ' must be an object, ' . $realType . ' given.');
260 }
261 break;
262 case 'array':
263 if (!is_array($mixedVar)) {
264 if (is_object($mixedVar)) {
265 if ($mixedVar instanceof ArrayAccess) {
266 return;
267 }
268 }
269 throw new TypeError('Argument ' . $argumentIndex . ' must be an array, ' . $realType . ' given.');
270 }
271 break;
272 default:
273 throw new SodiumException('Unknown type (' . $realType .') does not match expect type (' . $type . ')');
274 }
275 }
276
277 /**
278 * Evaluate whether or not two strings are equal (in constant-time)
279 *
280 * @param string $left
281 * @param string $right
282 * @return bool
283 * @throws SodiumException
284 * @throws TypeError
285 */
286 public static function hashEquals($left, $right)
287 {
288 /* Type checks: */
289 if (!is_string($left)) {
290 throw new TypeError('Argument 1 must be a string, ' . gettype($left) . ' given.');
291 }
292 if (!is_string($right)) {
293 throw new TypeError('Argument 2 must be a string, ' . gettype($right) . ' given.');
294 }
295
296 if (is_callable('hash_equals')) {
297 return hash_equals($left, $right);
298 }
299 $d = 0;
300 /** @var int $len */
301 $len = self::strlen($left);
302 if ($len !== self::strlen($right)) {
303 return false;
304 }
305 for ($i = 0; $i < $len; ++$i) {
306 $d |= self::chrToInt($left[$i]) ^ self::chrToInt($right[$i]);
307 }
308
309 if ($d !== 0) {
310 return false;
311 }
312 return $left === $right;
313 }
314
315 /**
316 * Catch hash_update() failures and throw instead of silently proceeding
317 *
318 * @param HashContext|resource &$hs
319 * @param string $data
320 * @return void
321 * @throws SodiumException
322 * @psalm-suppress PossiblyInvalidArgument
323 */
324 protected static function hash_update(&$hs, $data)
325 {
326 if (!hash_update($hs, $data)) {
327 throw new SodiumException('hash_update() failed');
328 }
329 }
330
331 /**
332 * Convert a hexadecimal string into a binary string without cache-timing
333 * leaks
334 *
335 * @internal You should not use this directly from another application
336 *
337 * @param string $hexString
338 * @param string $ignore
339 * @param bool $strictPadding
340 * @return string (raw binary)
341 *
342 * @throws SodiumException
343 * @throws TypeError
344 */
345 public static function hex2bin($hexString, $ignore = '', $strictPadding = false)
346 {
347 /* Type checks: */
348 if (!is_string($hexString)) {
349 throw new TypeError('Argument 1 must be a string, ' . gettype($hexString) . ' given.');
350 }
351 if (!is_string($ignore)) {
352 throw new TypeError('Argument 2 must be a string, ' . gettype($hexString) . ' given.');
353 }
354
355 $hex_pos = 0;
356 $bin = '';
357 $c_acc = 0;
358 $hex_len = self::strlen($hexString);
359 $state = 0;
360
361 $chunk = unpack('C*', $hexString);
362 while ($hex_pos < $hex_len) {
363 ++$hex_pos;
364 /** @var int $c */
365 $c = $chunk[$hex_pos];
366 $c_num = $c ^ 48;
367 $c_num0 = ($c_num - 10) >> 8;
368 $c_alpha = ($c & ~32) - 55;
369 $c_alpha0 = (($c_alpha - 10) ^ ($c_alpha - 16)) >> 8;
370 if (($c_num0 | $c_alpha0) === 0) {
371 if ($ignore && $state === 0 && strpos($ignore, self::intToChr($c)) !== false) {
372 continue;
373 }
374 throw new RangeException(
375 'hex2bin() only expects hexadecimal characters'
376 );
377 }
378 $c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0);
379 if ($state === 0) {
380 $c_acc = $c_val * 16;
381 } else {
382 $bin .= pack('C', $c_acc | $c_val);
383 }
384 $state ^= 1;
385 }
386 if ($strictPadding && $state !== 0) {
387 throw new SodiumException(
388 'Expected an even number of hexadecimal characters'
389 );
390 }
391 return $bin;
392 }
393
394 /**
395 * Turn an array of integers into a string
396 *
397 * @internal You should not use this directly from another application
398 *
399 * @param array<int, int> $ints
400 * @return string
401 */
402 public static function intArrayToString(array $ints)
403 {
404 $args = $ints;
405 foreach ($args as $i => $v) {
406 $args[$i] = (int) ($v & 0xff);
407 }
408 array_unshift($args, str_repeat('C', count($ints)));
409 return (string) (call_user_func_array('pack', $args));
410 }
411
412 /**
413 * Cache-timing-safe variant of ord()
414 *
415 * @internal You should not use this directly from another application
416 *
417 * @param int $int
418 * @return string
419 * @throws TypeError
420 */
421 public static function intToChr($int)
422 {
423 return pack('C', $int);
424 }
425
426 /**
427 * Load a 3 character substring into an integer
428 *
429 * @internal You should not use this directly from another application
430 *
431 * @param string $string
432 * @return int
433 * @throws RangeException
434 * @throws TypeError
435 */
436 public static function load_3($string)
437 {
438 /* Type checks: */
439 if (!is_string($string)) {
440 throw new TypeError('Argument 1 must be a string, ' . gettype($string) . ' given.');
441 }
442
443 /* Input validation: */
444 if (self::strlen($string) < 3) {
445 throw new RangeException(
446 'String must be 3 bytes or more; ' . self::strlen($string) . ' given.'
447 );
448 }
449 /** @var array<int, int> $unpacked */
450 $unpacked = unpack('V', $string . "\0");
451 return (int) ($unpacked[1] & 0xffffff);
452 }
453
454 /**
455 * Load a 4 character substring into an integer
456 *
457 * @internal You should not use this directly from another application
458 *
459 * @param string $string
460 * @return int
461 * @throws RangeException
462 * @throws TypeError
463 */
464 public static function load_4($string)
465 {
466 /* Type checks: */
467 if (!is_string($string)) {
468 throw new TypeError('Argument 1 must be a string, ' . gettype($string) . ' given.');
469 }
470
471 /* Input validation: */
472 if (self::strlen($string) < 4) {
473 throw new RangeException(
474 'String must be 4 bytes or more; ' . self::strlen($string) . ' given.'
475 );
476 }
477 /** @var array<int, int> $unpacked */
478 $unpacked = unpack('V', $string);
479 return (int) $unpacked[1];
480 }
481
482 /**
483 * Load a 8 character substring into an integer
484 *
485 * @internal You should not use this directly from another application
486 *
487 * @param string $string
488 * @return int
489 * @throws RangeException
490 * @throws SodiumException
491 * @throws TypeError
492 */
493 public static function load64_le($string)
494 {
495 /* Type checks: */
496 if (!is_string($string)) {
497 throw new TypeError('Argument 1 must be a string, ' . gettype($string) . ' given.');
498 }
499
500 /* Input validation: */
501 if (self::strlen($string) < 4) {
502 throw new RangeException(
503 'String must be 4 bytes or more; ' . self::strlen($string) . ' given.'
504 );
505 }
506 if (PHP_VERSION_ID >= 50603 && PHP_INT_SIZE === 8) {
507 /** @var array<int, int> $unpacked */
508 $unpacked = unpack('P', $string);
509 return (int) $unpacked[1];
510 }
511
512 /** @var int $result */
513 $result = (self::chrToInt($string[0]) & 0xff);
514 $result |= (self::chrToInt($string[1]) & 0xff) << 8;
515 $result |= (self::chrToInt($string[2]) & 0xff) << 16;
516 $result |= (self::chrToInt($string[3]) & 0xff) << 24;
517 $result |= (self::chrToInt($string[4]) & 0xff) << 32;
518 $result |= (self::chrToInt($string[5]) & 0xff) << 40;
519 $result |= (self::chrToInt($string[6]) & 0xff) << 48;
520 $result |= (self::chrToInt($string[7]) & 0xff) << 56;
521 return (int) $result;
522 }
523
524 /**
525 * @internal You should not use this directly from another application
526 *
527 * @param string $left
528 * @param string $right
529 * @return int
530 * @throws SodiumException
531 * @throws TypeError
532 */
533 public static function memcmp($left, $right)
534 {
535 $e = (int) !self::hashEquals($left, $right);
536 return 0 - $e;
537 }
538
539 /**
540 * Multiply two integers in constant-time
541 *
542 * Micro-architecture timing side-channels caused by how your CPU
543 * implements multiplication are best prevented by never using the
544 * multiplication operators and ensuring that our code always takes
545 * the same number of operations to complete, regardless of the values
546 * of $a and $b.
547 *
548 * @internal You should not use this directly from another application
549 *
550 * @param int $a
551 * @param int $b
552 * @param int $size Limits the number of operations (useful for small,
553 * constant operands)
554 * @return int
555 */
556 public static function mul($a, $b, $size = 0)
557 {
558 if (ParagonIE_Sodium_Compat::$fastMult) {
559 return (int) ($a * $b);
560 }
561
562 static $defaultSize = null;
563 /** @var int $defaultSize */
564 if (!$defaultSize) {
565 /** @var int $defaultSize */
566 $defaultSize = (PHP_INT_SIZE << 3) - 1;
567 }
568 if ($size < 1) {
569 /** @var int $size */
570 $size = $defaultSize;
571 }
572 /** @var int $size */
573
574 $c = 0;
575
576 /**
577 * Mask is either -1 or 0.
578 *
579 * -1 in binary looks like 0x1111 ... 1111
580 * 0 in binary looks like 0x0000 ... 0000
581 *
582 * @var int
583 */
584 $mask = -(($b >> ((int) $defaultSize)) & 1);
585
586 /**
587 * Ensure $b is a positive integer, without creating
588 * a branching side-channel
589 *
590 * @var int $b
591 */
592 $b = ($b & ~$mask) | ($mask & -$b);
593
594 /**
595 * Unless $size is provided:
596 *
597 * This loop always runs 32 times when PHP_INT_SIZE is 4.
598 * This loop always runs 64 times when PHP_INT_SIZE is 8.
599 */
600 for ($i = $size; $i >= 0; --$i) {
601 $c += (int) ($a & -($b & 1));
602 $a <<= 1;
603 $b >>= 1;
604 }
605 $c = (int) @($c & -1);
606
607 /**
608 * If $b was negative, we then apply the same value to $c here.
609 * It doesn't matter much if $a was negative; the $c += above would
610 * have produced a negative integer to begin with. But a negative $b
611 * makes $b >>= 1 never return 0, so we would end up with incorrect
612 * results.
613 *
614 * The end result is what we'd expect from integer multiplication.
615 */
616 return (int) (($c & ~$mask) | ($mask & -$c));
617 }
618
619 /**
620 * Convert any arbitrary numbers into two 32-bit integers that represent
621 * a 64-bit integer.
622 *
623 * @internal You should not use this directly from another application
624 *
625 * @param int|float $num
626 * @return array<int, int>
627 */
628 public static function numericTo64BitInteger($num)
629 {
630 $high = 0;
631 /** @var int $low */
632 if (PHP_INT_SIZE === 4) {
633 $low = (int) $num;
634 } else {
635 $low = $num & 0xffffffff;
636 }
637
638 if ((+(abs($num))) >= 1) {
639 if ($num > 0) {
640 /** @var int $high */
641 $high = min((+(floor($num/4294967296))), 4294967295);
642 } else {
643 /** @var int $high */
644 $high = ~~((+(ceil(($num - (+((~~($num)))))/4294967296))));
645 }
646 }
647 return array((int) $high, (int) $low);
648 }
649
650 /**
651 * Store a 24-bit integer into a string, treating it as big-endian.
652 *
653 * @internal You should not use this directly from another application
654 *
655 * @param int $int
656 * @return string
657 * @throws TypeError
658 */
659 public static function store_3($int)
660 {
661 /* Type checks: */
662 if (!is_int($int)) {
663 if (is_numeric($int)) {
664 $int = (int) $int;
665 } else {
666 throw new TypeError('Argument 1 must be an integer, ' . gettype($int) . ' given.');
667 }
668 }
669 /** @var string $packed */
670 $packed = pack('N', $int);
671 return self::substr($packed, 1, 3);
672 }
673
674 /**
675 * Store a 32-bit integer into a string, treating it as little-endian.
676 *
677 * @internal You should not use this directly from another application
678 *
679 * @param int $int
680 * @return string
681 * @throws TypeError
682 */
683 public static function store32_le($int)
684 {
685 /* Type checks: */
686 if (!is_int($int)) {
687 if (is_numeric($int)) {
688 $int = (int) $int;
689 } else {
690 throw new TypeError('Argument 1 must be an integer, ' . gettype($int) . ' given.');
691 }
692 }
693
694 /** @var string $packed */
695 $packed = pack('V', $int);
696 return $packed;
697 }
698
699 /**
700 * Store a 32-bit integer into a string, treating it as big-endian.
701 *
702 * @internal You should not use this directly from another application
703 *
704 * @param int $int
705 * @return string
706 * @throws TypeError
707 */
708 public static function store_4($int)
709 {
710 /* Type checks: */
711 if (!is_int($int)) {
712 if (is_numeric($int)) {
713 $int = (int) $int;
714 } else {
715 throw new TypeError('Argument 1 must be an integer, ' . gettype($int) . ' given.');
716 }
717 }
718
719 /** @var string $packed */
720 $packed = pack('N', $int);
721 return $packed;
722 }
723
724 /**
725 * Stores a 64-bit integer as an string, treating it as little-endian.
726 *
727 * @internal You should not use this directly from another application
728 *
729 * @param int $int
730 * @return string
731 * @throws TypeError
732 */
733 public static function store64_le($int)
734 {
735 /* Type checks: */
736 if (!is_int($int)) {
737 if (is_numeric($int)) {
738 $int = (int) $int;
739 } else {
740 throw new TypeError('Argument 1 must be an integer, ' . gettype($int) . ' given.');
741 }
742 }
743
744 if (PHP_INT_SIZE === 8) {
745 if (PHP_VERSION_ID >= 50603) {
746 /** @var string $packed */
747 $packed = pack('P', $int);
748 return $packed;
749 }
750 return self::intToChr($int & 0xff) .
751 self::intToChr(($int >> 8) & 0xff) .
752 self::intToChr(($int >> 16) & 0xff) .
753 self::intToChr(($int >> 24) & 0xff) .
754 self::intToChr(($int >> 32) & 0xff) .
755 self::intToChr(($int >> 40) & 0xff) .
756 self::intToChr(($int >> 48) & 0xff) .
757 self::intToChr(($int >> 56) & 0xff);
758 }
759 if ($int > PHP_INT_MAX) {
760 list($hiB, $int) = self::numericTo64BitInteger($int);
761 } else {
762 $hiB = 0;
763 }
764 return
765 self::intToChr(($int ) & 0xff) .
766 self::intToChr(($int >> 8) & 0xff) .
767 self::intToChr(($int >> 16) & 0xff) .
768 self::intToChr(($int >> 24) & 0xff) .
769 self::intToChr($hiB & 0xff) .
770 self::intToChr(($hiB >> 8) & 0xff) .
771 self::intToChr(($hiB >> 16) & 0xff) .
772 self::intToChr(($hiB >> 24) & 0xff);
773 }
774
775 /**
776 * Safe string length
777 *
778 * @internal You should not use this directly from another application
779 *
780 * @ref mbstring.func_overload
781 *
782 * @param string $str
783 * @return int
784 * @throws TypeError
785 */
786 public static function strlen($str)
787 {
788 /* Type checks: */
789 if (!is_string($str)) {
790 throw new TypeError('String expected');
791 }
792
793 return (int) (
794 self::isMbStringOverride()
795 ? mb_strlen($str, '8bit')
796 : strlen($str)
797 );
798 }
799
800 /**
801 * Turn a string into an array of integers
802 *
803 * @internal You should not use this directly from another application
804 *
805 * @param string $string
806 * @return array<int, int>
807 * @throws TypeError
808 */
809 public static function stringToIntArray($string)
810 {
811 if (!is_string($string)) {
812 throw new TypeError('String expected');
813 }
814 /**
815 * @var array<int, int>
816 */
817 $values = array_values(
818 unpack('C*', $string)
819 );
820 return $values;
821 }
822
823 /**
824 * Safe substring
825 *
826 * @internal You should not use this directly from another application
827 *
828 * @ref mbstring.func_overload
829 *
830 * @param string $str
831 * @param int $start
832 * @param int $length
833 * @return string
834 * @throws TypeError
835 */
836 public static function substr($str, $start = 0, $length = null)
837 {
838 /* Type checks: */
839 if (!is_string($str)) {
840 throw new TypeError('String expected');
841 }
842
843 if ($length === 0) {
844 return '';
845 }
846
847 if (self::isMbStringOverride()) {
848 if (PHP_VERSION_ID < 50400 && $length === null) {
849 $length = self::strlen($str);
850 }
851 $sub = (string) mb_substr($str, $start, $length, '8bit');
852 } elseif ($length === null) {
853 $sub = (string) substr($str, $start);
854 } else {
855 $sub = (string) substr($str, $start, $length);
856 }
857 if ($sub !== '') {
858 return $sub;
859 }
860 return '';
861 }
862
863 /**
864 * Compare a 16-character byte string in constant time.
865 *
866 * @internal You should not use this directly from another application
867 *
868 * @param string $a
869 * @param string $b
870 * @return bool
871 * @throws SodiumException
872 * @throws TypeError
873 */
874 public static function verify_16($a, $b)
875 {
876 /* Type checks: */
877 if (!is_string($a)) {
878 throw new TypeError('String expected');
879 }
880 if (!is_string($b)) {
881 throw new TypeError('String expected');
882 }
883 return self::hashEquals(
884 self::substr($a, 0, 16),
885 self::substr($b, 0, 16)
886 );
887 }
888
889 /**
890 * Compare a 32-character byte string in constant time.
891 *
892 * @internal You should not use this directly from another application
893 *
894 * @param string $a
895 * @param string $b
896 * @return bool
897 * @throws SodiumException
898 * @throws TypeError
899 */
900 public static function verify_32($a, $b)
901 {
902 /* Type checks: */
903 if (!is_string($a)) {
904 throw new TypeError('String expected');
905 }
906 if (!is_string($b)) {
907 throw new TypeError('String expected');
908 }
909 return self::hashEquals(
910 self::substr($a, 0, 32),
911 self::substr($b, 0, 32)
912 );
913 }
914
915 /**
916 * Calculate $a ^ $b for two strings.
917 *
918 * @internal You should not use this directly from another application
919 *
920 * @param string $a
921 * @param string $b
922 * @return string
923 * @throws TypeError
924 */
925 public static function xorStrings($a, $b)
926 {
927 /* Type checks: */
928 if (!is_string($a)) {
929 throw new TypeError('Argument 1 must be a string');
930 }
931 if (!is_string($b)) {
932 throw new TypeError('Argument 2 must be a string');
933 }
934
935 return (string) ($a ^ $b);
936 }
937
938 /**
939 * Returns whether or not mbstring.func_overload is in effect.
940 *
941 * @internal You should not use this directly from another application
942 *
943 * Note: MB_OVERLOAD_STRING === 2, but we don't reference the constant
944 * (for nuisance-free PHP 8 support)
945 *
946 * @return bool
947 */
948 protected static function isMbStringOverride()
949 {
950 static $mbstring = null;
951
952 if ($mbstring === null) {
953 if (!defined('MB_OVERLOAD_STRING')) {
954 $mbstring = false;
955 return $mbstring;
956 }
957 $mbstring = extension_loaded('mbstring')
958 && defined('MB_OVERLOAD_STRING')
959 &&
960 ((int) (ini_get('mbstring.func_overload')) & 2);
961 // MB_OVERLOAD_STRING === 2
962 }
963 /** @var bool $mbstring */
964
965 return $mbstring;
966 }
967}
968