pssht  latest
SSH server library written in PHP
Putty.php
1 <?php
2 
3 /*
4 * This file is part of pssht.
5 *
6 * (c) François Poirotte <clicky@erebot.net>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11 
13 
19 class Putty
20 {
21  private static function parseHeaderAndBody($line)
22  {
23  $res = explode(': ', $line, 2);
24  if (count($res) !== 2) {
25  return array(null, null);
26  }
27  return $res;
28  }
29 
30  private static function parsePPK($data)
31  {
32  // Replace "\r" with "\n" THEN replace "\n\n" with "\n"
33  // so that "\r\n" becomes just "\n", and split the result.
34  $data = explode(
35  "\n",
36  str_replace(array("\r", "\n\n"), "\n", $data)
37  );
38  $len = count($data);
39  $pos = 0;
40  $metadata = array();
41 
42  // Key type
43  if ($len <= $pos) {
44  throw new \InvalidArgumentException('Invalid PPK');
45  }
46  list($header, $body) = self::parseHeaderAndBody($data[$pos++]);
47  if ($header === 'PuTTY-User-Key-File-2') {
48  $metadata['type'] = $body;
49  } elseif (!strncasecmp($header, 'PuTTY-User-Key-File-', 20)) {
50  throw new \InvalidArgumentException('PuTTY key format too new');
51  } else {
52  throw new \InvalidArgumentException('not a PuTTY SSH-2 private key');
53  }
54 
55  // Encryption
56  if ($len <= $pos) {
57  throw new \InvalidArgumentException('Invalid PPK');
58  }
59  list($header, $body) = self::parseHeaderAndBody($data[$pos++]);
60  if ($header !== 'Encryption' || !in_array($body, array('none', 'aes256-cbc'))) {
61  throw new \InvalidArgumentException('Invalid PPK');
62  }
63  $metadata['cipher'] = $body;
64 
65  // Comment
66  if ($len <= $pos) {
67  throw new \InvalidArgumentException('Invalid PPK');
68  }
69  list($header, $body) = self::parseHeaderAndBody($data[$pos++]);
70  if ($header !== 'Comment') {
71  throw new \InvalidArgumentException('Invalid PPK');
72  }
73  $metadata['comment'] = $body;
74 
75  // Public-Lines
76  if ($len <= $pos) {
77  throw new \InvalidArgumentException('Invalid PPK');
78  }
79  list($header, $body) = self::parseHeaderAndBody($data[$pos++]);
80  if ($header !== 'Public-Lines' || !$body || !ctype_digit($body)) {
81  throw new \InvalidArgumentException('Invalid PPK');
82  }
83  $lines = (int) $body;
84  if ($len < $pos + $lines) {
85  throw new \InvalidArgumentException('Invalid PPK');
86  }
87  $publicKey = array_slice($data, $pos, $lines);
88  $metadata['pub_key'] = base64_decode(implode('', $publicKey));
89  $pos += $lines;
90 
91  // Private-Lines
92  if ($len <= $pos) {
93  throw new \InvalidArgumentException('Invalid PPK');
94  }
95  list($header, $body) = self::parseHeaderAndBody($data[$pos++]);
96  if ($header !== 'Private-Lines' || !$body || !ctype_digit($body)) {
97  throw new \InvalidArgumentException('Invalid PPK');
98  }
99  $lines = (int) $body;
100  if ($len < $pos + $lines) {
101  throw new \InvalidArgumentException('Invalid PPK');
102  }
103  $metadata['priv_key'] = base64_decode(
104  implode('', array_slice($data, $pos, $lines))
105  );
106  $pos += $lines;
107 
108  // Private-MAC
109  if ($len <= $pos) {
110  throw new \InvalidArgumentException('Invalid PPK');
111  }
112  list($header, $body) = self::parseHeaderAndBody($data[$pos++]);
113  if ($header !== 'Private-MAC' || !$body || !ctype_xdigit($body)) {
114  throw new \InvalidArgumentException('Invalid PPK');
115  }
116  $metadata['mac'] = pack('H*', $body);
117 
118 
119  return $metadata;
120  }
121 
122  public static function loadPublic($data)
123  {
124  if (!is_string($data)) {
125  throw new \InvalidArgumentException();
126  }
127 
128  $metadata = self::parsePPK($data);
129  $decoder = new \fpoirotte\Pssht\Wire\Decoder();
130  $decoder->getBuffer()->push($metadata['pub_key']);
131 
132  $algos = \fpoirotte\pssht\Algorithms::factory();
133  $cls = $algos->getClass('Key', $decoder->decodeString());
134  if ($cls === null) {
135  throw new \InvalidArgumentException();
136  }
137  return $cls::unserialize($decoder);
138  }
139 
140  public static function loadPrivate($data, $passphrase = '')
141  {
142  if (!is_string($data)) {
143  throw new \InvalidArgumentException();
144  }
145 
146  if (!is_string($passphrase)) {
147  throw new \InvalidArgumentException();
148  }
149 
150  $metadata = self::parsePPK($data);
151 
152  // Decrypt the private key.
153  if ($metadata['cipher'] !== 'none') {
154  $blockSize = 16; // 256 bits for AES.
155  if ($passphrase === '' ||
156  strlen($metadata['priv_key']) % $blockSize) {
157  throw new \InvalidArgumentException('Invalid PPK');
158  }
159 
160  $key =
161  sha1("\x00\x00\x00\x00" . $passphrase, true) .
162  sha1("\x00\x00\x00\x01" . $passphrase, true);
163 
164  $res = mcrypt_module_open(
165  MCRYPT_RIJNDAEL_128,
166  null,
167  MCRYPT_MODE_CBC,
168  null
169  );
170 
171  mcrypt_generic_init(
172  $res,
173  substr($key, 0, mcrypt_enc_get_key_size($res)),
174  str_repeat("\x00", mcrypt_enc_get_iv_size($res))
175  );
176 
177  $metadata['priv_key'] = mdecrypt_generic($res, $metadata['priv_key']);
178  mcrypt_generic_deinit($res);
179  mcrypt_module_close($res);
180  }
181 
182  // Verify the MAC.
183  $blob = '';
184  $fields = array(
185  $metadata['type'],
186  $metadata['cipher'],
187  $metadata['comment'],
188  $metadata['pub_key'],
189  $metadata['priv_key'],
190  );
191  foreach ($fields as $value) {
192  $blob .= pack('N', strlen($value)) . $value;
193  }
194  $key = 'putty-private-key-file-mac-key';
195  if ($metadata['cipher'] !== 'none' && $passphrase !== '') {
196  $key .= $passphrase;
197  }
198  $mac = hash_hmac('sha1', $blob, sha1($key, true), true);
199  if ($mac !== $metadata['mac']) {
200  // Burn the memory.
201  $metadata['priv_key'] = $blob = $key = $passphrase = null;
202  if ($metadata['cipher'] !== 'none') {
203  throw new \InvalidArgumentException('Wrong passphrase');
204  } else {
205  throw new \InvalidArgumentException('MAC failed');
206  }
207  }
208 
209  // Decode private key.
210  $decoder = new \fpoirotte\Pssht\Wire\Decoder();
211  $decoder->getBuffer()->push($type['priv_key']);
212  switch ($metadata['type']) {
213  case 'ssh-rsa':
214  // Decode RSA private parameter (d).
215  $private = $decoder->decodeMpint();
216  break;
217 
218  case 'ssh-dss':
219  // Decode DSA private parameter (x).
220  $private = $decoder->decodeMpint();
221  break;
222 
223  default:
224  $metadata['priv_key'] = null;
225  throw new \InvalidArgumentException();
226  }
227 
228  if ($private === null) {
229  $metadata['priv_key'] = null;
230  throw new \InvalidArgumentException();
231  }
232 
233  // Decode the public key and create the final object.
234  $decoder = new \fpoirotte\Pssht\Wire\Decoder();
235  $decoder->getBuffer()->push($metadata['pub_key']);
236 
237  $algos = \fpoirotte\pssht\Algorithms::factory();
238  $cls = $algos->getClass('Key', $decoder->decodeString());
239  if ($cls === null) {
240  $metadata['priv_key'] = null;
241  throw new \InvalidArgumentException();
242  }
243  return $cls::unserialize($decoder, $private);
244  }
245 }