mirror of
https://github.com/danog/tgseclib.git
synced 2024-12-12 17:17:34 +01:00
228 lines
8.5 KiB
PHP
228 lines
8.5 KiB
PHP
<?php
|
|
|
|
/**
|
|
* OpenSSH Key Handler
|
|
*
|
|
* PHP version 5
|
|
*
|
|
* Place in $HOME/.ssh/authorized_keys
|
|
*
|
|
* @category Crypt
|
|
* @package Common
|
|
* @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
|
|
*/
|
|
|
|
namespace phpseclib\Crypt\Common\Formats\Keys;
|
|
|
|
use ParagonIE\ConstantTime\Base64;
|
|
use phpseclib\Common\Functions\Strings;
|
|
use phpseclib\Crypt\Random;
|
|
|
|
/**
|
|
* OpenSSH Formatted RSA Key Handler
|
|
*
|
|
* @package Common
|
|
* @author Jim Wigginton <terrafrost@php.net>
|
|
* @access public
|
|
*/
|
|
abstract class OpenSSH
|
|
{
|
|
/**
|
|
* Default comment
|
|
*
|
|
* @var string
|
|
* @access private
|
|
*/
|
|
protected static $comment = 'phpseclib-generated-key';
|
|
|
|
/**
|
|
* Binary key flag
|
|
*
|
|
* @var bool
|
|
* @access private
|
|
*/
|
|
protected static $binary = false;
|
|
|
|
/**
|
|
* Sets the default comment
|
|
*
|
|
* @access public
|
|
* @param string $comment
|
|
*/
|
|
public static function setComment($comment)
|
|
{
|
|
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
|
|
*
|
|
* @access public
|
|
* @param string $key
|
|
* @param string $type
|
|
* @return array
|
|
*/
|
|
public static function load($key, $password = '')
|
|
{
|
|
if (!is_string($key)) {
|
|
throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key));
|
|
}
|
|
|
|
// key format is described here:
|
|
// https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD
|
|
|
|
if (strpos($key, 'BEGIN OPENSSH PRIVATE KEY') !== false) {
|
|
$key = preg_replace('#(?:^-.*?-[\r\n]*$)|\s#ms', '', $key);
|
|
$key = Base64::decode($key);
|
|
$magic = Strings::shift($key, 15);
|
|
if ($magic != "openssh-key-v1\0") {
|
|
throw new \RuntimeException('Expected openssh-key-v1');
|
|
}
|
|
list($ciphername, $kdfname, $kdfoptions, $numKeys) = Strings::unpackSSH2('sssN', $key);
|
|
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
|
|
throw new \RuntimeException('Although the OpenSSH private key format supports multiple keys phpseclib does not');
|
|
}
|
|
if (strlen($kdfoptions) || $kdfname != 'none' || $ciphername != 'none') {
|
|
/*
|
|
OpenSSH private keys use a customized version of bcrypt. specifically, instead of encrypting
|
|
OrpheanBeholderScryDoubt 64 times OpenSSH's bcrypt variant encrypts
|
|
OxychromaticBlowfishSwatDynamite 64 times. so we can't use crypt().
|
|
|
|
bcrypt is basically Blowfish with an altered key expansion. whereas Blowfish just runs the
|
|
key through the key expansion bcrypt interleaves the key expansion with the salt and
|
|
password. this renders openssl / mcrypt unusuable. this forces us to use a pure-PHP implementation
|
|
of bcrypt. the problem with that is that pure-PHP is too slow to be practically useful.
|
|
|
|
in addition to encrypting a different string 64 times the OpenSSH implementation also performs bcrypt
|
|
from scratch $rounds times. calling crypt() 64x with bcrypt takes 0.7s. PHP is going to be naturally
|
|
slower. pure-PHP is 215x slower than OpenSSL for AES and pure-PHP is 43x slower for bcrypt.
|
|
43 * 0.7 = 30s. no one wants to wait 30s to load a private key.
|
|
|
|
another way to think about this.. according to wikipedia's article on Blowfish,
|
|
"Each new key requires pre-processing equivalent to encrypting about 4 kilobytes of text".
|
|
key expansion is done (9+64*2)*160 times. multiply that by 4 and it turns out that Blowfish,
|
|
OpenSSH style, is the equivalent of encrypting ~80mb of text.
|
|
|
|
more supporting evidence: sodium_compat does not implement Argon2 (another password hashing
|
|
algorithm) because "It's not feasible to polyfill scrypt or Argon2 into PHP and get reasonable
|
|
performance. Users would feel motivated to select parameters that downgrade security to avoid
|
|
denial of service (DoS) attacks. The only winning move is not to play"
|
|
-- https://github.com/paragonie/sodium_compat/blob/master/README.md
|
|
*/
|
|
throw new \RuntimeException('Encrypted OpenSSH private keys are not supported');
|
|
//list($salt, $rounds) = Strings::unpackSSH2('sN', $kdfoptions);
|
|
}
|
|
|
|
list($publicKey, $paddedKey) = Strings::unpackSSH2('ss', $key);
|
|
list($type) = Strings::unpackSSH2('s', $publicKey);
|
|
list($checkint1, $checkint2) = Strings::unpackSSH2('NN', $paddedKey);
|
|
// any leftover bytes in $paddedKey are for padding? but they should be sequential bytes. eg. 1, 2, 3, etc.
|
|
if ($checkint1 != $checkint2) {
|
|
throw new \RuntimeException('The two checkints do not match');
|
|
}
|
|
self::checkType($type);
|
|
|
|
return compact('type', 'publicKey', 'paddedKey');
|
|
}
|
|
|
|
$parts = explode(' ', $key, 3);
|
|
|
|
if (!isset($parts[1])) {
|
|
$key = base64_decode($parts[0]);
|
|
$comment = isset($parts[1]) ? $parts[1] : false;
|
|
} else {
|
|
$asciiType = $parts[0];
|
|
self::checkType($parts[0]);
|
|
$key = base64_decode($parts[1]);
|
|
$comment = isset($parts[2]) ? $parts[2] : false;
|
|
}
|
|
if ($key === false) {
|
|
throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key));
|
|
}
|
|
|
|
list($type) = Strings::unpackSSH2('s', $key);
|
|
self::checkType($type);
|
|
if (isset($asciiType) && $asciiType != $type) {
|
|
throw new \RuntimeException('Two different types of keys are claimed: ' . $asciiType . ' and ' . $type);
|
|
}
|
|
if (strlen($key) <= 4) {
|
|
throw new \UnexpectedValueException('Key appears to be malformed');
|
|
}
|
|
|
|
$publicKey = $key;
|
|
|
|
return compact('type', 'publicKey', 'comment');
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @access public
|
|
* @param bool $enabled
|
|
*/
|
|
public static function setBinaryOutput($enabled)
|
|
{
|
|
self::$binary = $enabled;
|
|
}
|
|
|
|
/**
|
|
* Checks to see if the type is valid
|
|
*
|
|
* @access private
|
|
* @param string $candidate
|
|
*/
|
|
private static function checkType($candidate)
|
|
{
|
|
if (!in_array($candidate, static::$types)) {
|
|
throw new \RuntimeException('The key type is not equal to: ' . implode(',', static::$types));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wrap a private key appropriately
|
|
*
|
|
* @access public
|
|
* @param string $publicKey
|
|
* @param string $privateKey
|
|
* @return string
|
|
*/
|
|
protected static function wrapPrivateKey($publicKey, $privateKey, $options)
|
|
{
|
|
list(, $checkint) = unpack('N', Random::string(4));
|
|
|
|
$comment = isset($options['comment']) ? $options['comment'] : self::$comment;
|
|
$paddedKey = Strings::packSSH2('NN', $checkint, $checkint) .
|
|
$privateKey .
|
|
Strings::packSSH2('s', $comment);
|
|
|
|
/*
|
|
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.
|
|
*/
|
|
$paddingLength = (7 * strlen($paddedKey)) % 8;
|
|
for ($i = 1; $i <= $paddingLength; $i++) {
|
|
$paddedKey.= chr($i);
|
|
}
|
|
$key = Strings::packSSH2('sssNss', 'none', 'none', '', 1, $publicKey, $paddedKey);
|
|
$key = "openssh-key-v1\0$key";
|
|
|
|
return "-----BEGIN OPENSSH PRIVATE KEY-----\r\n" .
|
|
chunk_split(Base64::encode($key), 70) .
|
|
"-----END OPENSSH PRIVATE KEY-----";
|
|
}
|
|
}
|