/** * Copyright (C) 2012 Iordan Iordanov * * This 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 software 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 software; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, * USA. */ package com.iiordanov.bVNC; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.net.InetSocketAddress; import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; import java.util.Arrays; import java.util.concurrent.CountDownLatch; import android.content.Context; import android.os.Handler; import android.os.Message; import android.util.Base64; import android.util.Log; import com.iiordanov.pubkeygenerator.PubkeyUtils; import com.trilead.ssh2.Connection; import com.trilead.ssh2.ConnectionInfo; import com.trilead.ssh2.InteractiveCallback; import com.trilead.ssh2.KnownHosts; import com.trilead.ssh2.Session; import com.iiordanov.bVNC.dialogs.GetTextFragment; import com.iiordanov.bVNC.*; import com.iiordanov.freebVNC.*; import com.iiordanov.aRDP.*; import com.iiordanov.freeaRDP.*; import com.iiordanov.aSPICE.*; import com.iiordanov.freeaSPICE.*; /** * @author Iordan K Iordanov * */ public class SSHConnection implements InteractiveCallback, GetTextFragment.OnFragmentDismissedListener { private final static String TAG = "SSHConnection"; private final static int MAXTRIES = 3; private Connection connection; private final int numPortTries = 1000; private ConnectionInfo connectionInfo; private String serverHostKey; private Session session; private boolean passwordAuth = false; private boolean keyboardInteractiveAuth = false; private boolean pubKeyAuth = false; private KeyPair kp; private PrivateKey privateKey; private PublicKey publicKey; // Connection parameters private String host; private String user; private String password; private String vncpassword; private String passphrase; private String savedServerHostKey; private int idHashAlg; private String idHash; // URI alternative to key private String savedIdHash; // alternative to key private String targetAddress; private int sshPort; private boolean usePubKey; private String sshPrivKey; private boolean useSshRemoteCommand; private int sshRemoteCommandType; private int sshRemoteCommandTimeout; private String sshRemoteCommand; private BufferedInputStream remoteStdout; private BufferedOutputStream remoteStdin; private boolean autoXEnabled; private int autoXType; private String autoXCommand; private boolean autoXUnixpw; private String autoXRandFileNm; private Context context; private Handler handler; // Used to communicate the MFA verification code obtained. private String verificationCode; private CountDownLatch vcLatch; public SSHConnection(ConnectionBean conn, Context cntxt, Handler handler) { host = conn.getSshServer(); sshPort = conn.getSshPort(); user = conn.getSshUser(); password = conn.getSshPassword(); vncpassword = conn.getPassword(); passphrase = conn.getSshPassPhrase(); savedServerHostKey = conn.getSshHostKey(); idHashAlg = conn.getIdHashAlgorithm(); savedIdHash = conn.getIdHash(); targetAddress = conn.getAddress(); usePubKey = conn.getUseSshPubKey(); sshPrivKey = conn.getSshPrivKey(); useSshRemoteCommand = conn.getUseSshRemoteCommand(); sshRemoteCommandType = conn.getSshRemoteCommandType(); sshRemoteCommand = conn.getSshRemoteCommand(); autoXEnabled = conn.getAutoXEnabled(); autoXType = conn.getAutoXType(); autoXCommand = conn.getAutoXCommand(); autoXUnixpw = conn.getAutoXUnixpw(); connection = new Connection(host, sshPort); autoXRandFileNm = conn.getAutoXRandFileNm(); context = cntxt; vcLatch = new CountDownLatch(1); this.verificationCode = new String(); this.handler = handler; } String getServerHostKey() { return serverHostKey; } String getIdHash() { return idHash; } public void setVerificationCode(String verificationCode) { this.verificationCode = verificationCode; this.vcLatch.countDown(); } /** * Initializes the SSH Tunnel * @return -1 if the target port was not determined, and the port obtained from x11vnc if it was * determined with AutoX. * @throws Exception */ public int initializeSSHTunnel () throws Exception { int port = -1; // Attempt to connect. if (!connect()) throw new Exception(context.getString(R.string.error_ssh_unable_to_connect)); // Verify host key against saved one. if (!verifyHostKey()) throw new Exception(context.getString(R.string.error_ssh_hostkey_changed)); // Authenticate and set up port forwarding. if (!usePubKey) { if (!canAuthWithPass()) { String authMethods = Arrays.toString(connection.getRemainingAuthMethods(user)); throw new Exception(context.getString(R.string.error_ssh_kbd_auth_method_unavail) + " " + authMethods); } if (!authenticateWithPassword()) throw new Exception(context.getString(R.string.error_ssh_pwd_auth_fail)); } else { if (canAuthWithPubKey()) { // Pubkey auth method is allowed so try it. if (!authenticateWithPubKey()) { if (!canAuthWithPubKey()) { // If pubkey authentication is now no longer available, we know pubkey // authentication succeeded but the server wants further authentication. if (!authenticateWithPassword()) { throw new Exception(context.getString(R.string.error_ssh_pwd_auth_fail)); } } else { throw new Exception(context.getString(R.string.error_ssh_key_auth_fail)); } } } else { // Pubkey authentication is not available, so try password if one was supplied. if (!password.isEmpty() && canAuthWithPass() && !authenticateWithPassword()) { if (!canAuthWithPass()) { // If password authentication is now no longer available, we know password // authentication succeeded but the server wants further authentication. if (!authenticateWithPubKey()) { throw new Exception(context.getString(R.string.error_ssh_key_auth_fail)); } } else { throw new Exception(context.getString(R.string.error_ssh_pwd_auth_fail)); } } else { String authMethods = Arrays.toString(connection.getRemainingAuthMethods(user)); throw new Exception(context.getString(R.string.error_ssh_pubkey_auth_method_unavail) + " " + authMethods); } } } // Run a remote command if commanded to. if (autoXEnabled) { int tries = 0; while (port < 0 && tries < MAXTRIES) { // If we're not using unix credentials, protect access with a temporary password file. if (!autoXUnixpw) { writeStringToRemoteCommand(vncpassword, Constants.AUTO_X_CREATE_PASSWDFILE+ Constants.AUTO_X_PWFILEBASENAME+autoXRandFileNm+ Constants.AUTO_X_SYNC); } // Execute AutoX command. execRemoteCommand(autoXCommand, 1); // If we are looking for the greeter, we give the password to sudo's stdin. if (autoXType == Constants.AUTOX_SELECT_SUDO_FIND) writeStringToStdin (password+"\n"); // Try to find PORT= port = parseRemoteStdoutForPort(); if (port < 0) { session.close(); tries++; // Wait a little for x11vnc to recover. if (tries < MAXTRIES) try { Thread.sleep(tries*3500); } catch (InterruptedException e1) { } } } if (port < 0) { throw new Exception (context.getString(R.string.error_ssh_x11vnc_no_port_failure)); } } return port; } /** * Creates a port forward to the given port and returns the local port forwarded. * @return the local port forwarded to the given remote port * @throws Exception */ int createLocalPortForward (int port) throws Exception { int localForwardedPort; // At this point we know we are authenticated. localForwardedPort = createPortForward(port, targetAddress, port); // If we got back a negative number, port forwarding failed. if (localForwardedPort < 0) { throw new Exception(context.getString(R.string.error_ssh_port_forwarding_failure)); } return localForwardedPort; } /** * Connects to remote server. * @return */ public boolean connect() { try { connection.setCompression(false); // TODO: Try using the provided KeyVerifier instead of verifying keys myself. connectionInfo = connection.connect(null, 6000, 24000); // Store a base64 encoded string representing the HostKey serverHostKey = Base64.encodeToString(connectionInfo.serverHostKey, Base64.DEFAULT); // Get information on supported authentication methods we're interested in. return true; } catch (IOException e) { e.printStackTrace(); return false; } } /** * Return a string holding a Hex representation of the signature of the remote host's key. */ public String getHostKeySignature () { return KnownHosts.createHexFingerprint(connectionInfo.serverHostKeyAlgorithm, connectionInfo.serverHostKey); } /** * Disconnects from remote server. */ public void terminateSSHTunnel () { connection.close(); } private boolean verifyHostKey () { // first check data against URI hash try { byte[] rawKey = connectionInfo.serverHostKey; boolean isValid = SecureTunnel.isSignatureEqual(idHashAlg, savedIdHash, rawKey); if (isValid) { Log.i(TAG, "Validated against provided hash."); return true; } } catch (Exception ex) { } // Because JSch returns the host key base64 encoded, and trilead ssh returns it not base64 encoded, // we compare savedHostKey to serverHostKey both base64 encoded and not. return savedServerHostKey.equals(serverHostKey) || savedServerHostKey.equals(new String(Base64.decode(serverHostKey, Base64.DEFAULT))); } /** * Returns whether the server can authenticate either with pass or keyboard-interactive methods. */ private boolean canAuthWithPass () { return hasPasswordAuth () || hasKeyboardInteractiveAuth (); } /** * Returns whether the server supports passworde * @return */ private boolean hasPasswordAuth () { boolean passwordAuth = false; try { passwordAuth = connection.isAuthMethodAvailable(user, "password"); } catch (IOException e) { e.printStackTrace(); } return passwordAuth; } /** * Returns whether the server supports passworde * @return */ private boolean hasKeyboardInteractiveAuth () { boolean keyboardInteractiveAuth = false; try { keyboardInteractiveAuth = connection.isAuthMethodAvailable(user, "keyboard-interactive"); } catch (IOException e) { e.printStackTrace(); } return keyboardInteractiveAuth; } /** * Returns whether the server can authenticate with a key. */ private boolean canAuthWithPubKey () { boolean pubKeyAuth = false; try { pubKeyAuth = connection.isAuthMethodAvailable(user, "publickey"); } catch (IOException e) { e.printStackTrace(); } return pubKeyAuth; } /** * Authenticates with a password. */ private boolean authenticateWithPassword () { boolean isAuthenticated = false; try { if (hasKeyboardInteractiveAuth()) { Log.i(TAG, "Trying SSH keyboard-interactive authentication."); isAuthenticated = connection.authenticateWithKeyboardInteractive(user, this); } if (!isAuthenticated && hasPasswordAuth()) { Log.i(TAG, "Trying SSH password authentication."); isAuthenticated = connection.authenticateWithPassword(user, password); } return isAuthenticated; } catch (IOException e) { e.printStackTrace(); return false; } } /** * Decrypts and recovers the key pair. * @throws Exception */ private void decryptAndRecoverKey () throws Exception { // Detect an empty key (not generated). if (sshPrivKey.length() == 0) throw new Exception (context.getString(R.string.error_ssh_keypair_missing)); // Detect passphrase entered when key unencrypted and report error. if (passphrase.length() != 0 && !PubkeyUtils.isEncrypted(sshPrivKey)) throw new Exception (context.getString(R.string.error_ssh_passphrase_but_keypair_unencrypted)); // Try to decrypt and recover keypair, and failing that, report error. kp = PubkeyUtils.decryptAndRecoverKeyPair(sshPrivKey, passphrase); if (kp == null) throw new Exception (context.getString(R.string.error_ssh_keypair_decryption_failure)); privateKey = kp.getPrivate(); publicKey = kp.getPublic(); } /** * Authenticates with a public/private key-pair. */ private boolean authenticateWithPubKey () throws Exception { decryptAndRecoverKey(); Log.i(TAG, "Trying SSH pubkey authentication."); return connection.authenticateWithPublicKey(user, kp); } private int createPortForward (int localPortStart, String remoteHost, int remotePort) { int portsTried = 0; while (portsTried < numPortTries) { try { connection.createLocalPortForwarder(new InetSocketAddress("127.0.0.1", localPortStart + portsTried), remoteHost, remotePort); return localPortStart + portsTried; } catch (IOException e) { portsTried++; } } return -1; } /** * Executes a remote command, and waits a certain amount of time. * @param command - the command to execute. * @param secTimeout - amount of time in seconds to wait afterward. * @throws Exception */ private void execRemoteCommand (String command, int secTimeout) throws Exception { Log.i (TAG, "Executing remote command: " + command); try { session = connection.openSession(); session.execCommand(command); remoteStdout = new BufferedInputStream(session.getStdout()); remoteStdin = new BufferedOutputStream(session.getStdin()); Thread.sleep(secTimeout*1000); } catch (Exception e) { e.printStackTrace(); throw new Exception (context.getString(R.string.error_ssh_could_not_exec_command)); } } /** * Writes the specified string to a stdin of a remote command. * @throws Exception */ private void writeStringToRemoteCommand (String s, String cmd) throws Exception { Log.i(TAG, "Writing string to stdin of remote command: " + cmd); execRemoteCommand(cmd, 0); remoteStdin.write(s.getBytes()); remoteStdin.flush(); remoteStdin.close(); session.close(); } /** * Writes the specified string to a stdin of open session. * @throws Exception */ private void writeStringToStdin (String s) throws Exception { Log.i(TAG, "Writing string to remote stdin."); remoteStdin.write(s.getBytes()); remoteStdin.flush(); } // TODO: This doesn't work at the moment. private void sendSudoPassword () throws Exception { Log.i (TAG, "Sending sudo password."); try { remoteStdin.write(new String (password + '\n').getBytes()); } catch (IOException e) { e.printStackTrace(); throw new Exception (context.getString(R.string.error_ssh_could_not_send_sudo_pwd)); } } /** * Parses the remote stdout for PORT= */ private int parseRemoteStdoutForPort () { Log.i (TAG, "Parsing remote stdout for PORT="); String sought = "PORT="; int soughtLength = sought.length(); int port = -1; try { int data = 0; int i = 0; while (data != -1 && i < soughtLength) { data = remoteStdout.read(); if (data == (int)sought.charAt(i)) { i = i + 1; } else { i = 0; } } if (i == soughtLength) { // Read in 5 bytes after PORT= byte[] buffer = new byte[5]; remoteStdout.read(buffer); // Get rid of any whitespace (e.g. if the port is less than 5 digits). buffer = new String(buffer).replaceAll("\\s","").getBytes(); port = Integer.parseInt(new String(buffer)); Log.i (TAG, "Found PORT=, set to: " + port); } else { Log.e (TAG, "Failed to find PORT= in remote stdout."); port = -1; } } catch (IOException e) { Log.e (TAG, "Failed to read from remote stdout."); e.printStackTrace(); port = -1; } catch (NumberFormatException e) { Log.e (TAG, "Failed to parse integer."); e.printStackTrace(); port = -1; } return port; } /** * Used for keyboard-interactive authentication. */ @Override public String[] replyToChallenge(String name, String instruction, int numPrompts, String[] prompt, boolean[] echo) throws Exception { String[] responses = new String[numPrompts]; for (int x=0; x < numPrompts; x++) { if (prompt[0].indexOf("Verification code:") != -1) { Log.e(TAG, prompt[x] + " Will request verification code from user"); if (Utils.isFree(context)) { handler.sendEmptyMessage(Constants.PRO_FEATURE); responses[x] = ""; } else { vcLatch = new CountDownLatch(1); Log.e(TAG, "Requesting verification code from user"); handler.sendEmptyMessage(Constants.GET_VERIFICATIONCODE); while (true) { try { vcLatch.await(); break; } catch (InterruptedException e) { e.printStackTrace(); } } Log.e(TAG, prompt[0] + " Sending verification code: " + verificationCode); responses[x] = verificationCode; } } else { Log.e(TAG, prompt[0] + " Sending SSH password"); responses[x] = password; } } return responses; } @Override public void onTextObtained(String obtainedString, boolean dialogCancelled) { setVerificationCode(obtainedString); } }