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                    extract(unpack('Nupper/Nlower', self::shift($data, 8)));
130                    $temp = $upper ? 4294967296 * $upper : 0;
131                    $temp += $lower < 0 ? ($lower & 0x7FFFFFFFF) + 0x80000000 : $lower;
132                    // $temp = hexdec(bin2hex(self::shift($data, 8)));
133                    $result[] = $temp;
134                    continue 2;
135            }
136            list(, $length) = unpack('N', self::shift($data, 4));
137            if (strlen($data) < $length) {
138                throw new \LengthException("$length bytes needed; " . strlen($data) . ' bytes available');
139            }
140            $temp = self::shift($data, $length);
141            switch ($format[$i]) {
142                case 'i':
143                    $result[] = new BigInteger($temp, -256);
144                    break;
145                case 's':
146                    $result[] = $temp;
147                    break;
148                case 'L':
149                    $result[] = explode(',', $temp);
150            }
151        }
152
153        return $result;
154    }
155
156    /**
157     * Create SSH2-style string
158     *
159     * @param string $format
160     * @param string|int|float|array|bool ...$elements
161     * @return string
162     */
163    public static function packSSH2($format, ...$elements)
164    {
165        $format = self::formatPack($format);
166        if (strlen($format) != count($elements)) {
167            throw new \InvalidArgumentException('There must be as many arguments as there are characters in the $format string');
168        }
169        $result = '';
170        for ($i = 0; $i < strlen($format); $i++) {
171            $element = $elements[$i];
172            switch ($format[$i]) {
173                case 'C':
174                    if (!is_int($element)) {
175                        throw new \InvalidArgumentException('Bytes must be represented as an integer between 0 and 255, inclusive.');
176                    }
177                    $result .= pack('C', $element);
178                    break;
179                case 'b':
180                    if (!is_bool($element)) {
181                        throw new \InvalidArgumentException('A boolean parameter was expected.');
182                    }
183                    $result .= $element ? "\1" : "\0";
184                    break;
185                case 'Q':
186                    if (!is_int($element) && !is_float($element)) {
187                        throw new \InvalidArgumentException('An integer was expected.');
188                    }
189                    // 4294967296 == 1 << 32
190                    $result .= pack('NN', $element / 4294967296, $element);
191                    break;
192                case 'N':
193                    if (is_float($element)) {
194                        $element = (int) $element;
195                    }
196                    if (!is_int($element)) {
197                        throw new \InvalidArgumentException('An integer was expected.');
198                    }
199                    $result .= pack('N', $element);
200                    break;
201                case 's':
202                    if (!self::is_stringable($element)) {
203                        throw new \InvalidArgumentException('A string was expected.');
204                    }
205                    $result .= pack('Na*', strlen($element), $element);
206                    break;
207                case 'i':
208                    if (!$element instanceof BigInteger && !$element instanceof FiniteField\Integer) {
209                        throw new \InvalidArgumentException('A phpseclib3\Math\BigInteger or phpseclib3\Math\Common\FiniteField\Integer object was expected.');
210                    }
211                    $element = $element->toBytes(true);
212                    $result .= pack('Na*', strlen($element), $element);
213                    break;
214                case 'L':
215                    if (!is_array($element)) {
216                        throw new \InvalidArgumentException('An array was expected.');
217                    }
218                    $element = implode(',', $element);
219                    $result .= pack('Na*', strlen($element), $element);
220                    break;
221                default:
222                    throw new \InvalidArgumentException('$format contains an invalid character');
223            }
224        }
225        return $result;
226    }
227
228    /**
229     * Expand a pack string
230     *
231     * Converts C5 to CCCCC, for example.
232     *
233     * @param string $format
234     * @return string
235     */
236    private static function formatPack($format)
237    {
238        $parts = preg_split('#(\d+)#', $format, -1, PREG_SPLIT_DELIM_CAPTURE);
239        $format = '';
240        for ($i = 1; $i < count($parts); $i += 2) {
241            $format .= substr($parts[$i - 1], 0, -1) . str_repeat(substr($parts[$i - 1], -1), $parts[$i]);
242        }
243        $format .= $parts[$i - 1];
244
245        return $format;
246    }
247
248    /**
249     * Convert binary data into bits
250     *
251     * bin2hex / hex2bin refer to base-256 encoded data as binary, whilst
252     * decbin / bindec refer to base-2 encoded data as binary. For the purposes
253     * of this function, bin refers to base-256 encoded data whilst bits refers
254     * to base-2 encoded data
255     *
256     * @param string $x
257     * @return string
258     */
259    public static function bits2bin($x)
260    {
261        /*
262        // the pure-PHP approach is faster than the GMP approach
263        if (function_exists('gmp_export')) {
264             return strlen($x) ? gmp_export(gmp_init($x, 2)) : gmp_init(0);
265        }
266        */
267
268        if (preg_match('#[^01]#', $x)) {
269            throw new \RuntimeException('The only valid characters are 0 and 1');
270        }
271
272        if (!defined('PHP_INT_MIN')) {
273            define('PHP_INT_MIN', ~PHP_INT_MAX);
274        }
275
276        $length = strlen($x);
277        if (!$length) {
278            return '';
279        }
280        $block_size = PHP_INT_SIZE << 3;
281        $pad = $block_size - ($length % $block_size);
282        if ($pad != $block_size) {
283            $x = str_repeat('0', $pad) . $x;
284        }
285
286        $parts = str_split($x, $block_size);
287        $str = '';
288        foreach ($parts as $part) {
289            $xor = $part[0] == '1' ? PHP_INT_MIN : 0;
290            $part[0] = '0';
291            $str .= pack(
292                PHP_INT_SIZE == 4 ? 'N' : 'J',
293                $xor ^ eval('return 0b' . $part . ';')
294            );
295        }
296        return ltrim($str, "\0");
297    }
298
299    /**
300     * Convert bits to binary data
301     *
302     * @param string $x
303     * @return string
304     */
305    public static function bin2bits($x, $trim = true)
306    {
307        /*
308        // the pure-PHP approach is slower than the GMP approach BUT
309        // i want to the pure-PHP version to be easily unit tested as well
310        if (function_exists('gmp_import')) {
311            return gmp_strval(gmp_import($x), 2);
312        }
313        */
314
315        $len = strlen($x);
316        $mod = $len % PHP_INT_SIZE;
317        if ($mod) {
318            $x = str_pad($x, $len + PHP_INT_SIZE - $mod, "\0", STR_PAD_LEFT);
319        }
320
321        $bits = '';
322        if (PHP_INT_SIZE == 4) {
323            $digits = unpack('N*', $x);
324            foreach ($digits as $digit) {
325                $bits .= sprintf('%032b', $digit);
326            }
327        } else {
328            $digits = unpack('J*', $x);
329            foreach ($digits as $digit) {
330                $bits .= sprintf('%064b', $digit);
331            }
332        }
333
334        return $trim ? ltrim($bits, '0') : $bits;
335    }
336
337    /**
338     * Switch Endianness Bit Order
339     *
340     * @param string $x
341     * @return string
342     */
343    public static function switchEndianness($x)
344    {
345        $r = '';
346        for ($i = strlen($x) - 1; $i >= 0; $i--) {
347            $b = ord($x[$i]);
348            if (PHP_INT_SIZE === 8) {
349                // 3 operations
350                // from http://graphics.stanford.edu/~seander/bithacks.html#ReverseByteWith64BitsDiv
351                $r .= chr((($b * 0x0202020202) & 0x010884422010) % 1023);
352            } else {
353                // 7 operations
354                // from http://graphics.stanford.edu/~seander/bithacks.html#ReverseByteWith32Bits
355                $p1 = ($b * 0x0802) & 0x22110;
356                $p2 = ($b * 0x8020) & 0x88440;
357                $r .= chr(
358                    (($p1 | $p2) * 0x10101) >> 16
359                );
360            }
361        }
362        return $r;
363    }
364
365    /**
366     * Increment the current string
367     *
368     * @param string $var
369     * @return string
370     */
371    public static function increment_str(&$var)
372    {
373        if (function_exists('sodium_increment')) {
374            $var = strrev($var);
375            sodium_increment($var);
376            $var = strrev($var);
377            return $var;
378        }
379
380        for ($i = 4; $i <= strlen($var); $i += 4) {
381            $temp = substr($var, -$i, 4);
382            switch ($temp) {
383                case "\xFF\xFF\xFF\xFF":
384                    $var = substr_replace($var, "\x00\x00\x00\x00", -$i, 4);
385                    break;
386                case "\x7F\xFF\xFF\xFF":
387                    $var = substr_replace($var, "\x80\x00\x00\x00", -$i, 4);
388                    return $var;
389                default:
390                    $temp = unpack('Nnum', $temp);
391                    $var = substr_replace($var, pack('N', $temp['num'] + 1), -$i, 4);
392                    return $var;
393            }
394        }
395
396        $remainder = strlen($var) % 4;
397
398        if ($remainder == 0) {
399            return $var;
400        }
401
402        $temp = unpack('Nnum', str_pad(substr($var, 0, $remainder), 4, "\0", STR_PAD_LEFT));
403        $temp = substr(pack('N', $temp['num'] + 1), -$remainder);
404        $var = substr_replace($var, $temp, 0, $remainder);
405
406        return $var;
407    }
408
409    /**
410     * Find whether the type of a variable is string (or could be converted to one)
411     *
412     * @param mixed $var
413     * @return bool
414     * @psalm-assert-if-true string|\Stringable $var
415     */
416    public static function is_stringable($var)
417    {
418        return is_string($var) || (is_object($var) && method_exists($var, '__toString'));
419    }
420
421    /**
422     * Constant Time Base64-decoding
423     *
424     * ParagoneIE\ConstantTime doesn't use libsodium if it's available so we'll do so
425     * ourselves. see https://github.com/paragonie/constant_time_encoding/issues/39
426     *
427     * @param string $data
428     * @return string
429     */
430    public static function base64_decode($data)
431    {
432        return function_exists('sodium_base642bin') ?
433            sodium_base642bin($data, SODIUM_BASE64_VARIANT_ORIGINAL_NO_PADDING, '=') :
434            Base64::decode($data);
435    }
436
437    /**
438     * Constant Time Base64-decoding (URL safe)
439     *
440     * @param string $data
441     * @return string
442     */
443    public static function base64url_decode($data)
444    {
445        // return self::base64_decode(str_replace(['-', '_'], ['+', '/'], $data));
446
447        return function_exists('sodium_base642bin') ?
448            sodium_base642bin($data, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING, '=') :
449            Base64UrlSafe::decode($data);
450    }
451
452    /**
453     * Constant Time Base64-encoding
454     *
455     * @param string $data
456     * @return string
457     */
458    public static function base64_encode($data)
459    {
460        return function_exists('sodium_bin2base64') ?
461            sodium_bin2base64($data, SODIUM_BASE64_VARIANT_ORIGINAL) :
462            Base64::encode($data);
463    }
464
465    /**
466     * Constant Time Base64-encoding (URL safe)
467     *
468     * @param string $data
469     * @return string
470     */
471    public static function base64url_encode($data)
472    {
473        // return str_replace(['+', '/'], ['-', '_'], self::base64_encode($data));
474
475        return function_exists('sodium_bin2base64') ?
476            sodium_bin2base64($data, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING) :
477            Base64UrlSafe::encode($data);
478    }
479
480    /**
481     * Constant Time Hex Decoder
482     *
483     * @param string $data
484     * @return string
485     */
486    public static function hex2bin($data)
487    {
488        return function_exists('sodium_hex2bin') ?
489            sodium_hex2bin($data) :
490            Hex::decode($data);
491    }
492
493    /**
494     * Constant Time Hex Encoder
495     *
496     * @param string $data
497     * @return string
498     */
499    public static function bin2hex($data)
500    {
501        return function_exists('sodium_bin2hex') ?
502            sodium_bin2hex($data) :
503            Hex::encode($data);
504    }
505}
506