PuTTY.php 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. <?php
  2. /**
  3. * PuTTY Formatted Key Handler
  4. *
  5. * PHP version 5
  6. *
  7. * @category Crypt
  8. * @package Common
  9. * @author Jim Wigginton <terrafrost@php.net>
  10. * @copyright 2016 Jim Wigginton
  11. * @license http://www.opensource.org/licenses/mit-license.html MIT License
  12. * @link http://phpseclib.sourceforge.net
  13. */
  14. namespace phpseclib\Crypt\Common\Keys;
  15. use ParagonIE\ConstantTime\Base64;
  16. use ParagonIE\ConstantTime\Hex;
  17. use phpseclib\Crypt\AES;
  18. use phpseclib\Crypt\Hash;
  19. use phpseclib\Crypt\Random;
  20. use phpseclib\Common\Functions\Strings;
  21. /**
  22. * PuTTY Formatted Key Handler
  23. *
  24. * @package Common
  25. * @author Jim Wigginton <terrafrost@php.net>
  26. * @access public
  27. */
  28. abstract class PuTTY
  29. {
  30. /**
  31. * Default comment
  32. *
  33. * @var string
  34. * @access private
  35. */
  36. private static $comment = 'phpseclib-generated-key';
  37. /**
  38. * Sets the default comment
  39. *
  40. * @access public
  41. * @param string $comment
  42. */
  43. public static function setComment($comment)
  44. {
  45. self::$comment = str_replace(["\r", "\n"], '', $comment);
  46. }
  47. /**
  48. * Generate a symmetric key for PuTTY keys
  49. *
  50. * @access public
  51. * @param string $password
  52. * @param int $length
  53. * @return string
  54. */
  55. private static function generateSymmetricKey($password, $length)
  56. {
  57. $symkey = '';
  58. $sequence = 0;
  59. while (strlen($symkey) < $length) {
  60. $temp = pack('Na*', $sequence++, $password);
  61. $symkey.= Hex::decode(sha1($temp));
  62. }
  63. return substr($symkey, 0, $length);
  64. }
  65. /**
  66. * Break a public or private key down into its constituent components
  67. *
  68. * @access public
  69. * @param string $key
  70. * @param string $publicHandler
  71. * @param string $type
  72. * @param string $password
  73. * @return array
  74. */
  75. protected static function load($key, $password)
  76. {
  77. if (!is_string($key)) {
  78. return false;
  79. }
  80. if (strpos($key, 'BEGIN SSH2 PUBLIC KEY')) {
  81. $data = preg_split('#[\r\n]+#', $key);
  82. $data = array_splice($data, 2, -1);
  83. $data = implode('', $data);
  84. $components = call_user_func([static::PUBLIC_HANDLER, 'load'], $data);
  85. if ($components === false) {
  86. return false;
  87. }
  88. if (!preg_match('#Comment: "(.+)"#', $key, $matches)) {
  89. return false;
  90. }
  91. $components['comment'] = str_replace(['\\\\', '\"'], ['\\', '"'], $matches[1]);
  92. return $components;
  93. }
  94. $components = [];
  95. $key = preg_split('#\r\n|\r|\n#', trim($key));
  96. $type = trim(preg_replace('#PuTTY-User-Key-File-2: (.+)#', '$1', $key[0]));
  97. if ($type != static::TYPE) {
  98. return false;
  99. }
  100. $encryption = trim(preg_replace('#Encryption: (.+)#', '$1', $key[1]));
  101. $components['comment'] = trim(preg_replace('#Comment: (.+)#', '$1', $key[2]));
  102. $publicLength = trim(preg_replace('#Public-Lines: (\d+)#', '$1', $key[3]));
  103. $public = Base64::decode(implode('', array_map('trim', array_slice($key, 4, $publicLength))));
  104. $source = Strings::packSSH2('ssss', static::TYPE, $encryption, $components['comment'], $public);
  105. extract(unpack('Nlength', Strings::shift($public, 4)));
  106. if (Strings::shift($public, $length) != static::TYPE) {
  107. return false;
  108. }
  109. $components['public'] = $public;
  110. $privateLength = trim(preg_replace('#Private-Lines: (\d+)#', '$1', $key[$publicLength + 4]));
  111. $private = Base64::decode(implode('', array_map('trim', array_slice($key, $publicLength + 5, $privateLength))));
  112. switch ($encryption) {
  113. case 'aes256-cbc':
  114. $symkey = self::generateSymmetricKey($password, 32);
  115. $crypto = new AES(AES::MODE_CBC);
  116. }
  117. $hashkey = 'putty-private-key-file-mac-key';
  118. if ($encryption != 'none') {
  119. $hashkey.= $password;
  120. $crypto->setKey($symkey);
  121. $crypto->setIV(str_repeat("\0", $crypto->getBlockLength() >> 3));
  122. $crypto->disablePadding();
  123. $private = $crypto->decrypt($private);
  124. }
  125. $source.= Strings::packSSH2('s', $private);
  126. $hash = new Hash('sha1');
  127. $hash->setKey(sha1($hashkey, true));
  128. $hmac = trim(preg_replace('#Private-MAC: (.+)#', '$1', $key[$publicLength + $privateLength + 5]));
  129. $hmac = Hex::decode($hmac);
  130. if (!Strings::equals($hash->hash($source), $hmac)) {
  131. throw new \UnexpectedValueException('MAC validation error');
  132. }
  133. $components['private'] = $private;
  134. return $components;
  135. }
  136. /**
  137. * Wrap a private key appropriately
  138. *
  139. * @access private
  140. * @param string $public
  141. * @param string $private
  142. * @param string $password
  143. * @return string
  144. */
  145. protected static function wrapPrivateKey($public, $private, $password)
  146. {
  147. $key = "PuTTY-User-Key-File-2: " . static::TYPE . "\r\nEncryption: ";
  148. $encryption = (!empty($password) || is_string($password)) ? 'aes256-cbc' : 'none';
  149. $key.= $encryption;
  150. $key.= "\r\nComment: " . self::$comment . "\r\n";
  151. $public = Strings::packSSH2('s', static::TYPE) . $public;
  152. $source = Strings::packSSH2('ssss', static::TYPE, $encryption, self::$comment, $public);
  153. $public = Base64::encode($public);
  154. $key.= "Public-Lines: " . ((strlen($public) + 63) >> 6) . "\r\n";
  155. $key.= chunk_split($public, 64);
  156. if (empty($password) && !is_string($password)) {
  157. $source.= Strings::packSSH2('s', $private);
  158. $hashkey = 'putty-private-key-file-mac-key';
  159. } else {
  160. $private.= Random::string(16 - (strlen($private) & 15));
  161. $source.= Strings::packSSH2('s', $private);
  162. $crypto = new AES(AES::MODE_CBC);
  163. $crypto->setKey(self::generateSymmetricKey($password, 32));
  164. $crypto->setIV(str_repeat("\0", $crypto->getBlockLength() >> 3));
  165. $crypto->disablePadding();
  166. $private = $crypto->encrypt($private);
  167. $hashkey = 'putty-private-key-file-mac-key' . $password;
  168. }
  169. $private = Base64::encode($private);
  170. $key.= 'Private-Lines: ' . ((strlen($private) + 63) >> 6) . "\r\n";
  171. $key.= chunk_split($private, 64);
  172. $hash = new Hash('sha1');
  173. $hash->setKey(sha1($hashkey, true));
  174. $key.= 'Private-MAC: ' . Hex::encode($hash->hash($source)) . "\r\n";
  175. return $key;
  176. }
  177. /**
  178. * Wrap a public key appropriately
  179. *
  180. * This is basically the format described in RFC 4716 (https://tools.ietf.org/html/rfc4716)
  181. *
  182. * @access private
  183. * @param string $key
  184. * @return string
  185. */
  186. protected static function wrapPublicKey($key)
  187. {
  188. $key = pack('Na*a*', strlen(static::TYPE), static::TYPE, $key);
  189. $key = "---- BEGIN SSH2 PUBLIC KEY ----\r\n" .
  190. 'Comment: "' . str_replace(['\\', '"'], ['\\\\', '\"'], self::$comment) . "\"\r\n" .
  191. chunk_split(Base64::encode($key), 64) .
  192. '---- END SSH2 PUBLIC KEY ----';
  193. return $key;
  194. }
  195. }