• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2006-2011 Christian Plattner. All rights reserved.
3  * Please refer to the LICENSE.txt for licensing details.
4  */
5 package ch.ethz.ssh2.transport;
6 
7 import java.io.IOException;
8 import java.security.SecureRandom;
9 
10 import ch.ethz.ssh2.ConnectionInfo;
11 import ch.ethz.ssh2.DHGexParameters;
12 import ch.ethz.ssh2.ServerHostKeyVerifier;
13 import ch.ethz.ssh2.crypto.CryptoWishList;
14 import ch.ethz.ssh2.crypto.KeyMaterial;
15 import ch.ethz.ssh2.crypto.cipher.BlockCipher;
16 import ch.ethz.ssh2.crypto.cipher.BlockCipherFactory;
17 import ch.ethz.ssh2.crypto.dh.DhExchange;
18 import ch.ethz.ssh2.crypto.dh.DhGroupExchange;
19 import ch.ethz.ssh2.crypto.digest.MAC;
20 import ch.ethz.ssh2.log.Logger;
21 import ch.ethz.ssh2.packets.PacketKexDHInit;
22 import ch.ethz.ssh2.packets.PacketKexDHReply;
23 import ch.ethz.ssh2.packets.PacketKexDhGexGroup;
24 import ch.ethz.ssh2.packets.PacketKexDhGexInit;
25 import ch.ethz.ssh2.packets.PacketKexDhGexReply;
26 import ch.ethz.ssh2.packets.PacketKexDhGexRequest;
27 import ch.ethz.ssh2.packets.PacketKexDhGexRequestOld;
28 import ch.ethz.ssh2.packets.PacketKexInit;
29 import ch.ethz.ssh2.packets.PacketNewKeys;
30 import ch.ethz.ssh2.packets.Packets;
31 import ch.ethz.ssh2.signature.DSAPublicKey;
32 import ch.ethz.ssh2.signature.DSASHA1Verify;
33 import ch.ethz.ssh2.signature.DSASignature;
34 import ch.ethz.ssh2.signature.RSAPublicKey;
35 import ch.ethz.ssh2.signature.RSASHA1Verify;
36 import ch.ethz.ssh2.signature.RSASignature;
37 
38 /**
39  * KexManager.
40  *
41  * @author Christian Plattner
42  * @version $Id: KexManager.java 45 2011-07-01 15:09:41Z dkocher@sudo.ch $
43  */
44 public class KexManager
45 {
46 	private static final Logger log = Logger.getLogger(KexManager.class);
47 
48 	KexState kxs;
49 	int kexCount = 0;
50 	KeyMaterial km;
51 	byte[] sessionId;
52 	ClientServerHello csh;
53 
54 	final Object accessLock = new Object();
55 	ConnectionInfo lastConnInfo = null;
56 
57 	boolean connectionClosed = false;
58 
59 	boolean ignore_next_kex_packet = false;
60 
61 	final TransportManager tm;
62 
63 	CryptoWishList nextKEXcryptoWishList;
64 	DHGexParameters nextKEXdhgexParameters;
65 
66 	ServerHostKeyVerifier verifier;
67 	final String hostname;
68 	final int port;
69 	final SecureRandom rnd;
70 
KexManager(TransportManager tm, ClientServerHello csh, CryptoWishList initialCwl, String hostname, int port, ServerHostKeyVerifier keyVerifier, SecureRandom rnd)71 	public KexManager(TransportManager tm, ClientServerHello csh, CryptoWishList initialCwl, String hostname, int port,
72 			ServerHostKeyVerifier keyVerifier, SecureRandom rnd)
73 	{
74 		this.tm = tm;
75 		this.csh = csh;
76 		this.nextKEXcryptoWishList = initialCwl;
77 		this.nextKEXdhgexParameters = new DHGexParameters();
78 		this.hostname = hostname;
79 		this.port = port;
80 		this.verifier = keyVerifier;
81 		this.rnd = rnd;
82 	}
83 
getOrWaitForConnectionInfo(int minKexCount)84 	public ConnectionInfo getOrWaitForConnectionInfo(int minKexCount) throws IOException
85 	{
86 		boolean wasInterrupted = false;
87 
88 		try
89 		{
90 			synchronized (accessLock)
91 			{
92 				while (true)
93 				{
94 					if ((lastConnInfo != null) && (lastConnInfo.keyExchangeCounter >= minKexCount))
95 						return lastConnInfo;
96 
97 					if (connectionClosed)
98 						throw (IOException) new IOException("Key exchange was not finished, connection is closed.")
99 								.initCause(tm.getReasonClosedCause());
100 
101 					try
102 					{
103 						accessLock.wait();
104 					}
105 					catch (InterruptedException e)
106 					{
107 						wasInterrupted = true;
108 					}
109 				}
110 			}
111 		}
112 		finally
113 		{
114 			if (wasInterrupted)
115 				Thread.currentThread().interrupt();
116 		}
117 	}
118 
getFirstMatch(String[] client, String[] server)119 	private String getFirstMatch(String[] client, String[] server) throws NegotiateException
120 	{
121 		if (client == null || server == null)
122 			throw new IllegalArgumentException();
123 
124 		if (client.length == 0)
125 			return null;
126 
127 		for (int i = 0; i < client.length; i++)
128 		{
129 			for (int j = 0; j < server.length; j++)
130 			{
131 				if (client[i].equals(server[j]))
132 					return client[i];
133 			}
134 		}
135 		throw new NegotiateException();
136 	}
137 
compareFirstOfNameList(String[] a, String[] b)138 	private boolean compareFirstOfNameList(String[] a, String[] b)
139 	{
140 		if (a == null || b == null)
141 			throw new IllegalArgumentException();
142 
143 		if ((a.length == 0) && (b.length == 0))
144 			return true;
145 
146 		if ((a.length == 0) || (b.length == 0))
147 			return false;
148 
149 		return (a[0].equals(b[0]));
150 	}
151 
isGuessOK(KexParameters cpar, KexParameters spar)152 	private boolean isGuessOK(KexParameters cpar, KexParameters spar)
153 	{
154 		if (cpar == null || spar == null)
155 			throw new IllegalArgumentException();
156 
157 		if (compareFirstOfNameList(cpar.kex_algorithms, spar.kex_algorithms) == false)
158 		{
159 			return false;
160 		}
161 
162 		if (compareFirstOfNameList(cpar.server_host_key_algorithms, spar.server_host_key_algorithms) == false)
163 		{
164 			return false;
165 		}
166 
167 		/*
168 		 * We do NOT check here if the other algorithms can be agreed on, this
169 		 * is just a check if kex_algorithms and server_host_key_algorithms were
170 		 * guessed right!
171 		 */
172 
173 		return true;
174 	}
175 
mergeKexParameters(KexParameters client, KexParameters server)176 	private NegotiatedParameters mergeKexParameters(KexParameters client, KexParameters server)
177 	{
178 		NegotiatedParameters np = new NegotiatedParameters();
179 
180 		try
181 		{
182 			np.kex_algo = getFirstMatch(client.kex_algorithms, server.kex_algorithms);
183 
184 			log.info("kex_algo=" + np.kex_algo);
185 
186 			np.server_host_key_algo = getFirstMatch(client.server_host_key_algorithms,
187 					server.server_host_key_algorithms);
188 
189 			log.info("server_host_key_algo=" + np.server_host_key_algo);
190 
191 			np.enc_algo_client_to_server = getFirstMatch(client.encryption_algorithms_client_to_server,
192 					server.encryption_algorithms_client_to_server);
193 			np.enc_algo_server_to_client = getFirstMatch(client.encryption_algorithms_server_to_client,
194 					server.encryption_algorithms_server_to_client);
195 
196 			log.info("enc_algo_client_to_server=" + np.enc_algo_client_to_server);
197 			log.info("enc_algo_server_to_client=" + np.enc_algo_server_to_client);
198 
199 			np.mac_algo_client_to_server = getFirstMatch(client.mac_algorithms_client_to_server,
200 					server.mac_algorithms_client_to_server);
201 			np.mac_algo_server_to_client = getFirstMatch(client.mac_algorithms_server_to_client,
202 					server.mac_algorithms_server_to_client);
203 
204 			log.info("mac_algo_client_to_server=" + np.mac_algo_client_to_server);
205 			log.info("mac_algo_server_to_client=" + np.mac_algo_server_to_client);
206 
207 			np.comp_algo_client_to_server = getFirstMatch(client.compression_algorithms_client_to_server,
208 					server.compression_algorithms_client_to_server);
209 			np.comp_algo_server_to_client = getFirstMatch(client.compression_algorithms_server_to_client,
210 					server.compression_algorithms_server_to_client);
211 
212 			log.info("comp_algo_client_to_server=" + np.comp_algo_client_to_server);
213 			log.info("comp_algo_server_to_client=" + np.comp_algo_server_to_client);
214 
215 		}
216 		catch (NegotiateException e)
217 		{
218 			return null;
219 		}
220 
221 		try
222 		{
223 			np.lang_client_to_server = getFirstMatch(client.languages_client_to_server,
224 					server.languages_client_to_server);
225 		}
226 		catch (NegotiateException e1)
227 		{
228 			np.lang_client_to_server = null;
229 		}
230 
231 		try
232 		{
233 			np.lang_server_to_client = getFirstMatch(client.languages_server_to_client,
234 					server.languages_server_to_client);
235 		}
236 		catch (NegotiateException e2)
237 		{
238 			np.lang_server_to_client = null;
239 		}
240 
241 		if (isGuessOK(client, server))
242 			np.guessOK = true;
243 
244 		return np;
245 	}
246 
initiateKEX(CryptoWishList cwl, DHGexParameters dhgex)247 	public synchronized void initiateKEX(CryptoWishList cwl, DHGexParameters dhgex) throws IOException
248 	{
249 		nextKEXcryptoWishList = cwl;
250 		nextKEXdhgexParameters = dhgex;
251 
252 		if (kxs == null)
253 		{
254 			kxs = new KexState();
255 
256 			kxs.dhgexParameters = nextKEXdhgexParameters;
257 			PacketKexInit kp = new PacketKexInit(nextKEXcryptoWishList, rnd);
258 			kxs.localKEX = kp;
259 			tm.sendKexMessage(kp.getPayload());
260 		}
261 	}
262 
establishKeyMaterial()263 	private boolean establishKeyMaterial()
264 	{
265 		try
266 		{
267 			int mac_cs_key_len = MAC.getKeyLen(kxs.np.mac_algo_client_to_server);
268 			int enc_cs_key_len = BlockCipherFactory.getKeySize(kxs.np.enc_algo_client_to_server);
269 			int enc_cs_block_len = BlockCipherFactory.getBlockSize(kxs.np.enc_algo_client_to_server);
270 
271 			int mac_sc_key_len = MAC.getKeyLen(kxs.np.mac_algo_server_to_client);
272 			int enc_sc_key_len = BlockCipherFactory.getKeySize(kxs.np.enc_algo_server_to_client);
273 			int enc_sc_block_len = BlockCipherFactory.getBlockSize(kxs.np.enc_algo_server_to_client);
274 
275 			km = KeyMaterial.create("SHA1", kxs.H, kxs.K, sessionId, enc_cs_key_len, enc_cs_block_len, mac_cs_key_len,
276 					enc_sc_key_len, enc_sc_block_len, mac_sc_key_len);
277 		}
278 		catch (IllegalArgumentException e)
279 		{
280 			return false;
281 		}
282 		return true;
283 	}
284 
finishKex()285 	private void finishKex() throws IOException
286 	{
287 		if (sessionId == null)
288 			sessionId = kxs.H;
289 
290 		establishKeyMaterial();
291 
292 		/* Tell the other side that we start using the new material */
293 
294 		PacketNewKeys ign = new PacketNewKeys();
295 		tm.sendKexMessage(ign.getPayload());
296 
297 		BlockCipher cbc;
298 		MAC mac;
299 
300 		try
301 		{
302 			cbc = BlockCipherFactory.createCipher(kxs.np.enc_algo_client_to_server, true, km.enc_key_client_to_server,
303 					km.initial_iv_client_to_server);
304 
305 			mac = new MAC(kxs.np.mac_algo_client_to_server, km.integrity_key_client_to_server);
306 
307 		}
308 		catch (IllegalArgumentException e1)
309 		{
310 			throw new IOException("Fatal error during MAC startup!");
311 		}
312 
313 		tm.changeSendCipher(cbc, mac);
314 		tm.kexFinished();
315 	}
316 
getDefaultServerHostkeyAlgorithmList()317 	public static String[] getDefaultServerHostkeyAlgorithmList()
318 	{
319 		return new String[] { "ssh-rsa", "ssh-dss" };
320 	}
321 
checkServerHostkeyAlgorithmsList(String[] algos)322 	public static void checkServerHostkeyAlgorithmsList(String[] algos)
323 	{
324 		for (int i = 0; i < algos.length; i++)
325 		{
326 			if (("ssh-rsa".equals(algos[i]) == false) && ("ssh-dss".equals(algos[i]) == false))
327 				throw new IllegalArgumentException("Unknown server host key algorithm '" + algos[i] + "'");
328 		}
329 	}
330 
getDefaultKexAlgorithmList()331 	public static String[] getDefaultKexAlgorithmList()
332 	{
333 		return new String[] { "diffie-hellman-group-exchange-sha1", "diffie-hellman-group14-sha1",
334 				"diffie-hellman-group1-sha1" };
335 	}
336 
checkKexAlgorithmList(String[] algos)337 	public static void checkKexAlgorithmList(String[] algos)
338 	{
339 		for (int i = 0; i < algos.length; i++)
340 		{
341 			if ("diffie-hellman-group-exchange-sha1".equals(algos[i]))
342 				continue;
343 
344 			if ("diffie-hellman-group14-sha1".equals(algos[i]))
345 				continue;
346 
347 			if ("diffie-hellman-group1-sha1".equals(algos[i]))
348 				continue;
349 
350 			throw new IllegalArgumentException("Unknown kex algorithm '" + algos[i] + "'");
351 		}
352 	}
353 
verifySignature(byte[] sig, byte[] hostkey)354 	private boolean verifySignature(byte[] sig, byte[] hostkey) throws IOException
355 	{
356 		if (kxs.np.server_host_key_algo.equals("ssh-rsa"))
357 		{
358 			RSASignature rs = RSASHA1Verify.decodeSSHRSASignature(sig);
359 			RSAPublicKey rpk = RSASHA1Verify.decodeSSHRSAPublicKey(hostkey);
360 
361 			log.debug("Verifying ssh-rsa signature");
362 
363 			return RSASHA1Verify.verifySignature(kxs.H, rs, rpk);
364 		}
365 
366 		if (kxs.np.server_host_key_algo.equals("ssh-dss"))
367 		{
368 			DSASignature ds = DSASHA1Verify.decodeSSHDSASignature(sig);
369 			DSAPublicKey dpk = DSASHA1Verify.decodeSSHDSAPublicKey(hostkey);
370 
371 			log.debug("Verifying ssh-dss signature");
372 
373 			return DSASHA1Verify.verifySignature(kxs.H, ds, dpk);
374 		}
375 
376 		throw new IOException("Unknown server host key algorithm '" + kxs.np.server_host_key_algo + "'");
377 	}
378 
handleMessage(byte[] msg, int msglen)379 	public synchronized void handleMessage(byte[] msg, int msglen) throws IOException
380 	{
381 		PacketKexInit kip;
382 
383 		if (msg == null)
384 		{
385 			synchronized (accessLock)
386 			{
387 				connectionClosed = true;
388 				accessLock.notifyAll();
389 				return;
390 			}
391 		}
392 
393 		if ((kxs == null) && (msg[0] != Packets.SSH_MSG_KEXINIT))
394 			throw new IOException("Unexpected KEX message (type " + msg[0] + ")");
395 
396 		if (ignore_next_kex_packet)
397 		{
398 			ignore_next_kex_packet = false;
399 			return;
400 		}
401 
402 		if (msg[0] == Packets.SSH_MSG_KEXINIT)
403 		{
404 			if ((kxs != null) && (kxs.state != 0))
405 				throw new IOException("Unexpected SSH_MSG_KEXINIT message during on-going kex exchange!");
406 
407 			if (kxs == null)
408 			{
409 				/*
410 				 * Ah, OK, peer wants to do KEX. Let's be nice and play
411 				 * together.
412 				 */
413 				kxs = new KexState();
414 				kxs.dhgexParameters = nextKEXdhgexParameters;
415 				kip = new PacketKexInit(nextKEXcryptoWishList, rnd);
416 				kxs.localKEX = kip;
417 				tm.sendKexMessage(kip.getPayload());
418 			}
419 
420 			kip = new PacketKexInit(msg, 0, msglen);
421 			kxs.remoteKEX = kip;
422 
423 			kxs.np = mergeKexParameters(kxs.localKEX.getKexParameters(), kxs.remoteKEX.getKexParameters());
424 
425 			if (kxs.np == null)
426 				throw new IOException("Cannot negotiate, proposals do not match.");
427 
428 			if (kxs.remoteKEX.isFirst_kex_packet_follows() && (kxs.np.guessOK == false))
429 			{
430 				/*
431 				 * Guess was wrong, we need to ignore the next kex packet.
432 				 */
433 
434 				ignore_next_kex_packet = true;
435 			}
436 
437 			if (kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha1"))
438 			{
439 				if (kxs.dhgexParameters.getMin_group_len() == 0)
440 				{
441 					PacketKexDhGexRequestOld dhgexreq = new PacketKexDhGexRequestOld(kxs.dhgexParameters);
442 					tm.sendKexMessage(dhgexreq.getPayload());
443 
444 				}
445 				else
446 				{
447 					PacketKexDhGexRequest dhgexreq = new PacketKexDhGexRequest(kxs.dhgexParameters);
448 					tm.sendKexMessage(dhgexreq.getPayload());
449 				}
450 				kxs.state = 1;
451 				return;
452 			}
453 
454 			if (kxs.np.kex_algo.equals("diffie-hellman-group1-sha1")
455 					|| kxs.np.kex_algo.equals("diffie-hellman-group14-sha1"))
456 			{
457 				kxs.dhx = new DhExchange();
458 
459 				if (kxs.np.kex_algo.equals("diffie-hellman-group1-sha1"))
460 					kxs.dhx.init(1, rnd);
461 				else
462 					kxs.dhx.init(14, rnd);
463 
464 				PacketKexDHInit kp = new PacketKexDHInit(kxs.dhx.getE());
465 				tm.sendKexMessage(kp.getPayload());
466 				kxs.state = 1;
467 				return;
468 			}
469 
470 			throw new IllegalStateException("Unkown KEX method!");
471 		}
472 
473 		if (msg[0] == Packets.SSH_MSG_NEWKEYS)
474 		{
475 			if (km == null)
476 				throw new IOException("Peer sent SSH_MSG_NEWKEYS, but I have no key material ready!");
477 
478 			BlockCipher cbc;
479 			MAC mac;
480 
481 			try
482 			{
483 				cbc = BlockCipherFactory.createCipher(kxs.np.enc_algo_server_to_client, false,
484 						km.enc_key_server_to_client, km.initial_iv_server_to_client);
485 
486 				mac = new MAC(kxs.np.mac_algo_server_to_client, km.integrity_key_server_to_client);
487 
488 			}
489 			catch (IllegalArgumentException e1)
490 			{
491 				throw new IOException("Fatal error during MAC startup!");
492 			}
493 
494 			tm.changeRecvCipher(cbc, mac);
495 
496 			ConnectionInfo sci = new ConnectionInfo();
497 
498 			kexCount++;
499 
500 			sci.keyExchangeAlgorithm = kxs.np.kex_algo;
501 			sci.keyExchangeCounter = kexCount;
502 			sci.clientToServerCryptoAlgorithm = kxs.np.enc_algo_client_to_server;
503 			sci.serverToClientCryptoAlgorithm = kxs.np.enc_algo_server_to_client;
504 			sci.clientToServerMACAlgorithm = kxs.np.mac_algo_client_to_server;
505 			sci.serverToClientMACAlgorithm = kxs.np.mac_algo_server_to_client;
506 			sci.serverHostKeyAlgorithm = kxs.np.server_host_key_algo;
507 			sci.serverHostKey = kxs.hostkey;
508 
509 			synchronized (accessLock)
510 			{
511 				lastConnInfo = sci;
512 				accessLock.notifyAll();
513 			}
514 
515 			kxs = null;
516 			return;
517 		}
518 
519 		if ((kxs == null) || (kxs.state == 0))
520 			throw new IOException("Unexpected Kex submessage!");
521 
522 		if (kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha1"))
523 		{
524 			if (kxs.state == 1)
525 			{
526 				PacketKexDhGexGroup dhgexgrp = new PacketKexDhGexGroup(msg, 0, msglen);
527 				kxs.dhgx = new DhGroupExchange(dhgexgrp.getP(), dhgexgrp.getG());
528 				kxs.dhgx.init(rnd);
529 				PacketKexDhGexInit dhgexinit = new PacketKexDhGexInit(kxs.dhgx.getE());
530 				tm.sendKexMessage(dhgexinit.getPayload());
531 				kxs.state = 2;
532 				return;
533 			}
534 
535 			if (kxs.state == 2)
536 			{
537 				PacketKexDhGexReply dhgexrpl = new PacketKexDhGexReply(msg, 0, msglen);
538 
539 				kxs.hostkey = dhgexrpl.getHostKey();
540 
541 				if (verifier != null)
542 				{
543 					boolean vres = false;
544 
545 					try
546 					{
547 						vres = verifier.verifyServerHostKey(hostname, port, kxs.np.server_host_key_algo, kxs.hostkey);
548 					}
549 					catch (Exception e)
550 					{
551 						throw (IOException) new IOException(
552 								"The server hostkey was not accepted by the verifier callback.").initCause(e);
553 					}
554 
555 					if (vres == false)
556 						throw new IOException("The server hostkey was not accepted by the verifier callback");
557 				}
558 
559 				kxs.dhgx.setF(dhgexrpl.getF());
560 
561 				try
562 				{
563 					kxs.H = kxs.dhgx.calculateH(csh.getClientString(), csh.getServerString(),
564 							kxs.localKEX.getPayload(), kxs.remoteKEX.getPayload(), dhgexrpl.getHostKey(),
565 							kxs.dhgexParameters);
566 				}
567 				catch (IllegalArgumentException e)
568 				{
569 					throw (IOException) new IOException("KEX error.").initCause(e);
570 				}
571 
572 				boolean res = verifySignature(dhgexrpl.getSignature(), kxs.hostkey);
573 
574 				if (res == false)
575 					throw new IOException("Hostkey signature sent by remote is wrong!");
576 
577 				kxs.K = kxs.dhgx.getK();
578 
579 				finishKex();
580 				kxs.state = -1;
581 				return;
582 			}
583 
584 			throw new IllegalStateException("Illegal State in KEX Exchange!");
585 		}
586 
587 		if (kxs.np.kex_algo.equals("diffie-hellman-group1-sha1")
588 				|| kxs.np.kex_algo.equals("diffie-hellman-group14-sha1"))
589 		{
590 			if (kxs.state == 1)
591 			{
592 
593 				PacketKexDHReply dhr = new PacketKexDHReply(msg, 0, msglen);
594 
595 				kxs.hostkey = dhr.getHostKey();
596 
597 				if (verifier != null)
598 				{
599 					boolean vres = false;
600 
601 					try
602 					{
603 						vres = verifier.verifyServerHostKey(hostname, port, kxs.np.server_host_key_algo, kxs.hostkey);
604 					}
605 					catch (Exception e)
606 					{
607 						throw (IOException) new IOException(
608 								"The server hostkey was not accepted by the verifier callback.").initCause(e);
609 					}
610 
611 					if (vres == false)
612 						throw new IOException("The server hostkey was not accepted by the verifier callback");
613 				}
614 
615 				kxs.dhx.setF(dhr.getF());
616 
617 				try
618 				{
619 					kxs.H = kxs.dhx.calculateH(csh.getClientString(), csh.getServerString(), kxs.localKEX.getPayload(),
620 							kxs.remoteKEX.getPayload(), dhr.getHostKey());
621 				}
622 				catch (IllegalArgumentException e)
623 				{
624 					throw (IOException) new IOException("KEX error.").initCause(e);
625 				}
626 
627 				boolean res = verifySignature(dhr.getSignature(), kxs.hostkey);
628 
629 				if (res == false)
630 					throw new IOException("Hostkey signature sent by remote is wrong!");
631 
632 				kxs.K = kxs.dhx.getK();
633 
634 				finishKex();
635 				kxs.state = -1;
636 				return;
637 			}
638 		}
639 
640 		throw new IllegalStateException("Unkown KEX method! (" + kxs.np.kex_algo + ")");
641 	}
642 }
643