[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Keys/ -> PuTTY.php (source)

   1  <?php
   2  
   3  /**
   4   * PuTTY Formatted Key Handler
   5   *
   6   * See PuTTY's SSHPUBK.C and https://tartarus.org/~simon/putty-snapshots/htmldoc/AppendixC.html
   7   *
   8   * PHP version 5
   9   *
  10   * @author    Jim Wigginton <terrafrost@php.net>
  11   * @copyright 2016 Jim Wigginton
  12   * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
  13   * @link      http://phpseclib.sourceforge.net
  14   */
  15  
  16  namespace phpseclib3\Crypt\Common\Formats\Keys;
  17  
  18  use phpseclib3\Common\Functions\Strings;
  19  use phpseclib3\Crypt\AES;
  20  use phpseclib3\Crypt\Hash;
  21  use phpseclib3\Crypt\Random;
  22  use phpseclib3\Exception\UnsupportedAlgorithmException;
  23  
  24  /**
  25   * PuTTY Formatted Key Handler
  26   *
  27   * @author  Jim Wigginton <terrafrost@php.net>
  28   */
  29  abstract class PuTTY
  30  {
  31      /**
  32       * Default comment
  33       *
  34       * @var string
  35       */
  36      private static $comment = 'phpseclib-generated-key';
  37  
  38      /**
  39       * Default version
  40       *
  41       * @var int
  42       */
  43      private static $version = 2;
  44  
  45      /**
  46       * Sets the default comment
  47       *
  48       * @param string $comment
  49       */
  50      public static function setComment($comment)
  51      {
  52          self::$comment = str_replace(["\r", "\n"], '', $comment);
  53      }
  54  
  55      /**
  56       * Sets the default version
  57       *
  58       * @param int $version
  59       */
  60      public static function setVersion($version)
  61      {
  62          if ($version != 2 && $version != 3) {
  63              throw new \RuntimeException('Only supported versions are 2 and 3');
  64          }
  65          self::$version = $version;
  66      }
  67  
  68      /**
  69       * Generate a symmetric key for PuTTY v2 keys
  70       *
  71       * @param string $password
  72       * @param int $length
  73       * @return string
  74       */
  75      private static function generateV2Key($password, $length)
  76      {
  77          $symkey = '';
  78          $sequence = 0;
  79          while (strlen($symkey) < $length) {
  80              $temp = pack('Na*', $sequence++, $password);
  81              $symkey .= Strings::hex2bin(sha1($temp));
  82          }
  83          return substr($symkey, 0, $length);
  84      }
  85  
  86      /**
  87       * Generate a symmetric key for PuTTY v3 keys
  88       *
  89       * @param string $password
  90       * @param string $flavour
  91       * @param int $memory
  92       * @param int $passes
  93       * @param string $salt
  94       * @return array
  95       */
  96      private static function generateV3Key($password, $flavour, $memory, $passes, $salt)
  97      {
  98          if (!function_exists('sodium_crypto_pwhash')) {
  99              throw new \RuntimeException('sodium_crypto_pwhash needs to exist for Argon2 password hasing');
 100          }
 101  
 102          switch ($flavour) {
 103              case 'Argon2i':
 104                  $flavour = SODIUM_CRYPTO_PWHASH_ALG_ARGON2I13;
 105                  break;
 106              case 'Argon2id':
 107                  $flavour = SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13;
 108                  break;
 109              default:
 110                  throw new UnsupportedAlgorithmException('Only Argon2i and Argon2id are supported');
 111          }
 112  
 113          $length = 80; // keylen + ivlen + mac_keylen
 114          $temp = sodium_crypto_pwhash($length, $password, $salt, $passes, $memory << 10, $flavour);
 115  
 116          $symkey = substr($temp, 0, 32);
 117          $symiv = substr($temp, 32, 16);
 118          $hashkey = substr($temp, -32);
 119  
 120          return compact('symkey', 'symiv', 'hashkey');
 121      }
 122  
 123      /**
 124       * Break a public or private key down into its constituent components
 125       *
 126       * @param string $key
 127       * @param string $password
 128       * @return array
 129       */
 130      public static function load($key, $password)
 131      {
 132          if (!Strings::is_stringable($key)) {
 133              throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key));
 134          }
 135  
 136          if (strpos($key, 'BEGIN SSH2 PUBLIC KEY') !== false) {
 137              $lines = preg_split('#[\r\n]+#', $key);
 138              switch (true) {
 139                  case $lines[0] != '---- BEGIN SSH2 PUBLIC KEY ----':
 140                      throw new \UnexpectedValueException('Key doesn\'t start with ---- BEGIN SSH2 PUBLIC KEY ----');
 141                  case $lines[count($lines) - 1] != '---- END SSH2 PUBLIC KEY ----':
 142                      throw new \UnexpectedValueException('Key doesn\'t end with ---- END SSH2 PUBLIC KEY ----');
 143              }
 144              $lines = array_splice($lines, 1, -1);
 145              $lines = array_map(function ($line) {
 146                  return rtrim($line, "\r\n");
 147              }, $lines);
 148              $data = $current = '';
 149              $values = [];
 150              $in_value = false;
 151              foreach ($lines as $line) {
 152                  switch (true) {
 153                      case preg_match('#^(.*?): (.*)#', $line, $match):
 154                          $in_value = $line[strlen($line) - 1] == '\\';
 155                          $current = strtolower($match[1]);
 156                          $values[$current] = $in_value ? substr($match[2], 0, -1) : $match[2];
 157                          break;
 158                      case $in_value:
 159                          $in_value = $line[strlen($line) - 1] == '\\';
 160                          $values[$current] .= $in_value ? substr($line, 0, -1) : $line;
 161                          break;
 162                      default:
 163                          $data .= $line;
 164                  }
 165              }
 166  
 167              $components = call_user_func([static::PUBLIC_HANDLER, 'load'], $data);
 168              if ($components === false) {
 169                  throw new \UnexpectedValueException('Unable to decode public key');
 170              }
 171              $components += $values;
 172              $components['comment'] = str_replace(['\\\\', '\"'], ['\\', '"'], $values['comment']);
 173  
 174              return $components;
 175          }
 176  
 177          $components = [];
 178  
 179          $key = preg_split('#\r\n|\r|\n#', trim($key));
 180          if (Strings::shift($key[0], strlen('PuTTY-User-Key-File-')) != 'PuTTY-User-Key-File-') {
 181              return false;
 182          }
 183          $version = (int) Strings::shift($key[0], 3); // should be either "2: " or "3: 0" prior to int casting
 184          if ($version != 2 && $version != 3) {
 185              throw new \RuntimeException('Only v2 and v3 PuTTY private keys are supported');
 186          }
 187          $components['type'] = $type = rtrim($key[0]);
 188          if (!in_array($type, static::$types)) {
 189              $error = count(static::$types) == 1 ?
 190                  'Only ' . static::$types[0] . ' keys are supported. ' :
 191                  '';
 192              throw new UnsupportedAlgorithmException($error . 'This is an unsupported ' . $type . ' key');
 193          }
 194          $encryption = trim(preg_replace('#Encryption: (.+)#', '$1', $key[1]));
 195          $components['comment'] = trim(preg_replace('#Comment: (.+)#', '$1', $key[2]));
 196  
 197          $publicLength = trim(preg_replace('#Public-Lines: (\d+)#', '$1', $key[3]));
 198          $public = Strings::base64_decode(implode('', array_map('trim', array_slice($key, 4, $publicLength))));
 199  
 200          $source = Strings::packSSH2('ssss', $type, $encryption, $components['comment'], $public);
 201  
 202          extract(unpack('Nlength', Strings::shift($public, 4)));
 203          $newtype = Strings::shift($public, $length);
 204          if ($newtype != $type) {
 205              throw new \RuntimeException('The binary type does not match the human readable type field');
 206          }
 207  
 208          $components['public'] = $public;
 209  
 210          switch ($version) {
 211              case 3:
 212                  $hashkey = '';
 213                  break;
 214              case 2:
 215                  $hashkey = 'putty-private-key-file-mac-key';
 216          }
 217  
 218          $offset = $publicLength + 4;
 219          switch ($encryption) {
 220              case 'aes256-cbc':
 221                  $crypto = new AES('cbc');
 222                  switch ($version) {
 223                      case 3:
 224                          $flavour = trim(preg_replace('#Key-Derivation: (.*)#', '$1', $key[$offset++]));
 225                          $memory = trim(preg_replace('#Argon2-Memory: (\d+)#', '$1', $key[$offset++]));
 226                          $passes = trim(preg_replace('#Argon2-Passes: (\d+)#', '$1', $key[$offset++]));
 227                          $parallelism = trim(preg_replace('#Argon2-Parallelism: (\d+)#', '$1', $key[$offset++]));
 228                          $salt = Strings::hex2bin(trim(preg_replace('#Argon2-Salt: ([0-9a-f]+)#', '$1', $key[$offset++])));
 229  
 230                          extract(self::generateV3Key($password, $flavour, $memory, $passes, $salt));
 231  
 232                          break;
 233                      case 2:
 234                          $symkey = self::generateV2Key($password, 32);
 235                          $symiv = str_repeat("\0", $crypto->getBlockLength() >> 3);
 236                          $hashkey .= $password;
 237                  }
 238          }
 239  
 240          switch ($version) {
 241              case 3:
 242                  $hash = new Hash('sha256');
 243                  $hash->setKey($hashkey);
 244                  break;
 245              case 2:
 246                  $hash = new Hash('sha1');
 247                  $hash->setKey(sha1($hashkey, true));
 248          }
 249  
 250          $privateLength = trim(preg_replace('#Private-Lines: (\d+)#', '$1', $key[$offset++]));
 251          $private = Strings::base64_decode(implode('', array_map('trim', array_slice($key, $offset, $privateLength))));
 252  
 253          if ($encryption != 'none') {
 254              $crypto->setKey($symkey);
 255              $crypto->setIV($symiv);
 256              $crypto->disablePadding();
 257              $private = $crypto->decrypt($private);
 258          }
 259  
 260          $source .= Strings::packSSH2('s', $private);
 261  
 262          $hmac = trim(preg_replace('#Private-MAC: (.+)#', '$1', $key[$offset + $privateLength]));
 263          $hmac = Strings::hex2bin($hmac);
 264  
 265          if (!hash_equals($hash->hash($source), $hmac)) {
 266              throw new \UnexpectedValueException('MAC validation error');
 267          }
 268  
 269          $components['private'] = $private;
 270  
 271          return $components;
 272      }
 273  
 274      /**
 275       * Wrap a private key appropriately
 276       *
 277       * @param string $public
 278       * @param string $private
 279       * @param string $type
 280       * @param string $password
 281       * @param array $options optional
 282       * @return string
 283       */
 284      protected static function wrapPrivateKey($public, $private, $type, $password, array $options = [])
 285      {
 286          $encryption = (!empty($password) || is_string($password)) ? 'aes256-cbc' : 'none';
 287          $comment = isset($options['comment']) ? $options['comment'] : self::$comment;
 288          $version = isset($options['version']) ? $options['version'] : self::$version;
 289  
 290          $key = "PuTTY-User-Key-File-$version: $type\r\n";
 291          $key .= "Encryption: $encryption\r\n";
 292          $key .= "Comment: $comment\r\n";
 293  
 294          $public = Strings::packSSH2('s', $type) . $public;
 295  
 296          $source = Strings::packSSH2('ssss', $type, $encryption, $comment, $public);
 297  
 298          $public = Strings::base64_encode($public);
 299          $key .= "Public-Lines: " . ((strlen($public) + 63) >> 6) . "\r\n";
 300          $key .= chunk_split($public, 64);
 301  
 302          if (empty($password) && !is_string($password)) {
 303              $source .= Strings::packSSH2('s', $private);
 304              switch ($version) {
 305                  case 3:
 306                      $hash = new Hash('sha256');
 307                      $hash->setKey('');
 308                      break;
 309                  case 2:
 310                      $hash = new Hash('sha1');
 311                      $hash->setKey(sha1('putty-private-key-file-mac-key', true));
 312              }
 313          } else {
 314              $private .= Random::string(16 - (strlen($private) & 15));
 315              $source .= Strings::packSSH2('s', $private);
 316              $crypto = new AES('cbc');
 317  
 318              switch ($version) {
 319                  case 3:
 320                      $salt = Random::string(16);
 321                      $key .= "Key-Derivation: Argon2id\r\n";
 322                      $key .= "Argon2-Memory: 8192\r\n";
 323                      $key .= "Argon2-Passes: 13\r\n";
 324                      $key .= "Argon2-Parallelism: 1\r\n";
 325                      $key .= "Argon2-Salt: " . Strings::bin2hex($salt) . "\r\n";
 326                      extract(self::generateV3Key($password, 'Argon2id', 8192, 13, $salt));
 327  
 328                      $hash = new Hash('sha256');
 329                      $hash->setKey($hashkey);
 330  
 331                      break;
 332                  case 2:
 333                      $symkey = self::generateV2Key($password, 32);
 334                      $symiv = str_repeat("\0", $crypto->getBlockLength() >> 3);
 335                      $hashkey = 'putty-private-key-file-mac-key' . $password;
 336  
 337                      $hash = new Hash('sha1');
 338                      $hash->setKey(sha1($hashkey, true));
 339              }
 340  
 341              $crypto->setKey($symkey);
 342              $crypto->setIV($symiv);
 343              $crypto->disablePadding();
 344              $private = $crypto->encrypt($private);
 345              $mac = $hash->hash($source);
 346          }
 347  
 348          $private = Strings::base64_encode($private);
 349          $key .= 'Private-Lines: ' . ((strlen($private) + 63) >> 6) . "\r\n";
 350          $key .= chunk_split($private, 64);
 351          $key .= 'Private-MAC: ' . Strings::bin2hex($hash->hash($source)) . "\r\n";
 352  
 353          return $key;
 354      }
 355  
 356      /**
 357       * Wrap a public key appropriately
 358       *
 359       * This is basically the format described in RFC 4716 (https://tools.ietf.org/html/rfc4716)
 360       *
 361       * @param string $key
 362       * @param string $type
 363       * @return string
 364       */
 365      protected static function wrapPublicKey($key, $type)
 366      {
 367          $key = pack('Na*a*', strlen($type), $type, $key);
 368          $key = "---- BEGIN SSH2 PUBLIC KEY ----\r\n" .
 369                 'Comment: "' . str_replace(['\\', '"'], ['\\\\', '\"'], self::$comment) . "\"\r\n" .
 370                 chunk_split(Strings::base64_encode($key), 64) .
 371                 '---- END SSH2 PUBLIC KEY ----';
 372          return $key;
 373      }
 374  }