/*
* This file was originally part of "The Java Telnet Application" prior to incorporation into midpssh and later bsshh.
*
* The remaining original code is (c) Matthias L. Jugel, Marcus Mei�ner 1996-2002.
*
* The file was changed by Radek Polak to work as midlet in MIDP 1.0 This file has been modified by Karl von Randow for
* MidpSSH. This file has been modified and largely rewritten by Marc A. Paradise for BBSHH.
*
*
* --LICENSE NOTICE-- This program is free software; you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*
* This program 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 this program; if not, write to the Free
* Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. --LICENSE NOTICE--
*/
package org.bbssh.ssh;
import java.io.IOException;
import java.util.Vector;
import net.rim.device.api.crypto.CryptoException;
import net.rim.device.api.crypto.CryptoTokenException;
import net.rim.device.api.crypto.CryptoUnsupportedOperationException;
import net.rim.device.api.crypto.DHCryptoSystem;
import net.rim.device.api.crypto.DHKeyAgreement;
import net.rim.device.api.crypto.DHPublicKey;
import net.rim.device.api.crypto.DSAKeyPair;
import net.rim.device.api.crypto.DSAPrivateKey;
import net.rim.device.api.crypto.InvalidCryptoSystemException;
import net.rim.device.api.crypto.InvalidKeyException;
import net.rim.device.api.crypto.KeyPair;
import net.rim.device.api.crypto.MD5Digest;
import net.rim.device.api.crypto.NoSuchAlgorithmException;
import net.rim.device.api.crypto.RSAKeyPair;
import net.rim.device.api.crypto.RSAPrivateKey;
import net.rim.device.api.crypto.RandomSource;
import net.rim.device.api.crypto.SHA1Digest;
import net.rim.device.api.crypto.UnsupportedCryptoSystemException;
import org.bbssh.crypto.SignatureTools;
import org.bbssh.crypto.TypesReader;
import org.bbssh.model.ConnectionProperties;
import org.bbssh.model.Key;
import org.bbssh.net.session.Session;
import org.bbssh.net.session.SshSession;
import org.bbssh.ssh.kex.CipherAttributes;
import org.bbssh.ssh.kex.CipherManager;
import org.bbssh.ssh.kex.KexAgreement;
import org.bbssh.ssh.kex.KexInitData;
import org.bbssh.ssh.kex.KexStateData;
import org.bbssh.ssh.packets.PacketUserauthRequestPublicKey;
import org.bbssh.ssh.packets.SshPacket;
import org.bbssh.ssh.packets.SshPacket2;
import org.bbssh.ssh.v2.SshCrypto2;
import org.bbssh.util.Logger;
import org.bbssh.util.Tools;
import org.bbssh.util.Version;
import ch.ethz.ssh2.crypto.PEMDecoder;
/**
* Secure Shell IO
*
* @author Marcus Meissner
* @version $Id: SshIO.java 517 2008-03-14 04:17:55Z karlvr $
*/
public class SshIO {
/** Authentication mode - publickey */
public static final byte MODE_PUBLICKEY = 1;
/** Authentication mode - password */
public static final byte MODE_PASSWORD = 2;
/** Authentication mode - keyboard interactive */
public static final byte MODE_KEYBOARD_INTERACTIVE = 3;
/** We have startred up and have a valid connection */
public static final byte STATE_INIT = 0;
/** Initializtion is ocmplete, waiting for KEX init from server */
public static final byte STATE_INIT_COMPLETE = 1;
/** We are waiting for KEX_INIT */
public static final byte STATE_KEX_INIT = 2;
/** We have begun key exchange */
public static final byte STATE_KEX_DH = 3;
/** KEX_INIT is completed, and we are starting DH exchange */
public static final byte STATE_KEX_DH_INIT = 4;
/** KEX is fully complete, and we have a secure link but are not authenticated */
public static final byte STATE_SECURE = 5;
/** Opening channels */
public static final byte STATE_OPENING_CHANNEL = 6;
/** We've requested auth and are awaiting reply */
public static final byte STATE_REQUESTING_AUTH = 7;
/** Authentication is in progress */
public static final byte STATE_AUTHENTICATING = 8;
/** this state indicates that we're fully connected and negotiated, and authenticating. */
public static final byte STATE_ONLINE = 9;
/** eof has been sent, no further outbound data is allowed. */
public static final byte STATE_EOF_SENT = 10;
/** eof has been sent, no further inbound data is allowed. */
public static final byte STATE_EOF_RECEIVED = 10;
/** disconnected */
public static final byte STATE_OFFLINE = 11;
/** Key exchange has failed */
public static final byte STATE_KEX_FAILED = 127;
private SshSession sshSession;
private SshPacket2 requestFailurePacket = new SshPacket2(SSHMessages.SSH_MSG_REQUEST_FAILURE);
private SshPacket2 channelFailurePacket = new SshPacket2(SSHMessages.SSH_MSG_CHANNEL_FAILURE);
/**
* key exchange state, containing client and server key data and agreement info.
*/
KexStateData kexState = null;
/**
* variables for the connection
*/
private String remoteSystemId = ""; // ("SSH-<protocolmajor>.<protocolminor>-<version>\n")
private SshCrypto2 crypto2;
private int remoteId = -1;
/**
* Username/password for auth.
*
* @todo cleanup required.
*/
private String userName, password;
protected StringBuffer dataToSend = new StringBuffer();
private byte state;
private byte authmode;
// phase : handleBytes
/**
* Used during state STATE_KEX_DH_INIT, indicates to ignore the next inbound packet due to server making wrong
* guesses as to crypto handshake
*/
private boolean ignoreNext;
protected int outgoingseq = 0;
SshPacket currentpacket;
ConnectionProperties properties;
/** Window size is the amount of data which */
private int inboundWindowSize = 0x100000; // 1MB
// note - original code had a 16k max, however spec says we MUST support packet
// size of at LEAST 32k
private int inboundMaxPacketSize = 0x8000; // //32k
/**
* max outbound window size provided to us for the channel we request.
*/
private int outboundWindowSize = 0; // 1MB
/**
* The channel ID of our terminal. This may need to be revisited as we expand to support additional channels, such
* as that required for tunnelling.
*/
private int localChannelId = -1;
// Some debugging tools for cases where we seem to be misplacing channel data.
public long dataAcceptedBytes = 0;
public long dataReceivedLen = 0;
/**
* connection has been closed for one reason or another.
*
* @todo not implemented yet.
*/
public static final byte STATE_DISCONNECTED = 10;
/**
* diffie-hellman-group-1 (oakley group 2) prime as specified in http://www.ietf.org/rfc/rfc2409.txt section 6.2
*/
public static final byte[] DIFFIE_HELLMAN_GROUP_1 = new byte[] { (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xC9, (byte) 0x0F, (byte) 0xDA,
(byte) 0xA2, (byte) 0x21, (byte) 0x68, (byte) 0xC2, (byte) 0x34, (byte) 0xC4, (byte) 0xC6, (byte) 0x62,
(byte) 0x8B, (byte) 0x80, (byte) 0xDC, (byte) 0x1C, (byte) 0xD1, (byte) 0x29, (byte) 0x02, (byte) 0x4E,
(byte) 0x08, (byte) 0x8A, (byte) 0x67, (byte) 0xCC, (byte) 0x74, (byte) 0x02, (byte) 0x0B, (byte) 0xBE,
(byte) 0xA6, (byte) 0x3B, (byte) 0x13, (byte) 0x9B, (byte) 0x22, (byte) 0x51, (byte) 0x4A, (byte) 0x08,
(byte) 0x79, (byte) 0x8E, (byte) 0x34, (byte) 0x04, (byte) 0xDD, (byte) 0xEF, (byte) 0x95, (byte) 0x19,
(byte) 0xB3, (byte) 0xCD, (byte) 0x3A, (byte) 0x43, (byte) 0x1B, (byte) 0x30, (byte) 0x2B, (byte) 0x0A,
(byte) 0x6D, (byte) 0xF2, (byte) 0x5F, (byte) 0x14, (byte) 0x37, (byte) 0x4F, (byte) 0xE1, (byte) 0x35,
(byte) 0x6D, (byte) 0x6D, (byte) 0x51, (byte) 0xC2, (byte) 0x45, (byte) 0xE4, (byte) 0x85, (byte) 0xB5,
(byte) 0x76, (byte) 0x62, (byte) 0x5E, (byte) 0x7E, (byte) 0xC6, (byte) 0xF4, (byte) 0x4C, (byte) 0x42,
(byte) 0xE9, (byte) 0xA6, (byte) 0x37, (byte) 0xED, (byte) 0x6B, (byte) 0x0B, (byte) 0xFF, (byte) 0x5C,
(byte) 0xB6, (byte) 0xF4, (byte) 0x06, (byte) 0xB7, (byte) 0xED, (byte) 0xEE, (byte) 0x38, (byte) 0x6B,
(byte) 0xFB, (byte) 0x5A, (byte) 0x89, (byte) 0x9F, (byte) 0xA5, (byte) 0xAE, (byte) 0x9F, (byte) 0x24,
(byte) 0x11, (byte) 0x7C, (byte) 0x4B, (byte) 0x1F, (byte) 0xE6, (byte) 0x49, (byte) 0x28, (byte) 0x66,
(byte) 0x51, (byte) 0xEC, (byte) 0xE6, (byte) 0x53, (byte) 0x81, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF };
/**
* diffie-hellman-group-14 (oakley group 14) prime as specified in http://www.ietf.org/rfc/rfc3526.txt section 3
*/
public static final byte[] DIFFIE_HELLMAN_GROUP_14 = new byte[] { (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xC9, (byte) 0x0F, (byte) 0xDA,
(byte) 0xA2, (byte) 0x21, (byte) 0x68, (byte) 0xC2, (byte) 0x34, (byte) 0xC4, (byte) 0xC6, (byte) 0x62,
(byte) 0x8B, (byte) 0x80, (byte) 0xDC, (byte) 0x1C, (byte) 0xD1, (byte) 0x29, (byte) 0x02, (byte) 0x4E,
(byte) 0x08, (byte) 0x8A, (byte) 0x67, (byte) 0xCC, (byte) 0x74, (byte) 0x02, (byte) 0x0B, (byte) 0xBE,
(byte) 0xA6, (byte) 0x3B, (byte) 0x13, (byte) 0x9B, (byte) 0x22, (byte) 0x51, (byte) 0x4A, (byte) 0x08,
(byte) 0x79, (byte) 0x8E, (byte) 0x34, (byte) 0x04, (byte) 0xDD, (byte) 0xEF, (byte) 0x95, (byte) 0x19,
(byte) 0xB3, (byte) 0xCD, (byte) 0x3A, (byte) 0x43, (byte) 0x1B, (byte) 0x30, (byte) 0x2B, (byte) 0x0A,
(byte) 0x6D, (byte) 0xF2, (byte) 0x5F, (byte) 0x14, (byte) 0x37, (byte) 0x4F, (byte) 0xE1, (byte) 0x35,
(byte) 0x6D, (byte) 0x6D, (byte) 0x51, (byte) 0xC2, (byte) 0x45, (byte) 0xE4, (byte) 0x85, (byte) 0xB5,
(byte) 0x76, (byte) 0x62, (byte) 0x5E, (byte) 0x7E, (byte) 0xC6, (byte) 0xF4, (byte) 0x4C, (byte) 0x42,
(byte) 0xE9, (byte) 0xA6, (byte) 0x37, (byte) 0xED, (byte) 0x6B, (byte) 0x0B, (byte) 0xFF, (byte) 0x5C,
(byte) 0xB6, (byte) 0xF4, (byte) 0x06, (byte) 0xB7, (byte) 0xED, (byte) 0xEE, (byte) 0x38, (byte) 0x6B,
(byte) 0xFB, (byte) 0x5A, (byte) 0x89, (byte) 0x9F, (byte) 0xA5, (byte) 0xAE, (byte) 0x9F, (byte) 0x24,
(byte) 0x11, (byte) 0x7C, (byte) 0x4B, (byte) 0x1F, (byte) 0xE6, (byte) 0x49, (byte) 0x28, (byte) 0x66,
(byte) 0x51, (byte) 0xEC, (byte) 0xE4, (byte) 0x5B, (byte) 0x3D, (byte) 0xC2, (byte) 0x00, (byte) 0x7C,
(byte) 0xB8, (byte) 0xA1, (byte) 0x63, (byte) 0xBF, (byte) 0x05, (byte) 0x98, (byte) 0xDA, (byte) 0x48,
(byte) 0x36, (byte) 0x1C, (byte) 0x55, (byte) 0xD3, (byte) 0x9A, (byte) 0x69, (byte) 0x16, (byte) 0x3F,
(byte) 0xA8, (byte) 0xFD, (byte) 0x24, (byte) 0xCF, (byte) 0x5F, (byte) 0x83, (byte) 0x65, (byte) 0x5D,
(byte) 0x23, (byte) 0xDC, (byte) 0xA3, (byte) 0xAD, (byte) 0x96, (byte) 0x1C, (byte) 0x62, (byte) 0xF3,
(byte) 0x56, (byte) 0x20, (byte) 0x85, (byte) 0x52, (byte) 0xBB, (byte) 0x9E, (byte) 0xD5, (byte) 0x29,
(byte) 0x07, (byte) 0x70, (byte) 0x96, (byte) 0x96, (byte) 0x6D, (byte) 0x67, (byte) 0x0C, (byte) 0x35,
(byte) 0x4E, (byte) 0x4A, (byte) 0xBC, (byte) 0x98, (byte) 0x04, (byte) 0xF1, (byte) 0x74, (byte) 0x6C,
(byte) 0x08, (byte) 0xCA, (byte) 0x18, (byte) 0x21, (byte) 0x7C, (byte) 0x32, (byte) 0x90, (byte) 0x5E,
(byte) 0x46, (byte) 0x2E, (byte) 0x36, (byte) 0xCE, (byte) 0x3B, (byte) 0xE3, (byte) 0x9E, (byte) 0x77,
(byte) 0x2C, (byte) 0x18, (byte) 0x0E, (byte) 0x86, (byte) 0x03, (byte) 0x9B, (byte) 0x27, (byte) 0x83,
(byte) 0xA2, (byte) 0xEC, (byte) 0x07, (byte) 0xA2, (byte) 0x8F, (byte) 0xB5, (byte) 0xC5, (byte) 0x5D,
(byte) 0xF0, (byte) 0x6F, (byte) 0x4C, (byte) 0x52, (byte) 0xC9, (byte) 0xDE, (byte) 0x2B, (byte) 0xCB,
(byte) 0xF6, (byte) 0x95, (byte) 0x58, (byte) 0x17, (byte) 0x18, (byte) 0x39, (byte) 0x95, (byte) 0x49,
(byte) 0x7C, (byte) 0xEA, (byte) 0x95, (byte) 0x6A, (byte) 0xE5, (byte) 0x15, (byte) 0xD2, (byte) 0x26,
(byte) 0x18, (byte) 0x98, (byte) 0xFA, (byte) 0x05, (byte) 0x10, (byte) 0x15, (byte) 0x72, (byte) 0x8E,
(byte) 0x5A, (byte) 0x8A, (byte) 0xAC, (byte) 0xAA, (byte) 0x68, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF
};
/**
* Generator for DH-group-14 and DH-group-1 is 2.
*/
public static final byte[] DIFFIE_HELLMAN_GENERATOR = new byte[] { 2 };
// authentication
/**
* Initialise SshIO
*
* @param sshSession
*/
public SshIO(SshSession sshSession) {
this.sshSession = sshSession;
// @todo this is REALLY ugly... This also needs to be cleaned up with session hanlding rewrite.
this.properties = sshSession.getProperties();
}
/**
* write data to our back end
*
* @param b
* data to write
*/
public void write(byte[] b) throws IOException {
sshSession.sendData(b);
}
/**
* Send SSHMessages.SSH_MSG_DISCONNECT.
*
* @param reason
* - reason code: reference RFC 4253 S 11.1 for details.
* @param reasonText
* human-readable disconnect reason.
* @throws IOException
*/
public void sendDisconnect(int reason, String reasonText) throws IOException {
if (remoteId != -1) {
SshPacket2 pn = new SshPacket2(SSHMessages.SSH_MSG_DISCONNECT);
pn.putInt32(reason);
pn.putString(reasonText);
pn.putString("en");
sendPacket2(pn);
setState(STATE_DISCONNECTED);
}
}
/**
* When connected, send outbound data. If not connected, the data will be buffered for when the connection is
* available.
*
* @param data
* @param offset
* @param length
* @throws IOException
*/
public void sendData(byte[] data, int offset, int length) throws IOException {
dataToSend.append(new String(data, offset, length));
if (state == STATE_ONLINE) {
// if (sshSession.getOutputBufferLength() == 0) {
sendOutboundChannelData();
// }
} else if (state >= STATE_EOF_SENT) {
Logger.warn("Received request to send channel data after EOF sent. Ignoring.");
}
}
/**
* If any outbound data is queued, send it in a CHANNEL_DATA packet and reset the data queue.
*
* @throws IOException
*/
protected void sendOutboundChannelData() throws IOException {
if (dataToSend == null)
return;
SshPacket2 pn = new SshPacket2(SSHMessages.SSH_MSG_CHANNEL_DATA);
pn.putInt32(remoteId); // channel id
pn.putString(dataToSend.toString());
sendPacket2(pn);
outboundWindowSize -= dataToSend.length();
dataToSend = new StringBuffer();
}
/**
* Handles all incoming data when we haev not yet initialized version strings.
*
* @param buff
* inbound data buffer
* @param offset
* starting offset within the buffer
* @param offsetEnd
* ending offset within the buffer
* @return new offset.
* @throws IOException
*/
private int handleInitState(byte[] buff, int offset, int length, int offsetEnd) throws IOException {
byte b; // of course, byte is a signed entity (-128 -> 127)
while (offset < offsetEnd) {
b = buff[offset++];
// both sides MUST send an identification string of the form
// "SSH-protocolversion-softwareversion comments",
// followed by newline character(asc 10)
remoteSystemId += (char) b;
// Note if server version is 1.99 then LINE FEED (\r - asc 13)
// MAY not be sent, but CR (\n - asc 10) WILL still be sent.
if (b == '\n') {
if (remoteSystemId.startsWith("SSH-")) {
int remotemajor = Integer.parseInt(remoteSystemId.substring(4, 5));
String minorverstr = remoteSystemId.substring(6, 8);
if (!Character.isDigit(minorverstr.charAt(1))) {
minorverstr = minorverstr.substring(0, 1);
}
int remoteminor = Integer.parseInt(minorverstr);
// reference RFC4253 section 5.1
if (remotemajor == 1 && remoteminor == 99) {
remotemajor = 2;
remoteminor = 0;
}
if (remotemajor != 2) {
throw new IOException("Unsupported SSH version: " + remotemajor);
}
setState(STATE_INIT_COMPLETE);
currentpacket = new SshPacket2();
break;
} else {
// Lines sent during init that do not start SSH- SHOULDbe ignored
remoteSystemId = "";
}
}
}
return offset;
}
private void setState(byte state) {
this.state = state;
if (state < STATE_ONLINE) {
sshSession.setConnectionState(Session.CONNSTATE_CONNECTING);
} else if (state == STATE_ONLINE) {
sshSession.setConnectionState(Session.CONNSTATE_CONNECTED);
} else if (state <= STATE_DISCONNECTED) {
sshSession.setConnectionState(Session.CONNSTATE_DISCONNECTING);
} else {
// This is up to our container - disconnection occurs when the network state
// is set to disconected; we can only say that the protocol is
// disconnected.
// sshSession.setConnectionState(Session.CONNSTATE_DISCONNECTED);
}
}
/**
* If server has also sent first KEX packet, we must check to make ure that server's "guesses" are valid. Wrong
* guesses means that the next packet will be sent based on wrong assumptions (eg, wrong KEX algorithm means that
* the next KEX algorithm packet can't be the right one), and so we must ignore it.
*/
private void checkIgnoreNextPacket() {
// @todo we must check ALL possible matches - as the spec does not exclude any of them
if (!Tools.doFirstElementsMatch(kexState.serverKEX.getKexAlgorithms(), kexState.clientKEX.getKexAlgorithms())
|| !Tools.doFirstElementsMatch(kexState.serverKEX.getServerHostKeyAlgorithms(), kexState.clientKEX
.getServerHostKeyAlgorithms())
|| !Tools.doFirstElementsMatch(kexState.serverKEX.getMACClientToServer(), kexState.clientKEX
.getMACClientToServer())
|| !Tools.doFirstElementsMatch(kexState.serverKEX.getMACServerToClient(), kexState.clientKEX
.getMACServerToClient())) {
ignoreNext = true;
}
}
private String handleChannelOpenConfirmation2(SshPacket2 p) throws IOException {
localChannelId = p.getInt32(); // localId
remoteId = p.getInt32();
// Open PTY
SshPacket2 pn = new SshPacket2(SSHMessages.SSH_MSG_CHANNEL_REQUEST);
pn.putInt32(remoteId);
pn.putString("pty-req");
pn.putByte((byte) 0); // want reply- no (for now)
pn.putString(getTerminalID());
// @todo maybe we want to enable replies so we can confirm that
// this request was accepted, before we send the next one or "shell"?
pn.putInt32(getTerminalWidth());
pn.putInt32(getTerminalHeight());
pn.putInt32(0);
pn.putInt32(0);
pn.putString("");
sendPacket2(pn);
// Open Shell
pn = new SshPacket2(SSHMessages.SSH_MSG_CHANNEL_REQUEST);
pn.putInt32(remoteId);
pn.putString("shell");
// @todo maybe we want to enable replies so we can confirm that
// this request was accepted, before we begin shunting data along the pipe?
pn.putByte((byte) 0); // want reply (for now)
sendPacket2(pn);
setState(STATE_ONLINE);
sendOutboundChannelData();
return "Shell opened\r\n";
}
private String handleUserAuthFailure2(SshPacket2 p) throws IOException {
if (state != STATE_AUTHENTICATING) {
throw new IOException("Received authentication failure ");
}
String message = authenticate2();
if (message != null) {
return message;
} else {
// Session.CONN_STATE_AUTH_FAILED
return "Authentication failed.\r\nAvailable methods are: " + p.getString() + "\r\n";
}
}
private String handleUserAuthBanner(SshPacket2 p) throws IOException {
if (state >= STATE_REQUESTING_AUTH && state < STATE_ONLINE) {
String message = p.getString();
// p.getString -- language tag
Logger.debug("BANNER received: " + message);
return "\r\n" + message + "\r\n";
}
Logger.error("BANNER received and not expected: not in auth.");
return "";
}
private String handleUserAuthSuccess2(SshPacket2 p) throws IOException {
if (state != STATE_AUTHENTICATING) {
// @todo in these cases, our exception handling mUST send a disconnect.
throw new IOException("Received authentication success with no auth request outstanding. ");
}
setState(STATE_OPENING_CHANNEL);
// Open channel
SshPacket2 pn = new SshPacket2(SSHMessages.SSH_MSG_CHANNEL_OPEN);
pn.putString("session");
pn.putInt32(0); // sender-channel ID
pn.putInt32(inboundWindowSize); // initial window size
pn.putInt32(inboundMaxPacketSize); // max packet size
sendPacket2(pn);
return "Authentication accepted\r\n";
}
public void sendUserauthInfoResponse(String[] responses) throws IOException {
if (state != STATE_AUTHENTICATING) {
// @todo in these cases, our exception handling mUST send a disconnect.
throw new IOException("Received AUTH REQUEST message with no auth request outstanding. ");
}
SshPacket2 buf = new SshPacket2(SSHMessages.SSH_MSG_USERAUTH_INFO_RESPONSE);
buf.putInt32(responses.length);
for (int i = 0; i < responses.length; i++) {
buf.putString(responses[i]);
}
sendPacket2(buf);
}
/**
* Acceptp data from the remote host. Blocks until data is available. Dispatches to appropriate method based on
* state (handleInitState or handlePacket)
*
* @param buff
* input buffer to parse.
* @param offset
* starting point within the buffer
* @param length
* number of bytes to use from the buffer.
* @return array of bytes to be displayed.
* @throws IOException
*/
public byte[] handleSSH(byte buff[], int offset, int length, Session session) throws IOException {
String result;
int end = offset + length;
if (state == STATE_INIT) {
offset = handleInitState(buff, offset, length, end);
}
result = "";
// While we haven't consumed all of the data, stuff it into one more more packets.
while (offset < end) {
offset = currentpacket.addPayload(buff, offset, (end - offset));
if (currentpacket.isFinished()) {
result = result + handlePacket2((SshPacket2) currentpacket, session);
if ((state < STATE_ONLINE || state == STATE_DISCONNECTED) && result != null && result.length() > 0) {
Logger.info(result);
}
currentpacket = new SshPacket2(crypto2);
}
}
return result.getBytes();
}
/**
* Given a host key return the finger print string for that key.
*
* @param host_key
* @return
*/
private String fingerprint(byte[] host_key) {
// @todo - is this all we use this member for? If so,
// it shouldn't be a member at all... never mind a STATIC member.
MD5Digest digest = new MD5Digest();
digest.update(host_key);
byte[] fprint = digest.getDigest();
return Tools.getBytesAsHexString(fprint, fprint.length);
// @todo host key validation has to occur - easiest to do it based
// on hash -> hostname mapping?
}
/**
* Handle SSH protocol Version 2
*
* @param p
* the packet we will process here.
* @param session
* the session sending us this data.
* @return a array of bytes
*/
protected String handlePacket2(SshPacket2 p, Session session) throws IOException {
sshSession.inputPacketCount++;
if (state == STATE_KEX_DH_INIT) {
if (ignoreNext) {
ignoreNext = false;
return "Invalid algorithm assumed by server, discarding...\r\n";
}
}
byte type = p.getType();
// don't assemble the string if there's no need...
switch (type) {
case SSHMessages.SSH_MSG_KEXINIT: {
try {
return handleKexInit2(p);
} catch (IOException e) {
sendDisconnect(3, e.getMessage());
return e.getMessage();
}
}
case SSHMessages.SSH_MSG_KEXDH_REPLY:
try {
return handleKexDHReply2(p);
} catch (IOException e) {
sendDisconnect(3, e.getMessage());
return e.getMessage();
}
case SSHMessages.SSH_MSG_NEWKEYS: {
// @todo make sure this works for ongoing sessions (rekeying)
try {
return handleNewKeysRequest2(p);
} catch (IOException e) {
sendDisconnect(3, "Key exchange failed.");
return e.getMessage();
}
}
case SSHMessages.SSH_MSG_SERVICE_ACCEPT:
if (state != STATE_REQUESTING_AUTH) {
throw new IOException("Received SERVICE_ACCEPT for unrecognized service: " + p.getString());
} else {
Logger.info("SERVICE_ACCEPT received: " + p.getString());
}
// Server has accepted our auth service request, let's start actual authentication now.
setState(STATE_AUTHENTICATING);
return authenticate2();
case SSHMessages.SSH_MSG_USERAUTH_BANNER:
return handleUserAuthBanner(p);
case SSHMessages.SSH_MSG_USERAUTH_FAILURE:
return handleUserAuthFailure2(p);
case SSHMessages.SSH_MSG_USERAUTH_INFO_REQUEST:
sendUserauthInfoResponse(handleUserAuthInfoRequest(p));
break;
case SSHMessages.SSH_MSG_CHANNEL_OPEN_FAILURE:
throw new IOException("Server refused channel open request, cannot continue.");
case SSHMessages.SSH_MSG_USERAUTH_SUCCESS:
return handleUserAuthSuccess2(p);
case SSHMessages.SSH_MSG_CHANNEL_OPEN_CONFIRMATION:
return handleChannelOpenConfirmation2(p);
// From the spec, we *shouldn't get this* unless the server
// requests a channel open and we accept...
case SSHMessages.SSH_MSG_CHANNEL_WINDOW_ADJUST:
return handleChannelWindowAdjust(p);
case SSHMessages.SSH_MSG_CHANNEL_DATA:
// @todo We'll need a ChannelHandler: onWindowAdjust, onData, onClose, onOpenConfirm
return handleChannelData(p, session);
case SSHMessages.SSH_MSG_CHANNEL_EOF:
// Indicates we wil receive no more data on this channel.
Logger.warn(" Received EOF for channel : " + p.getInt32() + " - CLOSE expected next.");
break;
case SSHMessages.SSH_MSG_CHANNEL_CLOSE:
Logger.warn(" Received CLOSE for channel : " + p.getInt32());
remoteId = -1;
localChannelId = -1;
// @todo once we supprot multiple channels, this will need to be refined:
// we can't close unless everything is done, including any active forwarding;
// in addition we need to check to make sure that the close channel is one we have open.
sendDisconnect(11, "Finished");
break;
case SSHMessages.SSH_MSG_DISCONNECT:
int reason = p.getInt32(); // disconnect reason
String msg = "\r\nDisconnected: " + p.getString() + "(RC " + reason + ")\r\n";
Logger.warn(msg);
session.disconnect();
return msg;
case SSHMessages.SSH_MSG_GLOBAL_REQUEST:
Logger.warn("Denying global request: " + p.getString());
// want-reply flag - some servers use this w/ a bogus request to manage keepalive.
if (p.getByte() == 1) {
sendPacket2(requestFailurePacket);
}
break;
case SSHMessages.SSH_MSG_CHANNEL_REQUEST:
return handleChannelRequest(p);
case SSHMessages.SSH_MSG_DEBUG:
if (p.getByte() > 0) { // always-display
// language-tag is also in this msg, but we don't really care
// about it.
// @todo control code filtering of text here and elsewhere
return "DEBUG_MSG: " + p.getString() + "\r\n";
} else {
Logger.debug("Remote debug message: " + p.getString());
}
break;
case SSHMessages.SSH_MSG_REQUEST_FAILURE:
Logger.warn("Unexpected SSH_MSG_REQUEST_FAILURE");
break;
case SSHMessages.SSH_MSG_REQUEST_SUCCESS:
Logger.warn("Unexpected SSH_MSG_REQUEST_SUCCESS");
break;
case SSHMessages.SSH_MSG_IGNORE:
Logger.debug("Unexpected SSH_MSG_IGNORE");
break;
}
return "";
}
private void sendChannelFailurePacket(String name, byte wantReply) throws IOException {
Logger.warn("Denying remote channel request: " + name);
// want-reply flag - some servers use this w/ a bogus request to manage keepalive
if (wantReply == 1) {
sendPacket2(channelFailurePacket);
}
}
public void sendChannelClose() throws IOException {
if (remoteId != -1) {
SshPacket2 eofChannel = new SshPacket2(SSHMessages.SSH_MSG_CHANNEL_EOF);
eofChannel.putInt32(remoteId);
sendPacket2(eofChannel);
SshPacket2 closeChannel = new SshPacket2(SSHMessages.SSH_MSG_CHANNEL_CLOSE);
closeChannel.putInt32(remoteId);
sendPacket2(closeChannel);
remoteId = -1;
localChannelId = -1;
}
}
/**
* @param signal
* one of: ABRT ALRM FPE HUP ILL INT KILL PIPE QUIT SEGV TERM USR1 USR2
* @throws IOException
*/
public void sendSIGRequest(String signal) throws IOException {
SshPacket2 sig = new SshPacket2(SSHMessages.SSH_MSG_CHANNEL_REQUEST);
sig.putInt32(remoteId);
sig.putByte((byte) 0);
sig.putString(signal);
sendPacket2(sig);
}
private String handleChannelRequest(SshPacket2 p) throws IOException {
int channel = p.getInt32();
String name = p.getString();
byte wantReply = p.getByte();
Logger.info("Received channel request: " + channel + " / " + name + " / " + wantReply);
if (channel != localChannelId) {
Logger.error("Channel incorrect - expected " + localChannelId + " and got " + channel);
sendChannelFailurePacket(name, wantReply);
return "";
}
// @todo - dispatch to channel request handler?
if (name.equalsIgnoreCase("exit-status")) { // RFC 4254 s 6.10
int exitStatus = p.getInt32();
sendChannelClose();
Logger.warn("Remote exit status: " + exitStatus);
this.sshSession.disconnect();
return "\r\nRemote channel closed\r\n";
} else if (name.equalsIgnoreCase("exit-signal")) { // RFC 4254 s 6.10
String sigName = p.getString();
boolean coreDump = (p.getByte() == 1);
String message = p.getString();
sendChannelClose();
Logger.info("exit-signal cdump: " + coreDump + " message: " + message);
// String languageTag = p.getString();
this.sshSession.disconnect();
// @todo in this and other cases of channel data, we need to filter out control codes.
return "\r\nRemote channel terminated with SIG " + sigName + "\r\n" + message + "\r\n";
}
// No other channelrequests supported right now.
sendChannelFailurePacket(name, wantReply);
return "";
}
private String handleChannelWindowAdjust(SshPacket2 p) {
int chan = p.getInt32();
// @todo - BUG - a lot of cases call for UINT32, not INT32!
int size = p.getInt32();
Logger.info(" remote session requested req " + size + " bytes on chan " + chan);
if (chan == localChannelId) {
// @todo - we should have some error handling if the total is > 2^32-1
outboundWindowSize += size;
} else {
// @todo - this shouldn't happen....
Logger.warn(" bad channel " + chan);
}
return "";
}
protected String handleChannelData(SshPacket2 p, Session session) throws IOException {
if (state >= STATE_EOF_RECEIVED) {
Logger.error("\r\nChannel data received after EOF, ignored.");
return "";
}
// @todo - set default log level to INFO , while DEBUG is only when specifically enabled.
// @todo we need to start tracking within channel - esp if we plan to support sftp /scp
p.getInt32(); // localId
// extended charset support
// @todo UTF8 - requires TRANSLATION LAYER
// byte[] b = p.getMpInt();
// String data = new String(b);// getString();
// No charset support
String data = p.getString();
int len = data.length();
dataReceivedLen += len;
if (len < inboundWindowSize) {
inboundWindowSize -= data.length();
}
if (len == inboundWindowSize) {
Logger.info("Window size matched, requesting adjustment.");
inboundWindowSize -= data.length();
sendChannelWindowAdjustPacket();
} else if (len > inboundWindowSize) {
Logger.info("Window size exceeded, truncating data and requesting adjustment.");
// Accept only up to the max data we'll allow
data = data.substring(0, inboundWindowSize);
inboundWindowSize = 0;
sendChannelWindowAdjustPacket();
}
dataAcceptedBytes += data.length();
return data;
}
private void sendChannelWindowAdjustPacket() throws IOException {
SshPacket2 pn = new SshPacket2(SSHMessages.SSH_MSG_CHANNEL_WINDOW_ADJUST);
// @todo - make this a constant 1MB or something... .
pn.putInt32(remoteId);
pn.putInt32(0x100000);
// @todo - is there no ack for this?! this could mean we start accepting data sent
// prior to the change notice sent to server... data we should be ignoring
inboundWindowSize += 0x100000;
sendPacket2(pn);
}
/**
* Attempt authentication. This is a rentrant method. If a given auth attempt fails, it will try the next level
* down, as follows:
* <ul>
* <li>Public Key</li>
* <li>Password</li>
* <li>Keyboard Interactive</li>
* </ul>
* Note that it currently does not respect the auth methods supporte dby the server, instead blindly trying all
* three down the list. - public key authentication if any key exists.
*
* @return
* @throws IOException
*/
private String authenticate2() throws IOException {
// @todo to be compliant with spec, I believe that we should be
// listening to what the server tells us it supports in terms of auth modes
// and only attempting those supported.
SshPacket2 buf = new SshPacket2(SSHMessages.SSH_MSG_USERAUTH_REQUEST);
Key k = sshSession.getKey();
if (k != null && k.getId() != Key.INVALID_ID && authmode < MODE_PUBLICKEY) {
Logger.debug("Attempting publickey auth.");
authmode = MODE_PUBLICKEY;
String type;
String pass = k.getPassphrase();
KeyPair key = null;
if (k.isNativeKey()) {
key = k.getKeyPair();
} else {
do {
try {
key = PEMDecoder.decode(k.getData(), pass);
} catch (InvalidKeyException e) {
throw new IOException("Key is not valid.");
} catch (IOException e) {
// Assume password failed.
// @todo - this is NOT then reliant on session,a nd SHOULD be handled in a separate interface
// eg not SessionListener
Logger.warn("IOException " + e.getMessage() + " - could not decrypt, assuming password error and prompting for password.");
pass = sshSession.getListener().getKeyPassword(sshSession.getSessionId(), k);
if (pass == null) {
Logger.error("New password didn't fix the problem- failing.");
throw e;
}
}
} while (key == null);
}
PacketUserauthRequestPublicKey ua;
try {
if (key instanceof DSAKeyPair) {
DSAPrivateKey pk = ((DSAKeyPair) key).getDSAPrivateKey();
ua = new PacketUserauthRequestPublicKey(pk, sessionId, "ssh-connection", userName, "ssh-dss");
type = "DSA";
} else if (key instanceof RSAKeyPair) {
RSAPrivateKey pk = ((RSAKeyPair) key).getRSAPrivateKey();
ua = new PacketUserauthRequestPublicKey(pk, sessionId, "ssh-connection", userName, "ssh-rsa");
type = "RSA";
} else {
throw new IOException("Unknown private key encryption.");
}
} catch (CryptoException e) {
throw new IOException("Unexpected CryptoException occurred: " + e.toString());
}
byte[] b = ua.getPayload();
// A bit of a bridging hack here between old and
// new auth systems - the packet will automaticlaly incldue the
// userauthrequest byte in our SshPacket2 implementation,
// so we need to skip past that in our raw data buffer from
// the new implementation. (The new implementation must include that
// byte as it must be present when the checksum is calculated.)
buf.putBytes(b, 1, b.length - 1);
sendPacket2(buf);
return "Sent public key: " + type + ".\r\n";
}
buf.putString(userName);
buf.putString("ssh-connection");
if (authmode < MODE_PASSWORD && password != null && password.length() > 0) {
// @todo - we aren't we checking to see if this is allowed? We
// *have* that info!
authmode = MODE_PASSWORD;
buf.putString("password");
buf.putByte((byte) 0);
buf.putString(password);
sendPacket2(buf);
// @todo Session.CONN_STATE_SENT_PASSWORD
return "Sent password\r\n";
} else if (authmode < MODE_KEYBOARD_INTERACTIVE) {
// @todo - we aren't we checking to see if this is allowed? We
// *have* that info!
/* Attempt keyboard-interactive auth */
authmode = MODE_KEYBOARD_INTERACTIVE;
buf.putString("keyboard-interactive");
buf.putString("");
buf.putString("");
sendPacket2(buf);
// @todo Session.CONN_STATE_BEGIN_KBD_INTERACTIVE
return "Start keyboard-interactive\r\n";
} else {
return null;
}
}
public void sendPacket2(SshPacket2 packet) throws IOException {
write(packet.getPayLoad(crypto2, outgoingseq));
sshSession.outputPacketCount++;
outgoingseq++;
}
private byte[] sessionId;
/**
* Obtains key from hash, as per RFC 4253 S 2, applying rules about minium lengths. Takes a further steps for bb
* crypto compatibiltiy, and ensures that the key is EXACTLY the expected length - it will trim anything longer than
* required.
*
* @param hash
* digest which contains the KEX data digest thus far.
* @param keyLen
* length of the key for the agreed-upon encrpytion type.
* @return
*/
private byte[] calculateFinalKey(SHA1Digest hash, int keyLen) {
byte[] source = hash.getDigest();
while (keyLen > source.length) {
SshPacket2 buf = new SshPacket2();
buf.putMpInt(kexState.K);
buf.putBytes(kexState.H);
buf.putBytes(source);
byte[] b = buf.getData();
hash.update(b);
byte[] foo = hash.getDigest();
byte[] bar = new byte[source.length + foo.length];
// First copy in what we started with
System.arraycopy(source, 0, bar, 0, source.length);
// Now append our new hash.
System.arraycopy(foo, 0, bar, source.length, foo.length);
source = bar;
}
return Tools.trimBytesToLength(source, keyLen);
}
/**
* Implements handling for KEX_INIT message, as per http://www.ietf.org/rfc/rfc4253.txt Section 7.1
*
* @todo replace DH kex init with DH group exchange, per rfc4419
* @todo - on ioexception thrown , make sure ew send disconnect on keys message.
*/
private String handleKexInit2(SshPacket2 srvPacket) throws IOException {
// @todo - test init key size of BigINteger(1024 bit)
try {
if (state == STATE_KEX_INIT) {
throw new IOException("Received KEX INIT while prior init was not resolved");
}
setState(STATE_KEX_INIT);
// We receive info on what the server will support; and
// must respond in kind.
kexState = new KexStateData();
kexState.serverKEX = KexInitData.createInstanceFromPacket(srvPacket);
kexState.clientKEX = KexInitData.createInstance();
SshPacket2 clnPacket = kexState.clientKEX.createOutboundPacket();
sendPacket2(clnPacket);
setState(STATE_KEX_DH_INIT);
// I_S and I_C are the payload of the client and server SSH_MSG_KEXINIT,
// including the message ID itself. Our packets don't include message ID,
// so we have to insert it in order for the hash to verify properly.
kexState.I_S = Tools.insertByteValue((byte) SSHMessages.SSH_MSG_KEXINIT, srvPacket.getData());
kexState.I_C = Tools.insertByteValue((byte) SSHMessages.SSH_MSG_KEXINIT, clnPacket.getData());
kexState.agreement = KexInitData.findAgreement(kexState.serverKEX, kexState.clientKEX);
if (kexState.serverKEX.isFirstKEXPacketFollowing()) {
checkIgnoreNextPacket();
}
byte[] prime;
if (kexState.agreement.kexAlgorithm.equals("diffie-hellman-group1-sha1")) {
prime = DIFFIE_HELLMAN_GROUP_1; // Oakley Group 2 - RFC2409
} else {
prime = DIFFIE_HELLMAN_GROUP_14; // group14-sha1 - Oakley Group 14 - RFC3526
}
try {
kexState.dhCryptoSystem = new DHCryptoSystem(prime, new byte[] { 2 });
} catch (InvalidCryptoSystemException ex) {
throw new IOException("InvalidCryptoSystemException: " + ex.getMessage());
} catch (UnsupportedCryptoSystemException ex) {
throw new IOException("UnsupportedCryptoSystemException: " + ex.getMessage());
}
kexState.keyPair = kexState.dhCryptoSystem.createDHKeyPair();
SshPacket2 dhInit = new SshPacket2(SSHMessages.SSH_MSG_KEXDH_INIT);
dhInit.putMpInt(kexState.keyPair.getDHPublicKey().getPublicKeyData()); // e
sendPacket2(dhInit);
} catch (CryptoTokenException ex) {
throw new IOException("CryptoTokenException: " + ex.getMessage());
} catch (CryptoUnsupportedOperationException ex) {
throw new IOException("CryptoUnsupportedOperationException: " + ex.getMessage());
}
return "Negotiating...";
}
/**
* This is received upon completion of key negotiation , wherein the server accepts our keys. If we do NOT accept
* the server keys, we MUST send a disconnect at this point. Note that once we receive this message, all subsequent
* messages MUST be decrypted using the data obtained during KEX.
*
* @param p
* new key request packet.
* @return
* @throws IOException
*/
private String handleNewKeysRequest2(SshPacket2 p) throws IOException {
if (state != STATE_SECURE) {
// SSH_DISCONNECT_KEY_EXCHANGE_FAILED = 3
if (state == STATE_KEX_FAILED) {
throw new IOException("Key exchange failed -- agreement could not be reached.");
} else {
throw new IOException("Received key acceptance from server but was not expectign it.");
}
}
// After MSG_NEWKEYS, everything must be encrypted - so we'll send this before updating keys.
sendPacket2(new SshPacket2(SSHMessages.SSH_MSG_NEWKEYS));
// Great, now that we've agreed and everyone is happy, let's
// update our keys, tell the server, and submit the user auth request.
// request user authentication
try {
updateKeys();
} catch (NoSuchAlgorithmException e) {
// This should never happen, but let's play it safe...
sendDisconnect(3, "Key exchange failed - invalid algorithm.");
throw new IOException("Key exchange failed -- algorithm not found.");
}
setState(STATE_REQUESTING_AUTH);
SshPacket2 pn = new SshPacket2(SSHMessages.SSH_MSG_SERVICE_REQUEST);
pn.putString("ssh-userauth");
sendPacket2(pn);
// Note: do we want to null out kexdata at this point? In no case do we
// need any of it - a key re-exchange will negate it, and everything else that
// needs those values will now have been initialize.
return "Requesting authentication\r\n";
}
/**
* Handler for message SSH_MSG_KEXDH_REPLY; reference RFC4253 sec. 8.0 for implementation explanation.
*
* @param p
* packet as receievd from the server
* @return status update string to be displayed on terminal
* @throws IOException
* in event of any failure
*/
private String handleKexDHReply2(SshPacket2 p) throws IOException {
try {
if (state != STATE_KEX_DH_INIT) {
throw new IOException("Received SSHMessages.SSH_MSG_KEXDH_REPLY but not in DH_INIT!");
}
kexState.K_S = p.getByteString();
kexState.serverPubKey = new DHPublicKey(kexState.dhCryptoSystem, p.getMpInt()); // f
kexState.SigOfH = p.getByteString(); // Signature of H
kexState.K = DHKeyAgreement.generateSharedSecret(kexState.keyPair.getDHPrivateKey(), kexState.serverPubKey,
false);
SshPacket2 buf = new SshPacket2();
buf.putString(Version.getVersionSSHIDString().trim().getBytes()); // client's identification string
buf.putString(remoteSystemId.trim().getBytes()); // server's identification string, excluding crlf
buf.putString(kexState.I_C); // payload of the client's SSH_MSG_KEXINIT (including message ID)
buf.putString(kexState.I_S); // payload of the server's SSH_MSG_KEXINIT (including message ID)
buf.putString(kexState.K_S); // server host key
buf.putMpInt(kexState.keyPair.getDHPublicKey().getPublicKeyData()); // e, the public key we sent
buf.putMpInt(kexState.serverPubKey.getPublicKeyData()); // f, the public key we received in this message.
buf.putMpInt(kexState.K); // K, the shared secret
SHA1Digest sha1 = new SHA1Digest();
sha1.update(buf.getBytes());
// This will be used in verifying that the message originated from the server's key
kexState.H = sha1.getDigest();
// if we update keys later, session ID is not allowed to change
if (sessionId == null) {
sessionId = new byte[kexState.H.length];
System.arraycopy(kexState.H, 0, sessionId, 0, kexState.H.length);
}
// Ah, finally - we can build our signature
// we have what we need to validate that the server has the correct data/keys.
// The specifics will vary based on host key
validateServerKeys();
// When we get the NEW_KEYS request, we'll refresh our crypto data.
setState(STATE_SECURE);
} catch (CryptoException ex) {
// We tell the user here -- but we won't actually disconnect until
// we receiev NEWKEYS, per the spec.
setState(STATE_KEX_FAILED);
// @todo we're not showing this retuned string?
return "\r\nKEX FAILED: " + ex.toString() + "(" + ex.getMessage() + ")\r\n";
}
// Okay, we're good. Tell the server that we accept this.
// @todo look at a better option for fingerprint: - visual fingerprint?
// @todo host checking.
return "OK\r\n" + kexState.keyAlgorithm + " " + fingerprint(kexState.K_S) + "\r\n";
}
/**
* Set up crypto and associated data keys as agreed upon in the key exchange. Reference RFC 4253 section 7.2 for
* explanation and details of the implementation.
*
* @throws NoSuchAlgorithmException
*/
private void updateKeys() throws NoSuchAlgorithmException {
CipherManager m = CipherManager.getInstance();
CipherAttributes sendAttr = m.getCipherAttributes(kexState.agreement.clientToServerCryptoAlgorithm);
CipherAttributes recvAttr = m.getCipherAttributes(kexState.agreement.serverToClientCryptoAlgorithm);
SHA1Digest hash = new SHA1Digest();
SshPacket2 buf = new SshPacket2();
int sendKeySize = sendAttr.getKeySize();
int recvKeySize = recvAttr.getKeySize();
buf.putMpInt(kexState.K);
buf.putBytes(kexState.H);
buf.putByte((byte) 0); // we'll be replacing this for each new hash we're generating .
int x = buf.getLength() - 1; // mark the location so we know to replace here.
buf.putBytes(sessionId);
byte[] data = buf.getData();
// @todo - can make all of these into a single fn call -
// getSizedHashValue(data, 'A', minLength = 0, maxLength = 0)
data[x] = 'A'; // HASH(K || H || "A" || session_id)
hash.update(data);
// We trim below b/c sometimes RIM crypto is picky about IVs that are too long.
byte[] sndIV = Tools.trimBytesToLength(hash.getDigest(), sendKeySize);
data[x] = 'B'; // HASH(K || H || "B" || session_id)
hash.update(data);
// We trim below b/c sometimes RIM crypto is picky about IVs that are too long.
byte[] rcvIV = Tools.trimBytesToLength(hash.getDigest(), recvKeySize);
data[x] = 'C'; // HASH(K || H || "C" || session_id)
hash.update(data);
byte[] sndKey = calculateFinalKey(hash, sendKeySize);
data[x] = 'D'; // HASH(K || H || "D" || session_id)
hash.update(data);
byte[] rcvKey = calculateFinalKey(hash, recvKeySize);
data[x] = 'E';
hash.update(data); // HASH(K || H || "E" || session_id)
byte[] sndMAC = hash.getDigest();
data[x] = 'F'; // HASH(K || H || "F" || session_id)
hash.update(data);
byte[] rcvMAC = hash.getDigest();
crypto2 = new SshCrypto2(sendAttr, recvAttr, sndIV, rcvIV, sndKey, rcvKey, sndMAC, rcvMAC);
}
public void sendTerminalSizeUpdate() throws IOException {
if (state < STATE_ONLINE) {
Logger.error("Received request to send terminal size update, but authentication not completed.");
return;
}
SshPacket2 pn = new SshPacket2(SSHMessages.SSH_MSG_CHANNEL_REQUEST);
pn.putInt32(remoteId); // v recipient channel
pn.putString("window-change");
pn.putByte((byte) 0); // want reply- no
pn.putInt32(getTerminalWidth()); // terminal width, columns
pn.putInt32(getTerminalHeight()); // terminal height, rows
pn.putInt32(0); // terminal width, pixels
pn.putInt32(0); // terminal height, pixels
sendPacket2(pn);
}
/**
* Send_SSH_NOOP (no arguments) Sends a NOOP packet to keep the connection alive.
*
* @return empty string.
* @throws IOException
*/
public String Send_SSH_NOOP() throws IOException {
SshPacket2 packet = new SshPacket2(SSHMessages.SSH_MSG_IGNORE);
packet.putString("");
sendPacket2(packet);
return "";
}
/** @todo - these passthroughs do NOT belong here */
protected String getTerminalID() {
return sshSession.getTerminalID();
}
/** @todo - these passthroughs do NOT belong here */
protected int getTerminalHeight() {
return sshSession.getTerminalHeight();
}
/** @todo - these passthroughs do NOT belong here */
protected int getTerminalWidth() {
return sshSession.getTerminalWidth();
}
static public byte getNotZeroRandomByte() {
byte ret = 0;
while (ret == 0) {
ret = RandomSource.getBytes(1)[0];
}
return ret;
}
private String[] handleUserAuthInfoRequest(SshPacket2 p) {
/*
* http://tools.ietf.org/html/rfc4256#ref-SSH-ARCH S 3.3
*/
String name = p.getString();
if (name == null || name.length() == 0) {
name = properties.getUsername();
}
final String instruction = p.getString(); // service name
p.getString(); // language tag
int numPrompts = p.getInt32();
final Vector prompts = new Vector();
for (int i = 0; i < numPrompts; i++) {
String text = p.getString();
// @todo - again, this REALLY doesn't belong here.
// @todo - 'password' is not set?
if (text.startsWith("password:")) {
prompts.addElement(new SSHPrompt(text, p.getByte() == 1, password));
} else {
prompts.addElement(new SSHPrompt(text, p.getByte() == 1));
}
}
return sshSession.authPrompt(name, instruction, prompts, password);
}
/**
* Validates that server key received in SSH_DH_KEX_INIT is correct.
*
* @throws IOException
*/
private void validateServerKeys() throws IOException, CryptoException {
TypesReader keyData = new TypesReader(kexState.K_S);
kexState.keyAlgorithm = keyData.readString();
// @todo RSA verify as RSACryptoSystem, etc.
if (kexState.keyAlgorithm.equals("ssh-dss")) {
// RIM DSS validation does not permit keys larger than 2048, so we must do our own.
SignatureTools.verifyDSASignature(kexState.H, kexState.SigOfH, keyData.readTrimmedCryptoIntegerString(),
keyData.readTrimmedCryptoIntegerString(), keyData.readTrimmedCryptoIntegerString(), keyData
.readTrimmedCryptoIntegerString());
} else if (kexState.keyAlgorithm.equals("ssh-rsa")) {
// Alas, RIM crypto is not recognizing our RSA pub key as valid, so once again
// we must do our own...
if (!SignatureTools.verifyRSASignature(keyData, kexState.SigOfH, kexState.H)) {
throw new IOException("RSA signature validation failed.");
}
} else {
// This might occur if the server sends us an invalid value, but otherwise
// is impossible since we have agreed on this algorithm
// before being able to reach this point.
throw new IOException("Unknown encoding algorithm: " + kexState.keyAlgorithm);
}
}
/**
* Set password to use for this connection when authentication request is received, if user/pass auth is supported
* and pub key auth is not used.
*
* @param password
*/
public void setPassword(String password) {
this.password = password;
}
/**
* Set username to use for this connection when authentication request is received, if user/pass auth is supported
* and pub key auth is not used.
*
* @param userName
*/
public void setUserName(String userName) {
this.userName = userName;
}
public KexAgreement getAgreement() {
if (kexState == null)
return null;
return kexState.agreement;
}
public void onConnected() throws IOException {
setState(STATE_INIT);
write(Version.getVersionSSHIDString().getBytes());
}
// public void onDataReceived(Session session, byte[] data) {
// }
//
// public void onWriteCompletion(Session session) {
// try {
// sendOutboundChannelData();
// } catch (IOException e) {
// Logger.error("IOException in SshIO.onWriteCompletion [ " + e.getMessage() + " ] ");
// }
// }
}