1
0
mirror of https://github.com/danog/phpseclib.git synced 2024-12-11 08:39:43 +01:00

PuTTY: add support for saving PuTTY v3 keys

This commit is contained in:
terrafrost 2022-02-17 21:46:42 -06:00
parent 9f6af761b0
commit 97eea332c5

View File

@ -42,6 +42,14 @@ abstract class PuTTY
*/ */
private static $comment = 'phpseclib-generated-key'; private static $comment = 'phpseclib-generated-key';
/**
* Default version
*
* @var int
* @access private
*/
private static $version = 2;
/** /**
* Sets the default comment * Sets the default comment
* *
@ -54,14 +62,28 @@ abstract class PuTTY
} }
/** /**
* Generate a symmetric key for PuTTY keys * Sets the default version
* *
* @access public * @access public
* @param int $version
*/
public static function setVersion($version)
{
if ($version != 2 && $version != 3) {
throw new \RuntimeException('Only supported versions are 2 and 3');
}
self::$version = $version;
}
/**
* Generate a symmetric key for PuTTY v2 keys
*
* @access private
* @param string $password * @param string $password
* @param int $length * @param int $length
* @return string * @return string
*/ */
private static function generateSymmetricKey($password, $length) private static function generateV2Key($password, $length)
{ {
$symkey = ''; $symkey = '';
$sequence = 0; $sequence = 0;
@ -72,6 +94,44 @@ abstract class PuTTY
return substr($symkey, 0, $length); return substr($symkey, 0, $length);
} }
/**
* Generate a symmetric key for PuTTY v3 keys
*
* @access private
* @param string $password
* @param string $flavour
* @param int $memory
* @param int $passes
* @param string $salt
* @return array
*/
private static function generateV3Key($password, $flavour, $memory, $passes, $salt)
{
if (!function_exists('sodium_crypto_pwhash')) {
throw new \RuntimeException('sodium_crypto_pwhash needs to exist for Argon2 password hasing');
}
switch ($flavour) {
case 'Argon2i':
$flavour = SODIUM_CRYPTO_PWHASH_ALG_ARGON2I13;
break;
case 'Argon2id':
$flavour = SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13;
break;
default:
throw new UnsupportedAlgorithmException('Only Argon2i and Argon2id are supported');
}
$length = 80; // keylen + ivlen + mac_keylen
$temp = sodium_crypto_pwhash($length, $password, $salt, $passes, $memory << 10, $flavour);
$symkey = substr($temp, 0, 32);
$symiv = substr($temp, 32, 16);
$hashkey = substr($temp, -32);
return compact('symkey', 'symiv', 'hashkey');
}
/** /**
* Break a public or private key down into its constituent components * Break a public or private key down into its constituent components
* *
@ -174,40 +234,17 @@ abstract class PuTTY
$crypto = new AES('cbc'); $crypto = new AES('cbc');
switch ($version) { switch ($version) {
case 3: case 3:
if (!function_exists('sodium_crypto_pwhash')) {
throw new \RuntimeException('sodium_crypto_pwhash needs to exist for Argon2 password hasing');
}
$flavour = trim(preg_replace('#Key-Derivation: (.*)#', '$1', $key[$offset++])); $flavour = trim(preg_replace('#Key-Derivation: (.*)#', '$1', $key[$offset++]));
switch ($flavour) {
case 'Argon2i':
$flavour = SODIUM_CRYPTO_PWHASH_ALG_ARGON2I13;
break;
case 'Argon2id':
$flavour = SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13;
break;
default:
throw new UnsupportedAlgorithmException('Only Argon2i and Argon2id are supported');
}
$memory = trim(preg_replace('#Argon2-Memory: (\d+)#', '$1', $key[$offset++])); $memory = trim(preg_replace('#Argon2-Memory: (\d+)#', '$1', $key[$offset++]));
$passes = trim(preg_replace('#Argon2-Passes: (\d+)#', '$1', $key[$offset++])); $passes = trim(preg_replace('#Argon2-Passes: (\d+)#', '$1', $key[$offset++]));
$parallelism = trim(preg_replace('#Argon2-Parallelism: (\d+)#', '$1', $key[$offset++])); $parallelism = trim(preg_replace('#Argon2-Parallelism: (\d+)#', '$1', $key[$offset++]));
$salt = pack('H*', trim(preg_replace('#Argon2-Salt: ([0-9a-f]+)#', '$1', $key[$offset++]))); $salt = Hex::decode(trim(preg_replace('#Argon2-Salt: ([0-9a-f]+)#', '$1', $key[$offset++])));
$length = 80; // keylen + ivlen + mac_keylen extract(self::generateV3Key($password, $flavour, $memory, $passes, $salt));
$temp = sodium_crypto_pwhash($length, $password, $salt, $passes, $memory << 10, $flavour);
$symkey = substr($temp, 0, 32);
$symiv = substr($temp, 32, 16);
$hashkey = substr($temp, -32);
break; break;
case 2: case 2:
$symkey = ''; $symkey = self::generateV2Key($password, 32);
$sequence = 0;
while (strlen($symkey) < 32) {
$temp = pack('Na*', $sequence++, $password);
$symkey.= pack('H*', sha1($temp));
}
$symkey = substr($symkey, 0, 32);
$symiv = str_repeat("\0", $crypto->getBlockLength() >> 3); $symiv = str_repeat("\0", $crypto->getBlockLength() >> 3);
$hashkey.= $password; $hashkey.= $password;
} }
@ -262,9 +299,11 @@ abstract class PuTTY
{ {
$encryption = (!empty($password) || is_string($password)) ? 'aes256-cbc' : 'none'; $encryption = (!empty($password) || is_string($password)) ? 'aes256-cbc' : 'none';
$comment = isset($options['comment']) ? $options['comment'] : self::$comment; $comment = isset($options['comment']) ? $options['comment'] : self::$comment;
$version = isset($options['version']) ? $options['version'] : self::$version;
$key = "PuTTY-User-Key-File-2: " . $type . "\r\nEncryption: "; $key.= $encryption; $key = "PuTTY-User-Key-File-$version: $type\r\n";
$key.= "\r\nComment: " . $comment . "\r\n"; $key.= "Encryption: $encryption\r\n";
$key.= "Comment: $comment\r\n";
$public = Strings::packSSH2('s', $type) . $public; $public = Strings::packSSH2('s', $type) . $public;
@ -276,24 +315,53 @@ abstract class PuTTY
if (empty($password) && !is_string($password)) { if (empty($password) && !is_string($password)) {
$source.= Strings::packSSH2('s', $private); $source.= Strings::packSSH2('s', $private);
$hashkey = 'putty-private-key-file-mac-key'; switch ($version) {
case 3:
$hash = new Hash('sha256');
$hash->setKey('');
break;
case 2:
$hash = new Hash('sha1');
$hash->setKey(sha1('putty-private-key-file-mac-key', true));
}
} else { } else {
$private.= Random::string(16 - (strlen($private) & 15)); $private.= Random::string(16 - (strlen($private) & 15));
$source.= Strings::packSSH2('s', $private); $source.= Strings::packSSH2('s', $private);
$crypto = new AES('cbc'); $crypto = new AES('cbc');
$crypto->setKey(self::generateSymmetricKey($password, 32)); switch ($version) {
$crypto->setIV(str_repeat("\0", $crypto->getBlockLength() >> 3)); case 3:
$salt = Random::string(16);
$key.= "Key-Derivation: Argon2id\r\n";
$key.= "Argon2-Memory: 8192\r\n";
$key.= "Argon2-Passes: 13\r\n";
$key.= "Argon2-Parallelism: 1\r\n";
$key.= "Argon2-Salt: " . Hex::encode($salt) . "\r\n";
extract(self::generateV3Key($password, 'Argon2id', 8192, 13, $salt));
$hash = new Hash('sha256');
$hash->setKey($hashkey);
break;
case 2:
$symkey = self::generateV2Key($password, 32);
$symiv = str_repeat("\0", $crypto->getBlockLength() >> 3);
$hashkey = 'putty-private-key-file-mac-key' . $password;
$hash = new Hash('sha1');
$hash->setKey(sha1($hashkey, true));
}
$crypto->setKey($symkey);
$crypto->setIV($symiv);
$crypto->disablePadding(); $crypto->disablePadding();
$private = $crypto->encrypt($private); $private = $crypto->encrypt($private);
$hashkey = 'putty-private-key-file-mac-key' . $password; $mac = $hash->hash($source);
} }
$private = Base64::encode($private); $private = Base64::encode($private);
$key.= 'Private-Lines: ' . ((strlen($private) + 63) >> 6) . "\r\n"; $key.= 'Private-Lines: ' . ((strlen($private) + 63) >> 6) . "\r\n";
$key.= chunk_split($private, 64); $key.= chunk_split($private, 64);
$hash = new Hash('sha1');
$hash->setKey(sha1($hashkey, true));
$key.= 'Private-MAC: ' . Hex::encode($hash->hash($source)) . "\r\n"; $key.= 'Private-MAC: ' . Hex::encode($hash->hash($source)) . "\r\n";
return $key; return $key;