Merge branch '3.0'

This commit is contained in:
terrafrost 2022-02-17 08:22:56 -06:00
commit 7cbd239124
2 changed files with 151 additions and 15 deletions

View File

@ -3,6 +3,8 @@
/**
* PuTTY Formatted Key Handler
*
* See PuTTY's SSHPUBK.C and https://tartarus.org/~simon/putty-snapshots/htmldoc/AppendixC.html
*
* PHP version 5
*
* @category Crypt
@ -128,8 +130,14 @@ abstract class PuTTY
$components = [];
$key = preg_split('#\r\n|\r|\n#', trim($key));
$type = trim(preg_replace('#PuTTY-User-Key-File-2: (.+)#', '$1', $key[0]));
$components['type'] = $type;
if (Strings::shift($key[0], strlen('PuTTY-User-Key-File-')) != 'PuTTY-User-Key-File-') {
return false;
}
$version = (int) Strings::shift($key[0], 3); // should be either "2: " or "3: 0" prior to int casting
if ($version != 2 && $version != 3) {
throw new \RuntimeException('Only v2 and v3 PuTTY private keys are supported');
}
$components['type'] = $type = rtrim($key[0]);
if (!in_array($type, static::$types)) {
$error = count(static::$types) == 1 ?
'Only ' . static::$types[0] . ' keys are supported. ' :
@ -152,30 +160,82 @@ abstract class PuTTY
$components['public'] = $public;
$privateLength = trim(preg_replace('#Private-Lines: (\d+)#', '$1', $key[$publicLength + 4]));
$private = Base64::decode(implode('', array_map('trim', array_slice($key, $publicLength + 5, $privateLength))));
switch ($encryption) {
case 'aes256-cbc':
$symkey = self::generateSymmetricKey($password, 32);
$crypto = new AES('cbc');
switch ($version) {
case 3:
$hashkey = '';
break;
case 2:
$hashkey = 'putty-private-key-file-mac-key';
}
$hashkey = 'putty-private-key-file-mac-key';
$offset = $publicLength + 4;
switch ($encryption) {
case 'aes256-cbc':
$crypto = new AES('cbc');
switch ($version) {
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++]));
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++]));
$passes = trim(preg_replace('#Argon2-Passes: (\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++])));
$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);
break;
case 2:
$symkey = '';
$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);
$hashkey.= $password;
}
}
switch ($version) {
case 3:
$hash = new Hash('sha256');
$hash->setKey($hashkey);
break;
case 2:
$hash = new Hash('sha1');
$hash->setKey(sha1($hashkey, true));
}
$privateLength = trim(preg_replace('#Private-Lines: (\d+)#', '$1', $key[$offset++]));
$private = Base64::decode(implode('', array_map('trim', array_slice($key, $offset, $privateLength))));
if ($encryption != 'none') {
$hashkey.= $password;
$crypto->setKey($symkey);
$crypto->setIV(str_repeat("\0", $crypto->getBlockLength() >> 3));
$crypto->setIV($symiv);
$crypto->disablePadding();
$private = $crypto->decrypt($private);
}
$source.= Strings::packSSH2('s', $private);
$hash = new Hash('sha1');
$hash->setKey(sha1($hashkey, true));
$hmac = trim(preg_replace('#Private-MAC: (.+)#', '$1', $key[$publicLength + $privateLength + 5]));
$hmac = trim(preg_replace('#Private-MAC: (.+)#', '$1', $key[$offset + $privateLength]));
$hmac = Hex::decode($hmac);
if (!hash_equals($hash->hash($source), $hmac)) {

View File

@ -1150,4 +1150,80 @@ n9dyFZYXxil/cgFG/PDMnuXy1Wcl8hb8iwQag4Y7ohiLXVTJa/0BAgMBAAE=
$this->assertSame($str1, $str2);
}
public function testPuTTYV3NoPW()
{
$key = 'PuTTY-User-Key-File-3: ssh-rsa
Encryption: none
Comment: rsa-key-20220216
Public-Lines: 6
AAAAB3NzaC1yc2EAAAADAQABAAABAQCJ39DLYw81oZmBMeRze+Plu0p8+kJezer4
mRpltRoqpZ0yRnyb5k0FtXrDeYL9IyCceOTsse/qks3CtVWQ2q7C2tqyezmk8mDf
aKXqnaSG3hHZo7vJcy76J7NNB6Mz2BxF9RGvb+sylEKdWOJdgmYC6dzyvpg/0qs6
yNPQGA5QOOzy2AstxnsujDl16I0GGsjw7ybc5844Hq4VhIQaft2Yd35UqGt5G1hs
nIZu1cLO/F+8xs+0xEY04FvJRNAoJGlVc8oPx7slU7vF5m22AmBqrhkljbid72OR
oXpI+4c7zc0dYZBIMoAEIJKTbliQE1WV0lYiXkS9RY3UjUyPLho9
Private-Lines: 14
AAABAQCI4IliEeMcpGVILOcXe2yCO1E1CCLyCc53pU/en0/t/OM18WJuR9I5k7Tf
8XeIpeIPVbo3/mMn5zydS/c5ytDrI+kwfkN5LSPdSABIDt8zAa6I+hNJaK+/q8BG
/gkZRDi1fxpiqGLAoQ4NNhvtJ7Lsu44d8/gkjJpvzsbx9Z/oJVK8ID10Wiiz9R7u
WPCOJbrETGU1LaY4N0hwhbqD28xtX4ypBh+HQ9umCqOMopeqVhebMolAZ62K5V+N
SbdN1JFk2FPQxMv3v4ApDW48AcJ1dNgO6euncaySLaQv3tnxYVjKVaf3JO0ALzoq
zsR2uj5bJUvhSapj9uWdDJTurGzFAAAAgQDY5t/F2Ruoa5wtF/XTiIxFpb//xQ+2
JhQOWd1fZZ+oMclqNS5E45E11TWnKthgr5NN4UB6TH4rtUETjsypD3w2PYZamTD1
QzeoOS0xRxjKfQu08ApDV94mx9LfX6Xi2IqTW0pC+IbBx8AUnK7J7scva8TYn7Qu
1QLSY4/tn3BBBwAAAIEAorolHJnR+w5FajTc8VeqN5E9bfc39Mr+2lQcqtARJGAM
2jLhN3ZWGIboG3Ttqcbfuicv/WzFe+gGRA8awvMS4v2C5/knZl4Vq859KCP7JOeW
63+5mLw5OKZOzWkguMu8+IfkUtIMv1JFuCU2eRL5elUthKlK6WFcMejuygNTrZsA
AACARP7yi23FNxAqHcgbx5MlrLYbMSjxp5yT+1XeNVTSpM/dvDVsy+8ETi/c1870
UfAzuIHQl2fu6NdtPBQUoqWKgBRtp46J/BoWF3Ty6klz+FAP2of4gojYvqa87H+6
dW7G8+QXxXM704cxjbBQAApItfVw3upWPrYP9FDy7xvtYRY=
Private-MAC: 7979eb6f604fb3e0bd191295479517f641598649167835402c6cbfde6cbf21ef';
$key = PublicKeyLoader::load($key);
$this->assertInstanceOf(PrivateKey::class, $key);
}
public function testPuTTYV3PW()
{
if (!function_exists('sodium_crypto_pwhash')) {
self::markTestSkipped('sodium_crypto_pwhash() function is not available.');
}
$key = 'PuTTY-User-Key-File-3: ssh-rsa
Encryption: aes256-cbc
Comment: rsa-key-20220216
Public-Lines: 6
AAAAB3NzaC1yc2EAAAADAQABAAABAQCJ39DLYw81oZmBMeRze+Plu0p8+kJezer4
mRpltRoqpZ0yRnyb5k0FtXrDeYL9IyCceOTsse/qks3CtVWQ2q7C2tqyezmk8mDf
aKXqnaSG3hHZo7vJcy76J7NNB6Mz2BxF9RGvb+sylEKdWOJdgmYC6dzyvpg/0qs6
yNPQGA5QOOzy2AstxnsujDl16I0GGsjw7ybc5844Hq4VhIQaft2Yd35UqGt5G1hs
nIZu1cLO/F+8xs+0xEY04FvJRNAoJGlVc8oPx7slU7vF5m22AmBqrhkljbid72OR
oXpI+4c7zc0dYZBIMoAEIJKTbliQE1WV0lYiXkS9RY3UjUyPLho9
Key-Derivation: Argon2id
Argon2-Memory: 8192
Argon2-Passes: 13
Argon2-Parallelism: 1
Argon2-Salt: d9bfa07d14a450a26ada4eb5d30c4dae
Private-Lines: 14
L3TUmo97jnxJVYIScxzPIaq19/yNQ5HDQKGSTz4vqUrQR3wXQEyhzxlN2mm5zZtT
pst7K61P0awtjs4kHUfsKxXh/upv7ndS9u9G7cnnBfP5mjs0wAE2VaghbP4UXprH
/MQC9Dr13Iuydv5Oih+PLpkvM3DbY5t+nrIWy/29yDLYe/QjLvy346Gz3pnLmCfb
hbEFfjefdppa+6QZ+qU6ai/NMAM/Q5OxjRlIo1brrKJNvMrbzP7irZ4+Ao2Or/hX
nb2ZZLY0eUotD8iFuOk2EjjqP9iakag1OHdvdy6EcPzkIObN5YeZGz9/hRDFr9Ml
xNxdaw5c1BhqU5pm0B0HUDqW5kmYTiugUKQiGr0+1ckliUt6jsb7YImnqJIgL7PS
vKcqNvz95u4on77gHPl2JdsXxuz6jOkDwc9jvsJCtIMJ8qhAVXGS7WaH2aF9ty7B
4E+f2yIbsRr0RFCZoTTjTmhtYsVd7DYo0Jftya3Sh/lVO1MLo1z8em0MFJdR683N
tRDA2lbRPOdKYaiKdyp5bAsl4fqPR1e2GR9ybalPn/XSFDRtDfdMr7hyQboBR7uC
X3nYsh5OiXakUSr2ST41pP27s8F48590M6xWb9LGFJA+JqmAZ5rxPTxFYjkz27y9
Yvlq6lvM+XsUREPrxhWrHya4Jyp4WtyVtJXDg626hoZBSEtcOY/mbPfwVFnoU9vz
V8TI/YU837mUceEJlEQEbT+bFJfh0W5jzAYx2xX6uPnDkodBMK2p6QS3ZKib0NJ7
W+jQr9TT40H0agZhtAmPKaLGxtgdpUps1CDPV+8Y/pBf28CsI2DjFaOYopZXcW9s
vCIjXopt4wAKbXiLyb5JXzFfB7CVron48NHB7wzuwvnUoYa/4dbjeEos+1y72xoP
Private-MAC: d26baf87446604974287b682ed9e0c00ce54e460e1cb719953a81291147b3c59
';
$key = PublicKeyLoader::load($key, 'demo');
$this->assertInstanceOf(PrivateKey::class, $key);
}
}