diff --git a/phpseclib/Crypt/Common/Formats/Keys/OpenSSH.php b/phpseclib/Crypt/Common/Formats/Keys/OpenSSH.php index ff66e256..3807e2a2 100644 --- a/phpseclib/Crypt/Common/Formats/Keys/OpenSSH.php +++ b/phpseclib/Crypt/Common/Formats/Keys/OpenSSH.php @@ -88,6 +88,9 @@ abstract class OpenSSH case 'none': break; case 'aes256-ctr': + if ($kdfname != 'bcrypt') { + throw new \RuntimeException('Only the bcrypt kdf is supported (' . $kdfname . ' encountered)'); + } list($salt, $rounds) = Strings::unpackSSH2('sN', $kdfoptions); $crypto = new AES('ctr'); //$crypto->setKeyLength(256); @@ -95,7 +98,7 @@ abstract class OpenSSH $crypto->setPassword($password, 'bcrypt', $salt, $rounds, 32); break; default: - throw new \RuntimeException('The only supported cipherse are: none, aes256-ctr (' . $ciphername . ' is being used'); + throw new \RuntimeException('The only supported cipherse are: none, aes256-ctr (' . $ciphername . ' is being used)'); } list($publicKey, $paddedKey) = Strings::unpackSSH2('ss', $key); @@ -178,10 +181,6 @@ abstract class OpenSSH */ protected static function wrapPrivateKey($publicKey, $privateKey, $password, $options) { - if (!empty($password) && is_string($password)) { - throw new UnsupportedFormatException('Encrypted OpenSSH private keys are not supported'); - } - list(, $checkint) = unpack('N', Random::string(4)); $comment = isset($options['comment']) ? $options['comment'] : self::$comment; @@ -189,6 +188,8 @@ abstract class OpenSSH $privateKey . Strings::packSSH2('s', $comment); + $usesEncryption = !empty($password) && is_string($password); + /* from http://tools.ietf.org/html/rfc4253#section-6 : @@ -196,11 +197,22 @@ abstract class OpenSSH 'padding_length', 'payload', and 'random padding' MUST be a multiple of the cipher block size or 8, whichever is larger. */ - $paddingLength = (7 * strlen($paddedKey)) % 8; + $blockSize = $usesEncryption ? 16 : 8; + $paddingLength = (($blockSize - 1) * strlen($paddedKey)) % $blockSize; for ($i = 1; $i <= $paddingLength; $i++) { $paddedKey .= chr($i); } - $key = Strings::packSSH2('sssNss', 'none', 'none', '', 1, $publicKey, $paddedKey); + if (!$usesEncryption) { + $key = Strings::packSSH2('sssNss', 'none', 'none', '', 1, $publicKey, $paddedKey); + } else { + $rounds = isset($options['rounds']) ? $options['rounds'] : 16; + $salt = Random::string(16); + $kdfoptions = Strings::packSSH2('sN', $salt, $rounds); + $crypto = new AES('ctr'); + $crypto->setPassword($password, 'bcrypt', $salt, $rounds, 32); + $paddedKey = $crypto->encrypt($paddedKey); + $key = Strings::packSSH2('sssNss', 'aes256-ctr', 'bcrypt', $kdfoptions, 1, $publicKey, $paddedKey); + } $key = "openssh-key-v1\0$key"; return "-----BEGIN OPENSSH PRIVATE KEY-----\n" . diff --git a/tests/Unit/Crypt/EC/KeyTest.php b/tests/Unit/Crypt/EC/KeyTest.php index 979d3c59..8b15b5af 100644 --- a/tests/Unit/Crypt/EC/KeyTest.php +++ b/tests/Unit/Crypt/EC/KeyTest.php @@ -565,4 +565,13 @@ MIIEDwIBADATBgcqhkjOPQIBBggqhkjOPQMBBwSCA/MwggPvAgEBBIID6P////// $this->assertSameNL($raw, $key->toString('MontgomeryPrivate')); } + + public function testOpenSSHEncryptedCreation() + { + $key = EC::createKey('Ed25519'); + $key = $key->withPassword('test')->toString('OpenSSH'); + + $key = PublicKeyLoader::load($key, 'test'); + $this->assertInstanceOf(PrivateKey::class, $key); + } }