Subversion Repositories oidplus

Rev

Rev 846 | Rev 1042 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

  1. <?php
  2.  
  3. /**
  4.  * OpenSSH Key Handler
  5.  *
  6.  * PHP version 5
  7.  *
  8.  * Place in $HOME/.ssh/authorized_keys
  9.  *
  10.  * @category  Crypt
  11.  * @package   Common
  12.  * @author    Jim Wigginton <terrafrost@php.net>
  13.  * @copyright 2015 Jim Wigginton
  14.  * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
  15.  * @link      http://phpseclib.sourceforge.net
  16.  */
  17.  
  18. namespace phpseclib3\Crypt\Common\Formats\Keys;
  19.  
  20. use ParagonIE\ConstantTime\Base64;
  21. use phpseclib3\Common\Functions\Strings;
  22. use phpseclib3\Crypt\Random;
  23. use phpseclib3\Exception\UnsupportedFormatException;
  24.  
  25. /**
  26.  * OpenSSH Formatted RSA Key Handler
  27.  *
  28.  * @package Common
  29.  * @author  Jim Wigginton <terrafrost@php.net>
  30.  * @access  public
  31.  */
  32. abstract class OpenSSH
  33. {
  34.     /**
  35.      * Default comment
  36.      *
  37.      * @var string
  38.      * @access private
  39.      */
  40.     protected static $comment = 'phpseclib-generated-key';
  41.  
  42.     /**
  43.      * Binary key flag
  44.      *
  45.      * @var bool
  46.      * @access private
  47.      */
  48.     protected static $binary = false;
  49.  
  50.     /**
  51.      * Sets the default comment
  52.      *
  53.      * @access public
  54.      * @param string $comment
  55.      */
  56.     public static function setComment($comment)
  57.     {
  58.         self::$comment = str_replace(["\r", "\n"], '', $comment);
  59.     }
  60.  
  61.     /**
  62.      * Break a public or private key down into its constituent components
  63.      *
  64.      * $type can be either ssh-dss or ssh-rsa
  65.      *
  66.      * @access public
  67.      * @param string $key
  68.      * @param string $password
  69.      * @return array
  70.      */
  71.     public static function load($key, $password = '')
  72.     {
  73.         if (!Strings::is_stringable($key)) {
  74.             throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key));
  75.         }
  76.  
  77.         // key format is described here:
  78.         // https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD
  79.  
  80.         if (strpos($key, 'BEGIN OPENSSH PRIVATE KEY') !== false) {
  81.             $key = preg_replace('#(?:^-.*?-[\r\n]*$)|\s#ms', '', $key);
  82.             $key = Base64::decode($key);
  83.             $magic = Strings::shift($key, 15);
  84.             if ($magic != "openssh-key-v1\0") {
  85.                 throw new \RuntimeException('Expected openssh-key-v1');
  86.             }
  87.             list($ciphername, $kdfname, $kdfoptions, $numKeys) = Strings::unpackSSH2('sssN', $key);
  88.             if ($numKeys != 1) {
  89.                 // if we wanted to support multiple keys we could update PublicKeyLoader to preview what the # of keys
  90.                 // would be; it'd then call Common\Keys\OpenSSH.php::load() and get the paddedKey. it'd then pass
  91.                 // that to the appropriate key loading parser $numKey times or something
  92.                 throw new \RuntimeException('Although the OpenSSH private key format supports multiple keys phpseclib does not');
  93.             }
  94.             if (strlen($kdfoptions) || $kdfname != 'none' || $ciphername != 'none') {
  95.                 /*
  96.                   OpenSSH private keys use a customized version of bcrypt. specifically, instead of encrypting
  97.                   OrpheanBeholderScryDoubt 64 times OpenSSH's bcrypt variant encrypts
  98.                   OxychromaticBlowfishSwatDynamite 64 times. so we can't use crypt().
  99.  
  100.                   bcrypt is basically Blowfish with an altered key expansion. whereas Blowfish just runs the
  101.                   key through the key expansion bcrypt interleaves the key expansion with the salt and
  102.                   password. this renders openssl / mcrypt unusuable. this forces us to use a pure-PHP implementation
  103.                   of bcrypt. the problem with that is that pure-PHP is too slow to be practically useful.
  104.  
  105.                   in addition to encrypting a different string 64 times the OpenSSH implementation also performs bcrypt
  106.                   from scratch $rounds times. calling crypt() 64x with bcrypt takes 0.7s. PHP is going to be naturally
  107.                   slower. pure-PHP is 215x slower than OpenSSL for AES and pure-PHP is 43x slower for bcrypt.
  108.                   43 * 0.7 = 30s. no one wants to wait 30s to load a private key.
  109.  
  110.                   another way to think about this..  according to wikipedia's article on Blowfish,
  111.                   "Each new key requires pre-processing equivalent to encrypting about 4 kilobytes of text".
  112.                   key expansion is done (9+64*2)*160 times. multiply that by 4 and it turns out that Blowfish,
  113.                   OpenSSH style, is the equivalent of encrypting ~80mb of text.
  114.  
  115.                   more supporting evidence: sodium_compat does not implement Argon2 (another password hashing
  116.                   algorithm) because "It's not feasible to polyfill scrypt or Argon2 into PHP and get reasonable
  117.                   performance. Users would feel motivated to select parameters that downgrade security to avoid
  118.                   denial of service (DoS) attacks. The only winning move is not to play"
  119.                     -- https://github.com/paragonie/sodium_compat/blob/master/README.md
  120.                 */
  121.                 throw new \RuntimeException('Encrypted OpenSSH private keys are not supported');
  122.                 //list($salt, $rounds) = Strings::unpackSSH2('sN', $kdfoptions);
  123.             }
  124.  
  125.             list($publicKey, $paddedKey) = Strings::unpackSSH2('ss', $key);
  126.             list($type) = Strings::unpackSSH2('s', $publicKey);
  127.             list($checkint1, $checkint2) = Strings::unpackSSH2('NN', $paddedKey);
  128.             // any leftover bytes in $paddedKey are for padding? but they should be sequential bytes. eg. 1, 2, 3, etc.
  129.             if ($checkint1 != $checkint2) {
  130.                 throw new \RuntimeException('The two checkints do not match');
  131.             }
  132.             self::checkType($type);
  133.  
  134.             return compact('type', 'publicKey', 'paddedKey');
  135.         }
  136.  
  137.         $parts = explode(' ', $key, 3);
  138.  
  139.         if (!isset($parts[1])) {
  140.             $key = base64_decode($parts[0]);
  141.             $comment = isset($parts[1]) ? $parts[1] : false;
  142.         } else {
  143.             $asciiType = $parts[0];
  144.             self::checkType($parts[0]);
  145.             $key = base64_decode($parts[1]);
  146.             $comment = isset($parts[2]) ? $parts[2] : false;
  147.         }
  148.         if ($key === false) {
  149.             throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key));
  150.         }
  151.  
  152.         list($type) = Strings::unpackSSH2('s', $key);
  153.         self::checkType($type);
  154.         if (isset($asciiType) && $asciiType != $type) {
  155.             throw new \RuntimeException('Two different types of keys are claimed: ' . $asciiType . ' and ' . $type);
  156.         }
  157.         if (strlen($key) <= 4) {
  158.             throw new \UnexpectedValueException('Key appears to be malformed');
  159.         }
  160.  
  161.         $publicKey = $key;
  162.  
  163.         return compact('type', 'publicKey', 'comment');
  164.     }
  165.  
  166.     /**
  167.      * Toggle between binary and printable keys
  168.      *
  169.      * Printable keys are what are generated by default. These are the ones that go in
  170.      * $HOME/.ssh/authorized_key.
  171.      *
  172.      * @access public
  173.      * @param bool $enabled
  174.      */
  175.     public static function setBinaryOutput($enabled)
  176.     {
  177.         self::$binary = $enabled;
  178.     }
  179.  
  180.     /**
  181.      * Checks to see if the type is valid
  182.      *
  183.      * @access private
  184.      * @param string $candidate
  185.      */
  186.     private static function checkType($candidate)
  187.     {
  188.         if (!in_array($candidate, static::$types)) {
  189.             throw new \RuntimeException("The key type ($candidate) is not equal to: " . implode(',', static::$types));
  190.         }
  191.     }
  192.  
  193.     /**
  194.      * Wrap a private key appropriately
  195.      *
  196.      * @access public
  197.      * @param string $publicKey
  198.      * @param string $privateKey
  199.      * @param string $password
  200.      * @param array $options
  201.      * @return string
  202.      */
  203.     protected static function wrapPrivateKey($publicKey, $privateKey, $password, $options)
  204.     {
  205.         if (!empty($password) && is_string($password)) {
  206.             throw new UnsupportedFormatException('Encrypted OpenSSH private keys are not supported');
  207.         }
  208.  
  209.         list(, $checkint) = unpack('N', Random::string(4));
  210.  
  211.         $comment = isset($options['comment']) ? $options['comment'] : self::$comment;
  212.         $paddedKey = Strings::packSSH2('NN', $checkint, $checkint) .
  213.                      $privateKey .
  214.                      Strings::packSSH2('s', $comment);
  215.  
  216.         /*
  217.            from http://tools.ietf.org/html/rfc4253#section-6 :
  218.  
  219.            Note that the length of the concatenation of 'packet_length',
  220.            'padding_length', 'payload', and 'random padding' MUST be a multiple
  221.            of the cipher block size or 8, whichever is larger.
  222.          */
  223.         $paddingLength = (7 * strlen($paddedKey)) % 8;
  224.         for ($i = 1; $i <= $paddingLength; $i++) {
  225.             $paddedKey .= chr($i);
  226.         }
  227.         $key = Strings::packSSH2('sssNss', 'none', 'none', '', 1, $publicKey, $paddedKey);
  228.         $key = "openssh-key-v1\0$key";
  229.  
  230.         return "-----BEGIN OPENSSH PRIVATE KEY-----\n" .
  231.                chunk_split(Base64::encode($key), 70, "\n") .
  232.                "-----END OPENSSH PRIVATE KEY-----\n";
  233.     }
  234. }
  235.