Subversion Repositories oidplus

Rev

Rev 1042 | 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.  * @author    Jim Wigginton <terrafrost@php.net>
  11.  * @copyright 2015 Jim Wigginton
  12.  * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
  13.  * @link      http://phpseclib.sourceforge.net
  14.  */
  15.  
  16. namespace phpseclib3\Crypt\Common\Formats\Keys;
  17.  
  18. use phpseclib3\Common\Functions\Strings;
  19. use phpseclib3\Crypt\AES;
  20. use phpseclib3\Crypt\Random;
  21. use phpseclib3\Exception\BadDecryptionException;
  22.  
  23. /**
  24.  * OpenSSH Formatted RSA Key Handler
  25.  *
  26.  * @author  Jim Wigginton <terrafrost@php.net>
  27.  */
  28. abstract class OpenSSH
  29. {
  30.     /**
  31.      * Default comment
  32.      *
  33.      * @var string
  34.      */
  35.     protected static $comment = 'phpseclib-generated-key';
  36.  
  37.     /**
  38.      * Binary key flag
  39.      *
  40.      * @var bool
  41.      */
  42.     protected static $binary = false;
  43.  
  44.     /**
  45.      * Sets the default comment
  46.      *
  47.      * @param string $comment
  48.      */
  49.     public static function setComment($comment)
  50.     {
  51.         self::$comment = str_replace(["\r", "\n"], '', $comment);
  52.     }
  53.  
  54.     /**
  55.      * Break a public or private key down into its constituent components
  56.      *
  57.      * $type can be either ssh-dss or ssh-rsa
  58.      *
  59.      * @param string $key
  60.      * @param string $password
  61.      * @return array
  62.      */
  63.     public static function load($key, $password = '')
  64.     {
  65.         if (!Strings::is_stringable($key)) {
  66.             throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key));
  67.         }
  68.  
  69.         // key format is described here:
  70.         // https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD
  71.  
  72.         if (strpos($key, 'BEGIN OPENSSH PRIVATE KEY') !== false) {
  73.             $key = preg_replace('#(?:^-.*?-[\r\n]*$)|\s#ms', '', $key);
  74.             $key = Strings::base64_decode($key);
  75.             $magic = Strings::shift($key, 15);
  76.             if ($magic != "openssh-key-v1\0") {
  77.                 throw new \RuntimeException('Expected openssh-key-v1');
  78.             }
  79.             list($ciphername, $kdfname, $kdfoptions, $numKeys) = Strings::unpackSSH2('sssN', $key);
  80.             if ($numKeys != 1) {
  81.                 // if we wanted to support multiple keys we could update PublicKeyLoader to preview what the # of keys
  82.                 // would be; it'd then call Common\Keys\OpenSSH.php::load() and get the paddedKey. it'd then pass
  83.                 // that to the appropriate key loading parser $numKey times or something
  84.                 throw new \RuntimeException('Although the OpenSSH private key format supports multiple keys phpseclib does not');
  85.             }
  86.             switch ($ciphername) {
  87.                 case 'none':
  88.                     break;
  89.                 case 'aes256-ctr':
  90.                     if ($kdfname != 'bcrypt') {
  91.                         throw new \RuntimeException('Only the bcrypt kdf is supported (' . $kdfname . ' encountered)');
  92.                     }
  93.                     list($salt, $rounds) = Strings::unpackSSH2('sN', $kdfoptions);
  94.                     $crypto = new AES('ctr');
  95.                     //$crypto->setKeyLength(256);
  96.                     //$crypto->disablePadding();
  97.                     $crypto->setPassword($password, 'bcrypt', $salt, $rounds, 32);
  98.                     break;
  99.                 default:
  100.                     throw new \RuntimeException('The only supported ciphers are: none, aes256-ctr (' . $ciphername . ' is being used)');
  101.             }
  102.  
  103.             list($publicKey, $paddedKey) = Strings::unpackSSH2('ss', $key);
  104.             list($type) = Strings::unpackSSH2('s', $publicKey);
  105.             if (isset($crypto)) {
  106.                 $paddedKey = $crypto->decrypt($paddedKey);
  107.             }
  108.             list($checkint1, $checkint2) = Strings::unpackSSH2('NN', $paddedKey);
  109.             // any leftover bytes in $paddedKey are for padding? but they should be sequential bytes. eg. 1, 2, 3, etc.
  110.             if ($checkint1 != $checkint2) {
  111.                 if (isset($crypto)) {
  112.                     throw new BadDecryptionException('Unable to decrypt key - please verify the password you are using');
  113.                 }
  114.                 throw new \RuntimeException("The two checkints do not match ($checkint1 vs. $checkint2)");
  115.             }
  116.             self::checkType($type);
  117.  
  118.             return compact('type', 'publicKey', 'paddedKey');
  119.         }
  120.  
  121.         $parts = explode(' ', $key, 3);
  122.  
  123.         if (!isset($parts[1])) {
  124.             $key = base64_decode($parts[0]);
  125.             $comment = false;
  126.         } else {
  127.             $asciiType = $parts[0];
  128.             self::checkType($parts[0]);
  129.             $key = base64_decode($parts[1]);
  130.             $comment = isset($parts[2]) ? $parts[2] : false;
  131.         }
  132.         if ($key === false) {
  133.             throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key));
  134.         }
  135.  
  136.         list($type) = Strings::unpackSSH2('s', $key);
  137.         self::checkType($type);
  138.         if (isset($asciiType) && $asciiType != $type) {
  139.             throw new \RuntimeException('Two different types of keys are claimed: ' . $asciiType . ' and ' . $type);
  140.         }
  141.         if (strlen($key) <= 4) {
  142.             throw new \UnexpectedValueException('Key appears to be malformed');
  143.         }
  144.  
  145.         $publicKey = $key;
  146.  
  147.         return compact('type', 'publicKey', 'comment');
  148.     }
  149.  
  150.     /**
  151.      * Toggle between binary and printable keys
  152.      *
  153.      * Printable keys are what are generated by default. These are the ones that go in
  154.      * $HOME/.ssh/authorized_key.
  155.      *
  156.      * @param bool $enabled
  157.      */
  158.     public static function setBinaryOutput($enabled)
  159.     {
  160.         self::$binary = $enabled;
  161.     }
  162.  
  163.     /**
  164.      * Checks to see if the type is valid
  165.      *
  166.      * @param string $candidate
  167.      */
  168.     private static function checkType($candidate)
  169.     {
  170.         if (!in_array($candidate, static::$types)) {
  171.             throw new \RuntimeException("The key type ($candidate) is not equal to: " . implode(',', static::$types));
  172.         }
  173.     }
  174.  
  175.     /**
  176.      * Wrap a private key appropriately
  177.      *
  178.      * @param string $publicKey
  179.      * @param string $privateKey
  180.      * @param string $password
  181.      * @param array $options
  182.      * @return string
  183.      */
  184.     protected static function wrapPrivateKey($publicKey, $privateKey, $password, $options)
  185.     {
  186.         list(, $checkint) = unpack('N', Random::string(4));
  187.  
  188.         $comment = isset($options['comment']) ? $options['comment'] : self::$comment;
  189.         $paddedKey = Strings::packSSH2('NN', $checkint, $checkint) .
  190.                      $privateKey .
  191.                      Strings::packSSH2('s', $comment);
  192.  
  193.         $usesEncryption = !empty($password) && is_string($password);
  194.  
  195.         /*
  196.            from http://tools.ietf.org/html/rfc4253#section-6 :
  197.  
  198.            Note that the length of the concatenation of 'packet_length',
  199.            'padding_length', 'payload', and 'random padding' MUST be a multiple
  200.            of the cipher block size or 8, whichever is larger.
  201.          */
  202.         $blockSize = $usesEncryption ? 16 : 8;
  203.         $paddingLength = (($blockSize - 1) * strlen($paddedKey)) % $blockSize;
  204.         for ($i = 1; $i <= $paddingLength; $i++) {
  205.             $paddedKey .= chr($i);
  206.         }
  207.         if (!$usesEncryption) {
  208.             $key = Strings::packSSH2('sssNss', 'none', 'none', '', 1, $publicKey, $paddedKey);
  209.         } else {
  210.             $rounds = isset($options['rounds']) ? $options['rounds'] : 16;
  211.             $salt = Random::string(16);
  212.             $kdfoptions = Strings::packSSH2('sN', $salt, $rounds);
  213.             $crypto = new AES('ctr');
  214.             $crypto->setPassword($password, 'bcrypt', $salt, $rounds, 32);
  215.             $paddedKey = $crypto->encrypt($paddedKey);
  216.             $key = Strings::packSSH2('sssNss', 'aes256-ctr', 'bcrypt', $kdfoptions, 1, $publicKey, $paddedKey);
  217.         }
  218.         $key = "openssh-key-v1\0$key";
  219.  
  220.         return "-----BEGIN OPENSSH PRIVATE KEY-----\n" .
  221.                chunk_split(Strings::base64_encode($key), 70, "\n") .
  222.                "-----END OPENSSH PRIVATE KEY-----\n";
  223.     }
  224. }
  225.