PageRenderTime 460ms CodeModel.GetById 101ms app.highlight 236ms RepoModel.GetById 75ms app.codeStats 1ms

/code/classes/Daemon/SSHd/Client.class.php

https://github.com/blekkzor/pinetd2
PHP | 903 lines | 745 code | 115 blank | 43 comment | 116 complexity | b869cb1be929a1cf23ecf7a1a703cc42 MD5 | raw file
  1<?php
  2
  3namespace Daemon\SSHd;
  4use pinetd\Logger;
  5use pinetd\SUID;
  6use pinetd\Crypto;
  7
  8class Client extends \pinetd\TCP\Client {
  9	private $state;
 10	private $agent;
 11	private $clearbuf = '';
 12	private $capa;
 13	private $payloads = array();
 14	private $skey;
 15	private $session_id = NULL;
 16	private $seq_recv = -1;
 17	private $seq_send = -1;
 18	private $login = NULL;
 19	private $channels = array();
 20	private $kex_sent = false;
 21
 22	private $cipher = array(); // encoding type
 23
 24	const SSH_MSG_DISCONNECT = 1;
 25	const SSH_MSG_IGNORE = 2;
 26	const SSH_MSG_UNIMPLEMENTED = 3;
 27	const SSH_MSG_DEBUG = 4;
 28	const SSH_MSG_SERVICE_REQUEST = 5;
 29	const SSH_MSG_SERVICE_ACCEPT = 6;
 30	const SSH_MSG_KEXINIT = 20;
 31	const SSH_MSG_NEWKEYS = 21;
 32
 33	const SSH_MSG_KEXDH_INIT = 30;
 34	const SSH_MSG_KEXDH_REPLY = 31;
 35
 36	const SSH_MSG_USERAUTH_REQUEST = 50;
 37	const SSH_MSG_USERAUTH_FAILURE = 51;
 38	const SSH_MSG_USERAUTH_SUCCESS = 52;
 39	const SSH_MSG_USERAUTH_BANNER = 53;
 40
 41	const SSH_MSG_USERAUTH_PASSWD_CHANGEREQ = 60;
 42	const SSH_MSG_USERAUTH_PK_OK = 60;
 43
 44	const SSH_MSG_GLOBAL_REQUEST = 80;
 45	const SSH_MSG_REQUEST_SUCCESS = 81;
 46	const SSH_MSG_REQUEST_FAILURE = 82;
 47
 48	const SSH_MSG_CHANNEL_OPEN = 90;
 49	const SSH_MSG_CHANNEL_OPEN_CONFIRMATION = 91;
 50	const SSH_MSG_CHANNEL_OPEN_FAILURE = 92;
 51	const SSH_MSG_CHANNEL_WINDOW_ADJUST = 93;
 52	const SSH_MSG_CHANNEL_DATA = 94;
 53	const SSH_MSG_CHANNEL_EXTENDED_DATA = 95;
 54	const SSH_MSG_CHANNEL_EOF = 96;
 55	const SSH_MSG_CHANNEL_CLOSE = 97;
 56	const SSH_MSG_CHANNEL_REQUEST = 98;
 57	const SSH_MSG_CHANNEL_SUCCESS = 99;
 58	const SSH_MSG_CHANNEL_FAILURE = 100;
 59
 60	const SSH_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT = 1;
 61	const SSH_DISCONNECT_PROTOCOL_ERROR = 2;
 62	const SSH_DISCONNECT_KEY_EXCHANGE_FAILED = 3;
 63	const SSH_DISCONNECT_RESERVED = 4;
 64	const SSH_DISCONNECT_MAC_ERROR = 5;
 65	const SSH_DISCONNECT_COMPRESSION_ERROR = 6;
 66	const SSH_DISCONNECT_SERVICE_NOT_AVAILABLE = 7;
 67	const SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED = 8;
 68	const SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE = 9;
 69	const SSH_DISCONNECT_CONNECTION_LOST = 10;
 70	const SSH_DISCONNECT_BY_APPLICATION = 11;
 71	const SSH_DISCONNECT_TOO_MANY_CONNECTIONS = 12;
 72	const SSH_DISCONNECT_AUTH_CANCELLED_BY_USER = 13;
 73	const SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 14;
 74	const SSH_DISCONNECT_ILLEGAL_USER_NAME = 15;
 75
 76	const SSH_OPEN_ADMINISTRATIVELY_PROHIBITED = 1;
 77	const SSH_OPEN_CONNECT_FAILED = 2;
 78	const SSH_OPEN_UNKNOWN_CHANNEL_TYPE = 3;
 79	const SSH_OPEN_RESOURCE_SHORTAGE = 4;
 80
 81	function welcomeUser() {
 82		$this->state = 'new';
 83		$this->cipher['send'] = array('name' => 'NULL', 'block_size' => 1, 'hmac' => 'none');
 84		$this->cipher['recv'] = array('name' => 'NULL', 'block_size' => 1, 'hmac' => 'none');
 85		$this->setMsgEnd('');
 86		$this->sendMsg("Please wait while resolving your hostname...\r\n");
 87		return true;
 88	}
 89
 90	public function sendBanner() {
 91		$this->sendMsg("SSH-2.0-pinetd PHP/".phpversion()."\r\n");
 92		$this->payloads['V_S'] = "SSH-2.0-pinetd PHP/".phpversion();
 93	}
 94
 95	public function getLogin() {
 96		return $this->login;
 97	}
 98
 99	protected function handlePkt($pkt) {
100		$id = ord($pkt[0]);
101		$pkt = substr($pkt, 1);
102
103		if (($id > 69) && (is_null($this->login))) {
104			$this->disconnect(self::SSH_DISCONNECT_PROTOCOL_ERROR, 'need to login first');
105			return;
106		}
107
108		switch($id) {
109			case self::SSH_MSG_DISCONNECT: $this->close(); break;
110			case self::SSH_MSG_IGNORE:
111			case self::SSH_MSG_UNIMPLEMENTED:
112			case self::SSH_MSG_DEBUG:
113				// fake/useless traffic to keep cnx alive and/or confuse listeners
114				return;
115			case self::SSH_MSG_SERVICE_REQUEST:
116				$text = $this->parseStr($pkt);
117				$res = $this->ssh_serviceInit($text);
118				if ($res) {
119					$pkt = chr(self::SSH_MSG_SERVICE_ACCEPT).$this->str($text);
120					$this->sendPacket($pkt);
121				} else {
122					$this->disconnect(self::SSH_DISCONNECT_SERVICE_NOT_AVAILABLE, 'requested service '.$text.' not available');
123				}
124				break;
125			case self::SSH_MSG_KEXINIT:
126				$this->payloads['I_C'] = chr($id).$pkt;
127				$data = array();
128				$data['cookie'] = bin2hex(substr($pkt, 0, 16));
129				$pkt = substr($pkt, 16); // remove packet code (1 byte) & cookie (16 bytes)
130				$list_list = array('kex_algorithms','server_host_key_algorithms','encryption_algorithms_client_to_server','encryption_algorithms_server_to_client','mac_algorithms_client_to_server','mac_algorithms_server_to_client','compression_algorithms_client_to_server','compression_algorithms_server_to_client','languages_client_to_server','languages_server_to_client');
131				foreach($list_list as $list) {
132					$res = $this->parseNameList($pkt);
133					if ($res === false) {
134						$this->disconnect(self::SSH_DISCONNECT_PROTOCOL_ERROR, 'Failed to parse KEX packet');
135						return;
136					}
137					$data[$list] = $res;
138				}
139				$data['first_kex_packet_follows'] = ord($pkt[0]);
140				$data['reserved'] = bin2hex(substr($pkt, 1));
141				$this->capa = $data;
142				$this->ssh_determineEncryption();
143				if (!$this->kex_sent) $this->ssh_sendAlgorithmNegotiationPacket();
144				$this->kex_sent = false;
145				break;
146			case self::SSH_MSG_NEWKEYS:
147				// initialize encryption [recv/send]
148				if (!isset($this->capa['K'])) {
149					Logger::log(Logger::LOG_WARN, "Client trying to enable encryption without key exchange");
150					$this->disconnect(self::SSH_DISCONNECT_PROTOCOL_ERROR, 'you cannot enable encryption without kex first');
151					break;
152				}
153				$this->sendPacket(chr(self::SSH_MSG_NEWKEYS)); // send before we enable send encryption
154				$this->ssh_initEncryption();
155				break;
156			case self::SSH_MSG_KEXDH_INIT: $this->ssh_KeyExchangeDHInit($pkt); break;
157
158				/***************************** USER AUTH ****************************/
159
160			case self::SSH_MSG_USERAUTH_REQUEST:
161				$user = $this->parseStr($pkt);
162				$service = $this->parseStr($pkt);
163				$method = $this->parseStr($pkt);
164				$this->ssh_handleUserAuthRequest($user, $service, $method, $pkt);
165				break;
166
167				/**************************** CHANNELS *****************************/
168
169			case self::SSH_MSG_CHANNEL_OPEN:
170				$type = $this->parseStr($pkt);
171				list(,$channel, $window, $packet_max) = unpack('N3', $pkt);
172				$pkt = substr($pkt, 12);
173				$this->ssh_openChannel($type, $channel, $window, $packet_max, $pkt);
174				break;
175			case self::SSH_MSG_CHANNEL_WINDOW_ADJUST:
176				list(, $channel, $bytes) = unpack('N2', $pkt);
177				if (!isset($this->channels[$channel])) break;
178				$this->channels[$channel]->windowAdjust($bytes);
179				break;
180			case self::SSH_MSG_CHANNEL_DATA:
181				list(,$channel) = unpack('N', substr($pkt, 0, 4));
182				$pkt = substr($pkt, 4);
183				$data = $this->parseStr($pkt);
184				if (!isset($this->channels[$channel])) break;
185				$this->channels[$channel]->recv($data);
186				break;
187			case self::SSH_MSG_CHANNEL_EOF:
188				list(,$channel) = unpack('N', substr($pkt, 0, 4));
189				if (!isset($this->channels[$channel])) break;
190				$this->channels[$channel]->gotEof();
191				break;
192			case self::SSH_MSG_CHANNEL_CLOSE:
193				list(,$channel) = unpack('N', substr($pkt, 0, 4));
194				if (!isset($this->channels[$channel])) break;
195				$this->channels[$channel]->close();
196				unset($this->channels[$channel]);
197				break;
198			case self::SSH_MSG_CHANNEL_REQUEST:
199				list(,$channel) = unpack('N', substr($pkt, 0, 4));
200				$pkt = substr($pkt, 4);
201				$request = $this->parseStr($pkt);
202				$want_reply = (bool)ord($pkt[0]);
203				$pkt = substr($pkt, 1);
204				if (!isset($this->channels[$channel])) {
205					if ($want_reply) {
206						$this->sendPacket(pack('CN', self::SSH_MSG_CHANNEL_FAILURE, $channel));
207					}
208					break;
209				}
210				$res = $this->channels[$channel]->request($request, $pkt);
211				if ($want_reply)
212					$this->sendPacket(pack('CN', $res ? self::SSH_MSG_CHANNEL_SUCCESS : self::SSH_MSG_CHANNEL_FAILURE, $channel));
213				break;
214			default:
215				echo "Unknown packet [$id]: ".bin2hex($pkt)."\n";
216				$pkt = pack('CN', self::SSH_MSG_UNIMPLEMENTED, $this->seq_recv);
217				$this->sendPacket($pkt);
218		}
219	}
220
221	protected function ssh_openChannelError($channel, $errno, $msg) {
222		$pkt = pack('CNN', self::SSH_MSG_CHANNEL_OPEN_FAILURE, $channel, $errno);
223		$pkt .= $this->str($msg);
224		$pkt .= $this->str('');
225		$this->sendPacket($pkt);
226	}
227
228	public function channelEof($channel) {
229		if (!isset($this->channels[$channel])) return false;
230		$pkt = pack('CN', self::SSH_MSG_CHANNEL_EOF, $channel);
231		$this->sendPacket($pkt);
232	}
233
234	public function channelClose($channel) {
235		if (!isset($this->channels[$channel])) return false;
236		$this->channels[$channel]->closed();
237		$pkt = pack('CN', self::SSH_MSG_CHANNEL_CLOSE, $channel);
238		$this->sendPacket($pkt);
239	}
240
241	public function channelWrite($channel, $str) {
242		if (!isset($this->channels[$channel])) return false;
243		$pkt = pack('CN', self::SSH_MSG_CHANNEL_DATA, $channel);
244		$pkt.= $this->str($str);
245		$this->sendPacket($pkt);
246	}
247
248	public function channelWindow($channel, $bytes) {
249		if (!isset($this->channels[$channel])) return false;
250		$pkt = pack('CNN', self::SSH_MSG_CHANNEL_WINDOW_ADJUST, $channel, $bytes);
251		$this->sendPacket($pkt);
252	}
253
254	public function channelChangeObject($channel, $obj) {
255		if (!isset($this->channels[$channel])) return false;
256		$this->channels[$channel] = $obj;
257		return true;
258	}
259
260	protected function ssh_openChannel($type, $channel, $window, $packet_max, $pkt) {
261		if (isset($this->channels[$channel])) {
262			$this->disconnect(self::SSH_DISCONNECT_PROTOCOL_ERROR, 'Protocol error: channel is already in use');
263			return;
264		}
265		if ($type != 'session')
266			return $this->ssh_openChannelError($channel, self::SSH_OPEN_UNKNOWN_CHANNEL_TYPE, 'unknown channel type requested');
267
268		$class = relativeclass($this, 'Session');
269		$class = new $class($this, $channel, $window, $packet_max, $pkt);
270		$this->channels[$channel] = $class;
271
272		$pkt = pack('CNNNN', self::SSH_MSG_CHANNEL_OPEN_CONFIRMATION, $channel, $channel, $class->remoteWindow(), $class->maxPacket());
273		$pkt.= $class->specificConfirmationData();
274
275		$this->sendPacket($pkt);
276	}
277
278	protected function login($login, $password, $service) {
279		$info = $this->IPC->checkAccess($login, $password, $this->peer, $service);
280		if (!$info) return false;
281		return $this->doLogin($info);
282	}
283
284	protected function doLogin($login) {
285		$SUID = $this->IPC->canSUID();
286		if ($SUID) {
287			$user = $login['suid_user'] ?: $SUID['User'];
288			$group = $login['suid_group'] ?: $SUID['Group'];
289
290			$SUID = new SUID($user, $group);
291		}
292
293// Cannot chroot yet, need to find how we'll handle the loading of the next classes
294//		if (($this->IPC->canChroot()) && ($login['root'] != '/')) {
295//			if (chroot($login['root'])) {
296//				$login['root'] = '/';
297//			}
298//		}
299
300		if ($SUID) {
301			if (!$SUID->setIt()) {
302				$this->disconnect(self::SSH_DISCONNECT_BY_APPLICATION, 'failed to suid to the correct user');
303				$this->IPC->killSelf($this->fd);
304				return false;
305			}
306		}
307
308		$this->login = $login;
309		return true;
310	}
311
312	protected function loginPK($login, $service, $pk_algo, $pk_key, $pk_fingerprint, $pk_signature) {
313		$info = $this->IPC->getPublicKeyAccess($login, $pk_algo.':'.$pk_fingerprint, $this->peer, $service);
314		if (!$info) return false;
315		if ($info['type'] != $pk_algo) return false; // ?!
316		if (base64_decode($info['key']) != $pk_key) return false; // not the right public key (md5 collision?)
317
318		if (is_null($pk_signature)) return true;
319
320		// make packet for sign
321		$signed = $this->str($this->session_id).chr(self::SSH_MSG_USERAUTH_REQUEST).$this->str($login).$this->str($service).$this->str('publickey')."\x01".$this->str($info['type']).$this->str(base64_decode($info['key']));
322
323		switch($pk_algo) {
324			case 'ssh-rsa':
325				// RSA signature RFC3447
326				// parse public key
327				$tmp = base64_decode($info['key']);
328				if ($this->parseStr($tmp) != 'ssh-rsa') return false;
329				$e_bin = $this->parseStr($tmp);
330				$n_bin = $this->parseStr($tmp);
331
332				// read signature
333				$tmp = $pk_signature;
334				if ($this->parseStr($tmp) != 'ssh-rsa') return false;
335				$s_bin = $this->parseStr($tmp);
336
337				// convert to gmp
338				$e = gmp_init(bin2hex($e_bin), 16);
339				$n = gmp_init(bin2hex($n_bin), 16);
340				$s = gmp_init(bin2hex($s_bin), 16); // signature
341
342				// check
343				if (gmp_cmp($s, gmp_sub($n, 1)) > 0) return false;
344
345				// decode signature
346				$m = gmp_powm($s, $e, $n);
347				$m_bin = "\0".$this->gmp_binval($m); // starts with 0x00 0x01, but gmp drops the first 0x00
348				$emLen = strlen($m_bin);
349
350				// EMSA-PKCS1-v1_5 encoding of our own hash
351				$t = pack('H*', '3021300906052b0e03021a05000414'); // RFC 3447 page 43 - means "SHA-1" in DER encoding
352				$t.= sha1($signed, true);
353				$ps = str_repeat("\xff", strlen($m_bin)-strlen($t)-3); // emLen - tLen - 3
354				$em = "\x00\x01".$ps."\x00".$t; // EM = 0x00 || 0x01 || PS || 0x00 || T
355
356				// check signature
357				if ($m_bin == $em) break;
358
359				return false;
360			case 'ssh-dss':
361				// DSA signature check as of csrc.nist.gov/publications/fips/archive/fips186-2/fips186-2.pdf
362				// parse public key
363				$tmp = base64_decode($info['key']);
364				if ($this->parseStr($tmp) != 'ssh-dss') return false;
365				$p_bin = $this->parseStr($tmp);
366				$q_bin = $this->parseStr($tmp);
367				$g_bin = $this->parseStr($tmp);
368				$y_bin = $this->parseStr($tmp);
369
370				// parse signature (r_s)
371				$tmp = $pk_signature;
372				if ($this->parseStr($tmp) != 'ssh-dss') return false;
373				$r_s = $this->parseStr($tmp);
374
375				if (strlen($r_s) != 40) return false; // 160bits r + 160bits s
376				$r_bin = substr($r_s, 0, 20);
377				$s_bin = substr($r_s, 20, 20);
378
379				// init gmp
380				$p = gmp_init(bin2hex($p_bin), 16);
381				$q = gmp_init(bin2hex($q_bin), 16);
382				$g = gmp_init(bin2hex($g_bin), 16);
383				$y = gmp_init(bin2hex($y_bin), 16);
384				$r = gmp_init(bin2hex($r_bin), 16);
385				$s = gmp_init(bin2hex($s_bin), 16);
386				$M = gmp_init(sha1($signed), 16);
387
388				if (gmp_cmp($r, $q) > 0) return false;
389				if (gmp_cmp($s, $q) > 0) return false;
390
391				// computation
392				$w = gmp_invert($s, $q);
393				$u1 = gmp_mod(gmp_mul($M, $w), $q);
394				$u2 = gmp_mod(gmp_mul($r, $w), $q);
395				$v = gmp_mod(gmp_mod(gmp_mul(gmp_powm($g, $u1, $p), gmp_powm($y, $u2, $p)), $p), $q);
396
397				if (gmp_cmp($v, $r) == 0) { // sign check success!
398					break;
399				}
400				return false;
401			default: return false;
402		}
403
404		return $this->doLogin($info);
405	}
406
407	protected function ssh_handleUserAuthRequest($user, $service, $method, $pkt) {
408		if ($service != 'ssh-connection') {
409			$pkt = chr(self::SSH_MSG_USERAUTH_FAILURE).$this->str('').chr(0);
410			$this->sendPacket($pkt);
411			return;
412		}
413		switch($method) {
414			case 'password':
415				$change = (bool)ord($pkt[0]);
416				$pkt = substr($pkt, 1);
417				$password = $this->parseStr($pkt);
418				if (!$this->login($user, $password, $service)) {
419					Logger::log(Logger::LOG_WARN, 'Logging in of '.$user.' failed from '.$this->peer[0]);
420					sleep(4); // avoid immediate retry
421					break;
422				}
423				Logger::log(Logger::LOG_WARN, 'User '.$user.' logged in from '.$this->peer[0]);
424				$pkt = chr(self::SSH_MSG_USERAUTH_SUCCESS);
425				$this->sendPacket($pkt);
426				return;
427			case 'publickey':
428				$authreq = (bool)ord($pkt[0]);
429				$pkt = substr($pkt, 1);
430				$pk_algo = $this->parseStr($pkt);
431				$pk_key = $this->parseStr($pkt);
432				$pk_fingerprint = md5($pk_key);
433				$pk_signature = null;
434				if ($authreq) $pk_signature = $this->parseStr($pkt);
435//				echo "Pubkey auth, ".($authreq?'AUTH':'KEY').' '.$pk_algo.' '.$pk_fingerprint."\n";
436
437				$try = $this->loginPK($user, $service, $pk_algo, $pk_key, $pk_fingerprint, $pk_signature);
438				if (!$try) break;
439
440				if ($authreq) {
441					// success
442					$pkt = chr(self::SSH_MSG_USERAUTH_SUCCESS);
443					$this->sendPacket($pkt);
444					return;
445				}
446
447				$pkt = chr(self::SSH_MSG_USERAUTH_PK_OK).$this->str($pk_algo).$this->str($pk_key);
448				$this->sendPacket($pkt);
449				return;
450		}
451		$pkt = chr(self::SSH_MSG_USERAUTH_FAILURE).$this->str('publickey,password').chr(0);
452		$this->sendPacket($pkt);
453	}
454
455	protected function ssh_serviceInit($svc) {
456		if ($svc == 'ssh-userauth') return true; // builtin
457		var_dump($svc);
458		return false;
459	}
460
461	protected function ssh_initEncryption() {
462		// init I/O encryption
463		$mapping = array(
464			'aes128-ctr' => array(MCRYPT_RIJNDAEL_128, 'ctr', 16),
465			'aes256-cbc' => array(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC, 32),
466			'aes192-cbc' => array(MCRYPT_RIJNDAEL_192, MCRYPT_MODE_CBC, 24),
467			'aes128-cbc' => array(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC, 16),
468			'blowfish-cbc' => array(MCRYPT_BLOWFISH, MCRYPT_MODE_CBC, 32),
469			'serpent256-cbc' => array(MCRYPT_SERPENT, MCRYPT_MODE_CBC, 16),
470//			'arcfour', // Only cbc for now
471			'cast128-cbc' => array(MCRYPT_CAST_128, MCRYPT_MODE_CBC, 16),
472			'3des-cbc' => array(MCRYPT_3DES, MCRYPT_MODE_CBC, 16),
473		);
474
475		$alg_recv = $mapping[$this->capa['cipher_recv']];
476		$alg_send = $mapping[$this->capa['cipher_send']];
477		$hmac_recv = $this->capa['hmac_recv'];
478		$hmac_send = $this->capa['hmac_send'];
479		// TODO: handle compression activation too
480
481		$iv_recv = sha1($this->capa['K'].$this->capa['H'].'A'.$this->session_id, true);
482		$iv_send = sha1($this->capa['K'].$this->capa['H'].'B'.$this->session_id, true);
483		$key_recv = sha1($this->capa['K'].$this->capa['H'].'C'.$this->session_id, true);
484		$key_send = sha1($this->capa['K'].$this->capa['H'].'D'.$this->session_id, true);
485		$mac_recv = sha1($this->capa['K'].$this->capa['H'].'E'.$this->session_id, true);
486		$mac_send = sha1($this->capa['K'].$this->capa['H'].'F'.$this->session_id, true);
487
488		$key_recv_len = $alg_recv[2];
489		$key_send_len = $alg_send[2];
490		$iv_recv_len = mcrypt_get_iv_size($alg_recv[0], $alg_recv[1]);
491		$iv_send_len = mcrypt_get_iv_size($alg_send[0], $alg_recv[1]);
492		$block_recv = mcrypt_module_get_algo_block_size($alg_recv[0]);
493		$block_send = mcrypt_module_get_algo_block_size($alg_send[0]);
494
495		$mac_key_len_recv = strlen($mac_recv);
496		$mac_key_len_send = strlen($mac_send);
497		switch($hmac_recv) {
498			case 'hmac-sha1': case 'hmac-sha1-96': $mac_key_len_recv = 20; break;
499			case 'hmac-md5': case 'hmac-md5-96': $mac_key_len_recv = 16; break;
500		}
501		switch($hmac_send) {
502			case 'hmac-sha1': case 'hmac-sha1-96': $mac_key_len_send = 20; break;
503			case 'hmac-md5': case 'hmac-md5-96': $mac_key_len_send = 16; break;
504		}
505
506		if ($key_recv_len < $block_recv) $key_recv_len = $block_recv;
507		if ($key_send_len < $block_send) $key_send_len = $block_send;
508
509		while(strlen($key_recv) < $key_recv_len) $key_recv .= sha1($this->capa['K'].$this->capa['H'].$key_recv, true);
510		while(strlen($key_send) < $key_send_len) $key_send .= sha1($this->capa['K'].$this->capa['H'].$key_send, true);
511		if (strlen($key_recv) > $key_recv_len) $key_recv = substr($key_recv, 0, $key_recv_len);
512		if (strlen($key_send) > $key_send_len) $key_send = substr($key_send, 0, $key_send_len);
513
514		while(strlen($iv_recv) < $iv_recv_len) $iv_recv .= sha1($this->capa['K'].$this->capa['H'].$iv_recv, true);
515		while(strlen($iv_send) < $iv_send_len) $iv_send .= sha1($this->capa['K'].$this->capa['H'].$iv_send, true);
516		if (strlen($iv_recv) > $iv_recv_len) $iv_recv = substr($iv_recv, 0, $iv_recv_len);
517		if (strlen($iv_send) > $iv_send_len) $iv_send = substr($iv_send, 0, $iv_send_len);
518
519		if (strlen($mac_recv) > $mac_key_len_recv) $mac_recv = substr($mac_recv, 0, $mac_key_len_recv);
520		if (strlen($mac_send) > $mac_key_len_send) $mac_send = substr($mac_send, 0, $mac_key_len_send);
521
522		$mod_recv = mcrypt_module_open($alg_recv[0], '', $alg_recv[1], '');
523		$mod_send = mcrypt_module_open($alg_send[0], '', $alg_send[1], '');
524		mcrypt_generic_init($mod_recv, $key_recv, $iv_recv);
525		mcrypt_generic_init($mod_send, $key_send, $iv_send);
526
527		$this->cipher['recv'] = array('name' => $this->capa['cipher_recv'], 'block_size' => $block_recv, 'mod' => $mod_recv, 'hmac' => $hmac_recv, 'hmac_key' => $mac_recv);
528		$this->cipher['send'] = array('name' => $this->capa['cipher_send'], 'block_size' => $block_send, 'mod' => $mod_send, 'hmac' => $hmac_send, 'hmac_key' => $mac_send);
529	}
530
531	protected function gmp_binval($g) {
532		$hex = gmp_strval($g, 16);
533		if (strlen($hex) & 1) $hex = '0'.$hex;
534		return pack('H*', $hex);
535	}
536
537	protected function ssh_KeyExchangeDHInit($pkt) {
538		// secure key exchange
539		if (!$this->loadSkey()) {
540			Logger::log(Logger::LOG_WARN, 'Could not load server key');
541			$this->disconnect(self::SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 'server misconfiguration');
542			return;
543		}
544
545		switch($this->capa['kex']) {
546			case 'diffie-hellman-group1-sha1':
547			case 'diffie-hellman-group14-sha1':
548				// RFC 4253 page 21: 8. Diffie-Hellman Key Exchange
549				// Both groups have the same generator
550				if ($this->capa['kex'] == 'diffie-hellman-group1-sha1') {
551					$p = gmp_init('179769313486231590770839156793787453197860296048756011706444423684197180216158519368947833795864925541502180565485980503646440548199239100050792877003355816639229553136239076508735759914822574862575007425302077447712589550957937778424442426617334727629299387668709205606050270810842907692932019128194467627007');
552					$y = gmp_init(bin2hex(openssl_random_pseudo_bytes(64)), 16);
553				} else { // diffie-hellman-group14-sha1
554					$p = gmp_init('FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF', 16);
555					$y = gmp_init(bin2hex(openssl_random_pseudo_bytes(128)), 16);
556				}
557
558				// read $e from client
559				$e_bin = $this->parseStr($pkt);
560				$e = gmp_init(bin2hex($e_bin), 16); // not really optimized but works
561
562
563				// compute $f and $K
564				$f = gmp_powm(2, $y, $p);
565				$f_bin = $this->gmp_binval($f);
566				if (ord($f_bin[0]) & 0x80) $f_bin = "\0" . $f_bin;
567				$K = gmp_powm($e, $y, $p);
568				$K_bin = $this->gmp_binval($K);
569				if (ord($K_bin[0]) & 0x80) $K_bin = "\0" . $K_bin;
570
571				$pub = $this->skey['pub'];
572
573				// H = hash(V_C || V_S || I_C || I_S || K_S || e || f || K)
574				$sha = array(
575					$this->str($this->payloads['V_C']),
576					$this->str($this->payloads['V_S']),
577					$this->str($this->payloads['I_C']),
578					$this->str($this->payloads['I_S']),
579					$this->str($pub),
580					$this->str($e_bin),
581					$this->str($f_bin),
582					$this->str($K_bin)
583				);
584				$H = sha1(implode('', $sha), true);
585
586				// store shared secret in session
587				$this->capa['K'] = $this->str($K_bin);
588				$this->capa['H'] = $H;
589				if (is_null($this->session_id)) $this->session_id = $H;
590
591				// sign $H
592				$s = Crypto::sign($H, $this->skey['pkeyid']);
593				$s = $this->str($this->skey['type']).$this->str($s);
594
595				$pkt = chr(self::SSH_MSG_KEXDH_REPLY);
596				$pkt .= $this->str($pub);
597				$pkt .= $this->str($f_bin);
598				$pkt .= $this->str($s);
599				$this->sendPacket($pkt);
600				break;
601			default:
602				$this->disconnect(self::SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 'Unsupported key exchange algo');
603		}
604	}
605
606	protected function ssh_determineEncryption() {
607		$my_kex_alg = array_flip($this->getKeyExchAlgList());
608		$my_key_alg = array_flip($this->getPKAlgList());
609		$my_cipher = array_flip($this->getCipherAlgList());
610		$my_hmac = array_flip($this->getHmacAlgList());
611		$my_comp = array_flip($this->getCompAlgList());
612
613		$fallback_cnt = 0;
614
615		$kex = null;
616		foreach($this->capa['kex_algorithms'] as $alg) {
617			if (isset($my_kex_alg[$alg])) { $kex = $alg; break; }
618			$fallback_cnt++;
619		}
620		if (is_null($kex)) { $this->disconnect(self::SSH_DISCONNECT_PROTOCOL_ERROR, 'No matching KEX'); return; }
621		$key_alg = null;
622		foreach($this->capa['server_host_key_algorithms'] as $alg) {
623			if (isset($my_key_alg[$alg])) { $key_alg = $alg; break; }
624			$fallback_cnt++;
625		}
626		if (is_null($key_alg)) { $this->close(); return; }
627		$cipher_send = null;
628		foreach($this->capa['encryption_algorithms_server_to_client'] as $alg) {
629			if (isset($my_cipher[$alg])) { $cipher_send = $alg; break; }
630			$fallback_cnt++;
631		}
632		if (is_null($cipher_send)) { $this->close(); return; }
633		$cipher_recv = null;
634		foreach($this->capa['encryption_algorithms_client_to_server'] as $alg) {
635			if (isset($my_cipher[$alg])) { $cipher_recv = $alg; break; }
636			$fallback_cnt++;
637		}
638		if (is_null($cipher_recv)) { $this->close(); return; }
639		$hmac_send = null;
640		foreach($this->capa['mac_algorithms_server_to_client'] as $alg) {
641			if (isset($my_hmac[$alg])) { $hmac_send = $alg; break; }
642			$fallback_cnt++;
643		}
644		if (is_null($hmac_send)) { $this->close(); return; }
645		$hmac_recv = null;
646		foreach($this->capa['mac_algorithms_client_to_server'] as $alg) {
647			if (isset($my_hmac[$alg])) { $hmac_recv = $alg; break; }
648			$fallback_cnt++;
649		}
650		if (is_null($hmac_recv)) { $this->close(); return; }
651		$comp_send = null;
652		foreach($this->capa['compression_algorithms_server_to_client'] as $alg) {
653			if (isset($my_comp[$alg])) { $comp_send = $alg; break; }
654			$fallback_cnt++;
655		}
656		if (is_null($comp_send)) { $this->close(); return; }
657		$comp_recv = null;
658		foreach($this->capa['compression_algorithms_client_to_server'] as $alg) {
659			if (isset($my_comp[$alg])) { $comp_recv = $alg; break; }
660			$fallback_cnt++;
661		}
662		if (is_null($comp_recv)) { $this->close(); return; }
663
664		$this->capa['kex'] = $kex;
665		$this->capa['key_alg'] = $key_alg;
666		$this->capa['cipher_send'] = $cipher_send;
667		$this->capa['cipher_recv'] = $cipher_recv;
668		$this->capa['hmac_send'] = $hmac_send;
669		$this->capa['hmac_recv'] = $hmac_recv;
670		$this->capa['comp_send'] = $comp_send;
671		$this->capa['comp_recv'] = $comp_recv;
672		$this->capa['fallback_cnt'] = $fallback_cnt; // if not 0, means the client didn't guess right
673	}
674
675	protected function ssh_sendAlgorithmNegotiationPacket() {
676		$pkt = pack('CNNNN', self::SSH_MSG_KEXINIT, mt_rand(0,0xffffffff), mt_rand(0,0xffffffff), mt_rand(0,0xffffffff), mt_rand(0,0xffffffff));
677		$pkt .= $this->nameList($this->getKeyExchAlgList());
678		$pkt .= $this->nameList($this->getPKAlgList());
679		$pkt .= $this->nameList($this->getCipherAlgList());
680		$pkt .= $this->nameList($this->getCipherAlgList());
681		$pkt .= $this->nameList($this->getHmacAlgList());
682		$pkt .= $this->nameList($this->getHmacAlgList());
683		$pkt .= $this->nameList($this->getCompAlgList());
684		$pkt .= $this->nameList($this->getCompAlgList());
685		$pkt .= str_repeat("\0", 8); // no languages_client_to_server / languages_server_to_client
686		$pkt .= "\0"; // boolean first_kex_packet_follows
687		$pkt .= str_repeat("\0", 4);
688		$this->payloads['I_S'] = $pkt;
689		$this->sendPacket($pkt);
690		$this->kex_sent = true;
691	}
692
693	protected function parseInt32(&$pkt) {
694		list(,$int) = unpack('N', substr($pkt, 0, 4));
695		$pkt = substr($pkt, 4);
696		return $int;
697	}
698
699	protected function parseStr(&$pkt) {
700		list(,$len) = unpack('N', substr($pkt, 0, 4));
701		if ($len == 0) {
702			$pkt = substr($pkt, 4);
703			return '';
704		}
705		if ($len+4 > strlen($pkt)) return false;
706		$res = substr($pkt, 4, $len);
707		$pkt = substr($pkt, $len+4);
708		return $res;
709	}
710
711	protected function parseNameList(&$pkt) {
712		list(,$len) = unpack('N', substr($pkt, 0, 4));
713		if ($len == 0) {
714			$pkt = substr($pkt, 4);
715			return array();
716		}
717		if ($len+4 > strlen($pkt)) return false;
718		$res = explode(',', substr($pkt, 4, $len));
719		$pkt = substr($pkt, $len+4);
720		return $res;
721	}
722
723	protected function loadSkey() {
724		switch($this->capa['key_alg']) { // ssh-dss,ssh-rsa
725			case 'ssh-rsa': $key = PINETD_ROOT.'/ssl/ssh_host_rsa_key'; $pkeyid = Crypto::rsa_read_pem(file_get_contents($key)); break;
726			case 'ssh-dss': $key = PINETD_ROOT.'/ssl/ssh_host_dsa_key'; $pkeyid = Crypto::dsa_read_pem(file_get_contents($key)); break;
727			default: return false;
728		}
729		if ((!file_exists($key)) || (!$pkeyid)) return false;
730		$pub = Crypto::make_ssh_pk($pkeyid);
731		$this->skey = array('pub' => $pub, 'pkeyid' => $pkeyid, 'type' => $this->capa['key_alg']);
732		return true;
733	}
734
735	protected function nameList(array $list) {
736		$res = implode(',', $list);
737		return pack('N', strlen($res)).$res;
738	}
739
740	protected function getCompAlgList() {
741		return array('none'); // zlib@openssl.com
742	}
743
744	protected function getCipherAlgList() {
745		$mapping = array(
746			'aes128-ctr' => 'rijndael-128',
747			'aes128-cbc' => 'rijndael-128',
748			'blowfish-cbc' => 'blowfish',
749			'serpent256-cbc' => 'serpent',
750			'cast128-cbc' => 'cast-128',
751			'3des-cbc' => 'tripledes',
752		);
753		$list = array_flip(mcrypt_list_algorithms());
754		$final = array();
755		foreach($mapping as $ssh_alg => $alg) {
756			if (!isset($list[$alg])) continue;
757			$final[] = $ssh_alg;
758		}
759		return $final;
760	}
761
762	protected function getHmacAlgList() {
763		return array('hmac-sha1','hmac-sha1-96','hmac-md5','hmac-md5-96','none');
764	}
765
766	protected function getKeyExchAlgList() {
767		return array('diffie-hellman-group14-sha1','diffie-hellman-group1-sha1');
768	}
769
770	protected function getPKAlgList() {
771		return array('ssh-dss','ssh-rsa');
772	}
773
774	protected function disconnect($code, $text) {
775		$pkt = chr(self::SSH_MSG_DISCONNECT);
776		$pkt .= pack('N', $code);
777		$pkt .= $this->str($text);
778		$pkt .= $this->str('');
779		$this->sendPacket($pkt);
780		$this->close();
781	}
782
783	protected function str($str) {
784		return pack('N', strlen($str)).$str;
785	}
786
787	protected function sendPacket($packet) {
788		$this->seq_send++;
789		$len = strlen($packet);
790		// compute padding length
791		$pad_len = max($this->cipher['send']['block_size'], 8) - (($len+5) % max($this->cipher['send']['block_size'], 8));
792		if ($pad_len < 4) $pad_len += max($this->cipher['send']['block_size'], 8);
793		$packet = pack('NC', $len+1+$pad_len, $pad_len).$packet.str_repeat("\0", $pad_len);
794		
795		// compute hmac
796		$mac = '';
797		switch($this->cipher['send']['hmac']) {
798			case 'hmac-sha1': $mac = hash_hmac('sha1', pack('N', $this->seq_send).$packet, $this->cipher['send']['hmac_key'], true); break;
799			case 'hmac-sha1-96': $mac = substr(hash_hmac('sha1', pack('N', $this->seq_send).$packet, $this->cipher['send']['hmac_key'], true), 0, 12); break;
800			case 'hmac-md5': $mac = hash_hmac('md5', pack('N', $this->seq_send).$packet, $this->cipher['send']['hmac_key'], true); break;
801			case 'hmac-md5-96': $mac = substr(hash_hmac('md5', pack('N', $this->seq_send).$packet, $this->cipher['send']['hmac_key'], true), 0, 12); break;
802		}
803		// encrypt packet
804		if (isset($this->cipher['send']['mod'])) {
805			$packet = mcrypt_generic($this->cipher['send']['mod'], $packet);
806		}
807		return $this->sendMsg($packet.$mac);
808	}
809
810	protected function handleProtocol($proto) {
811		if (!preg_match('/^SSH-2\\.0-([^ -]+)( .*)?$/', $proto, $matches)) {
812			$this->disconnect(self::SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED, 'could not understand your protocol version');
813			return;
814		}
815		$this->payloads['V_C'] = $proto;
816		$this->agent = $matches[1].$matches[2];
817		$this->state = 'login';
818		$this->ssh_sendAlgorithmNegotiationPacket();
819	}
820
821	protected function parseBuffer() {
822		while($this->ok) {
823			if($this->state == 'new') {
824				$pos = strpos($this->buf, "\n");
825				if ($pos === false) break;
826				$pos++;
827				$lin = substr($this->buf, 0, $pos);
828				$this->buf = substr($this->buf, $pos);
829				$this->handleProtocol(rtrim($lin));
830				continue;
831			}
832
833			$block_size = max(8, $this->cipher['recv']['block_size']);
834
835			if (strlen($this->clearbuf) < 8) {
836				if (strlen($this->buf) < $block_size) break; // not enough data yet
837				// decrypt first block
838				$tmp = substr($this->buf, 0, $block_size);
839				$this->buf = substr($this->buf, $block_size);
840				if (isset($this->cipher['recv']['mod']))
841					$tmp = mdecrypt_generic($this->cipher['recv']['mod'], $tmp);
842				$this->clearbuf .= $tmp;
843			}
844
845			// decode length from clearbuf
846			list(,$len) = unpack('N', substr($this->clearbuf, 0, 4));
847
848			if ($len > 256*1024) {
849				$this->disconnect(self::SSH_DISCONNECT_PROTOCOL_ERROR, 'disconnected: packet size over 256kB');
850			}
851
852			if ((4+$len) > strlen($this->clearbuf)) { // missing data
853				$missing_len = (4+$len)-strlen($this->clearbuf);
854				if (strlen($this->buf) < $missing_len) break; // need more data
855				$tmp = substr($this->buf, 0, $missing_len);
856				$this->buf = substr($this->buf, $missing_len);
857				if (isset($this->cipher['recv']['mod']))
858					$tmp = mdecrypt_generic($this->cipher['recv']['mod'], $tmp);
859				$this->clearbuf .= $tmp;
860			}
861
862			// we got one full packet, check for mac...
863			$mac_len = 0;
864			switch($this->cipher['recv']['hmac']) {
865				case 'hmac-sha1': $mac_len = 20; break;
866				case 'hmac-md5': $mac_len = 16; break;
867				case 'hmac-sha1-96': 
868				case 'hmac-md5-96': $mac_len = 12; break;
869			}
870			if (strlen($this->buf) < $mac_len) break; // need more data
871
872			$this->seq_recv++; // we got all the data we needed, we can increment this counter
873
874			if ($mac_len > 0) {
875				switch($this->cipher['recv']['hmac']) {
876					case 'hmac-sha1': $mac = hash_hmac('sha1', pack('N', $this->seq_recv).$this->clearbuf, $this->cipher['recv']['hmac_key'], true); break;
877					case 'hmac-sha1-96': $mac = substr(hash_hmac('sha1', pack('N', $this->seq_recv).$this->clearbuf, $this->cipher['recv']['hmac_key'], true), 0, 12); break;
878					case 'hmac-md5': $mac = hash_hmac('md5', pack('N', $this->seq_recv).$this->clearbuf, $this->cipher['recv']['hmac_key'], true); break;
879					case 'hmac-md5-96': $mac = substr(hash_hmac('md5', pack('N', $this->seq_recv).$this->clearbuf, $this->cipher['recv']['hmac_key'], true), 0, 12); break;
880				}
881				$tmp = substr($this->buf, 0, $mac_len);
882				$this->buf = substr($this->buf, $mac_len);
883				if ($tmp != $mac) {
884					Logger::log(Logger::LOG_WARN, 'Invalid MAC for packet in input stream!');
885					$this->disconnect(self::SSH_DISCONNECT_MAC_ERROR, 'could not understand mac');
886					break;
887				}
888			}
889
890			$pkt = substr($this->clearbuf, 4);
891			$this->clearbuf = '';
892
893			$padding = ord($pkt[0]);
894			$pkt = substr($pkt, 1, 0-$padding);
895			$this->handlePkt($pkt);
896		}
897		$this->setProcessStatus(); // back to idle
898	}
899
900	function shutdown() {
901	}
902}
903