2016-12-23 16:02:07 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
/**
|
|
|
|
* OpenSSH Key Handler
|
|
|
|
*
|
|
|
|
* PHP version 5
|
|
|
|
*
|
|
|
|
* Place in $HOME/.ssh/authorized_keys
|
|
|
|
*
|
|
|
|
* @author Jim Wigginton <terrafrost@php.net>
|
|
|
|
* @copyright 2015 Jim Wigginton
|
|
|
|
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
|
|
|
* @link http://phpseclib.sourceforge.net
|
|
|
|
*/
|
|
|
|
|
2022-06-04 15:31:21 +00:00
|
|
|
declare(strict_types=1);
|
|
|
|
|
2019-11-07 05:41:40 +00:00
|
|
|
namespace phpseclib3\Crypt\Common\Formats\Keys;
|
2016-12-23 16:02:07 +00:00
|
|
|
|
2019-11-07 05:41:40 +00:00
|
|
|
use phpseclib3\Common\Functions\Strings;
|
2022-07-30 23:07:26 +00:00
|
|
|
use phpseclib3\Crypt\AES;
|
2019-11-07 05:41:40 +00:00
|
|
|
use phpseclib3\Crypt\Random;
|
2024-04-10 09:43:30 +00:00
|
|
|
use phpseclib3\Exception\BadDecryptionException;
|
2022-08-18 13:05:57 +00:00
|
|
|
use phpseclib3\Exception\RuntimeException;
|
|
|
|
use phpseclib3\Exception\UnexpectedValueException;
|
2016-12-23 16:02:07 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* OpenSSH Formatted RSA Key Handler
|
|
|
|
*
|
|
|
|
* @author Jim Wigginton <terrafrost@php.net>
|
|
|
|
*/
|
|
|
|
abstract class OpenSSH
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* Default comment
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected static $comment = 'phpseclib-generated-key';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Binary key flag
|
|
|
|
*
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
protected static $binary = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the default comment
|
|
|
|
*/
|
2022-06-04 15:31:21 +00:00
|
|
|
public static function setComment(string $comment): void
|
2016-12-23 16:02:07 +00:00
|
|
|
{
|
|
|
|
self::$comment = str_replace(["\r", "\n"], '', $comment);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Break a public or private key down into its constituent components
|
|
|
|
*
|
|
|
|
* $type can be either ssh-dss or ssh-rsa
|
|
|
|
*
|
2022-06-04 15:31:21 +00:00
|
|
|
* @param string|array $key
|
2016-12-23 16:02:07 +00:00
|
|
|
*/
|
2022-07-09 02:42:28 +00:00
|
|
|
public static function load($key, ?string $password = null): array
|
2016-12-23 16:02:07 +00:00
|
|
|
{
|
2020-04-13 12:58:00 +00:00
|
|
|
if (!Strings::is_stringable($key)) {
|
2022-08-18 13:05:57 +00:00
|
|
|
throw new UnexpectedValueException('Key should be a string - not a ' . gettype($key));
|
2016-12-23 16:02:07 +00:00
|
|
|
}
|
|
|
|
|
2019-06-08 11:22:57 +00:00
|
|
|
// key format is described here:
|
|
|
|
// https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD
|
|
|
|
|
2022-08-11 13:25:16 +00:00
|
|
|
if (str_contains($key, 'BEGIN OPENSSH PRIVATE KEY')) {
|
2019-06-08 11:22:57 +00:00
|
|
|
$key = preg_replace('#(?:^-.*?-[\r\n]*$)|\s#ms', '', $key);
|
2022-08-19 02:52:09 +00:00
|
|
|
$key = Strings::base64_decode($key);
|
2019-06-08 11:22:57 +00:00
|
|
|
$magic = Strings::shift($key, 15);
|
|
|
|
if ($magic != "openssh-key-v1\0") {
|
2022-08-18 13:05:57 +00:00
|
|
|
throw new RuntimeException('Expected openssh-key-v1');
|
2019-06-08 11:22:57 +00:00
|
|
|
}
|
2022-06-04 15:31:21 +00:00
|
|
|
[$ciphername, $kdfname, $kdfoptions, $numKeys] = Strings::unpackSSH2('sssN', $key);
|
2019-06-08 11:22:57 +00:00
|
|
|
if ($numKeys != 1) {
|
|
|
|
// if we wanted to support multiple keys we could update PublicKeyLoader to preview what the # of keys
|
|
|
|
// would be; it'd then call Common\Keys\OpenSSH.php::load() and get the paddedKey. it'd then pass
|
|
|
|
// that to the appropriate key loading parser $numKey times or something
|
2022-08-18 13:05:57 +00:00
|
|
|
throw new RuntimeException('Although the OpenSSH private key format supports multiple keys phpseclib does not');
|
2019-06-08 11:22:57 +00:00
|
|
|
}
|
|
|
|
|
2022-07-30 23:07:26 +00:00
|
|
|
switch ($ciphername) {
|
|
|
|
case 'none':
|
|
|
|
break;
|
|
|
|
case 'aes256-ctr':
|
2022-07-31 22:14:20 +00:00
|
|
|
if ($kdfname != 'bcrypt') {
|
2022-08-18 13:05:57 +00:00
|
|
|
throw new RuntimeException('Only the bcrypt kdf is supported (' . $kdfname . ' encountered)');
|
2022-07-31 22:14:20 +00:00
|
|
|
}
|
2022-07-31 00:49:05 +00:00
|
|
|
[$salt, $rounds] = Strings::unpackSSH2('sN', $kdfoptions);
|
2022-07-30 23:07:26 +00:00
|
|
|
$crypto = new AES('ctr');
|
|
|
|
//$crypto->setKeyLength(256);
|
|
|
|
//$crypto->disablePadding();
|
|
|
|
$crypto->setPassword($password, 'bcrypt', $salt, $rounds, 32);
|
2022-07-31 13:01:12 +00:00
|
|
|
break;
|
|
|
|
default:
|
2024-04-10 09:46:02 +00:00
|
|
|
throw new RuntimeException('The only supported ciphers are: none, aes256-ctr (' . $ciphername . ' is being used)');
|
2019-06-08 11:22:57 +00:00
|
|
|
}
|
|
|
|
|
2022-06-04 15:31:21 +00:00
|
|
|
[$publicKey, $paddedKey] = Strings::unpackSSH2('ss', $key);
|
|
|
|
[$type] = Strings::unpackSSH2('s', $publicKey);
|
2022-07-30 23:07:26 +00:00
|
|
|
if (isset($crypto)) {
|
|
|
|
$paddedKey = $crypto->decrypt($paddedKey);
|
|
|
|
}
|
2022-06-04 15:31:21 +00:00
|
|
|
[$checkint1, $checkint2] = Strings::unpackSSH2('NN', $paddedKey);
|
2019-06-08 11:22:57 +00:00
|
|
|
// any leftover bytes in $paddedKey are for padding? but they should be sequential bytes. eg. 1, 2, 3, etc.
|
|
|
|
if ($checkint1 != $checkint2) {
|
2024-04-10 09:43:30 +00:00
|
|
|
if (isset($crypto)) {
|
|
|
|
throw new BadDecryptionException('Unable to decrypt key - please verify the password you are using');
|
|
|
|
}
|
2024-04-10 09:46:02 +00:00
|
|
|
throw new RuntimeException("The two checkints do not match ($checkint1 vs. $checkint2)");
|
2019-06-08 11:22:57 +00:00
|
|
|
}
|
|
|
|
self::checkType($type);
|
|
|
|
|
|
|
|
return compact('type', 'publicKey', 'paddedKey');
|
|
|
|
}
|
|
|
|
|
2016-12-23 16:02:07 +00:00
|
|
|
$parts = explode(' ', $key, 3);
|
|
|
|
|
|
|
|
if (!isset($parts[1])) {
|
2019-06-08 11:22:57 +00:00
|
|
|
$key = base64_decode($parts[0]);
|
2022-07-07 01:43:09 +00:00
|
|
|
$comment = false;
|
2016-12-23 16:02:07 +00:00
|
|
|
} else {
|
2019-06-08 11:22:57 +00:00
|
|
|
$asciiType = $parts[0];
|
|
|
|
self::checkType($parts[0]);
|
|
|
|
$key = base64_decode($parts[1]);
|
2022-06-04 15:31:21 +00:00
|
|
|
$comment = $parts[2] ?? false;
|
2016-12-23 16:02:07 +00:00
|
|
|
}
|
|
|
|
if ($key === false) {
|
2022-08-18 13:05:57 +00:00
|
|
|
throw new UnexpectedValueException('Key should be a string - not a ' . gettype($key));
|
2016-12-23 16:02:07 +00:00
|
|
|
}
|
|
|
|
|
2022-06-04 15:31:21 +00:00
|
|
|
[$type] = Strings::unpackSSH2('s', $key);
|
2019-06-08 11:22:57 +00:00
|
|
|
self::checkType($type);
|
|
|
|
if (isset($asciiType) && $asciiType != $type) {
|
2022-08-18 13:05:57 +00:00
|
|
|
throw new RuntimeException('Two different types of keys are claimed: ' . $asciiType . ' and ' . $type);
|
2016-12-23 16:02:07 +00:00
|
|
|
}
|
|
|
|
if (strlen($key) <= 4) {
|
2022-08-18 13:05:57 +00:00
|
|
|
throw new UnexpectedValueException('Key appears to be malformed');
|
2016-12-23 16:02:07 +00:00
|
|
|
}
|
|
|
|
|
2019-06-08 11:22:57 +00:00
|
|
|
$publicKey = $key;
|
2016-12-23 16:02:07 +00:00
|
|
|
|
2019-06-08 11:22:57 +00:00
|
|
|
return compact('type', 'publicKey', 'comment');
|
2016-12-23 16:02:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Toggle between binary and printable keys
|
|
|
|
*
|
|
|
|
* Printable keys are what are generated by default. These are the ones that go in
|
|
|
|
* $HOME/.ssh/authorized_key.
|
|
|
|
*/
|
2022-06-04 15:31:21 +00:00
|
|
|
public static function setBinaryOutput(bool $enabled): void
|
2016-12-23 16:02:07 +00:00
|
|
|
{
|
|
|
|
self::$binary = $enabled;
|
|
|
|
}
|
2019-06-08 11:22:57 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks to see if the type is valid
|
|
|
|
*/
|
2022-06-04 15:31:21 +00:00
|
|
|
private static function checkType(string $candidate): void
|
2019-06-08 11:22:57 +00:00
|
|
|
{
|
|
|
|
if (!in_array($candidate, static::$types)) {
|
2022-08-18 13:05:57 +00:00
|
|
|
throw new RuntimeException("The key type ($candidate) is not equal to: " . implode(',', static::$types));
|
2019-06-08 11:22:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Wrap a private key appropriately
|
|
|
|
*
|
2022-06-04 15:31:21 +00:00
|
|
|
* @param string|false $password
|
2019-06-08 11:22:57 +00:00
|
|
|
*/
|
2022-06-04 15:31:21 +00:00
|
|
|
protected static function wrapPrivateKey(string $publicKey, string $privateKey, $password, array $options): string
|
2019-06-08 11:22:57 +00:00
|
|
|
{
|
2022-06-04 15:31:21 +00:00
|
|
|
[, $checkint] = unpack('N', Random::string(4));
|
2019-06-08 11:22:57 +00:00
|
|
|
|
2022-06-04 15:31:21 +00:00
|
|
|
$comment = $options['comment'] ?? self::$comment;
|
2019-06-08 11:22:57 +00:00
|
|
|
$paddedKey = Strings::packSSH2('NN', $checkint, $checkint) .
|
|
|
|
$privateKey .
|
|
|
|
Strings::packSSH2('s', $comment);
|
|
|
|
|
2022-07-31 22:14:20 +00:00
|
|
|
$usesEncryption = !empty($password) && is_string($password);
|
|
|
|
|
2019-06-08 11:22:57 +00:00
|
|
|
/*
|
|
|
|
from http://tools.ietf.org/html/rfc4253#section-6 :
|
|
|
|
|
|
|
|
Note that the length of the concatenation of 'packet_length',
|
|
|
|
'padding_length', 'payload', and 'random padding' MUST be a multiple
|
|
|
|
of the cipher block size or 8, whichever is larger.
|
|
|
|
*/
|
2022-07-31 22:14:20 +00:00
|
|
|
$blockSize = $usesEncryption ? 16 : 8;
|
|
|
|
$paddingLength = (($blockSize - 1) * strlen($paddedKey)) % $blockSize;
|
2019-06-08 11:22:57 +00:00
|
|
|
for ($i = 1; $i <= $paddingLength; $i++) {
|
2022-02-17 02:25:59 +00:00
|
|
|
$paddedKey .= chr($i);
|
2019-06-08 11:22:57 +00:00
|
|
|
}
|
2022-07-31 22:14:20 +00:00
|
|
|
if (!$usesEncryption) {
|
|
|
|
$key = Strings::packSSH2('sssNss', 'none', 'none', '', 1, $publicKey, $paddedKey);
|
|
|
|
} else {
|
2022-07-31 22:28:30 +00:00
|
|
|
$rounds = $options['rounds'] ?? 16;
|
2022-07-31 22:14:20 +00:00
|
|
|
$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);
|
|
|
|
}
|
2019-06-08 11:22:57 +00:00
|
|
|
$key = "openssh-key-v1\0$key";
|
|
|
|
|
2021-09-28 14:00:00 +00:00
|
|
|
return "-----BEGIN OPENSSH PRIVATE KEY-----\n" .
|
2022-08-19 02:52:09 +00:00
|
|
|
chunk_split(Strings::base64_encode($key), 70, "\n") .
|
2021-09-28 14:00:00 +00:00
|
|
|
"-----END OPENSSH PRIVATE KEY-----\n";
|
2019-06-08 11:22:57 +00:00
|
|
|
}
|
2016-12-23 16:02:07 +00:00
|
|
|
}
|