[ Index ] |
PHP Cross Reference of DokuWiki |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body