Subversion Repositories oidplus

Rev

Rev 846 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

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