From 2f5f3754dce85212a71138fd80c2300b73461908 Mon Sep 17 00:00:00 2001 From: Kenny Root Date: Sat, 18 Jul 2015 11:30:18 -0700 Subject: Rename project to sshlib --- .../trilead/ssh2/transport/ClientServerHello.java | 125 ++++ .../com/trilead/ssh2/transport/KexManager.java | 690 +++++++++++++++++++ .../com/trilead/ssh2/transport/KexParameters.java | 24 + .../java/com/trilead/ssh2/transport/KexState.java | 33 + .../com/trilead/ssh2/transport/MessageHandler.java | 14 + .../trilead/ssh2/transport/NegotiateException.java | 12 + .../ssh2/transport/NegotiatedParameters.java | 22 + .../ssh2/transport/TransportConnection.java | 343 ++++++++++ .../trilead/ssh2/transport/TransportManager.java | 750 +++++++++++++++++++++ 9 files changed, 2013 insertions(+) create mode 100644 sshlib/src/main/java/com/trilead/ssh2/transport/ClientServerHello.java create mode 100644 sshlib/src/main/java/com/trilead/ssh2/transport/KexManager.java create mode 100644 sshlib/src/main/java/com/trilead/ssh2/transport/KexParameters.java create mode 100644 sshlib/src/main/java/com/trilead/ssh2/transport/KexState.java create mode 100644 sshlib/src/main/java/com/trilead/ssh2/transport/MessageHandler.java create mode 100644 sshlib/src/main/java/com/trilead/ssh2/transport/NegotiateException.java create mode 100644 sshlib/src/main/java/com/trilead/ssh2/transport/NegotiatedParameters.java create mode 100644 sshlib/src/main/java/com/trilead/ssh2/transport/TransportConnection.java create mode 100644 sshlib/src/main/java/com/trilead/ssh2/transport/TransportManager.java (limited to 'sshlib/src/main/java/com/trilead/ssh2/transport') diff --git a/sshlib/src/main/java/com/trilead/ssh2/transport/ClientServerHello.java b/sshlib/src/main/java/com/trilead/ssh2/transport/ClientServerHello.java new file mode 100644 index 0000000..d7a5ee5 --- /dev/null +++ b/sshlib/src/main/java/com/trilead/ssh2/transport/ClientServerHello.java @@ -0,0 +1,125 @@ + +package com.trilead.ssh2.transport; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + +import com.trilead.ssh2.Connection; + +/** + * ClientServerHello. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: ClientServerHello.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class ClientServerHello +{ + String server_line; + String client_line; + + String server_versioncomment; + + public final static int readLineRN(InputStream is, byte[] buffer) throws IOException + { + int pos = 0; + boolean need10 = false; + int len = 0; + while (true) + { + int c = is.read(); + if (c == -1) + throw new IOException("Premature connection close"); + + buffer[pos++] = (byte) c; + + if (c == 13) + { + need10 = true; + continue; + } + + if (c == 10) + break; + + if (need10 == true) + throw new IOException("Malformed line sent by the server, the line does not end correctly."); + + len++; + if (pos >= buffer.length) + throw new IOException("The server sent a too long line."); + } + + return len; + } + + public ClientServerHello(InputStream bi, OutputStream bo) throws IOException + { + client_line = "SSH-2.0-" + Connection.identification; + + bo.write((client_line + "\r\n").getBytes("ISO-8859-1")); + bo.flush(); + + byte[] serverVersion = new byte[512]; + + for (int i = 0; i < 50; i++) + { + int len = readLineRN(bi, serverVersion); + + server_line = new String(serverVersion, 0, len, "ISO-8859-1"); + + if (server_line.startsWith("SSH-")) + break; + } + + if (server_line.startsWith("SSH-") == false) + throw new IOException( + "Malformed server identification string. There was no line starting with 'SSH-' amongst the first 50 lines."); + + if (server_line.startsWith("SSH-1.99-")) + server_versioncomment = server_line.substring(9); + else if (server_line.startsWith("SSH-2.0-")) + server_versioncomment = server_line.substring(8); + else + throw new IOException("Server uses incompatible protocol, it is not SSH-2 compatible."); + } + + /** + * @return Returns the client_versioncomment. + */ + public byte[] getClientString() + { + byte[] result; + + try + { + result = client_line.getBytes("ISO-8859-1"); + } + catch (UnsupportedEncodingException ign) + { + result = client_line.getBytes(); + } + + return result; + } + + /** + * @return Returns the server_versioncomment. + */ + public byte[] getServerString() + { + byte[] result; + + try + { + result = server_line.getBytes("ISO-8859-1"); + } + catch (UnsupportedEncodingException ign) + { + result = server_line.getBytes(); + } + + return result; + } +} diff --git a/sshlib/src/main/java/com/trilead/ssh2/transport/KexManager.java b/sshlib/src/main/java/com/trilead/ssh2/transport/KexManager.java new file mode 100644 index 0000000..ee0784a --- /dev/null +++ b/sshlib/src/main/java/com/trilead/ssh2/transport/KexManager.java @@ -0,0 +1,690 @@ + +package com.trilead.ssh2.transport; + +import java.io.IOException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPublicKey; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.TreeSet; + +import com.trilead.ssh2.ConnectionInfo; +import com.trilead.ssh2.DHGexParameters; +import com.trilead.ssh2.ServerHostKeyVerifier; +import com.trilead.ssh2.compression.CompressionFactory; +import com.trilead.ssh2.compression.ICompressor; +import com.trilead.ssh2.crypto.CryptoWishList; +import com.trilead.ssh2.crypto.KeyMaterial; +import com.trilead.ssh2.crypto.cipher.BlockCipher; +import com.trilead.ssh2.crypto.cipher.BlockCipherFactory; +import com.trilead.ssh2.crypto.dh.DhGroupExchange; +import com.trilead.ssh2.crypto.dh.GenericDhExchange; +import com.trilead.ssh2.crypto.digest.MAC; +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.PacketKexDHInit; +import com.trilead.ssh2.packets.PacketKexDHReply; +import com.trilead.ssh2.packets.PacketKexDhGexGroup; +import com.trilead.ssh2.packets.PacketKexDhGexInit; +import com.trilead.ssh2.packets.PacketKexDhGexReply; +import com.trilead.ssh2.packets.PacketKexDhGexRequest; +import com.trilead.ssh2.packets.PacketKexDhGexRequestOld; +import com.trilead.ssh2.packets.PacketKexInit; +import com.trilead.ssh2.packets.PacketNewKeys; +import com.trilead.ssh2.packets.Packets; +import com.trilead.ssh2.signature.DSASHA1Verify; +import com.trilead.ssh2.signature.ECDSASHA2Verify; +import com.trilead.ssh2.signature.RSASHA1Verify; + + +/** + * KexManager. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: KexManager.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class KexManager +{ + private static final Logger log = Logger.getLogger(KexManager.class); + + private static final boolean supportsEc; + static { + KeyFactory keyFact; + try { + keyFact = KeyFactory.getInstance("EC"); + } catch (NoSuchAlgorithmException ignored) { + keyFact = null; + log.log(10, "Disabling EC support due to lack of KeyFactory"); + } + supportsEc = keyFact != null; + } + + private static final Set HOSTKEY_ALGS = new LinkedHashSet(); + static { + if (supportsEc) { + HOSTKEY_ALGS.add("ecdsa-sha2-nistp256"); + HOSTKEY_ALGS.add("ecdsa-sha2-nistp384"); + HOSTKEY_ALGS.add("ecdsa-sha2-nistp521"); + } + HOSTKEY_ALGS.add("ssh-rsa"); + HOSTKEY_ALGS.add("ssh-dss"); + } + + private static final Set KEX_ALGS = new LinkedHashSet(); + static { + if (supportsEc) { + KEX_ALGS.add("ecdh-sha2-nistp256"); + KEX_ALGS.add("ecdh-sha2-nistp384"); + KEX_ALGS.add("ecdh-sha2-nistp521"); + } + KEX_ALGS.add("diffie-hellman-group-exchange-sha256"); + KEX_ALGS.add("diffie-hellman-group-exchange-sha1"); + KEX_ALGS.add("diffie-hellman-group14-sha1"); + KEX_ALGS.add("diffie-hellman-group1-sha1"); + } + + KexState kxs; + int kexCount = 0; + KeyMaterial km; + byte[] sessionId; + ClientServerHello csh; + + final Object accessLock = new Object(); + ConnectionInfo lastConnInfo = null; + + boolean connectionClosed = false; + + boolean ignore_next_kex_packet = false; + + final TransportManager tm; + + CryptoWishList nextKEXcryptoWishList; + DHGexParameters nextKEXdhgexParameters; + + ServerHostKeyVerifier verifier; + final String hostname; + final int port; + final SecureRandom rnd; + + public KexManager(TransportManager tm, ClientServerHello csh, CryptoWishList initialCwl, String hostname, int port, + ServerHostKeyVerifier keyVerifier, SecureRandom rnd) + { + this.tm = tm; + this.csh = csh; + this.nextKEXcryptoWishList = initialCwl; + this.nextKEXdhgexParameters = new DHGexParameters(); + this.hostname = hostname; + this.port = port; + this.verifier = keyVerifier; + this.rnd = rnd; + } + + public ConnectionInfo getOrWaitForConnectionInfo(int minKexCount) throws IOException + { + synchronized (accessLock) + { + while (true) + { + if ((lastConnInfo != null) && (lastConnInfo.keyExchangeCounter >= minKexCount)) + return lastConnInfo; + + if (connectionClosed) + throw (IOException) new IOException("Key exchange was not finished, connection is closed.") + .initCause(tm.getReasonClosedCause()); + + try + { + accessLock.wait(); + } + catch (InterruptedException e) + { + } + } + } + } + + private String getFirstMatch(String[] client, String[] server) throws NegotiateException + { + if (client == null || server == null) + throw new IllegalArgumentException(); + + if (client.length == 0) + return null; + + for (int i = 0; i < client.length; i++) + { + for (int j = 0; j < server.length; j++) + { + if (client[i].equals(server[j])) + return client[i]; + } + } + throw new NegotiateException(); + } + + private boolean compareFirstOfNameList(String[] a, String[] b) + { + if (a == null || b == null) + throw new IllegalArgumentException(); + + if ((a.length == 0) && (b.length == 0)) + return true; + + if ((a.length == 0) || (b.length == 0)) + return false; + + return (a[0].equals(b[0])); + } + + private boolean isGuessOK(KexParameters cpar, KexParameters spar) + { + if (cpar == null || spar == null) + throw new IllegalArgumentException(); + + if (compareFirstOfNameList(cpar.kex_algorithms, spar.kex_algorithms) == false) + { + return false; + } + + if (compareFirstOfNameList(cpar.server_host_key_algorithms, spar.server_host_key_algorithms) == false) + { + return false; + } + + /* + * We do NOT check here if the other algorithms can be agreed on, this + * is just a check if kex_algorithms and server_host_key_algorithms were + * guessed right! + */ + + return true; + } + + private NegotiatedParameters mergeKexParameters(KexParameters client, KexParameters server) + { + NegotiatedParameters np = new NegotiatedParameters(); + + try + { + np.kex_algo = getFirstMatch(client.kex_algorithms, server.kex_algorithms); + + log.log(20, "kex_algo=" + np.kex_algo); + + np.server_host_key_algo = getFirstMatch(client.server_host_key_algorithms, + server.server_host_key_algorithms); + + log.log(20, "server_host_key_algo=" + np.server_host_key_algo); + + np.enc_algo_client_to_server = getFirstMatch(client.encryption_algorithms_client_to_server, + server.encryption_algorithms_client_to_server); + np.enc_algo_server_to_client = getFirstMatch(client.encryption_algorithms_server_to_client, + server.encryption_algorithms_server_to_client); + + log.log(20, "enc_algo_client_to_server=" + np.enc_algo_client_to_server); + log.log(20, "enc_algo_server_to_client=" + np.enc_algo_server_to_client); + + np.mac_algo_client_to_server = getFirstMatch(client.mac_algorithms_client_to_server, + server.mac_algorithms_client_to_server); + np.mac_algo_server_to_client = getFirstMatch(client.mac_algorithms_server_to_client, + server.mac_algorithms_server_to_client); + + log.log(20, "mac_algo_client_to_server=" + np.mac_algo_client_to_server); + log.log(20, "mac_algo_server_to_client=" + np.mac_algo_server_to_client); + + np.comp_algo_client_to_server = getFirstMatch(client.compression_algorithms_client_to_server, + server.compression_algorithms_client_to_server); + np.comp_algo_server_to_client = getFirstMatch(client.compression_algorithms_server_to_client, + server.compression_algorithms_server_to_client); + + log.log(20, "comp_algo_client_to_server=" + np.comp_algo_client_to_server); + log.log(20, "comp_algo_server_to_client=" + np.comp_algo_server_to_client); + + } + catch (NegotiateException e) + { + return null; + } + + try + { + np.lang_client_to_server = getFirstMatch(client.languages_client_to_server, + server.languages_client_to_server); + } + catch (NegotiateException e1) + { + np.lang_client_to_server = null; + } + + try + { + np.lang_server_to_client = getFirstMatch(client.languages_server_to_client, + server.languages_server_to_client); + } + catch (NegotiateException e2) + { + np.lang_server_to_client = null; + } + + if (isGuessOK(client, server)) + np.guessOK = true; + + return np; + } + + public synchronized void initiateKEX(CryptoWishList cwl, DHGexParameters dhgex) throws IOException + { + nextKEXcryptoWishList = cwl; + nextKEXdhgexParameters = dhgex; + + if (kxs == null) + { + kxs = new KexState(); + + kxs.dhgexParameters = nextKEXdhgexParameters; + PacketKexInit kp = new PacketKexInit(nextKEXcryptoWishList); + kxs.localKEX = kp; + tm.sendKexMessage(kp.getPayload()); + } + } + + private boolean establishKeyMaterial() + { + try + { + int mac_cs_key_len = MAC.getKeyLen(kxs.np.mac_algo_client_to_server); + int enc_cs_key_len = BlockCipherFactory.getKeySize(kxs.np.enc_algo_client_to_server); + int enc_cs_block_len = BlockCipherFactory.getBlockSize(kxs.np.enc_algo_client_to_server); + + int mac_sc_key_len = MAC.getKeyLen(kxs.np.mac_algo_server_to_client); + int enc_sc_key_len = BlockCipherFactory.getKeySize(kxs.np.enc_algo_server_to_client); + int enc_sc_block_len = BlockCipherFactory.getBlockSize(kxs.np.enc_algo_server_to_client); + + km = KeyMaterial.create(kxs.hashAlgo, kxs.H, kxs.K, sessionId, enc_cs_key_len, enc_cs_block_len, mac_cs_key_len, + enc_sc_key_len, enc_sc_block_len, mac_sc_key_len); + } + catch (IllegalArgumentException e) + { + return false; + } + return true; + } + + private void finishKex() throws IOException + { + if (sessionId == null) + sessionId = kxs.H; + + establishKeyMaterial(); + + /* Tell the other side that we start using the new material */ + + PacketNewKeys ign = new PacketNewKeys(); + tm.sendKexMessage(ign.getPayload()); + + BlockCipher cbc; + MAC mac; + ICompressor comp; + + try + { + cbc = BlockCipherFactory.createCipher(kxs.np.enc_algo_client_to_server, true, km.enc_key_client_to_server, + km.initial_iv_client_to_server); + + mac = new MAC(kxs.np.mac_algo_client_to_server, km.integrity_key_client_to_server); + + comp = CompressionFactory.createCompressor(kxs.np.comp_algo_client_to_server); + + } + catch (IllegalArgumentException e1) + { + throw new IOException("Fatal error during MAC startup!"); + } + + tm.changeSendCipher(cbc, mac); + tm.changeSendCompression(comp); + tm.kexFinished(); + } + + public static final String[] getDefaultServerHostkeyAlgorithmList() + { + return HOSTKEY_ALGS.toArray(new String[HOSTKEY_ALGS.size()]); + } + + public static final void checkServerHostkeyAlgorithmsList(String[] algos) + { + for (int i = 0; i < algos.length; i++) + { + if (!HOSTKEY_ALGS.contains(algos[i])) + throw new IllegalArgumentException("Unknown server host key algorithm '" + algos[i] + "'"); + } + } + + public static final String[] getDefaultKexAlgorithmList() + { + return KEX_ALGS.toArray(new String[KEX_ALGS.size()]); + } + + public static final void checkKexAlgorithmList(String[] algos) + { + for (int i = 0; i < algos.length; i++) + { + if (!KEX_ALGS.contains(algos[i])) + throw new IllegalArgumentException("Unknown kex algorithm '" + algos[i] + "'"); + } + } + + private boolean verifySignature(byte[] sig, byte[] hostkey) throws IOException + { + if (kxs.np.server_host_key_algo.startsWith("ecdsa-sha2-")) + { + byte[] rs = ECDSASHA2Verify.decodeSSHECDSASignature(sig); + ECPublicKey epk = ECDSASHA2Verify.decodeSSHECDSAPublicKey(hostkey); + + log.log(50, "Verifying ecdsa signature"); + + return ECDSASHA2Verify.verifySignature(kxs.H, rs, epk); + } + + if (kxs.np.server_host_key_algo.equals("ssh-rsa")) + { + byte[] rs = RSASHA1Verify.decodeSSHRSASignature(sig); + RSAPublicKey rpk = RSASHA1Verify.decodeSSHRSAPublicKey(hostkey); + + log.log(50, "Verifying ssh-rsa signature"); + + return RSASHA1Verify.verifySignature(kxs.H, rs, rpk); + } + + if (kxs.np.server_host_key_algo.equals("ssh-dss")) + { + byte[] ds = DSASHA1Verify.decodeSSHDSASignature(sig); + DSAPublicKey dpk = DSASHA1Verify.decodeSSHDSAPublicKey(hostkey); + + log.log(50, "Verifying ssh-dss signature"); + + return DSASHA1Verify.verifySignature(kxs.H, ds, dpk); + } + + throw new IOException("Unknown server host key algorithm '" + kxs.np.server_host_key_algo + "'"); + } + + public synchronized void handleMessage(byte[] msg, int msglen) throws IOException + { + PacketKexInit kip; + + if (msg == null) + { + synchronized (accessLock) + { + connectionClosed = true; + accessLock.notifyAll(); + return; + } + } + + if ((kxs == null) && (msg[0] != Packets.SSH_MSG_KEXINIT)) + throw new IOException("Unexpected KEX message (type " + msg[0] + ")"); + + if (ignore_next_kex_packet) + { + ignore_next_kex_packet = false; + return; + } + + if (msg[0] == Packets.SSH_MSG_KEXINIT) + { + if ((kxs != null) && (kxs.state != 0)) + throw new IOException("Unexpected SSH_MSG_KEXINIT message during on-going kex exchange!"); + + if (kxs == null) + { + /* + * Ah, OK, peer wants to do KEX. Let's be nice and play + * together. + */ + kxs = new KexState(); + kxs.dhgexParameters = nextKEXdhgexParameters; + kip = new PacketKexInit(nextKEXcryptoWishList); + kxs.localKEX = kip; + tm.sendKexMessage(kip.getPayload()); + } + + kip = new PacketKexInit(msg, 0, msglen); + kxs.remoteKEX = kip; + + kxs.np = mergeKexParameters(kxs.localKEX.getKexParameters(), kxs.remoteKEX.getKexParameters()); + + if (kxs.np == null) + throw new IOException("Cannot negotiate, proposals do not match."); + + if (kxs.remoteKEX.isFirst_kex_packet_follows() && (kxs.np.guessOK == false)) + { + /* + * Guess was wrong, we need to ignore the next kex packet. + */ + + ignore_next_kex_packet = true; + } + + if (kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha1") + || kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha256")) + { + if (kxs.dhgexParameters.getMin_group_len() == 0 || csh.server_versioncomment.matches("OpenSSH_2\\.([0-4]\\.|5\\.[0-2]).*")) + { + PacketKexDhGexRequestOld dhgexreq = new PacketKexDhGexRequestOld(kxs.dhgexParameters); + tm.sendKexMessage(dhgexreq.getPayload()); + } + else + { + PacketKexDhGexRequest dhgexreq = new PacketKexDhGexRequest(kxs.dhgexParameters); + tm.sendKexMessage(dhgexreq.getPayload()); + } + if (kxs.np.kex_algo.endsWith("sha1")) { + kxs.hashAlgo = "SHA1"; + } else { + kxs.hashAlgo = "SHA-256"; + } + kxs.state = 1; + return; + } + + if (kxs.np.kex_algo.equals("diffie-hellman-group1-sha1") + || kxs.np.kex_algo.equals("diffie-hellman-group14-sha1") + || kxs.np.kex_algo.equals("ecdh-sha2-nistp256") + || kxs.np.kex_algo.equals("ecdh-sha2-nistp384") + || kxs.np.kex_algo.equals("ecdh-sha2-nistp521")) { + kxs.dhx = GenericDhExchange.getInstance(kxs.np.kex_algo); + + kxs.dhx.init(kxs.np.kex_algo); + kxs.hashAlgo = kxs.dhx.getHashAlgo(); + + PacketKexDHInit kp = new PacketKexDHInit(kxs.dhx.getE()); + tm.sendKexMessage(kp.getPayload()); + kxs.state = 1; + return; + } + + throw new IllegalStateException("Unknown KEX method!"); + } + + if (msg[0] == Packets.SSH_MSG_NEWKEYS) + { + if (km == null) + throw new IOException("Peer sent SSH_MSG_NEWKEYS, but I have no key material ready!"); + + BlockCipher cbc; + MAC mac; + ICompressor comp; + + try + { + cbc = BlockCipherFactory.createCipher(kxs.np.enc_algo_server_to_client, false, + km.enc_key_server_to_client, km.initial_iv_server_to_client); + + mac = new MAC(kxs.np.mac_algo_server_to_client, km.integrity_key_server_to_client); + + comp = CompressionFactory.createCompressor(kxs.np.comp_algo_server_to_client); + } + catch (IllegalArgumentException e1) + { + throw new IOException("Fatal error during MAC startup!"); + } + + tm.changeRecvCipher(cbc, mac); + tm.changeRecvCompression(comp); + + ConnectionInfo sci = new ConnectionInfo(); + + kexCount++; + + sci.keyExchangeAlgorithm = kxs.np.kex_algo; + sci.keyExchangeCounter = kexCount; + sci.clientToServerCryptoAlgorithm = kxs.np.enc_algo_client_to_server; + sci.serverToClientCryptoAlgorithm = kxs.np.enc_algo_server_to_client; + sci.clientToServerMACAlgorithm = kxs.np.mac_algo_client_to_server; + sci.serverToClientMACAlgorithm = kxs.np.mac_algo_server_to_client; + sci.serverHostKeyAlgorithm = kxs.np.server_host_key_algo; + sci.serverHostKey = kxs.hostkey; + + synchronized (accessLock) + { + lastConnInfo = sci; + accessLock.notifyAll(); + } + + kxs = null; + return; + } + + if ((kxs == null) || (kxs.state == 0)) + throw new IOException("Unexpected Kex submessage!"); + + if (kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha1") + || kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha256")) + { + if (kxs.state == 1) + { + PacketKexDhGexGroup dhgexgrp = new PacketKexDhGexGroup(msg, 0, msglen); + kxs.dhgx = new DhGroupExchange(dhgexgrp.getP(), dhgexgrp.getG()); + kxs.dhgx.init(rnd); + PacketKexDhGexInit dhgexinit = new PacketKexDhGexInit(kxs.dhgx.getE()); + tm.sendKexMessage(dhgexinit.getPayload()); + kxs.state = 2; + return; + } + + if (kxs.state == 2) + { + PacketKexDhGexReply dhgexrpl = new PacketKexDhGexReply(msg, 0, msglen); + + kxs.hostkey = dhgexrpl.getHostKey(); + + if (verifier != null) + { + boolean vres = false; + + try + { + vres = verifier.verifyServerHostKey(hostname, port, kxs.np.server_host_key_algo, kxs.hostkey); + } + catch (Exception e) + { + throw (IOException) new IOException( + "The server hostkey was not accepted by the verifier callback.").initCause(e); + } + + if (vres == false) + throw new IOException("The server hostkey was not accepted by the verifier callback"); + } + + kxs.dhgx.setF(dhgexrpl.getF()); + + try + { + kxs.H = kxs.dhgx.calculateH(kxs.hashAlgo, + csh.getClientString(), csh.getServerString(), + kxs.localKEX.getPayload(), kxs.remoteKEX.getPayload(), + dhgexrpl.getHostKey(), kxs.dhgexParameters); + } + catch (IllegalArgumentException e) + { + throw (IOException) new IOException("KEX error.").initCause(e); + } + + boolean res = verifySignature(dhgexrpl.getSignature(), kxs.hostkey); + + if (res == false) + throw new IOException("Hostkey signature sent by remote is wrong!"); + + kxs.K = kxs.dhgx.getK(); + + finishKex(); + kxs.state = -1; + return; + } + + throw new IllegalStateException("Illegal State in KEX Exchange!"); + } + + if (kxs.np.kex_algo.equals("diffie-hellman-group1-sha1") + || kxs.np.kex_algo.equals("diffie-hellman-group14-sha1") + || kxs.np.kex_algo.equals("ecdh-sha2-nistp256") + || kxs.np.kex_algo.equals("ecdh-sha2-nistp384") + || kxs.np.kex_algo.equals("ecdh-sha2-nistp521")) + { + if (kxs.state == 1) + { + + PacketKexDHReply dhr = new PacketKexDHReply(msg, 0, msglen); + + kxs.hostkey = dhr.getHostKey(); + + if (verifier != null) + { + boolean vres = false; + + try + { + vres = verifier.verifyServerHostKey(hostname, port, kxs.np.server_host_key_algo, kxs.hostkey); + } + catch (Exception e) + { + throw (IOException) new IOException( + "The server hostkey was not accepted by the verifier callback.").initCause(e); + } + + if (vres == false) + throw new IOException("The server hostkey was not accepted by the verifier callback"); + } + + kxs.dhx.setF(dhr.getF()); + + try + { + kxs.H = kxs.dhx.calculateH(csh.getClientString(), csh.getServerString(), kxs.localKEX.getPayload(), + kxs.remoteKEX.getPayload(), dhr.getHostKey()); + } + catch (IllegalArgumentException e) + { + throw (IOException) new IOException("KEX error.").initCause(e); + } + + boolean res = verifySignature(dhr.getSignature(), kxs.hostkey); + + if (res == false) + throw new IOException("Hostkey signature sent by remote is wrong!"); + + kxs.K = kxs.dhx.getK(); + + finishKex(); + kxs.state = -1; + return; + } + } + + throw new IllegalStateException("Unkown KEX method! (" + kxs.np.kex_algo + ")"); + } +} diff --git a/sshlib/src/main/java/com/trilead/ssh2/transport/KexParameters.java b/sshlib/src/main/java/com/trilead/ssh2/transport/KexParameters.java new file mode 100644 index 0000000..70bcf3e --- /dev/null +++ b/sshlib/src/main/java/com/trilead/ssh2/transport/KexParameters.java @@ -0,0 +1,24 @@ +package com.trilead.ssh2.transport; + +/** + * KexParameters. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: KexParameters.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class KexParameters +{ + public byte[] cookie; + public String[] kex_algorithms; + public String[] server_host_key_algorithms; + public String[] encryption_algorithms_client_to_server; + public String[] encryption_algorithms_server_to_client; + public String[] mac_algorithms_client_to_server; + public String[] mac_algorithms_server_to_client; + public String[] compression_algorithms_client_to_server; + public String[] compression_algorithms_server_to_client; + public String[] languages_client_to_server; + public String[] languages_server_to_client; + public boolean first_kex_packet_follows; + public int reserved_field1; +} diff --git a/sshlib/src/main/java/com/trilead/ssh2/transport/KexState.java b/sshlib/src/main/java/com/trilead/ssh2/transport/KexState.java new file mode 100644 index 0000000..8611f3f --- /dev/null +++ b/sshlib/src/main/java/com/trilead/ssh2/transport/KexState.java @@ -0,0 +1,33 @@ +package com.trilead.ssh2.transport; + + +import java.math.BigInteger; + +import com.trilead.ssh2.DHGexParameters; +import com.trilead.ssh2.crypto.dh.DhGroupExchange; +import com.trilead.ssh2.crypto.dh.GenericDhExchange; +import com.trilead.ssh2.packets.PacketKexInit; + +/** + * KexState. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: KexState.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $ + */ +public class KexState +{ + public PacketKexInit localKEX; + public PacketKexInit remoteKEX; + public NegotiatedParameters np; + public int state = 0; + + public BigInteger K; + public byte[] H; + + public byte[] hostkey; + + public String hashAlgo; + public GenericDhExchange dhx; + public DhGroupExchange dhgx; + public DHGexParameters dhgexParameters; +} diff --git a/sshlib/src/main/java/com/trilead/ssh2/transport/MessageHandler.java b/sshlib/src/main/java/com/trilead/ssh2/transport/MessageHandler.java new file mode 100644 index 0000000..039d473 --- /dev/null +++ b/sshlib/src/main/java/com/trilead/ssh2/transport/MessageHandler.java @@ -0,0 +1,14 @@ +package com.trilead.ssh2.transport; + +import java.io.IOException; + +/** + * MessageHandler. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: MessageHandler.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public interface MessageHandler +{ + public void handleMessage(byte[] msg, int msglen) throws IOException; +} diff --git a/sshlib/src/main/java/com/trilead/ssh2/transport/NegotiateException.java b/sshlib/src/main/java/com/trilead/ssh2/transport/NegotiateException.java new file mode 100644 index 0000000..ff53097 --- /dev/null +++ b/sshlib/src/main/java/com/trilead/ssh2/transport/NegotiateException.java @@ -0,0 +1,12 @@ +package com.trilead.ssh2.transport; + +/** + * NegotiateException. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: NegotiateException.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class NegotiateException extends Exception +{ + private static final long serialVersionUID = 3689910669428143157L; +} diff --git a/sshlib/src/main/java/com/trilead/ssh2/transport/NegotiatedParameters.java b/sshlib/src/main/java/com/trilead/ssh2/transport/NegotiatedParameters.java new file mode 100644 index 0000000..e9f3a0a --- /dev/null +++ b/sshlib/src/main/java/com/trilead/ssh2/transport/NegotiatedParameters.java @@ -0,0 +1,22 @@ +package com.trilead.ssh2.transport; + +/** + * NegotiatedParameters. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: NegotiatedParameters.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $ + */ +public class NegotiatedParameters +{ + public boolean guessOK; + public String kex_algo; + public String server_host_key_algo; + public String enc_algo_client_to_server; + public String enc_algo_server_to_client; + public String mac_algo_client_to_server; + public String mac_algo_server_to_client; + public String comp_algo_client_to_server; + public String comp_algo_server_to_client; + public String lang_client_to_server; + public String lang_server_to_client; +} diff --git a/sshlib/src/main/java/com/trilead/ssh2/transport/TransportConnection.java b/sshlib/src/main/java/com/trilead/ssh2/transport/TransportConnection.java new file mode 100644 index 0000000..906c3c9 --- /dev/null +++ b/sshlib/src/main/java/com/trilead/ssh2/transport/TransportConnection.java @@ -0,0 +1,343 @@ + +package com.trilead.ssh2.transport; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.SecureRandom; + +import com.trilead.ssh2.compression.ICompressor; +import com.trilead.ssh2.crypto.cipher.BlockCipher; +import com.trilead.ssh2.crypto.cipher.CipherInputStream; +import com.trilead.ssh2.crypto.cipher.CipherOutputStream; +import com.trilead.ssh2.crypto.cipher.NullCipher; +import com.trilead.ssh2.crypto.digest.MAC; +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.Packets; + + +/** + * TransportConnection. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: TransportConnection.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $ + */ +public class TransportConnection +{ + private static final Logger log = Logger.getLogger(TransportConnection.class); + + int send_seq_number = 0; + + int recv_seq_number = 0; + + CipherInputStream cis; + + CipherOutputStream cos; + + boolean useRandomPadding = false; + + /* Depends on current MAC and CIPHER */ + + MAC send_mac; + + byte[] send_mac_buffer; + + int send_padd_blocksize = 8; + + MAC recv_mac; + + byte[] recv_mac_buffer; + + byte[] recv_mac_buffer_cmp; + + int recv_padd_blocksize = 8; + + ICompressor recv_comp = null; + + ICompressor send_comp = null; + + boolean can_recv_compress = false; + + boolean can_send_compress = false; + + byte[] recv_comp_buffer; + + byte[] send_comp_buffer; + + /* won't change */ + + final byte[] send_padding_buffer = new byte[256]; + + final byte[] send_packet_header_buffer = new byte[5]; + + final byte[] recv_padding_buffer = new byte[256]; + + final byte[] recv_packet_header_buffer = new byte[5]; + + boolean recv_packet_header_present = false; + + ClientServerHello csh; + + final SecureRandom rnd; + + public TransportConnection(InputStream is, OutputStream os, SecureRandom rnd) + { + this.cis = new CipherInputStream(new NullCipher(), is); + this.cos = new CipherOutputStream(new NullCipher(), os); + this.rnd = rnd; + } + + public void changeRecvCipher(BlockCipher bc, MAC mac) + { + cis.changeCipher(bc); + recv_mac = mac; + recv_mac_buffer = (mac != null) ? new byte[mac.size()] : null; + recv_mac_buffer_cmp = (mac != null) ? new byte[mac.size()] : null; + recv_padd_blocksize = bc.getBlockSize(); + if (recv_padd_blocksize < 8) + recv_padd_blocksize = 8; + } + + public void changeSendCipher(BlockCipher bc, MAC mac) + { + if ((bc instanceof NullCipher) == false) + { + /* Only use zero byte padding for the first few packets */ + useRandomPadding = true; + /* Once we start encrypting, there is no way back */ + } + + cos.changeCipher(bc); + send_mac = mac; + send_mac_buffer = (mac != null) ? new byte[mac.size()] : null; + send_padd_blocksize = bc.getBlockSize(); + if (send_padd_blocksize < 8) + send_padd_blocksize = 8; + } + + public void changeRecvCompression(ICompressor comp) + { + recv_comp = comp; + + if (comp != null) { + recv_comp_buffer = new byte[comp.getBufferSize()]; + can_recv_compress |= recv_comp.canCompressPreauth(); + } + } + + public void changeSendCompression(ICompressor comp) + { + send_comp = comp; + + if (comp != null) { + send_comp_buffer = new byte[comp.getBufferSize()]; + can_send_compress |= send_comp.canCompressPreauth(); + } + } + + public void sendMessage(byte[] message) throws IOException + { + sendMessage(message, 0, message.length, 0); + } + + public void sendMessage(byte[] message, int off, int len) throws IOException + { + sendMessage(message, off, len, 0); + } + + public int getPacketOverheadEstimate() + { + // return an estimate for the paket overhead (for send operations) + return 5 + 4 + (send_padd_blocksize - 1) + send_mac_buffer.length; + } + + public void sendMessage(byte[] message, int off, int len, int padd) throws IOException + { + if (padd < 4) + padd = 4; + else if (padd > 64) + padd = 64; + + if (send_comp != null && can_send_compress) { + if (send_comp_buffer.length < message.length + 1024) + send_comp_buffer = new byte[message.length + 1024]; + len = send_comp.compress(message, off, len, send_comp_buffer); + message = send_comp_buffer; + } + + int packet_len = 5 + len + padd; /* Minimum allowed padding is 4 */ + + int slack = packet_len % send_padd_blocksize; + + if (slack != 0) + { + packet_len += (send_padd_blocksize - slack); + } + + if (packet_len < 16) + packet_len = 16; + + int padd_len = packet_len - (5 + len); + + if (useRandomPadding) + { + for (int i = 0; i < padd_len; i = i + 4) + { + /* + * don't waste calls to rnd.nextInt() (by using only 8bit of the + * output). just believe me: even though we may write here up to 3 + * bytes which won't be used, there is no "buffer overflow" (i.e., + * arrayindexoutofbounds). the padding buffer is big enough =) (256 + * bytes, and that is bigger than any current cipher block size + 64). + */ + + int r = rnd.nextInt(); + send_padding_buffer[i] = (byte) r; + send_padding_buffer[i + 1] = (byte) (r >> 8); + send_padding_buffer[i + 2] = (byte) (r >> 16); + send_padding_buffer[i + 3] = (byte) (r >> 24); + } + } + else + { + /* use zero padding for unencrypted traffic */ + for (int i = 0; i < padd_len; i++) + send_padding_buffer[i] = 0; + /* Actually this code is paranoid: we never filled any + * bytes into the padding buffer so far, therefore it should + * consist of zeros only. + */ + } + + send_packet_header_buffer[0] = (byte) ((packet_len - 4) >> 24); + send_packet_header_buffer[1] = (byte) ((packet_len - 4) >> 16); + send_packet_header_buffer[2] = (byte) ((packet_len - 4) >> 8); + send_packet_header_buffer[3] = (byte) ((packet_len - 4)); + send_packet_header_buffer[4] = (byte) padd_len; + + cos.write(send_packet_header_buffer, 0, 5); + cos.write(message, off, len); + cos.write(send_padding_buffer, 0, padd_len); + + if (send_mac != null) + { + send_mac.initMac(send_seq_number); + send_mac.update(send_packet_header_buffer, 0, 5); + send_mac.update(message, off, len); + send_mac.update(send_padding_buffer, 0, padd_len); + + send_mac.getMac(send_mac_buffer, 0); + cos.writePlain(send_mac_buffer, 0, send_mac_buffer.length); + } + + cos.flush(); + + if (log.isEnabled()) + { + log.log(90, "Sent " + Packets.getMessageName(message[off] & 0xff) + " " + len + " bytes payload"); + } + + send_seq_number++; + } + + public int peekNextMessageLength() throws IOException + { + if (recv_packet_header_present == false) + { + cis.read(recv_packet_header_buffer, 0, 5); + recv_packet_header_present = true; + } + + int packet_length = ((recv_packet_header_buffer[0] & 0xff) << 24) + | ((recv_packet_header_buffer[1] & 0xff) << 16) | ((recv_packet_header_buffer[2] & 0xff) << 8) + | ((recv_packet_header_buffer[3] & 0xff)); + + int padding_length = recv_packet_header_buffer[4] & 0xff; + + if (packet_length > 35000 || packet_length < 12) + throw new IOException("Illegal packet size! (" + packet_length + ")"); + + int payload_length = packet_length - padding_length - 1; + + if (payload_length < 0) + throw new IOException("Illegal padding_length in packet from remote (" + padding_length + ")"); + + return payload_length; + } + + public int receiveMessage(byte buffer[], int off, int len) throws IOException + { + if (recv_packet_header_present == false) + { + cis.read(recv_packet_header_buffer, 0, 5); + } + else + recv_packet_header_present = false; + + int packet_length = ((recv_packet_header_buffer[0] & 0xff) << 24) + | ((recv_packet_header_buffer[1] & 0xff) << 16) | ((recv_packet_header_buffer[2] & 0xff) << 8) + | ((recv_packet_header_buffer[3] & 0xff)); + + int padding_length = recv_packet_header_buffer[4] & 0xff; + + if (packet_length > 35000 || packet_length < 12) + throw new IOException("Illegal packet size! (" + packet_length + ")"); + + int payload_length = packet_length - padding_length - 1; + + if (payload_length < 0) + throw new IOException("Illegal padding_length in packet from remote (" + padding_length + ")"); + + if (payload_length >= len) + throw new IOException("Receive buffer too small (" + len + ", need " + payload_length + ")"); + + cis.read(buffer, off, payload_length); + cis.read(recv_padding_buffer, 0, padding_length); + + if (recv_mac != null) + { + cis.readPlain(recv_mac_buffer, 0, recv_mac_buffer.length); + + recv_mac.initMac(recv_seq_number); + recv_mac.update(recv_packet_header_buffer, 0, 5); + recv_mac.update(buffer, off, payload_length); + recv_mac.update(recv_padding_buffer, 0, padding_length); + recv_mac.getMac(recv_mac_buffer_cmp, 0); + + for (int i = 0; i < recv_mac_buffer.length; i++) + { + if (recv_mac_buffer[i] != recv_mac_buffer_cmp[i]) + throw new IOException("Remote sent corrupt MAC."); + } + } + + recv_seq_number++; + + if (log.isEnabled()) + { + log.log(90, "Received " + Packets.getMessageName(buffer[off] & 0xff) + " " + payload_length + + " bytes payload"); + } + + if (recv_comp != null && can_recv_compress) { + int[] uncomp_len = new int[] { payload_length }; + buffer = recv_comp.uncompress(buffer, off, uncomp_len); + + if (buffer == null) { + throw new IOException("Error while inflating remote data"); + } else { + return uncomp_len[0]; + } + } else { + return payload_length; + } + } + + /** + * + */ + public void startCompression() { + can_recv_compress = true; + can_send_compress = true; + } +} diff --git a/sshlib/src/main/java/com/trilead/ssh2/transport/TransportManager.java b/sshlib/src/main/java/com/trilead/ssh2/transport/TransportManager.java new file mode 100644 index 0000000..8f3406e --- /dev/null +++ b/sshlib/src/main/java/com/trilead/ssh2/transport/TransportManager.java @@ -0,0 +1,750 @@ + +package com.trilead.ssh2.transport; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.security.SecureRandom; +import java.util.Vector; + +import com.trilead.ssh2.ConnectionInfo; +import com.trilead.ssh2.ConnectionMonitor; +import com.trilead.ssh2.DHGexParameters; +import com.trilead.ssh2.HTTPProxyData; +import com.trilead.ssh2.HTTPProxyException; +import com.trilead.ssh2.ProxyData; +import com.trilead.ssh2.ServerHostKeyVerifier; +import com.trilead.ssh2.compression.ICompressor; +import com.trilead.ssh2.crypto.Base64; +import com.trilead.ssh2.crypto.CryptoWishList; +import com.trilead.ssh2.crypto.cipher.BlockCipher; +import com.trilead.ssh2.crypto.digest.MAC; +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.PacketDisconnect; +import com.trilead.ssh2.packets.Packets; +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.util.Tokenizer; + + +/* + * Yes, the "standard" is a big mess. On one side, the say that arbitary channel + * packets are allowed during kex exchange, on the other side we need to blindly + * ignore the next _packet_ if the KEX guess was wrong. Where do we know from that + * the next packet is not a channel data packet? Yes, we could check if it is in + * the KEX range. But the standard says nothing about this. The OpenSSH guys + * block local "normal" traffic during KEX. That's fine - however, they assume + * that the other side is doing the same. During re-key, if they receive traffic + * other than KEX, they become horribly irritated and kill the connection. Since + * we are very likely going to communicate with OpenSSH servers, we have to play + * the same game - even though we could do better. + * + * btw: having stdout and stderr on the same channel, with a shared window, is + * also a VERY good idea... =( + */ + +/** + * TransportManager. + * + * @author Christian Plattner, plattner@trilead.com + * @version $Id: TransportManager.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $ + */ +public class TransportManager +{ + private static final Logger log = Logger.getLogger(TransportManager.class); + + class HandlerEntry + { + MessageHandler mh; + int low; + int high; + } + + private final Vector asynchronousQueue = new Vector(); + private Thread asynchronousThread = null; + + class AsynchronousWorker extends Thread + { + public void run() + { + while (true) + { + byte[] msg = null; + + synchronized (asynchronousQueue) + { + if (asynchronousQueue.size() == 0) + { + /* After the queue is empty for about 2 seconds, stop this thread */ + + try + { + asynchronousQueue.wait(2000); + } + catch (InterruptedException e) + { + /* OKOK, if somebody interrupts us, then we may die earlier. */ + } + + if (asynchronousQueue.size() == 0) + { + asynchronousThread = null; + return; + } + } + + msg = asynchronousQueue.remove(0); + } + + /* The following invocation may throw an IOException. + * There is no point in handling it - it simply means + * that the connection has a problem and we should stop + * sending asynchronously messages. We do not need to signal that + * we have exited (asynchronousThread = null): further + * messages in the queue cannot be sent by this or any + * other thread. + * Other threads will sooner or later (when receiving or + * sending the next message) get the same IOException and + * get to the same conclusion. + */ + + try + { + sendMessage(msg); + } + catch (IOException e) + { + return; + } + } + } + } + + String hostname; + int port; + final Socket sock = new Socket(); + + Object connectionSemaphore = new Object(); + + boolean flagKexOngoing = false; + boolean connectionClosed = false; + + Throwable reasonClosedCause = null; + + TransportConnection tc; + KexManager km; + + Vector messageHandlers = new Vector(); + + Thread receiveThread; + + Vector connectionMonitors = new Vector(); + boolean monitorsWereInformed = false; + + public TransportManager(String host, int port) throws IOException + { + this.hostname = host; + this.port = port; + } + + public int getPacketOverheadEstimate() + { + return tc.getPacketOverheadEstimate(); + } + + public void setTcpNoDelay(boolean state) throws IOException + { + sock.setTcpNoDelay(state); + } + + public void setSoTimeout(int timeout) throws IOException + { + sock.setSoTimeout(timeout); + } + + public ConnectionInfo getConnectionInfo(int kexNumber) throws IOException + { + return km.getOrWaitForConnectionInfo(kexNumber); + } + + public Throwable getReasonClosedCause() + { + synchronized (connectionSemaphore) + { + return reasonClosedCause; + } + } + + public byte[] getSessionIdentifier() + { + return km.sessionId; + } + + public void close(Throwable cause, boolean useDisconnectPacket) + { + if (useDisconnectPacket == false) + { + /* OK, hard shutdown - do not aquire the semaphore, + * perhaps somebody is inside (and waits until the remote + * side is ready to accept new data). */ + + try + { + sock.close(); + } + catch (IOException ignore) + { + } + + /* OK, whoever tried to send data, should now agree that + * there is no point in further waiting =) + * It is safe now to aquire the semaphore. + */ + } + + synchronized (connectionSemaphore) + { + if (connectionClosed == false) + { + if (useDisconnectPacket == true) + { + try + { + byte[] msg = new PacketDisconnect(Packets.SSH_DISCONNECT_BY_APPLICATION, cause.getMessage(), "") + .getPayload(); + if (tc != null) + tc.sendMessage(msg); + } + catch (IOException ignore) + { + } + + try + { + sock.close(); + } + catch (IOException ignore) + { + } + } + + connectionClosed = true; + reasonClosedCause = cause; /* may be null */ + } + connectionSemaphore.notifyAll(); + } + + /* No check if we need to inform the monitors */ + + Vector monitors = null; + + synchronized (this) + { + /* Short term lock to protect "connectionMonitors" + * and "monitorsWereInformed" + * (they may be modified concurrently) + */ + + if (monitorsWereInformed == false) + { + monitorsWereInformed = true; + monitors = (Vector) connectionMonitors.clone(); + } + } + + if (monitors != null) + { + for (int i = 0; i < monitors.size(); i++) + { + try + { + ConnectionMonitor cmon = (ConnectionMonitor) monitors.elementAt(i); + cmon.connectionLost(reasonClosedCause); + } + catch (Exception ignore) + { + } + } + } + } + + private static void tryAllAddresses(Socket sock, String host, int port, int connectTimeout) throws IOException { + InetAddress[] addresses = InetAddress.getAllByName(host); + for (InetAddress addr : addresses) { + try { + sock.connect(new InetSocketAddress(addr, port), connectTimeout); + return; + } catch (SocketTimeoutException e) { + } + } + throw new SocketTimeoutException("Could not connect; socket timed out"); + } + + private void establishConnection(ProxyData proxyData, int connectTimeout) throws IOException + { + if (proxyData == null) + { + tryAllAddresses(sock, hostname, port, connectTimeout); + sock.setSoTimeout(0); + return; + } + + if (proxyData instanceof HTTPProxyData) + { + HTTPProxyData pd = (HTTPProxyData) proxyData; + + /* At the moment, we only support HTTP proxies */ + + tryAllAddresses(sock, pd.proxyHost, pd.proxyPort, connectTimeout); + sock.setSoTimeout(0); + + /* OK, now tell the proxy where we actually want to connect to */ + + StringBuffer sb = new StringBuffer(); + + sb.append("CONNECT "); + sb.append(hostname); + sb.append(':'); + sb.append(port); + sb.append(" HTTP/1.0\r\n"); + + if ((pd.proxyUser != null) && (pd.proxyPass != null)) + { + String credentials = pd.proxyUser + ":" + pd.proxyPass; + char[] encoded = Base64.encode(credentials.getBytes("ISO-8859-1")); + sb.append("Proxy-Authorization: Basic "); + sb.append(encoded); + sb.append("\r\n"); + } + + if (pd.requestHeaderLines != null) + { + for (int i = 0; i < pd.requestHeaderLines.length; i++) + { + if (pd.requestHeaderLines[i] != null) + { + sb.append(pd.requestHeaderLines[i]); + sb.append("\r\n"); + } + } + } + + sb.append("\r\n"); + + OutputStream out = sock.getOutputStream(); + + out.write(sb.toString().getBytes("ISO-8859-1")); + out.flush(); + + /* Now parse the HTTP response */ + + byte[] buffer = new byte[1024]; + InputStream in = sock.getInputStream(); + + int len = ClientServerHello.readLineRN(in, buffer); + + String httpReponse = new String(buffer, 0, len, "ISO-8859-1"); + + if (httpReponse.startsWith("HTTP/") == false) + throw new IOException("The proxy did not send back a valid HTTP response."); + + /* "HTTP/1.X XYZ X" => 14 characters minimum */ + + if ((httpReponse.length() < 14) || (httpReponse.charAt(8) != ' ') || (httpReponse.charAt(12) != ' ')) + throw new IOException("The proxy did not send back a valid HTTP response."); + + int errorCode = 0; + + try + { + errorCode = Integer.parseInt(httpReponse.substring(9, 12)); + } + catch (NumberFormatException ignore) + { + throw new IOException("The proxy did not send back a valid HTTP response."); + } + + if ((errorCode < 0) || (errorCode > 999)) + throw new IOException("The proxy did not send back a valid HTTP response."); + + if (errorCode != 200) + { + throw new HTTPProxyException(httpReponse.substring(13), errorCode); + } + + /* OK, read until empty line */ + + while (true) + { + len = ClientServerHello.readLineRN(in, buffer); + if (len == 0) + break; + } + return; + } + + throw new IOException("Unsupported ProxyData"); + } + + public void initialize(CryptoWishList cwl, ServerHostKeyVerifier verifier, DHGexParameters dhgex, + int connectTimeout, SecureRandom rnd, ProxyData proxyData) throws IOException + { + /* First, establish the TCP connection to the SSH-2 server */ + + establishConnection(proxyData, connectTimeout); + + /* Parse the server line and say hello - important: this information is later needed for the + * key exchange (to stop man-in-the-middle attacks) - that is why we wrap it into an object + * for later use. + */ + + ClientServerHello csh = new ClientServerHello(sock.getInputStream(), sock.getOutputStream()); + + tc = new TransportConnection(sock.getInputStream(), sock.getOutputStream(), rnd); + + km = new KexManager(this, csh, cwl, hostname, port, verifier, rnd); + km.initiateKEX(cwl, dhgex); + + receiveThread = new Thread(new Runnable() + { + public void run() + { + try + { + receiveLoop(); + } + catch (IOException e) + { + close(e, false); + + if (log.isEnabled()) + log.log(10, "Receive thread: error in receiveLoop: " + e.getMessage()); + } + + if (log.isEnabled()) + log.log(50, "Receive thread: back from receiveLoop"); + + /* Tell all handlers that it is time to say goodbye */ + + if (km != null) + { + try + { + km.handleMessage(null, 0); + } + catch (IOException e) + { + } + } + + for (int i = 0; i < messageHandlers.size(); i++) + { + HandlerEntry he = messageHandlers.elementAt(i); + try + { + he.mh.handleMessage(null, 0); + } + catch (Exception ignore) + { + } + } + } + }); + + receiveThread.setDaemon(true); + receiveThread.start(); + } + + public void registerMessageHandler(MessageHandler mh, int low, int high) + { + HandlerEntry he = new HandlerEntry(); + he.mh = mh; + he.low = low; + he.high = high; + + synchronized (messageHandlers) + { + messageHandlers.addElement(he); + } + } + + public void removeMessageHandler(MessageHandler mh, int low, int high) + { + synchronized (messageHandlers) + { + for (int i = 0; i < messageHandlers.size(); i++) + { + HandlerEntry he = messageHandlers.elementAt(i); + if ((he.mh == mh) && (he.low == low) && (he.high == high)) + { + messageHandlers.removeElementAt(i); + break; + } + } + } + } + + public void sendKexMessage(byte[] msg) throws IOException + { + synchronized (connectionSemaphore) + { + if (connectionClosed) + { + throw (IOException) new IOException("Sorry, this connection is closed.").initCause(reasonClosedCause); + } + + flagKexOngoing = true; + + try + { + tc.sendMessage(msg); + } + catch (IOException e) + { + close(e, false); + throw e; + } + } + } + + public void kexFinished() throws IOException + { + synchronized (connectionSemaphore) + { + flagKexOngoing = false; + connectionSemaphore.notifyAll(); + } + } + + public void forceKeyExchange(CryptoWishList cwl, DHGexParameters dhgex) throws IOException + { + km.initiateKEX(cwl, dhgex); + } + + public void changeRecvCipher(BlockCipher bc, MAC mac) + { + tc.changeRecvCipher(bc, mac); + } + + public void changeSendCipher(BlockCipher bc, MAC mac) + { + tc.changeSendCipher(bc, mac); + } + + /** + * @param comp + */ + public void changeRecvCompression(ICompressor comp) { + tc.changeRecvCompression(comp); + } + + /** + * @param comp + */ + public void changeSendCompression(ICompressor comp) { + tc.changeSendCompression(comp); + } + + /** + * + */ + public void startCompression() { + tc.startCompression(); + } + + public void sendAsynchronousMessage(byte[] msg) throws IOException + { + synchronized (asynchronousQueue) + { + asynchronousQueue.addElement(msg); + + /* This limit should be flexible enough. We need this, otherwise the peer + * can flood us with global requests (and other stuff where we have to reply + * with an asynchronous message) and (if the server just sends data and does not + * read what we send) this will probably put us in a low memory situation + * (our send queue would grow and grow and...) */ + + if (asynchronousQueue.size() > 100) + throw new IOException("Error: the peer is not consuming our asynchronous replies."); + + /* Check if we have an asynchronous sending thread */ + + if (asynchronousThread == null) + { + asynchronousThread = new AsynchronousWorker(); + asynchronousThread.setDaemon(true); + asynchronousThread.start(); + + /* The thread will stop after 2 seconds of inactivity (i.e., empty queue) */ + } + } + } + + public void setConnectionMonitors(Vector monitors) + { + synchronized (this) + { + connectionMonitors = (Vector) monitors.clone(); + } + } + + public void sendMessage(byte[] msg) throws IOException + { + if (Thread.currentThread() == receiveThread) + throw new IOException("Assertion error: sendMessage may never be invoked by the receiver thread!"); + + synchronized (connectionSemaphore) + { + while (true) + { + if (connectionClosed) + { + throw (IOException) new IOException("Sorry, this connection is closed.") + .initCause(reasonClosedCause); + } + + if (flagKexOngoing == false) + break; + + try + { + connectionSemaphore.wait(); + } + catch (InterruptedException e) + { + } + } + + try + { + tc.sendMessage(msg); + } + catch (IOException e) + { + close(e, false); + throw e; + } + } + } + + public void receiveLoop() throws IOException + { + byte[] msg = new byte[35000]; + + while (true) + { + int msglen = tc.receiveMessage(msg, 0, msg.length); + + int type = msg[0] & 0xff; + + if (type == Packets.SSH_MSG_IGNORE) + continue; + + if (type == Packets.SSH_MSG_DEBUG) + { + if (log.isEnabled()) + { + TypesReader tr = new TypesReader(msg, 0, msglen); + tr.readByte(); + tr.readBoolean(); + StringBuffer debugMessageBuffer = new StringBuffer(); + debugMessageBuffer.append(tr.readString("UTF-8")); + + for (int i = 0; i < debugMessageBuffer.length(); i++) + { + char c = debugMessageBuffer.charAt(i); + + if ((c >= 32) && (c <= 126)) + continue; + debugMessageBuffer.setCharAt(i, '\uFFFD'); + } + + log.log(50, "DEBUG Message from remote: '" + debugMessageBuffer.toString() + "'"); + } + continue; + } + + if (type == Packets.SSH_MSG_UNIMPLEMENTED) + { + throw new IOException("Peer sent UNIMPLEMENTED message, that should not happen."); + } + + if (type == Packets.SSH_MSG_DISCONNECT) + { + TypesReader tr = new TypesReader(msg, 0, msglen); + tr.readByte(); + int reason_code = tr.readUINT32(); + StringBuffer reasonBuffer = new StringBuffer(); + reasonBuffer.append(tr.readString("UTF-8")); + + /* + * Do not get fooled by servers that send abnormal long error + * messages + */ + + if (reasonBuffer.length() > 255) + { + reasonBuffer.setLength(255); + reasonBuffer.setCharAt(254, '.'); + reasonBuffer.setCharAt(253, '.'); + reasonBuffer.setCharAt(252, '.'); + } + + /* + * Also, check that the server did not send charcaters that may + * screw up the receiver -> restrict to reasonable US-ASCII + * subset -> "printable characters" (ASCII 32 - 126). Replace + * all others with 0xFFFD (UNICODE replacement character). + */ + + for (int i = 0; i < reasonBuffer.length(); i++) + { + char c = reasonBuffer.charAt(i); + + if ((c >= 32) && (c <= 126)) + continue; + reasonBuffer.setCharAt(i, '\uFFFD'); + } + + throw new IOException("Peer sent DISCONNECT message (reason code " + reason_code + "): " + + reasonBuffer.toString()); + } + + /* + * Is it a KEX Packet? + */ + + if ((type == Packets.SSH_MSG_KEXINIT) || (type == Packets.SSH_MSG_NEWKEYS) + || ((type >= 30) && (type <= 49))) + { + km.handleMessage(msg, msglen); + continue; + } + + if (type == Packets.SSH_MSG_USERAUTH_SUCCESS) { + tc.startCompression(); + } + + MessageHandler mh = null; + + for (int i = 0; i < messageHandlers.size(); i++) + { + HandlerEntry he = messageHandlers.elementAt(i); + if ((he.low <= type) && (type <= he.high)) + { + mh = he.mh; + break; + } + } + + if (mh == null) + throw new IOException("Unexpected SSH message (type " + type + ")"); + + mh.handleMessage(msg, msglen); + } + } +} -- cgit v1.2.3