Subversion Repositories oidplus

Rev

Rev 874 | Blame | Compare with Previous | Last modification | View Log | RSS feed

  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.  
  14. namespace phpseclib3\Common\Functions;
  15.  
  16. use ParagonIE\ConstantTime\Base64;
  17. use ParagonIE\ConstantTime\Base64UrlSafe;
  18. use ParagonIE\ConstantTime\Hex;
  19. use phpseclib3\Math\BigInteger;
  20. use phpseclib3\Math\Common\FiniteField;
  21.  
  22. /**
  23.  * Common String Functions
  24.  *
  25.  * @author  Jim Wigginton <terrafrost@php.net>
  26.  */
  27. abstract 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) :
  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.