/**
* Copyright 2003-2016 SSHTOOLS Limited. All Rights Reserved.
*
* For product documentation visit https://www.sshtools.com/
*
* This file is part of J2SSH Maverick.
*
* J2SSH Maverick is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* J2SSH Maverick is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with J2SSH Maverick. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sshtools.ssh2;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.StringTokenizer;
import java.util.Vector;
import com.sshtools.events.Event;
import com.sshtools.events.EventServiceImplementation;
import com.sshtools.events.J2SSHEventCodes;
import com.sshtools.logging.Log;
import com.sshtools.ssh.SocketTimeoutSupport;
import com.sshtools.ssh.SshException;
import com.sshtools.ssh.SshIOException;
import com.sshtools.ssh.SshTransport;
import com.sshtools.ssh.components.ComponentManager;
import com.sshtools.ssh.components.Digest;
import com.sshtools.ssh.components.SshCipher;
import com.sshtools.ssh.components.SshHmac;
import com.sshtools.ssh.components.SshKeyExchangeClient;
import com.sshtools.ssh.components.SshPublicKey;
import com.sshtools.ssh.compression.SshCompression;
import com.sshtools.ssh.message.SshMessageReader;
import com.sshtools.util.ByteArrayReader;
import com.sshtools.util.ByteArrayWriter;
/**
* <p>
* Main implementation of the SSH Transport Protocol. The transport is designed
* to run over a provider such as a Socket or StreamConnection. To use first
* create an instance of the protocol and set its parameters and when ready
* start the protocol using <a
* href="#startTransportProtocol(TransportProvider)">startTransportProtocol</a>
* supplying a <a href="TransportProvider.html">TransportProvider</a> instance.
* This example uses the SocketProvider example implementation demonstrated in
* <a href="TransportProvider.html">TransportProvider Help File</a>.
* <blockquote>
*
* <pre>
* TransportProtocol transport = new TransportProtocol();
* transport.ignoreHostKeyVerification(true);
* transport.startTransportProtocol(new SocketProvider("titan", 22));
* </pre>
*
* </blockquote>
* </p>
*
* <p>
* Host key verification is recommended and a callback interface is provided by
* <a href="HostKeyVerification.html">HostKeyVerifcation</a>. To force
* verification use <a href="#setHostKeyVerification(HostKeyVerification)">
* setHostKeyVerification()</a> before starting the protocol. This is required
* by default but can be ignored with <a
* href="#ignoreHostKeyVerification(boolean)"> ignoreHostKeyVerification(boolean
* ignore)</a>.
* </p>
*
* <p>
* Additional cipher, message authentication and compression components can be
* supported by providing a custom <a href="TransportContext.html">
* TransportContext</a>. The default context provides all the required
* components of the protocol but others can be added by creating an instance
* and adding the various implementations to the component factories.
* <blockquote>
*
* <pre>
* TransportContext context = new TransportContext();
* context.supportedCiphers().add("blowfish-cbc", "com.mycrypt.Blowfish");
*
* TransportProtocol transport = new TransportProtocol(
* TransportProtocol.CLIENT_MODE, context);
* </pre>
*
* </blockquote>
* </p>
* <p>
* The context can also be used to specify the preferred methods of encryption
* for the connection which you may want to use should you add a new method to
* the context. <blockquote>
*
* <pre>
* context.setPreferredCipherCS("blowfish-cbc");
* </pre>
*
* </blockquote>
* </p>
*
* @see TransportProvider
* @author Lee David Painter
*/
public class TransportProtocol implements SshMessageReader {
/**
* Character set encoding. All input/output strings created by the API are
* created with this encoding. The default is "UTF-8" and it may be changed,
* the results however are unexpected.
*/
public static String CHARSET_ENCODING = "UTF8";
DataInputStream transportIn;
OutputStream transportOut;
SshTransport provider;
Ssh2Context transportContext;
Ssh2Client client;
String localIdentification;
String remoteIdentification;
byte[] localkex;
byte[] remotekex;
byte[] sessionIdentifier;
static final int SSH_MSG_DISCONNECT = 1;
static final int SSH_MSG_IGNORE = 2;
static final int SSH_MSG_UNIMPLEMENTED = 3;
static final int SSH_MSG_DEBUG = 4;
static final int SSH_MSG_SERVICE_REQUEST = 5;
static final int SSH_MSG_SERVICE_ACCEPT = 6;
static final int SSH_MSG_KEX_INIT = 20;
static final int SSH_MSG_NEWKEYS = 21;
/**
* Protocol state: Negotation of the protocol version
*/
public final static int NEGOTIATING_PROTOCOL = 1;
/**
* Protocol state: The protocol is performing key exchange
*/
public final static int PERFORMING_KEYEXCHANGE = 2;
/**
* Protocol state: The transport protocol is connected and services can be
* started or may already be active.
*/
public final static int CONNECTED = 3;
/**
* Protocol state: The transport protocol has disconnected.
*
* @see #getLastError()
*/
public final static int DISCONNECTED = 4;
int currentState;
Throwable lastError;
String disconnectReason;
SshKeyExchangeClient keyExchange;
SshKeyExchangeClient guessedKeyExchange;
SshCipher encryption;
SshCipher decryption;
SshHmac outgoingMac;
SshHmac incomingMac;
SshCompression outgoingCompression;
SshCompression incomingCompression;
SshPublicKey hostkey;
boolean isIncomingCompressing = false;
boolean isOutgoingCompressing = false;
int outgoingCipherLength = 8;
int outgoingMacLength = 0;
boolean ignoreHostKeyifEmpty = false;
byte[] incomingMessage;
ByteArrayWriter outgoingMessage;
int incomingCipherLength = 8;
int incomingMacLength = 0;
long outgoingSequence = 0;
long incomingSequence = 0;
final static int MAX_NUM_PACKETS_BEFORE_REKEY = 2147483647;
final static int MAX_NUM_BYTES_BEFORE_REKEY = 1073741824;
int numIncomingBytesSinceKEX;
int numIncomingPacketsSinceKEX;
int numOutgoingBytesSinceKEX;
int numOutgoingPacketsSinceKEX;
long outgoingBytes = 0;
long incomingBytes = 0;
Vector<byte[]> kexqueue = new Vector<byte[]>();
Vector<Runnable> shutdownHooks = new Vector<Runnable>();
Vector<TransportProtocolListener> listeners = new Vector<TransportProtocolListener>();
long lastActivity = System.currentTimeMillis();
/** Disconnect reason: The host is not allowed */
public final static int HOST_NOT_ALLOWED = 1;
/** Disconnect reason: A protocol error occurred */
public final static int PROTOCOL_ERROR = 2;
/** Disconnect reason: Key exchange failed */
public final static int KEY_EXCHANGE_FAILED = 3;
/** Disconnect reason: Reserved */
public final static int RESERVED = 4;
/** Disconnect reason: An error occurred verifying the MAC */
public final static int MAC_ERROR = 5;
/** Disconnect reason: A compression error occurred */
public final static int COMPRESSION_ERROR = 6;
/** Disconnect reason: The requested service is not available */
public final static int SERVICE_NOT_AVAILABLE = 7;
/** Disconnect reason: The protocol version is not supported */
public final static int PROTOCOL_VERSION_NOT_SUPPORTED = 8;
/** Disconnect reason: The host key supplied could not be verified */
public final static int HOST_KEY_NOT_VERIFIABLE = 9;
/** Disconnect reason: The connection was lost */
public final static int CONNECTION_LOST = 10;
/** Disconnect reason: The application disconnected */
public final static int BY_APPLICATION = 11;
/** Disconnect reason: Too many connections, try later */
public final static int TOO_MANY_CONNECTIONS = 12;
/** Disconnect reason: Authentication was cancelled */
public final static int AUTH_CANCELLED_BY_USER = 13;
/** Disconnect reason: No more authentication methods are available */
public final static int NO_MORE_AUTH_METHODS_AVAILABLE = 14;
/** Disconnect reason: The user's name is illegal */
public final static int ILLEGAL_USER_NAME = 15;
boolean verbose = Boolean.valueOf(
System.getProperty("maverick.verbose", "false")).booleanValue();
/**
* Create a default transport protocol instance in CLIENT_MODE.
*
* @throws IOException
*/
public TransportProtocol() {
}
public SshTransport getProvider() {
return provider;
}
public void addListener(TransportProtocolListener listener) {
listeners.addElement(listener);
}
/**
* Get the SshClient instance that created this transport.
*
* @return
*/
public Ssh2Client getClient() {
return client;
}
/**
* Returns the connected state
*
* @return <tt>true</tt> if the transport is connected, otherwise
* <tt>false</tt>
*/
public boolean isConnected() {
return currentState == CONNECTED
|| currentState == PERFORMING_KEYEXCHANGE;
}
/**
* Returns the last error detected by the protocol. If a disconnect occurs
* this may provide a reason.
*
* @return a last error detected by the transport protocol.
*/
public Throwable getLastError() {
return lastError;
}
public Ssh2Context getContext() {
return transportContext;
}
public boolean getIgnoreHostKeyifEmpty() {
return ignoreHostKeyifEmpty;
}
public void setIgnoreHostKeyifEmpty(boolean ignoreHostKeyifEmpty) {
this.ignoreHostKeyifEmpty = ignoreHostKeyifEmpty;
}
/**
* Starts the protocol on the provider.
*/
public void startTransportProtocol(SshTransport provider,
Ssh2Context context, String localIdentification,
String remoteIdentification, Ssh2Client client) throws SshException {
try {
this.transportIn = new DataInputStream(provider.getInputStream());
this.transportOut = provider.getOutputStream();
this.provider = provider;
this.localIdentification = localIdentification;
this.remoteIdentification = remoteIdentification;
this.transportContext = context;
this.incomingMessage = new byte[transportContext
.getMaximumPacketLength()];
this.outgoingMessage = new ByteArrayWriter(
transportContext.getMaximumPacketLength());
this.client = client;
// Negotiate the protocol version
currentState = TransportProtocol.NEGOTIATING_PROTOCOL;
// Perform key exchange
sendKeyExchangeInit(false);
if (Log.isDebugEnabled()) {
Log.debug(this,
"Waiting for transport protocol to complete initialization");
}
while (processMessage(readMessage()) && currentState != CONNECTED) {
;
}
} catch (IOException ex) {
throw new SshException(ex, SshException.CONNECT_FAILED);
}
if (Log.isDebugEnabled()) {
Log.debug(this, "Transport protocol initialized");
}
}
/**
* Get the identification string sent by the server during protocol
* negotiation
*
* @return String
*/
public String getRemoteIdentification() {
return remoteIdentification;
}
/**
* Get the session identifier
*
* @return byte[]
*/
public byte[] getSessionIdentifier() {
return sessionIdentifier;
}
/**
* Disconnect from the remote host. No more messages can be sent after this
* method has been called.
*
* @param reason
* @param disconnectReason
* , description
* @throws IOException
*/
public void disconnect(int reason, String disconnectReason) {
ByteArrayWriter baw = new ByteArrayWriter();
try {
this.disconnectReason = disconnectReason;
baw.write(SSH_MSG_DISCONNECT);
baw.writeInt(reason);
baw.writeString(disconnectReason);
baw.writeString("");
Log.info(this, "Sending SSH_MSG_DISCONNECT ["
+ disconnectReason + "]");
sendMessage(baw.toByteArray(), true);
} catch (Throwable t) {
} finally {
try {
baw.close();
} catch (IOException e) {
}
internalDisconnect();
}
}
/**
* <p>
* Send a transport protocol message. The format of the message should be:
* <blockquote>
*
* <pre>
* byte Message ID
* byte[] Payload
* </pre>
*
* </blockquote>
* </p>
*
* @param msgdata
* @throws IOException
*/
public void sendMessage(byte[] msgdata, boolean isActivity)
throws SshException {
synchronized (kexqueue) {
if (currentState == PERFORMING_KEYEXCHANGE
&& !isTransportMessage(msgdata[0])) {
kexqueue.addElement(msgdata);
return;
}
if (Log.isDebugEnabled()) {
if (verbose) {
Log.debug(this,
"Sending transport protocol message");
}
}
try {
outgoingMessage.reset();
int padding = 4;
// Compress the payload if necersary
if (outgoingCompression != null && isOutgoingCompressing) {
msgdata = outgoingCompression.compress(msgdata, 0,
msgdata.length);
}
// Determine the padding length
padding += ((outgoingCipherLength - ((msgdata.length + 5 + padding) % outgoingCipherLength)) % outgoingCipherLength);
// Write the packet length field
outgoingMessage.writeInt(msgdata.length + 1 + padding);
// Write the padding length
outgoingMessage.write(padding);
// Write the message payload
outgoingMessage.write(msgdata, 0, msgdata.length);
// Create some random data for the padding
ComponentManager
.getInstance()
.getRND()
.nextBytes(outgoingMessage.array(),
outgoingMessage.size(), padding);
outgoingMessage.move(padding);
// Generate the MAC
if (outgoingMac != null) {
outgoingMac.generate(outgoingSequence,
outgoingMessage.array(), 0, outgoingMessage.size(),
outgoingMessage.array(), outgoingMessage.size());
}
// Perfrom encrpytion
if (encryption != null) {
encryption.transform(outgoingMessage.array(), 0,
outgoingMessage.array(), 0, outgoingMessage.size());
}
outgoingMessage.move(outgoingMacLength);
outgoingBytes += outgoingMessage.size();
// Send!
transportOut.write(outgoingMessage.array(), 0,
outgoingMessage.size());
transportOut.flush();
if (isActivity)
lastActivity = System.currentTimeMillis();
if (Log.isDebugEnabled()) {
if (verbose) {
Log.debug(
this,
"Sent "
+ outgoingMessage.size()
+ " bytes of transport data outgoingSequence="
+ outgoingSequence
+ " totalBytesSinceKEX="
+ numOutgoingBytesSinceKEX);
}
}
outgoingSequence++;
numOutgoingBytesSinceKEX += msgdata.length;
numOutgoingPacketsSinceKEX++;
if (outgoingSequence >= 4294967296L) {
outgoingSequence = 0;
}
if (!transportContext.isKeyReExchangeDisabled()) {
if (numOutgoingBytesSinceKEX >= MAX_NUM_BYTES_BEFORE_REKEY
|| numOutgoingPacketsSinceKEX >= MAX_NUM_PACKETS_BEFORE_REKEY) {
if (Log.isDebugEnabled()) {
Log.debug(this,
"Requesting key re-exchange");
}
sendKeyExchangeInit(false);
}
}
} catch (IOException ex) {
internalDisconnect();
throw new SshException("Unexpected termination: "
+ ex.getMessage(), SshException.UNEXPECTED_TERMINATION);
}
}
}
/**
* Get the next message. The message returned will be the full message data
* so skipping the first 5 bytes is required before the message data can be
* read.
*
* @return a byte array containing all the message data
* @throws IOException
*/
public byte[] nextMessage() throws SshException {
if (Log.isDebugEnabled()) {
if (verbose) {
Log.debug(this, "transport next message");
}
}
synchronized (transportIn) {
byte[] msg;
do {
msg = readMessage();
} while (processMessage(msg));
return msg;
}
}
void readWithTimeout(byte[] buf, int off, int len, int timeoutMillis,
boolean isPartialMessage) throws SshException {
int count = 0;
int timeout = 0;
if (isPartialMessage) {
// save current timeout value and restore later
timeout = configureSocketTimeout(transportContext
.getPartialMessageTimeout());
}
try {
do {
try {
int read = transportIn.read(buf, off + count, len - count);
if (read == -1)
throw new SshException("EOF received from remote side",
SshException.UNEXPECTED_TERMINATION);
count += read;
} catch (InterruptedIOException ex) {
if (Log.isDebugEnabled()) {
Log.debug(this,
"Socket timed out during read! "
+ " isPartialMessage="
+ isPartialMessage
+ " bytesTransfered="
+ ex.bytesTransferred);
}
if (isPartialMessage) {
if (ex.bytesTransferred > 0) {
count += ex.bytesTransferred;
continue;
}
}
if (isPartialMessage) {
internalDisconnect();
throw new SshException(
"Remote host failed to respond during message receive!",
SshException.SOCKET_TIMEOUT);
} else {
if (getContext().getIdleConnectionTimeoutSeconds() > 0
&& (System.currentTimeMillis() - lastActivity) > (getContext()
.getIdleConnectionTimeoutSeconds() * 1000)) {
if (Log.isDebugEnabled()) {
Log.debug(
this,
"Connection is idle, disconnecting idleMax="
+ getContext()
.getIdleConnectionTimeoutSeconds());
}
disconnect(TransportProtocol.BY_APPLICATION,
"Idle connection");
throw new SshException(
"Connection has been dropped as it reached max idle time of "
+ getContext()
.getIdleConnectionTimeoutSeconds()
+ " seconds.",
SshException.CONNECTION_CLOSED);
} else if (getContext().isSendIgnorePacketOnIdle()) {
ByteArrayWriter baw = new ByteArrayWriter();
try {
if (Log.isDebugEnabled()) {
Log.debug(this,
"Sending SSH_MSG_IGNORE");
}
baw.write(SSH_MSG_IGNORE);
int tmplen = (int) (Math.random()
* (getContext()
.getKeepAliveMaxDataLength()) + 1);
byte[] tmp = new byte[tmplen];
ComponentManager.getInstance().getRND()
.nextBytes(tmp);
baw.writeBinaryString(tmp);
sendMessage(baw.toByteArray(), false);
} catch (IOException e) {
// Disconnected
internalDisconnect(
"Connection failed during SSH_MSG_IGNORE packet",
CONNECTION_LOST);
} finally {
try {
baw.close();
} catch (IOException e) {
}
}
}
if (getContext().getSocketTimeout() > 0) {
for (Enumeration<TransportProtocolListener> e = listeners
.elements(); e.hasMoreElements();) {
TransportProtocolListener l = e.nextElement();
try {
l.onIdle(lastActivity);
} catch (Throwable t) {
}
}
} else {
throw new SshException(
"Socket connection timed out.",
SshException.SOCKET_TIMEOUT);
}
}
/**
* throw new SshException( "Socket connection timed out.",
* SshException.SOCKET_TIMEOUT);
**/
} catch (IOException ex) {
throw new SshException("IO error received from remote"
+ ex.getMessage(),
SshException.UNEXPECTED_TERMINATION, ex);
}
} while (count < len);
} finally {
if (isPartialMessage) {
// set socket timeout back to its original value
configureSocketTimeout(timeout);
}
}
}
private int configureSocketTimeout(int timeout) {
if (provider instanceof SocketTimeoutSupport) {
try {
SocketTimeoutSupport sock = (SocketTimeoutSupport) provider;
int ret = sock.getSoTimeout();
sock.setSoTimeout(timeout);
return ret;
} catch (IOException ex) {
}
}
return 0;
}
byte[] readMessage() throws SshException {
if (Log.isDebugEnabled()) {
if (verbose) {
Log.debug(this, "transport read message");
}
}
synchronized (transportIn) {
try {
if (Log.isDebugEnabled()) {
if (verbose) {
Log.debug(this, "Waiting for transport message");
}
}
readWithTimeout(incomingMessage, 0, incomingCipherLength,
transportContext.getPartialMessageTimeout(), false);
// Decrypt the data if we have a valid cipher
if (decryption != null) {
decryption.transform(incomingMessage, 0, incomingMessage,
0, incomingCipherLength);
// Preview the message length
}
int msglen = (int) ByteArrayReader.readInt(incomingMessage, 0);
if (msglen <= 0)
throw new SshException(
"Server sent invalid message length of " + msglen
+ "!", SshException.PROTOCOL_VIOLATION);
int padlen = (incomingMessage[4] & 0xFF);
int remaining = (msglen - (incomingCipherLength - 4));
if (Log.isDebugEnabled()) {
if (verbose) {
Log.debug(this,
"Incoming transport message msglen=" + msglen
+ " padlen=" + padlen);
}
}
// Verify that the packet length is good
if (remaining < 0) {
internalDisconnect();
throw new SshException(
"EOF whilst reading message data block",
SshException.UNEXPECTED_TERMINATION);
} else if (remaining > incomingMessage.length
- incomingCipherLength) {
if (remaining + incomingCipherLength + incomingMacLength > transportContext
.getMaximumPacketLength()) {
internalDisconnect();
throw new SshException(
"Incoming packet length violates SSH protocol ["
+ remaining + incomingCipherLength
+ " bytes]",
SshException.UNEXPECTED_TERMINATION);
}
// Resize the incomingMessage buffer
byte[] tmp = new byte[remaining + incomingCipherLength
+ incomingMacLength];
System.arraycopy(incomingMessage, 0, tmp, 0,
incomingCipherLength);
incomingMessage = tmp;
}
// Read, decrypt and save the remaining data
if (remaining > 0) {
readWithTimeout(incomingMessage, incomingCipherLength,
remaining,
transportContext.getPartialMessageTimeout(), true);
if (decryption != null) {
decryption.transform(incomingMessage,
incomingCipherLength, incomingMessage,
incomingCipherLength, remaining);
}
// Verify the message
}
if (incomingMac != null) {
readWithTimeout(incomingMessage, incomingCipherLength
+ remaining, incomingMacLength,
transportContext.getPartialMessageTimeout(), true);
// Verify the mac
if (!incomingMac.verify(incomingSequence, incomingMessage,
0, incomingCipherLength + remaining,
incomingMessage, incomingCipherLength + remaining)) {
disconnect(TransportProtocol.MAC_ERROR,
"Corrupt Mac on input");
throw new SshException("Corrupt Mac on input",
SshException.PROTOCOL_VIOLATION);
}
}
if (++incomingSequence >= 4294967296L) {
incomingSequence = 0;
}
incomingBytes += incomingCipherLength + remaining
+ incomingMacLength;
byte[] payload = new byte[(msglen + 4) - padlen - 5];
System.arraycopy(incomingMessage, 5, payload, 0, payload.length);
// Uncompress the message payload if necersary
if (incomingCompression != null && isIncomingCompressing) {
return incomingCompression.uncompress(payload, 0,
payload.length);
}
numIncomingBytesSinceKEX += payload.length;
numIncomingPacketsSinceKEX++;
if (!transportContext.isKeyReExchangeDisabled()) {
if (numIncomingBytesSinceKEX >= MAX_NUM_BYTES_BEFORE_REKEY
|| numIncomingPacketsSinceKEX >= MAX_NUM_PACKETS_BEFORE_REKEY) {
sendKeyExchangeInit(false);
}
}
if (Log.isDebugEnabled()) {
if (verbose) {
Log.debug(this,
"Completed incoming transport message");
}
}
return payload;
} catch (InterruptedIOException ex) {
throw new SshException(
"Interrupted IO; possible socket timeout detected?",
SshException.SOCKET_TIMEOUT);
} catch (IOException ex) {
internalDisconnect();
throw new SshException("Unexpected terminaton: "
+ (ex.getMessage() != null ? ex.getMessage() : ex
.getClass().getName()) + " sequenceNo = "
+ incomingSequence + " bytesIn = " + incomingBytes
+ " bytesOut = " + outgoingBytes,
SshException.UNEXPECTED_TERMINATION, ex);
}
}
}
public SshKeyExchangeClient getKeyExchange() {
return keyExchange;
}
public static boolean Arrayequals(byte[] a, byte[] a2) {
if (a == a2)
return true;
if (a == null || a2 == null)
return false;
int length = a.length;
if (a2.length != length)
return false;
for (int i = 0; i < length; i++)
if (a[i] != a2[i])
return false;
return true;
}
void performKeyExchange(byte[] msg) throws SshException {
ByteArrayReader bar = new ByteArrayReader(msg, 0, msg.length);
try {
synchronized (kexqueue) {
// If were not already in a key exchange state then send our kex
// init
if (localkex == null) {
sendKeyExchangeInit(false);
}
// Set the state to performing key exchange now that we have
// both msgs
currentState = TransportProtocol.PERFORMING_KEYEXCHANGE;
// Extract the remote's side kex init taking away the header and
// padding
remotekex = msg;
bar.skip(17);
String remoteKeyExchanges = checkValidString("key exchange",
bar.readString());
String remotePublicKeys = checkValidString("public key",
bar.readString());
String remoteCiphersCS = checkValidString(
"client->server cipher", bar.readString());
String remoteCiphersSC = checkValidString(
"server->client cipher", bar.readString());
String serverCSMacs = checkValidString("client->server mac",
bar.readString());
String serverSCMacs = checkValidString("server->client mac",
bar.readString());
String serverCSCompressions = checkValidString(
"client->server comp", bar.readString());
String serverSCCompressions = checkValidString(
"server->client comp", bar.readString());
@SuppressWarnings("unused")
String lang1 = bar.readString();
@SuppressWarnings("unused")
String lang2 = bar.readString();
boolean guessed = bar.readBoolean();
EventServiceImplementation
.getInstance()
.fireEvent(
(new Event(
this,
J2SSHEventCodes.EVENT_KEY_EXCHANGE_INIT,
true))
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_REMOTE_KEY_EXCHANGES,
remoteKeyExchanges)
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_LOCAL_KEY_EXCHANGES,
transportContext
.supportedKeyExchanges()
.list(transportContext
.getPreferredKeyExchange()))
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_REMOTE_PUBLICKEYS,
remotePublicKeys)
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_LOCAL_PUBLICKEYS,
transportContext
.supportedPublicKeys()
.list(transportContext
.getPreferredPublicKey()))
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_REMOTE_CIPHERS_CS,
remoteCiphersCS)
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_LOCAL_CIPHERS_CS,
transportContext
.supportedCiphersCS()
.list(transportContext
.getPreferredCipherCS()))
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_REMOTE_CIPHERS_SC,
remoteCiphersSC)
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_LOCAL_CIPHERS_SC,
transportContext
.supportedCiphersSC()
.list(transportContext
.getPreferredCipherSC()))
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_REMOTE_CS_MACS,
serverCSMacs)
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_LOCAL_CS_MACS,
transportContext
.supportedMacsCS()
.list(transportContext
.getPreferredMacCS()))
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_REMOTE_SC_MACS,
serverSCMacs)
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_LOCAL_SC_MACS,
transportContext
.supportedMacsSC()
.list(transportContext
.getPreferredMacSC()))
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_REMOTE_CS_COMPRESSIONS,
serverCSCompressions)
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_LOCAL_CS_COMPRESSIONS,
transportContext
.supportedCompressionsCS()
.list(transportContext
.getPreferredCompressionCS()))
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_REMOTE_SC_COMPRESSIONS,
serverSCCompressions)
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_LOCAL_SC_COMPRESSIONS,
transportContext
.supportedCompressionsSC()
.list(transportContext
.getPreferredCompressionSC())));
if (Log.isDebugEnabled()) {
Log.debug(this,
"Remote computer supports key exchanges: "
+ remoteKeyExchanges);
}
if (Log.isDebugEnabled()) {
Log.debug(this,
"Remote computer supports public keys: "
+ remotePublicKeys);
}
if (Log.isDebugEnabled()) {
Log.debug(this,
"Remote computer supports client->server ciphers: "
+ remoteCiphersCS);
}
String cipherCS = selectNegotiatedComponent(
transportContext.supportedCiphersCS().list(
transportContext.getPreferredCipherCS()),
remoteCiphersCS);
if (Log.isDebugEnabled()) {
Log.debug(this,
"Negotiated client->server cipher: " + cipherCS);
}
if (Log.isDebugEnabled()) {
Log.debug(this,
"Remote computer supports client->server ciphers: "
+ remoteCiphersCS);
}
String cipherSC = selectNegotiatedComponent(
transportContext.supportedCiphersSC().list(
transportContext.getPreferredCipherSC()),
remoteCiphersSC);
if (Log.isDebugEnabled()) {
Log.debug(this,
"Negotiated server->client cipher: " + cipherSC);
}
SshCipher encryption = (SshCipher) transportContext
.supportedCiphersCS().getInstance(cipherCS);
SshCipher decryption = (SshCipher) transportContext
.supportedCiphersSC().getInstance(cipherSC);
String macCS = selectNegotiatedComponent(
transportContext.supportedMacsCS().list(
transportContext.getPreferredMacCS()),
checkValidString("client->server hmac", serverCSMacs));
String macSC = selectNegotiatedComponent(
transportContext.supportedMacsSC().list(
transportContext.getPreferredMacSC()),
checkValidString("server->client hmac", serverSCMacs));
SshHmac outgoingMac = (SshHmac) transportContext
.supportedMacsCS().getInstance(macCS);
SshHmac incomingMac = (SshHmac) transportContext
.supportedMacsSC().getInstance(macSC);
String compressionCS = selectNegotiatedComponent(
transportContext.supportedCompressionsCS().list(
transportContext.getPreferredCompressionCS()),
checkValidString("client->server compression",
serverCSCompressions));
String compressionSC = selectNegotiatedComponent(
transportContext.supportedCompressionsSC().list(
transportContext.getPreferredCompressionSC()),
checkValidString("server->client compression",
serverSCCompressions));
SshCompression outgoingCompression = null;
if (!compressionCS.equals(Ssh2Context.COMPRESSION_NONE)) {
outgoingCompression = (SshCompression) transportContext
.supportedCompressionsCS().getInstance(
compressionCS);
outgoingCompression.init(SshCompression.DEFLATER, 6);
}
SshCompression incomingCompression = null;
if (!compressionSC.equals(Ssh2Context.COMPRESSION_NONE)) {
incomingCompression = (SshCompression) transportContext
.supportedCompressionsSC().getInstance(
compressionSC);
incomingCompression.init(SshCompression.INFLATER, 6);
}
boolean ignoreFirstPacket = false;
String keyExchangeAlg = selectNegotiatedComponent(
transportContext.supportedKeyExchanges().list(
transportContext.getPreferredKeyExchange()),
remoteKeyExchanges);
if (guessedKeyExchange == null
|| !keyExchangeAlg.equals(guessedKeyExchange
.getAlgorithm())) {
// Determine the negotiated key exchange
keyExchange = (SshKeyExchangeClient) transportContext
.supportedKeyExchanges()
.getInstance(keyExchangeAlg);
}
if (Log.isDebugEnabled()) {
Log.debug(this, "Negotiated key exchange: "
+ keyExchange.getAlgorithm());
}
if (guessed) {
// Should we ignore the first key exchange packet?
if (!keyExchangeAlg.equals(transportContext
.getPreferredKeyExchange())) {
ignoreFirstPacket = true;
}
String hostkey = selectNegotiatedComponent(
transportContext.supportedPublicKeys().list(
transportContext.getPreferredPublicKey()),
remotePublicKeys);
if (!ignoreFirstPacket
&& !hostkey.equals(transportContext
.getPreferredPublicKey())) {
// Guess should be considered correct
ignoreFirstPacket = true;
}
}
keyExchange.init(this, ignoreFirstPacket);
keyExchange.performClientExchange(localIdentification,
remoteIdentification, localkex, remotekex);
String hostKeyAlg = selectNegotiatedComponent(
transportContext.supportedPublicKeys().list(
transportContext.getPreferredPublicKey()),
remotePublicKeys);
hostkey = (SshPublicKey) transportContext.supportedPublicKeys()
.getInstance(hostKeyAlg);
if (!(ignoreHostKeyifEmpty && Arrayequals(
keyExchange.getHostKey(), "".getBytes()))) {
EventServiceImplementation.getInstance().fireEvent(
(new Event(this,
J2SSHEventCodes.EVENT_HOSTKEY_RECEIVED,
true)).addAttribute(
J2SSHEventCodes.ATTRIBUTE_HOST_KEY,
new String(keyExchange.getHostKey())));
hostkey.init(keyExchange.getHostKey(), 0,
keyExchange.getHostKey().length);
if (transportContext.getHostKeyVerification() != null) {
if (!transportContext.getHostKeyVerification()
.verifyHost(provider.getHost(), hostkey)) {
EventServiceImplementation
.getInstance()
.fireEvent(
new Event(
this,
J2SSHEventCodes.EVENT_HOSTKEY_REJECTED,
false));
disconnect(
TransportProtocol.HOST_KEY_NOT_VERIFIABLE,
"Host key not accepted");
throw new SshException(
"The host key was not accepted",
SshException.CANCELLED_CONNECTION);
}
if (!hostkey.verifySignature(
keyExchange.getSignature(),
keyExchange.getExchangeHash())) {
EventServiceImplementation
.getInstance()
.fireEvent(
new Event(
this,
J2SSHEventCodes.EVENT_HOSTKEY_REJECTED,
false));
disconnect(
TransportProtocol.HOST_KEY_NOT_VERIFIABLE,
"Invalid host key signature");
throw new SshException(
"The host key signature is invalid",
SshException.PROTOCOL_VIOLATION);
}
EventServiceImplementation.getInstance().fireEvent(
new Event(this,
J2SSHEventCodes.EVENT_HOSTKEY_ACCEPTED,
true));
}
}
// Set the first exchange hash as the session identifier
if (sessionIdentifier == null) {
sessionIdentifier = keyExchange.getExchangeHash();
}
// We now have all the necersary values to perform encrpytion
// so lets send our new keys message and wait for the other
// sides
// response
sendMessage(new byte[] { (byte) SSH_MSG_NEWKEYS }, true);
// Put the outgoing components into use
encryption.init(SshCipher.ENCRYPT_MODE, makeSshKey('A'),
makeSshKey('C'));
outgoingCipherLength = encryption.getBlockSize();
outgoingMac.init(makeSshKey('E'));
outgoingMacLength = outgoingMac.getMacLength();
this.encryption = encryption;
this.outgoingMac = outgoingMac;
this.outgoingCompression = outgoingCompression;
do {
msg = readMessage();
// Process the transport protocol message, must only be
// SSH_MSH_INGORE, SSH_MSG_DEBUG, SSH_MSG_DISCONNECT or
// SSH_MSG_NEWKEYS
if (!processMessage(msg)) {
EventServiceImplementation
.getInstance()
.fireEvent(
new Event(
this,
J2SSHEventCodes.EVENT_KEY_EXCHANGE_FAILURE,
true));
disconnect(TransportProtocol.PROTOCOL_ERROR,
"Invalid message received");
throw new SshException(
"Invalid message received during key exchange",
SshException.PROTOCOL_VIOLATION);
}
} while (msg[0] != SSH_MSG_NEWKEYS);
EventServiceImplementation
.getInstance()
.fireEvent(
(new Event(
this,
J2SSHEventCodes.EVENT_KEY_EXCHANGE_COMPLETE,
true))
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_USING_PUBLICKEY,
hostKeyAlg)
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_USING_KEY_EXCHANGE,
keyExchangeAlg)
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_USING_CS_CIPHER,
cipherCS)
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_USING_SC_CIPHER,
cipherSC)
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_USING_CS_MAC,
macCS)
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_USING_SC_MAC,
macSC)
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_USING_CS_COMPRESSION,
compressionCS)
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_USING_SC_COMPRESSION,
compressionSC));
// Put the incoming components into use
decryption.init(SshCipher.DECRYPT_MODE, makeSshKey('B'),
makeSshKey('D'));
incomingCipherLength = decryption.getBlockSize();
incomingMac.init(makeSshKey('F'));
incomingMacLength = incomingMac.getMacLength();
this.decryption = decryption;
this.incomingMac = incomingMac;
this.incomingCompression = incomingCompression;
// Nasty hack for zlib@openssh.com compression type
if (incomingCompression != null
&& !incomingCompression.getAlgorithm().equals(
"zlib@openssh.com"))
isIncomingCompressing = true;
if (outgoingCompression != null
&& !outgoingCompression.getAlgorithm().equals(
"zlib@openssh.com"))
isOutgoingCompressing = true;
// synchronized(kexqueue) {
currentState = TransportProtocol.CONNECTED;
for (Enumeration<byte[]> e = kexqueue.elements(); e
.hasMoreElements();) {
sendMessage(e.nextElement(), true);
}
kexqueue.removeAllElements();
// }
// Clean up and reset any parameters
localkex = null;
remotekex = null;
}
} catch (IOException ex) {
EventServiceImplementation.getInstance().fireEvent(
new Event(this, J2SSHEventCodes.EVENT_KEY_EXCHANGE_FAILURE,
true));
throw new SshException(ex, SshException.INTERNAL_ERROR);
} catch (SshException sshe) {
EventServiceImplementation.getInstance().fireEvent(
new Event(this, J2SSHEventCodes.EVENT_KEY_EXCHANGE_FAILURE,
true));
throw sshe;
} finally {
try {
bar.close();
} catch (IOException e) {
}
}
}
void completedAuthentication() {
if (incomingCompression != null
&& incomingCompression.getAlgorithm()
.equals("zlib@openssh.com"))
isIncomingCompressing = true;
if (outgoingCompression != null
&& outgoingCompression.getAlgorithm()
.equals("zlib@openssh.com"))
isOutgoingCompressing = true;
}
/**
* Request that the remote server starts a transport protocol service. This
* is only available in CLIENT_MODE.
*
* @param servicename
* @throws IOException
*/
public void startService(String servicename) throws SshException {
ByteArrayWriter baw = new ByteArrayWriter();
try {
baw.write(SSH_MSG_SERVICE_REQUEST);
baw.writeString(servicename);
if (Log.isDebugEnabled()) {
Log.debug(this, "Sending SSH_MSG_SERVICE_REQUEST");
}
sendMessage(baw.toByteArray(), true);
byte[] msg;
do {
msg = readMessage();
} while (processMessage(msg) || msg[0] != SSH_MSG_SERVICE_ACCEPT);
if (Log.isDebugEnabled()) {
Log.debug(this, "Received SSH_MSG_SERVICE_ACCEPT");
}
} catch (IOException ex) {
throw new SshException(ex, SshException.INTERNAL_ERROR);
} finally {
try {
baw.close();
} catch (IOException e) {
}
}
}
void internalDisconnect(String msg, int reason) {
currentState = DISCONNECTED;
try {
provider.close();
} catch (IOException ex) {
}
for (Enumeration<TransportProtocolListener> e = listeners.elements(); e
.hasMoreElements();) {
TransportProtocolListener l = e.nextElement();
try {
l.onDisconnect(msg, reason);
} catch (Throwable t) {
}
}
for (int i = 0; i < shutdownHooks.size(); i++) {
try {
((Runnable) shutdownHooks.elementAt(i)).run();
} catch (Throwable t) {
}
}
}
void internalDisconnect() {
currentState = DISCONNECTED;
try {
provider.close();
} catch (IOException ex) {
}
for (int i = 0; i < shutdownHooks.size(); i++) {
try {
((Runnable) shutdownHooks.elementAt(i)).run();
} catch (Throwable t) {
}
}
}
void addShutdownHook(Runnable r) {
if (r != null)
shutdownHooks.addElement(r);
}
/**
* Process a message. This should be called when reading messages from
* outside of the transport protocol so that the transport protocol can
* parse its own messages.
*
* @param msg
* @return <code>true</code> if the message was processed by the transport
* and can be discarded, otherwise <code>false</code>.
* @throws SshException
*/
public boolean processMessage(byte[] msg) throws SshException {
try {
if (msg.length < 1) {
disconnect(TransportProtocol.PROTOCOL_ERROR,
"Invalid message received");
throw new SshException("Invalid transport protocol message",
SshException.INTERNAL_ERROR);
}
switch (msg[0]) {
case SSH_MSG_DISCONNECT: {
if (Log.isDebugEnabled()) {
Log.debug(this, "Received SSH_MSG_DISCONNECT");
}
internalDisconnect();
ByteArrayReader bar = new ByteArrayReader(msg, 5,
msg.length - 5);
try {
EventServiceImplementation.getInstance().fireEvent(
new Event(this,
J2SSHEventCodes.EVENT_RECEIVED_DISCONNECT,
true));
throw new SshException(bar.readString(),
SshException.REMOTE_HOST_DISCONNECTED);
} finally {
bar.close();
}
}
case SSH_MSG_IGNORE: {
if (Log.isDebugEnabled()) {
Log.debug(this, "Received SSH_MSG_IGNORE");
}
return true;
}
case SSH_MSG_DEBUG: {
lastActivity = System.currentTimeMillis();
if (Log.isDebugEnabled()) {
Log.debug(this, "Received SSH_MSG_DEBUG");
}
return true;
}
case SSH_MSG_NEWKEYS: {
lastActivity = System.currentTimeMillis();
if (Log.isDebugEnabled()) {
Log.debug(this, "Received SSH_MSG_NEWKEYS");
}
return true;
}
case SSH_MSG_KEX_INIT: {
lastActivity = System.currentTimeMillis();
if (Log.isDebugEnabled()) {
Log.debug(this, "Received SSH_MSG_KEX_INIT");
}
if (remotekex != null) {
disconnect(TransportProtocol.PROTOCOL_ERROR,
"Key exchange already in progress!");
throw new SshException("Key exchange already in progress!",
SshException.PROTOCOL_VIOLATION);
}
performKeyExchange(msg);
return true;
}
default: {
lastActivity = System.currentTimeMillis();
// Not a transport protocol message
return false;
}
}
} catch (IOException ex1) {
throw new SshException(ex1.getMessage(),
SshException.INTERNAL_ERROR);
}
}
boolean isTransportMessage(int messageid) {
switch (messageid) {
case SSH_MSG_DISCONNECT:
case SSH_MSG_IGNORE:
case SSH_MSG_DEBUG:
case SSH_MSG_NEWKEYS:
case SSH_MSG_KEX_INIT: {
return true;
}
default: {
if (keyExchange != null) {
return keyExchange.isKeyExchangeMessage(messageid);
}
// Not a transport protocol message
return false;
}
}
}
String selectNegotiatedComponent(String locallist, String remotelist)
throws SshException {
String list = remotelist;
String llist = locallist;
Vector<String> r = new Vector<String>();
int idx;
String name;
while ((idx = list.indexOf(",")) > -1) {
r.addElement(list.substring(0, idx).trim());
list = list.substring(idx + 1).trim();
}
r.addElement(list.trim());
while ((idx = llist.indexOf(",")) > -1) {
name = llist.substring(0, idx).trim();
if (r.contains(name)) {
return name;
}
llist = llist.substring(idx + 1).trim();
}
if (r.contains(llist)) {
return llist;
}
EventServiceImplementation
.getInstance()
.fireEvent(
(new Event(
this,
J2SSHEventCodes.EVENT_FAILED_TO_NEGOTIATE_TRANSPORT_COMPONENT,
true))
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_LOCAL_COMPONENT_LIST,
locallist)
.addAttribute(
J2SSHEventCodes.ATTRIBUTE_REMOTE_COMPONENT_LIST,
remotelist));
throw new SshException("Failed to negotiate a transport component ["
+ locallist + "] [" + remotelist + "]",
SshException.KEY_EXCHANGE_FAILED);
}
void sendKeyExchangeInit(boolean guess) throws SshException {
ByteArrayWriter msg = new ByteArrayWriter();
try {
synchronized (kexqueue) {
numIncomingBytesSinceKEX = 0;
numIncomingPacketsSinceKEX = 0;
numOutgoingBytesSinceKEX = 0;
numOutgoingPacketsSinceKEX = 0;
currentState = TransportProtocol.PERFORMING_KEYEXCHANGE;
byte[] cookie = new byte[16];
ComponentManager.getInstance().getRND().nextBytes(cookie);
msg.write(SSH_MSG_KEX_INIT);
msg.write(cookie);
msg.writeString(transportContext.supportedKeyExchanges().list(
transportContext.getPreferredKeyExchange()));
msg.writeString(transportContext.supportedPublicKeys().list(
transportContext.getPreferredPublicKey()));
msg.writeString(transportContext.supportedCiphersCS().list(
transportContext.getPreferredCipherCS()));
msg.writeString(transportContext.supportedCiphersSC().list(
transportContext.getPreferredCipherSC()));
msg.writeString(transportContext.supportedMacsCS().list(
transportContext.getPreferredMacCS()));
msg.writeString(transportContext.supportedMacsSC().list(
transportContext.getPreferredMacSC()));
msg.writeString(transportContext.supportedCompressionsCS()
.list(transportContext.getPreferredCompressionCS()));
msg.writeString(transportContext.supportedCompressionsSC()
.list(transportContext.getPreferredCompressionSC()));
msg.writeString("");
msg.writeString("");
msg.writeBoolean(guess); // First packet follows
msg.writeInt(0);
if (Log.isDebugEnabled()) {
Log.debug(this, "Sending SSH_MSG_KEX_INIT");
}
sendMessage(localkex = msg.toByteArray(), true);
}
} catch (IOException ex) {
throw new SshException(ex, SshException.INTERNAL_ERROR);
} finally {
try {
msg.close();
} catch (IOException e) {
}
}
}
byte[] makeSshKey(char chr) throws IOException {
ByteArrayWriter keydata = new ByteArrayWriter();
try {
// Create the first 20 bytes of key data
byte[] data = new byte[20];
Digest hash = (Digest) ComponentManager.getInstance()
.supportedDigests()
.getInstance(keyExchange.getHashAlgorithm());
// Put the dh k value
hash.putBigInteger(keyExchange.getSecret());
// Put in the exchange hash
hash.putBytes(keyExchange.getExchangeHash());
// Put in the character
hash.putByte((byte) chr);
// Put the exchange hash in again
hash.putBytes(sessionIdentifier);
// Create the fist 20 bytes
data = hash.doFinal();
keydata.write(data);
// Now do the next 20
hash.reset();
// Put the dh k value in again
hash.putBigInteger(keyExchange.getSecret());
// And the exchange hash
hash.putBytes(keyExchange.getExchangeHash());
// Finally the first 20 bytes of data we created
hash.putBytes(data);
data = hash.doFinal();
// Put it all together
keydata.write(data);
// Return it
return keydata.toByteArray();
} catch (SshException e) {
throw new SshIOException(e);
} finally {
keydata.close();
}
}
private String checkValidString(String id, String str) throws IOException {
if (str.trim().equals(""))
throw new IOException("Server sent invalid " + id + " value '"
+ str + "'");
StringTokenizer t = new StringTokenizer(str, ",");
if (!t.hasMoreElements())
throw new IOException("Server sent invalid " + id + " value '"
+ str + "'");
return str;
}
}