From 45d2ddcbc4d1bd1d011a1a8b87585c0ca0b30021 Mon Sep 17 00:00:00 2001 From: terrafrost Date: Wed, 16 Feb 2022 23:56:27 -0600 Subject: [PATCH] RSA: add support for loading PuTTY v3 keys --- phpseclib/Crypt/RSA.php | 63 +++++++++++++++++----- tests/Unit/Crypt/RSA/LoadKeyTest.php | 81 ++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 12 deletions(-) diff --git a/phpseclib/Crypt/RSA.php b/phpseclib/Crypt/RSA.php index 4ac86a4a..03022492 100644 --- a/phpseclib/Crypt/RSA.php +++ b/phpseclib/Crypt/RSA.php @@ -1487,11 +1487,18 @@ class Crypt_RSA unset($xml); return isset($this->components['modulus']) && isset($this->components['publicExponent']) ? $this->components : false; - // from PuTTY's SSHPUBK.C + // see PuTTY's SSHPUBK.C and https://tartarus.org/~simon/putty-snapshots/htmldoc/AppendixC.html case CRYPT_RSA_PRIVATE_FORMAT_PUTTY: $components = array(); $key = preg_split('#\r\n|\r|\n#', $key); - $type = trim(preg_replace('#PuTTY-User-Key-File-2: (.+)#', '$1', $key[0])); + if ($this->_string_shift($key[0], strlen('PuTTY-User-Key-File-')) != 'PuTTY-User-Key-File-') { + return false; + } + $version = (int) $this->_string_shift($key[0], 3); // should be either "2: " or "3: 0" prior to int casting + if ($version != 2 && $version != 3) { + return false; + } + $type = rtrim($key[0]); if ($type != 'ssh-rsa') { return false; } @@ -1506,26 +1513,58 @@ class Crypt_RSA extract(unpack('Nlength', $this->_string_shift($public, 4))); $components['modulus'] = new Math_BigInteger($this->_string_shift($public, $length), -256); - $privateLength = trim(preg_replace('#Private-Lines: (\d+)#', '$1', $key[$publicLength + 4])); - $private = base64_decode(implode('', array_map('trim', array_slice($key, $publicLength + 5, $privateLength)))); - + $offset = $publicLength + 4; switch ($encryption) { case 'aes256-cbc': if (!class_exists('Crypt_AES')) { include_once 'Crypt/AES.php'; } - $symkey = ''; - $sequence = 0; - while (strlen($symkey) < 32) { - $temp = pack('Na*', $sequence++, $this->password); - $symkey.= pack('H*', sha1($temp)); - } - $symkey = substr($symkey, 0, 32); $crypto = new Crypt_AES(); + switch ($version) { + case 3: + if (!function_exists('sodium_crypto_pwhash')) { + return false; + } + $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: + return false; + } + $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, $this->password, $salt, $passes, $memory << 10, $flavour); + + $symkey = substr($temp, 0, 32); + $symiv = substr($temp, 32, 16); + break; + case 2: + $symkey = ''; + $sequence = 0; + while (strlen($symkey) < 32) { + $temp = pack('Na*', $sequence++, $this->password); + $symkey.= pack('H*', sha1($temp)); + } + $symkey = substr($symkey, 0, 32); + $symiv = str_repeat("\0", 16); + } } + $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') { $crypto->setKey($symkey); + $crypto->setIV($symiv); $crypto->disablePadding(); $private = $crypto->decrypt($private); if ($private === false) { diff --git a/tests/Unit/Crypt/RSA/LoadKeyTest.php b/tests/Unit/Crypt/RSA/LoadKeyTest.php index 3b624e66..ba959d04 100644 --- a/tests/Unit/Crypt/RSA/LoadKeyTest.php +++ b/tests/Unit/Crypt/RSA/LoadKeyTest.php @@ -535,4 +535,85 @@ Vyaqr/WTPzxdXJAAAADHJvb3RAdmFncmFudAECAwQFBg== $this->assertTrue($rsa->verify('zzz', $sig)); } + + 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'; + + $rsa = new Crypt_RSA(); + $this->assertTrue($rsa->loadKey($key)); + $this->assertIsString($rsa->getPublicKey()); + $this->assertIsString($rsa->getPrivateKey()); + } + + 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 +'; + + $rsa = new Crypt_RSA(); + $rsa->setPassword('demo'); + $this->assertTrue($rsa->loadKey($key)); + $this->assertIsString($rsa->getPublicKey()); + $this->assertIsString($rsa->getPrivateKey()); + } }