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:
parent
9f6af761b0
commit
97eea332c5
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user