make it so OpenSSH encrypted keys can be created

This commit is contained in:
terrafrost 2022-07-31 17:14:20 -05:00
parent e54624c085
commit 826d8d6670
2 changed files with 28 additions and 7 deletions

View File

@ -88,6 +88,9 @@ abstract class OpenSSH
case 'none': case 'none':
break; break;
case 'aes256-ctr': case 'aes256-ctr':
if ($kdfname != 'bcrypt') {
throw new \RuntimeException('Only the bcrypt kdf is supported (' . $kdfname . ' encountered)');
}
list($salt, $rounds) = Strings::unpackSSH2('sN', $kdfoptions); list($salt, $rounds) = Strings::unpackSSH2('sN', $kdfoptions);
$crypto = new AES('ctr'); $crypto = new AES('ctr');
//$crypto->setKeyLength(256); //$crypto->setKeyLength(256);
@ -95,7 +98,7 @@ abstract class OpenSSH
$crypto->setPassword($password, 'bcrypt', $salt, $rounds, 32); $crypto->setPassword($password, 'bcrypt', $salt, $rounds, 32);
break; break;
default: 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); list($publicKey, $paddedKey) = Strings::unpackSSH2('ss', $key);
@ -178,10 +181,6 @@ abstract class OpenSSH
*/ */
protected static function wrapPrivateKey($publicKey, $privateKey, $password, $options) 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)); list(, $checkint) = unpack('N', Random::string(4));
$comment = isset($options['comment']) ? $options['comment'] : self::$comment; $comment = isset($options['comment']) ? $options['comment'] : self::$comment;
@ -189,6 +188,8 @@ abstract class OpenSSH
$privateKey . $privateKey .
Strings::packSSH2('s', $comment); Strings::packSSH2('s', $comment);
$usesEncryption = !empty($password) && is_string($password);
/* /*
from http://tools.ietf.org/html/rfc4253#section-6 : 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 'padding_length', 'payload', and 'random padding' MUST be a multiple
of the cipher block size or 8, whichever is larger. 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++) { for ($i = 1; $i <= $paddingLength; $i++) {
$paddedKey .= chr($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"; $key = "openssh-key-v1\0$key";
return "-----BEGIN OPENSSH PRIVATE KEY-----\n" . return "-----BEGIN OPENSSH PRIVATE KEY-----\n" .

View File

@ -565,4 +565,13 @@ MIIEDwIBADATBgcqhkjOPQIBBggqhkjOPQMBBwSCA/MwggPvAgEBBIID6P//////
$this->assertSameNL($raw, $key->toString('MontgomeryPrivate')); $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);
}
} }