xref: /dokuwiki/vendor/phpseclib/phpseclib/phpseclib/Common/Functions/Strings.php (revision b2c5d21049ac0969066d59237f16a2a155c53677)
1<?php
2
3/**
4 * Common String Functions
5 *
6 * PHP version 5
7 *
8 * @author    Jim Wigginton <terrafrost@php.net>
9 * @copyright 2016 Jim Wigginton
10 * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
11 * @link      http://phpseclib.sourceforge.net
12 */
13
14namespace phpseclib3\Common\Functions;
15
16use ParagonIE\ConstantTime\Base64;
17use ParagonIE\ConstantTime\Base64UrlSafe;
18use ParagonIE\ConstantTime\Hex;
19use phpseclib3\Math\BigInteger;
20use phpseclib3\Math\Common\FiniteField;
21
22/**
23 * Common String Functions
24 *
25 * @author  Jim Wigginton <terrafrost@php.net>
26 */
27abstract class Strings
28{
29    /**
30     * String Shift
31     *
32     * Inspired by array_shift
33     *
34     * @param string $string
35     * @param int $index
36     * @return string
37     */
38    public static function shift(&$string, $index = 1)
39    {
40        $substr = substr($string, 0, $index);
41        $string = substr($string, $index);
42        return $substr;
43    }
44
45    /**
46     * String Pop
47     *
48     * Inspired by array_pop
49     *
50     * @param string $string
51     * @param int $index
52     * @return string
53     */
54    public static function pop(&$string, $index = 1)
55    {
56        $substr = substr($string, -$index);
57        $string = substr($string, 0, -$index);
58        return $substr;
59    }
60
61    /**
62     * Parse SSH2-style string
63     *
64     * Returns either an array or a boolean if $data is malformed.
65     *
66     * Valid characters for $format are as follows:
67     *
68     * C = byte
69     * b = boolean (true/false)
70     * N = uint32
71     * Q = uint64
72     * s = string
73     * i = mpint
74     * L = name-list
75     *
76     * uint64 is not supported.
77     *
78     * @param string $format
79     * @param string $data
80     * @return mixed
81     */
82    public static function unpackSSH2($format, &$data)
83    {
84        $format = self::formatPack($format);
85        $result = [];
86        for ($i = 0; $i < strlen($format); $i++) {
87            switch ($format[$i]) {
88                case 'C':
89                case 'b':
90                    if (!strlen($data)) {
91                        throw new \LengthException('At least one byte needs to be present for successful C / b decodes');
92                    }
93                    break;
94                case 'N':
95                case 'i':
96                case 's':
97                case 'L':
98                    if (strlen($data) < 4) {
99                        throw new \LengthException('At least four byte needs to be present for successful N / i / s / L decodes');
100                    }
101                    break;
102                case 'Q':
103                    if (strlen($data) < 8) {
104                        throw new \LengthException('At least eight byte needs to be present for successful N / i / s / L decodes');
105                    }
106                    break;
107
108                default:
109                    throw new \InvalidArgumentException('$format contains an invalid character');
110            }
111            switch ($format[$i]) {
112                case 'C':
113                    $result[] = ord(self::shift($data));
114                    continue 2;
115                case 'b':
116                    $result[] = ord(self::shift($data)) != 0;
117                    continue 2;
118                case 'N':
119                    list(, $temp) = unpack('N', self::shift($data, 4));
120                    $result[] = $temp;
121                    continue 2;
122                case 'Q':
123                    // pack() added support for Q in PHP 5.6.3 and PHP 5.6 is phpseclib 3's minimum version
124                    // so in theory we could support this BUT, "64-bit format codes are not available for
125                    // 32-bit versions" and phpseclib works on 32-bit installs. on 32-bit installs
126                    // 64-bit floats can be used to get larger numbers then 32-bit signed ints would allow
127                    // for. sure, you're not gonna get the full precision of 64-bit numbers but just because
128                    // you need > 32-bit precision doesn't mean you need the full 64-bit precision
129                    $unpacked = unpack('Nupper/Nlower', self::shift($data, 8));
130                    $upper = $unpacked['upper'];
131                    $lower = $unpacked['lower'];
132                    $temp = $upper ? 4294967296 * $upper : 0;
133                    $temp += $lower < 0 ? ($lower & 0x7FFFFFFFF) + 0x80000000 : $lower;
134                    // $temp = hexdec(bin2hex(self::shift($data, 8)));
135                    $result[] = $temp;
136                    continue 2;
137            }
138            list(, $length) = unpack('N', self::shift($data, 4));
139            if (strlen($data) < $length) {
140                throw new \LengthException("$length bytes needed; " . strlen($data) . ' bytes available');
141            }
142            $temp = self::shift($data, $length);
143            switch ($format[$i]) {
144                case 'i':
145                    $result[] = new BigInteger($temp, -256);
146                    break;
147                case 's':
148                    $result[] = $temp;
149                    break;
150                case 'L':
151                    $result[] = explode(',', $temp);
152            }
153        }
154
155        return $result;
156    }
157
158    /**
159     * Create SSH2-style string
160     *
161     * @param string $format
162     * @param string|int|float|array|bool ...$elements
163     * @return string
164     */
165    public static function packSSH2($format, ...$elements)
166    {
167        $format = self::formatPack($format);
168        if (strlen($format) != count($elements)) {
169            throw new \InvalidArgumentException('There must be as many arguments as there are characters in the $format string');
170        }
171        $result = '';
172        for ($i = 0; $i < strlen($format); $i++) {
173            $element = $elements[$i];
174            switch ($format[$i]) {
175                case 'C':
176                    if (!is_int($element)) {
177                        throw new \InvalidArgumentException('Bytes must be represented as an integer between 0 and 255, inclusive.');
178                    }
179                    $result .= pack('C', $element);
180                    break;
181                case 'b':
182                    if (!is_bool($element)) {
183                        throw new \InvalidArgumentException('A boolean parameter was expected.');
184                    }
185                    $result .= $element ? "\1" : "\0";
186                    break;
187                case 'Q':
188                    if (!is_int($element) && !is_float($element)) {
189                        throw new \InvalidArgumentException('An integer was expected.');
190                    }
191                    // 4294967296 == 1 << 32
192                    $result .= pack('NN', $element / 4294967296, $element);
193                    break;
194                case 'N':
195                    if (is_float($element)) {
196                        $element = (int) $element;
197                    }
198                    if (!is_int($element)) {
199                        throw new \InvalidArgumentException('An integer was expected.');
200                    }
201                    $result .= pack('N', $element);
202                    break;
203                case 's':
204                    if (!self::is_stringable($element)) {
205                        throw new \InvalidArgumentException('A string was expected.');
206                    }
207                    $result .= pack('Na*', strlen($element), $element);
208                    break;
209                case 'i':
210                    if (!$element instanceof BigInteger && !$element instanceof FiniteField\Integer) {
211                        throw new \InvalidArgumentException('A phpseclib3\Math\BigInteger or phpseclib3\Math\Common\FiniteField\Integer object was expected.');
212                    }
213                    $element = $element->toBytes(true);
214                    $result .= pack('Na*', strlen($element), $element);
215                    break;
216                case 'L':
217                    if (!is_array($element)) {
218                        throw new \InvalidArgumentException('An array was expected.');
219                    }
220                    $element = implode(',', $element);
221                    $result .= pack('Na*', strlen($element), $element);
222                    break;
223                default:
224                    throw new \InvalidArgumentException('$format contains an invalid character');
225            }
226        }
227        return $result;
228    }
229
230    /**
231     * Expand a pack string
232     *
233     * Converts C5 to CCCCC, for example.
234     *
235     * @param string $format
236     * @return string
237     */
238    private static function formatPack($format)
239    {
240        $parts = preg_split('#(\d+)#', $format, -1, PREG_SPLIT_DELIM_CAPTURE);
241        $format = '';
242        for ($i = 1; $i < count($parts); $i += 2) {
243            $format .= substr($parts[$i - 1], 0, -1) . str_repeat(substr($parts[$i - 1], -1), $parts[$i]);
244        }
245        $format .= $parts[$i - 1];
246
247        return $format;
248    }
249
250    /**
251     * Convert binary data into bits
252     *
253     * bin2hex / hex2bin refer to base-256 encoded data as binary, whilst
254     * decbin / bindec refer to base-2 encoded data as binary. For the purposes
255     * of this function, bin refers to base-256 encoded data whilst bits refers
256     * to base-2 encoded data
257     *
258     * @param string $x
259     * @return string
260     */
261    public static function bits2bin($x)
262    {
263        /*
264        // the pure-PHP approach is faster than the GMP approach
265        if (function_exists('gmp_export')) {
266             return strlen($x) ? gmp_export(gmp_init($x, 2)) : gmp_init(0);
267        }
268        */
269
270        if (preg_match('#[^01]#', $x)) {
271            throw new \RuntimeException('The only valid characters are 0 and 1');
272        }
273
274        if (!defined('PHP_INT_MIN')) {
275            define('PHP_INT_MIN', ~PHP_INT_MAX);
276        }
277
278        $length = strlen($x);
279        if (!$length) {
280            return '';
281        }
282        $block_size = PHP_INT_SIZE << 3;
283        $pad = $block_size - ($length % $block_size);
284        if ($pad != $block_size) {
285            $x = str_repeat('0', $pad) . $x;
286        }
287
288        $parts = str_split($x, $block_size);
289        $str = '';
290        foreach ($parts as $part) {
291            $xor = $part[0] == '1' ? PHP_INT_MIN : 0;
292            $part[0] = '0';
293            $str .= pack(
294                PHP_INT_SIZE == 4 ? 'N' : 'J',
295                $xor ^ eval('return 0b' . $part . ';')
296            );
297        }
298        return ltrim($str, "\0");
299    }
300
301    /**
302     * Convert bits to binary data
303     *
304     * @param string $x
305     * @return string
306     */
307    public static function bin2bits($x, $trim = true)
308    {
309        /*
310        // the pure-PHP approach is slower than the GMP approach BUT
311        // i want to the pure-PHP version to be easily unit tested as well
312        if (function_exists('gmp_import')) {
313            return gmp_strval(gmp_import($x), 2);
314        }
315        */
316
317        $len = strlen($x);
318        $mod = $len % PHP_INT_SIZE;
319        if ($mod) {
320            $x = str_pad($x, $len + PHP_INT_SIZE - $mod, "\0", STR_PAD_LEFT);
321        }
322
323        $bits = '';
324        if (PHP_INT_SIZE == 4) {
325            $digits = unpack('N*', $x);
326            foreach ($digits as $digit) {
327                $bits .= sprintf('%032b', $digit);
328            }
329        } else {
330            $digits = unpack('J*', $x);
331            foreach ($digits as $digit) {
332                $bits .= sprintf('%064b', $digit);
333            }
334        }
335
336        return $trim ? ltrim($bits, '0') : $bits;
337    }
338
339    /**
340     * Switch Endianness Bit Order
341     *
342     * @param string $x
343     * @return string
344     */
345    public static function switchEndianness($x)
346    {
347        $r = '';
348        for ($i = strlen($x) - 1; $i >= 0; $i--) {
349            $b = ord($x[$i]);
350            if (PHP_INT_SIZE === 8) {
351                // 3 operations
352                // from http://graphics.stanford.edu/~seander/bithacks.html#ReverseByteWith64BitsDiv
353                $r .= chr((($b * 0x0202020202) & 0x010884422010) % 1023);
354            } else {
355                // 7 operations
356                // from http://graphics.stanford.edu/~seander/bithacks.html#ReverseByteWith32Bits
357                $p1 = ($b * 0x0802) & 0x22110;
358                $p2 = ($b * 0x8020) & 0x88440;
359                $temp = ($p1 | $p2) * 0x10101;
360                if (is_float($temp)) {
361                    $temp = (int) fmod($temp, 0x7FFFFFFF);
362                }
363                $r .= chr(($temp >> 16) & 0xFF);
364            }
365        }
366        return $r;
367    }
368
369    /**
370     * Increment the current string
371     *
372     * @param string $var
373     * @return string
374     */
375    public static function increment_str(&$var)
376    {
377        if (function_exists('sodium_increment')) {
378            $var = strrev($var);
379            sodium_increment($var);
380            $var = strrev($var);
381            return $var;
382        }
383
384        for ($i = 4; $i <= strlen($var); $i += 4) {
385            $temp = substr($var, -$i, 4);
386            switch ($temp) {
387                case "\xFF\xFF\xFF\xFF":
388                    $var = substr_replace($var, "\x00\x00\x00\x00", -$i, 4);
389                    break;
390                case "\x7F\xFF\xFF\xFF":
391                    $var = substr_replace($var, "\x80\x00\x00\x00", -$i, 4);
392                    return $var;
393                default:
394                    $temp = unpack('Nnum', $temp);
395                    $var = substr_replace($var, pack('N', $temp['num'] + 1), -$i, 4);
396                    return $var;
397            }
398        }
399
400        $remainder = strlen($var) % 4;
401
402        if ($remainder == 0) {
403            return $var;
404        }
405
406        $temp = unpack('Nnum', str_pad(substr($var, 0, $remainder), 4, "\0", STR_PAD_LEFT));
407        $temp = substr(pack('N', $temp['num'] + 1), -$remainder);
408        $var = substr_replace($var, $temp, 0, $remainder);
409
410        return $var;
411    }
412
413    /**
414     * Find whether the type of a variable is string (or could be converted to one)
415     *
416     * @param mixed $var
417     * @return bool
418     * @psalm-assert-if-true string|\Stringable $var
419     */
420    public static function is_stringable($var)
421    {
422        return is_string($var) || (is_object($var) && method_exists($var, '__toString'));
423    }
424
425    /**
426     * Constant Time Base64-decoding
427     *
428     * ParagoneIE\ConstantTime doesn't use libsodium if it's available so we'll do so
429     * ourselves. see https://github.com/paragonie/constant_time_encoding/issues/39
430     *
431     * @param string $data
432     * @return string
433     */
434    public static function base64_decode($data)
435    {
436        return function_exists('sodium_base642bin') ?
437            sodium_base642bin($data, SODIUM_BASE64_VARIANT_ORIGINAL_NO_PADDING, '=') :
438            Base64::decode($data);
439    }
440
441    /**
442     * Constant Time Base64-decoding (URL safe)
443     *
444     * @param string $data
445     * @return string
446     */
447    public static function base64url_decode($data)
448    {
449        // return self::base64_decode(str_replace(['-', '_'], ['+', '/'], $data));
450
451        return function_exists('sodium_base642bin') ?
452            sodium_base642bin($data, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING, '=') :
453            Base64UrlSafe::decode($data);
454    }
455
456    /**
457     * Constant Time Base64-encoding
458     *
459     * @param string $data
460     * @return string
461     */
462    public static function base64_encode($data)
463    {
464        return function_exists('sodium_bin2base64') ?
465            sodium_bin2base64($data, SODIUM_BASE64_VARIANT_ORIGINAL) :
466            Base64::encode($data);
467    }
468
469    /**
470     * Constant Time Base64-encoding (URL safe)
471     *
472     * @param string $data
473     * @return string
474     */
475    public static function base64url_encode($data)
476    {
477        // return str_replace(['+', '/'], ['-', '_'], self::base64_encode($data));
478
479        return function_exists('sodium_bin2base64') ?
480            sodium_bin2base64($data, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING) :
481            Base64UrlSafe::encode($data);
482    }
483
484    /**
485     * Constant Time Hex Decoder
486     *
487     * @param string $data
488     * @return string
489     */
490    public static function hex2bin($data)
491    {
492        return function_exists('sodium_hex2bin') ?
493            sodium_hex2bin($data) :
494            Hex::decode($data);
495    }
496
497    /**
498     * Constant Time Hex Encoder
499     *
500     * @param string $data
501     * @return string
502     */
503    public static function bin2hex($data)
504    {
505        return function_exists('sodium_bin2hex') ?
506            sodium_bin2hex($data) :
507            Hex::encode($data);
508    }
509}
510