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