diff options
author | Kenny Root <kenny@the-b.org> | 2009-06-25 22:46:30 +0000 |
---|---|---|
committer | Kenny Root <kenny@the-b.org> | 2009-06-25 22:46:30 +0000 |
commit | 12c1e528b5c5dc325d2b3887104f0cf277b83d0b (patch) | |
tree | 64d509ba6704f4d6937b48e248d106d8378c33e5 /src | |
parent | 93d7a03dc05cb6bc230fff3b1d0b124263f00320 (diff) | |
download | connectbot-12c1e528b5c5dc325d2b3887104f0cf277b83d0b.tar.gz connectbot-12c1e528b5c5dc325d2b3887104f0cf277b83d0b.tar.bz2 connectbot-12c1e528b5c5dc325d2b3887104f0cf277b83d0b.zip |
Add authentication agent forwarding
git-svn-id: https://connectbot.googlecode.com/svn/trunk/connectbot@331 df292f66-193f-0410-a5fc-6d59da041ff2
Diffstat (limited to 'src')
-rw-r--r-- | src/com/trilead/ssh2/AuthAgentCallback.java | 45 | ||||
-rw-r--r-- | src/com/trilead/ssh2/Session.java | 23 | ||||
-rw-r--r-- | src/com/trilead/ssh2/channel/AuthAgentForwardThread.java | 408 | ||||
-rw-r--r-- | src/com/trilead/ssh2/channel/ChannelManager.java | 55 | ||||
-rw-r--r-- | src/com/trilead/ssh2/packets/PacketChannelAuthAgentReq.java | 33 | ||||
-rw-r--r-- | src/org/connectbot/bean/HostBean.java | 8 | ||||
-rw-r--r-- | src/org/connectbot/service/TerminalBridge.java | 3 | ||||
-rw-r--r-- | src/org/connectbot/service/TerminalManager.java | 72 | ||||
-rw-r--r-- | src/org/connectbot/transport/AbsTransport.java | 4 | ||||
-rw-r--r-- | src/org/connectbot/transport/SSH.java | 81 | ||||
-rw-r--r-- | src/org/connectbot/util/HostDatabase.java | 13 | ||||
-rw-r--r-- | src/org/connectbot/util/PubkeyUtils.java | 23 |
12 files changed, 752 insertions, 16 deletions
diff --git a/src/com/trilead/ssh2/AuthAgentCallback.java b/src/com/trilead/ssh2/AuthAgentCallback.java new file mode 100644 index 0000000..2bd10e0 --- /dev/null +++ b/src/com/trilead/ssh2/AuthAgentCallback.java @@ -0,0 +1,45 @@ +package com.trilead.ssh2; + +import java.util.Map; + +/** + * AuthAgentCallback. + * + * @author Kenny Root + * @version $Id$ + */ +public interface AuthAgentCallback { + + /** + * @return array of blobs containing the OpenSSH-format encoded public keys + */ + Map<String,byte[]> retrieveIdentities(); + + /** + * @param key A <code>RSAPrivateKey</code> or <code>DSAPrivateKey</code> + * containing a DSA or RSA private key of + * the user in Trilead object format. + * @param comment comment associated with this key + * @return success or failure + */ + boolean addIdentity(Object key, String comment); + + /** + * @param publicKey byte blob containing the OpenSSH-format encoded public key + * @return success or failure + */ + boolean removeIdentity(byte[] publicKey); + + /** + * @return success or failure + */ + boolean removeAllIdentities(); + + /** + * @param publicKey byte blob containing the OpenSSH-format encoded public key + * @return A <code>RSAPrivateKey</code> or <code>DSAPrivateKey</code> + * containing a DSA or RSA private key of + * the user in Trilead object format. + */ + Object getPrivateKey(byte[] publicKey); +} diff --git a/src/com/trilead/ssh2/Session.java b/src/com/trilead/ssh2/Session.java index 30efa6f..fa0c36f 100644 --- a/src/com/trilead/ssh2/Session.java +++ b/src/com/trilead/ssh2/Session.java @@ -353,7 +353,28 @@ public class Session cm.requestChannelTrileadPing(cn);
}
-
+
+ /**
+ * Request authentication agent forwarding.
+ * @param agent object that implements the callbacks
+ *
+ * @throws IOException in case of any problem or when the session is closed
+ */
+ public synchronized boolean requestAuthAgentForwarding(AuthAgentCallback agent) throws IOException
+ {
+ synchronized (this)
+ {
+ /*
+ * The following is just a nicer error, we would catch it anyway
+ * later in the channel code
+ */
+ if (flag_closed)
+ throw new IOException("This session is closed.");
+ }
+
+ return cm.requestChannelAgentForwarding(cn, agent);
+ }
+
public InputStream getStdout()
{
return cn.getStdoutStream();
diff --git a/src/com/trilead/ssh2/channel/AuthAgentForwardThread.java b/src/com/trilead/ssh2/channel/AuthAgentForwardThread.java new file mode 100644 index 0000000..f517ee4 --- /dev/null +++ b/src/com/trilead/ssh2/channel/AuthAgentForwardThread.java @@ -0,0 +1,408 @@ +package com.trilead.ssh2.channel; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Map; +import java.util.Map.Entry; + +import android.util.Log; + +import com.trilead.ssh2.AuthAgentCallback; +import com.trilead.ssh2.log.Logger; +import com.trilead.ssh2.packets.TypesReader; +import com.trilead.ssh2.packets.TypesWriter; +import com.trilead.ssh2.signature.DSAPrivateKey; +import com.trilead.ssh2.signature.DSASHA1Verify; +import com.trilead.ssh2.signature.DSASignature; +import com.trilead.ssh2.signature.RSAPrivateKey; +import com.trilead.ssh2.signature.RSASHA1Verify; +import com.trilead.ssh2.signature.RSASignature; + +/** + * AuthAgentForwardThread. + * + * @author Kenny Root + * @version $Id$ + */ +public class AuthAgentForwardThread extends Thread implements IChannelWorkerThread +{ + public static final int SSH_AGENT_CONSTRAIN_LIFETIME = 1; + public static final int SSH_AGENT_CONSTRAIN_CONFIRM = 2; + + private static final byte[] SSH_AGENT_FAILURE = {0, 0, 0, 1, 5}; + private static final byte[] SSH_AGENT_SUCCESS = {0, 0, 0, 1, 6}; +// public static final int SSH_AGENT_FAILURE = 5; +// public static final int SSH_AGENT_SUCCESS = 6; + + public static final int SSH2_AGENTC_REQUEST_IDENTITIES = 11; + public static final int SSH2_AGENT_IDENTITIES_ANSWER = 12; + + public static final int SSH2_AGENTC_SIGN_REQUEST = 13; + public static final int SSH2_AGENT_SIGN_RESPONSE = 14; + + public static final int SSH2_AGENTC_ADD_IDENTITY = 17; + public static final int SSH2_AGENTC_REMOVE_IDENTITY = 18; + public static final int SSH2_AGENTC_REMOVE_ALL_IDENTITIES = 19; + + public static final int SSH_AGENTC_ADD_SMARTCARD_KEY = 20; + public static final int SSH_AGENTC_REMOVE_SMARTCARD_KEY = 21; + + public static final int SSH_AGENTC_LOCK = 22; + public static final int SSH_AGENTC_UNLOCK = 23; + + public static final int SSH2_AGENTC_ADD_ID_CONSTRAINED = 25; + public static final int SSH_AGENTC_ADD_SMARTCARD_KEY_CONSTRAINED = 26; + + public static final int SSH_AGENT_OLD_SIGNATURE = 1; + + private static final Logger log = Logger.getLogger(RemoteAcceptThread.class); + + AuthAgentCallback authAgent; + OutputStream os; + InputStream is; + Channel c; + byte[] buffer = new byte[Channel.CHANNEL_BUFFER_SIZE]; + + public AuthAgentForwardThread(Channel c, AuthAgentCallback authAgent) + { + this.c = c; + this.authAgent = authAgent; + + if (log.isEnabled()) + log.log(20, "AuthAgentForwardThread started"); + } + + @Override + public void run() + { + try + { + c.cm.registerThread(this); + } catch (IOException e) { + stopWorking(); + return; + } + + try + { + c.cm.sendOpenConfirmation(c); + + is = c.getStdoutStream(); + os = c.getStdinStream(); + + int totalSize = 4; + int readSoFar = 0; + + while (true) { + int len; + + try + { + len = is.read(buffer, readSoFar, buffer.length - readSoFar); + } + catch (IOException e) { + stopWorking(); + return; + } + + if (len <= 0) + break; + + readSoFar += len; + + Log.d("AuthAgent", "read " + readSoFar + " bytes"); + + if (readSoFar >= 4) { + TypesReader tr = new TypesReader(buffer, 0, 4); + totalSize = tr.readUINT32() + 4; + Log.d("AuthAgent", "message is " + totalSize + " bytes"); + } + + if (totalSize == readSoFar) { +// debugPacket(buffer, readSoFar); + TypesReader tr = new TypesReader(buffer, 4, readSoFar - 4); + int messageType = tr.readByte(); + + Log.d("AuthAgent", "Got a message type " + messageType); + switch (messageType) { + case SSH2_AGENTC_REQUEST_IDENTITIES: + sendIdentities(); + break; + case SSH2_AGENTC_ADD_IDENTITY: + addIdentity(tr); + break; + case SSH2_AGENTC_REMOVE_IDENTITY: + removeIdentity(tr); + break; + case SSH2_AGENTC_REMOVE_ALL_IDENTITIES: + removeAllIdentities(tr); + break; + case SSH2_AGENTC_SIGN_REQUEST: + processSignRequest(tr); + break; + default: + os.write(SSH_AGENT_FAILURE); + break; + } + + readSoFar = 0; + } + // TODO write actual agent forwarding stuff! +// log.log(0, "Received an agent request; sending failure"); +// os.write(AGENT_FAILURE); + } + + c.cm.closeChannel(c, "EOF on both streams reached.", true); + } + catch (IOException e) + { + log.log(50, "IOException in agent forwarder: " + e.getMessage()); + + try + { + is.close(); + } + catch (IOException e1) + { + } + try + { + os.close(); + } + catch (IOException e2) + { + } + try + { + c.cm.closeChannel(c, "IOException in agent forwarder (" + e.getMessage() + ")", true); + } + catch (IOException e3) + { + } + } + } + + public void stopWorking() { + try + { + /* This will lead to an IOException in the is.read() call */ + is.close(); + } + catch (IOException e) + { + } + } + + private void sendIdentities() throws IOException + { + Map<String,byte[]> keys = authAgent.retrieveIdentities(); + + TypesWriter tw = new TypesWriter(); + tw.writeByte(SSH2_AGENT_IDENTITIES_ANSWER); + int numKeys = 0; + if (keys != null) + numKeys = keys.size(); + tw.writeUINT32(numKeys); + + if (keys != null) { + for (Entry<String,byte[]> entry : keys.entrySet()) { + byte[] keyBytes = entry.getValue(); + tw.writeString(keyBytes, 0, keyBytes.length); + tw.writeString(entry.getKey()); + } + } + + Log.d("AuthAgent", "Sending " + numKeys + " to server"); + sendPacket(tw.getBytes()); + } + + /** + * @param tr + */ + private void addIdentity(TypesReader tr) { + try + { + String type = tr.readString(); + + Object key; + String comment; + + if (type.equals("ssh-rsa")) { + BigInteger n = tr.readMPINT(); + BigInteger e = tr.readMPINT(); + BigInteger d = tr.readMPINT(); + tr.readMPINT(); // iqmp + tr.readMPINT(); // p + tr.readMPINT(); // q + comment = tr.readString(); + + key = new RSAPrivateKey(d, e, n); + } else if (type.equals("ssh-dss")) { + BigInteger p = tr.readMPINT(); + BigInteger q = tr.readMPINT(); + BigInteger g = tr.readMPINT(); + BigInteger y = tr.readMPINT(); + BigInteger x = tr.readMPINT(); + comment = tr.readString(); + + key = new DSAPrivateKey(p, q, g, y, x); + } else { + os.write(SSH_AGENT_FAILURE); + return; + } + + if (authAgent.addIdentity(key, comment)) + os.write(SSH_AGENT_SUCCESS); + else + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e) + { + try + { + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e1) + { + } + } + } + + /** + * @param tr + */ + private void removeIdentity(TypesReader tr) { + try + { + byte[] publicKey = tr.readByteString(); + if (authAgent.removeIdentity(publicKey)) + os.write(SSH_AGENT_SUCCESS); + else + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e) + { + try + { + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e1) + { + } + } + } + + /** + * @param tr + */ + private void removeAllIdentities(TypesReader tr) { + try + { + if (authAgent.removeAllIdentities()) + os.write(SSH_AGENT_SUCCESS); + else + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e) + { + try + { + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e1) + { + } + } + } + + private void processSignRequest(TypesReader tr) + { + try + { + byte[] publicKey = tr.readByteString(); + byte[] challenge = tr.readByteString(); + + int flags = tr.readUINT32(); + + Object trileadKey = authAgent.getPrivateKey(publicKey); + + if (trileadKey == null) { + Log.d("AuthAgent", "Key not known to us; failing signature. Public key:"); +// debugPacket(publicKey); + os.write(SSH_AGENT_FAILURE); + return; + } + + byte[] response; + + if (trileadKey instanceof RSAPrivateKey) { + RSASignature signature = RSASHA1Verify.generateSignature(challenge, + (RSAPrivateKey) trileadKey); + response = RSASHA1Verify.encodeSSHRSASignature(signature); + } else if (trileadKey instanceof DSAPrivateKey) { + if ((flags & SSH_AGENT_OLD_SIGNATURE) != 0) + Log.d("AuthAgent", "Want old signature type"); + DSASignature signature = DSASHA1Verify.generateSignature(challenge, + (DSAPrivateKey) trileadKey, new SecureRandom()); + response = DSASHA1Verify.encodeSSHDSASignature(signature); + } else { + Log.d("AuthAgent", "Unknown key type; failing signature request"); + os.write(SSH_AGENT_FAILURE); + return; + } + + TypesWriter tw = new TypesWriter(); + tw.writeByte(SSH2_AGENT_SIGN_RESPONSE); + tw.writeString(response, 0, response.length); + + sendPacket(tw.getBytes()); + } + catch (IOException e) { + try + { + os.write(SSH_AGENT_FAILURE); + } + catch (IOException e1) { + } + } + } + + /** + * @param tw + * @throws IOException + */ + private void sendPacket(byte[] message) throws IOException { + TypesWriter packet = new TypesWriter(); + packet.writeUINT32(message.length); + packet.writeBytes(message); +// debugPacket(packet.getBytes()); + os.write(packet.getBytes()); + } + +// private static final char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; +// +// private void debugPacket(byte[] packet) { +// debugPacket(packet, packet.length); +// } +// +// private void debugPacket(byte[] packet, int len) { +// StringBuilder sb = new StringBuilder(); +// sb.append("Packet dump:"); +// +// for (int i = 0; i < len; i++) { +// if (packet[i] < 32 || packet[i] > 0x7e) { +// sb.append(" 0x"); +// sb.append(hexDigits[(packet[i] >> 4) & 0xF]); +// sb.append(hexDigits[packet[i] & 0xF]); +// } else { +// sb.append(" "); +// sb.append((char)packet[i]); +// } +// } +// +// Log.d("AuthAgent", sb.toString()); +// } +} diff --git a/src/com/trilead/ssh2/channel/ChannelManager.java b/src/com/trilead/ssh2/channel/ChannelManager.java index fb4beae..630e0cc 100644 --- a/src/com/trilead/ssh2/channel/ChannelManager.java +++ b/src/com/trilead/ssh2/channel/ChannelManager.java @@ -5,8 +5,10 @@ import java.io.IOException; import java.util.HashMap;
import java.util.Vector;
+import com.trilead.ssh2.AuthAgentCallback;
import com.trilead.ssh2.ChannelCondition;
import com.trilead.ssh2.log.Logger;
+import com.trilead.ssh2.packets.PacketChannelAuthAgentReq;
import com.trilead.ssh2.packets.PacketChannelOpenConfirmation;
import com.trilead.ssh2.packets.PacketChannelOpenFailure;
import com.trilead.ssh2.packets.PacketChannelTrileadPing;
@@ -50,6 +52,8 @@ public class ChannelManager implements MessageHandler private HashMap remoteForwardings = new HashMap();
+ private AuthAgentCallback authAgent;
+
private Vector listenerThreads = new Vector();
private boolean listenerThreadsAllowed = true;
@@ -530,6 +534,38 @@ public class ChannelManager implements MessageHandler }
+ /**
+ * @param agent
+ * @throws IOException
+ */
+ public boolean requestChannelAgentForwarding(Channel c, AuthAgentCallback authAgent) throws IOException {
+ synchronized (this)
+ {
+ if (this.authAgent != null)
+ throw new IllegalStateException("Auth agent already exists");
+
+ this.authAgent = authAgent;
+ }
+
+ synchronized (channels)
+ {
+ globalSuccessCounter = globalFailedCounter = 0;
+ }
+
+ if (log.isEnabled())
+ log.log(50, "Requesting agent forwarding");
+
+ PacketChannelAuthAgentReq aar = new PacketChannelAuthAgentReq(c.remoteID);
+ tm.sendMessage(aar.getPayload());
+
+ if (waitForChannelRequestResult(c) == false) {
+ authAgent = null;
+ return false;
+ }
+
+ return true;
+ }
+
public void registerThread(IChannelWorkerThread thr) throws IOException
{
synchronized (listenerThreads)
@@ -1274,6 +1310,25 @@ public class ChannelManager implements MessageHandler return;
}
+ if ("auth-agent@openssh.com".equals(channelType)) {
+ Channel c = new Channel(this);
+
+ synchronized (c)
+ {
+ c.remoteID = remoteID;
+ c.remoteWindow = remoteWindow & 0xFFFFffffL; /* properly convert UINT32 to long */
+ c.remoteMaxPacketSize = remoteMaxPacketSize;
+ c.localID = addChannel(c);
+ }
+
+ AuthAgentForwardThread aat = new AuthAgentForwardThread(c, authAgent);
+
+ aat.setDaemon(true);
+ aat.start();
+
+ return;
+ }
+
/* Tell the server that we have no idea what it is talking about */
PacketChannelOpenFailure pcof = new PacketChannelOpenFailure(remoteID, Packets.SSH_OPEN_UNKNOWN_CHANNEL_TYPE,
diff --git a/src/com/trilead/ssh2/packets/PacketChannelAuthAgentReq.java b/src/com/trilead/ssh2/packets/PacketChannelAuthAgentReq.java new file mode 100644 index 0000000..95fa396 --- /dev/null +++ b/src/com/trilead/ssh2/packets/PacketChannelAuthAgentReq.java @@ -0,0 +1,33 @@ +package com.trilead.ssh2.packets; + +/** + * PacketGlobalAuthAgent. + * + * @author Kenny Root, kenny@the-b.org + * @version $Id$ + */ +public class PacketChannelAuthAgentReq +{ + byte[] payload; + + public int recipientChannelID; + + public PacketChannelAuthAgentReq(int recipientChannelID) + { + this.recipientChannelID = recipientChannelID; + } + + public byte[] getPayload() + { + if (payload == null) + { + TypesWriter tw = new TypesWriter(); + tw.writeByte(Packets.SSH_MSG_CHANNEL_REQUEST); + tw.writeUINT32(recipientChannelID); + tw.writeString("auth-agent-req@openssh.com"); + tw.writeBoolean(true); // want reply + payload = tw.getBytes(); + } + return payload; + } +} diff --git a/src/org/connectbot/bean/HostBean.java b/src/org/connectbot/bean/HostBean.java index f1be4a3..7f08fdd 100644 --- a/src/org/connectbot/bean/HostBean.java +++ b/src/org/connectbot/bean/HostBean.java @@ -41,6 +41,7 @@ public class HostBean extends AbstractBean { private long lastConnect = -1; private String color; private boolean useKeys = true; + private String useAuthAgent = HostDatabase.AUTHAGENT_NO; private String postLogin = null; private long pubkeyId = -1; private boolean wantSession = true; @@ -140,6 +141,12 @@ public class HostBean extends AbstractBean { public boolean getUseKeys() { return useKeys; } + public void setUseAuthAgent(String useAuthAgent) { + this.useAuthAgent = useAuthAgent; + } + public String getUseAuthAgent() { + return useAuthAgent; + } public void setPostLogin(String postLogin) { this.postLogin = postLogin; } @@ -202,6 +209,7 @@ public class HostBean extends AbstractBean { values.put(HostDatabase.FIELD_HOST_LASTCONNECT, lastConnect); values.put(HostDatabase.FIELD_HOST_COLOR, color); values.put(HostDatabase.FIELD_HOST_USEKEYS, Boolean.toString(useKeys)); + values.put(HostDatabase.FIELD_HOST_USEAUTHAGENT, useAuthAgent); values.put(HostDatabase.FIELD_HOST_POSTLOGIN, postLogin); values.put(HostDatabase.FIELD_HOST_PUBKEYID, pubkeyId); values.put(HostDatabase.FIELD_HOST_WANTSESSION, Boolean.toString(wantSession)); diff --git a/src/org/connectbot/service/TerminalBridge.java b/src/org/connectbot/service/TerminalBridge.java index b0d4dbe..efb35b3 100644 --- a/src/org/connectbot/service/TerminalBridge.java +++ b/src/org/connectbot/service/TerminalBridge.java @@ -272,8 +272,9 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener { transport.setManager(manager); transport.setHost(host); - // Should be more abstract? + // TODO make this more abstract so we don't litter on AbsTransport transport.setCompression(host.getCompression()); + transport.setUseAuthAgent(host.getUseAuthAgent()); transport.setEmulation(emulation); if (transport.canForwardPorts()) { diff --git a/src/org/connectbot/service/TerminalManager.java b/src/org/connectbot/service/TerminalManager.java index 74bc180..1b0955a 100644 --- a/src/org/connectbot/service/TerminalManager.java +++ b/src/org/connectbot/service/TerminalManager.java @@ -21,12 +21,14 @@ package org.connectbot.service; import java.io.IOException; import java.security.PrivateKey; import java.security.PublicKey; +import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Timer; import java.util.TimerTask; +import java.util.Map.Entry; import org.connectbot.ConsoleActivity; import org.connectbot.R; @@ -83,7 +85,7 @@ public class TerminalManager extends Service implements BridgeDisconnectedListen public Handler disconnectHandler = null; - public HashMap<String, Object> loadedPubkeys = new HashMap<String, Object>(); + public Map<String, KeyHolder> loadedKeypairs = new HashMap<String, KeyHolder>(); public Resources res; @@ -137,8 +139,7 @@ public class TerminalManager extends Service implements BridgeDisconnectedListen PublicKey pubKey = PubkeyUtils.decodePublic(pubkey.getPublicKey(), pubkey.getType()); Object trileadKey = PubkeyUtils.convertToTrilead(privKey, pubKey); - loadedPubkeys.put(pubkey.getNickname(), trileadKey); - Log.d(TAG, String.format("Added key '%s' to in-memory cache", pubkey.getNickname())); + addKey(pubkey.getNickname(), trileadKey); } catch (Exception e) { Log.d(TAG, String.format("Problem adding key '%s' to in-memory cache", pubkey.getNickname()), e); } @@ -312,27 +313,73 @@ public class TerminalManager extends Service implements BridgeDisconnectedListen } public boolean isKeyLoaded(String nickname) { - return loadedPubkeys.containsKey(nickname); + return loadedKeypairs.containsKey(nickname); } public void addKey(String nickname, Object trileadKey) { - loadedPubkeys.remove(nickname); - loadedPubkeys.put(nickname, trileadKey); + removeKey(nickname); + + byte[] sshPubKey = PubkeyUtils.extractOpenSSHPublic(trileadKey); + + KeyHolder keyHolder = new KeyHolder(); + keyHolder.trileadKey = trileadKey; + keyHolder.openSSHPubkey = sshPubKey; + + loadedKeypairs.put(nickname, keyHolder); + + Log.d(TAG, String.format("Added key '%s' to in-memory cache", nickname)); + } + + public boolean removeKey(String nickname) { + Log.d(TAG, String.format("Removed key '%s' to in-memory cache", nickname)); + return loadedKeypairs.remove(nickname) != null; } - public void removeKey(String nickname) { - loadedPubkeys.remove(nickname); + public boolean removeKey(byte[] publicKey) { + String nickname = null; + for (Entry<String,KeyHolder> entry : loadedKeypairs.entrySet()) { + if (Arrays.equals(entry.getValue().openSSHPubkey, publicKey)) { + nickname = entry.getKey(); + break; + } + } + + if (nickname != null) { + Log.d(TAG, String.format("Removed key '%s' to in-memory cache", nickname)); + return removeKey(nickname); + } else + return false; } public Object getKey(String nickname) { - return loadedPubkeys.get(nickname); + if (loadedKeypairs.containsKey(nickname)) { + KeyHolder keyHolder = loadedKeypairs.get(nickname); + return keyHolder.trileadKey; + } else + return null; + } + + public Object getKey(byte[] publicKey) { + for (KeyHolder keyHolder : loadedKeypairs.values()) { + if (Arrays.equals(keyHolder.openSSHPubkey, publicKey)) + return keyHolder.trileadKey; + } + return null; + } + + public String getKeyNickname(byte[] publicKey) { + for (Entry<String,KeyHolder> entry : loadedKeypairs.entrySet()) { + if (Arrays.equals(entry.getValue().openSSHPubkey, publicKey)) + return entry.getKey(); + } + return null; } private void stopWithDelay() { // TODO add in a way to check whether keys loaded are encrypted and only // set timer when we have an encrypted key loaded - if (loadedPubkeys.size() > 0) { + if (loadedKeypairs.size() > 0) { synchronized (this) { if (idleTimer == null) idleTimer = new Timer(true); @@ -535,4 +582,9 @@ public class TerminalManager extends Service implements BridgeDisconnectedListen public boolean isResizeAllowed() { return resizeAllowed; } + + public class KeyHolder { + public Object trileadKey; + public byte[] openSSHPubkey; + } } diff --git a/src/org/connectbot/transport/AbsTransport.java b/src/org/connectbot/transport/AbsTransport.java index e2cbc33..d00e7f1 100644 --- a/src/org/connectbot/transport/AbsTransport.java +++ b/src/org/connectbot/transport/AbsTransport.java @@ -133,6 +133,10 @@ public abstract class AbsTransport { // do nothing } + public void setUseAuthAgent(String useAuthAgent) { + // do nothing + } + public void setEmulation(String emulation) { this.emulation = emulation; } diff --git a/src/org/connectbot/transport/SSH.java b/src/org/connectbot/transport/SSH.java index 7ddbe7c..9d08592 100644 --- a/src/org/connectbot/transport/SSH.java +++ b/src/org/connectbot/transport/SSH.java @@ -32,6 +32,7 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -41,6 +42,7 @@ import org.connectbot.bean.PortForwardBean; import org.connectbot.bean.PubkeyBean; import org.connectbot.service.TerminalBridge; import org.connectbot.service.TerminalManager; +import org.connectbot.service.TerminalManager.KeyHolder; import org.connectbot.util.HostDatabase; import org.connectbot.util.PubkeyDatabase; import org.connectbot.util.PubkeyUtils; @@ -49,6 +51,7 @@ import android.content.Context; import android.net.Uri; import android.util.Log; +import com.trilead.ssh2.AuthAgentCallback; import com.trilead.ssh2.ChannelCondition; import com.trilead.ssh2.Connection; import com.trilead.ssh2.ConnectionInfo; @@ -60,12 +63,18 @@ import com.trilead.ssh2.LocalPortForwarder; import com.trilead.ssh2.ServerHostKeyVerifier; import com.trilead.ssh2.Session; import com.trilead.ssh2.crypto.PEMDecoder; +import com.trilead.ssh2.signature.DSAPrivateKey; +import com.trilead.ssh2.signature.DSAPublicKey; +import com.trilead.ssh2.signature.DSASHA1Verify; +import com.trilead.ssh2.signature.RSAPrivateKey; +import com.trilead.ssh2.signature.RSAPublicKey; +import com.trilead.ssh2.signature.RSASHA1Verify; /** * @author Kenny Root * */ -public class SSH extends AbsTransport implements ConnectionMonitor, InteractiveCallback { +public class SSH extends AbsTransport implements ConnectionMonitor, InteractiveCallback, AuthAgentCallback { public SSH() { super(); } @@ -121,6 +130,8 @@ public class SSH extends AbsTransport implements ConnectionMonitor, InteractiveC private int width; private int height; + private String useAuthAgent = HostDatabase.AUTHAGENT_NO; + public class HostKeyVerifier implements ServerHostKeyVerifier { public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException { @@ -216,8 +227,8 @@ public class SSH extends AbsTransport implements ConnectionMonitor, InteractiveC // try each of the in-memory keys bridge.outputLine(manager.res .getString(R.string.terminal_auth_pubkey_any)); - for(String nickname : manager.loadedPubkeys.keySet()) { - Object trileadKey = manager.loadedPubkeys.get(nickname); + for(String nickname : manager.loadedKeypairs.keySet()) { + Object trileadKey = manager.loadedKeypairs.get(nickname).trileadKey; if(this.tryPublicKey(host.getUsername(), nickname, trileadKey)) { finishConnection(); break; @@ -361,6 +372,9 @@ public class SSH extends AbsTransport implements ConnectionMonitor, InteractiveC try { session = connection.openSession(); + if (!useAuthAgent.equals(HostDatabase.AUTHAGENT_NO)) + session.requestAuthAgentForwarding(this); + session.requestPTY(getEmulation(), columns, rows, width, height, null); session.startShell(); @@ -808,4 +822,65 @@ public class SSH extends AbsTransport implements ConnectionMonitor, InteractiveC context.getString(R.string.format_hostname), context.getString(R.string.format_port)); } + + @Override + public void setUseAuthAgent(String useAuthAgent) { + this.useAuthAgent = useAuthAgent; + } + + public Map<String,byte[]> retrieveIdentities() { + Map<String,byte[]> pubKeys = new HashMap<String,byte[]>(manager.loadedKeypairs.size()); + + for (Entry<String,KeyHolder> entry : manager.loadedKeypairs.entrySet()) { + Object trileadKey = entry.getValue().trileadKey; + + try { + if (trileadKey instanceof RSAPrivateKey) { + RSAPublicKey pubkey = ((RSAPrivateKey) trileadKey).getPublicKey(); + pubKeys.put(entry.getKey(), RSASHA1Verify.encodeSSHRSAPublicKey(pubkey)); + } else if (trileadKey instanceof DSAPrivateKey) { + DSAPublicKey pubkey = ((DSAPrivateKey) trileadKey).getPublicKey(); + pubKeys.put(entry.getKey(), DSASHA1Verify.encodeSSHDSAPublicKey(pubkey)); + } else + continue; + } catch (IOException e) { + continue; + } + } + + return pubKeys; + } + + public Object getPrivateKey(byte[] publicKey) { + String nickname = manager.getKeyNickname(publicKey); + + if (nickname == null) + return null; + + if (useAuthAgent.equals(HostDatabase.AUTHAGENT_NO)) { + Log.e(TAG, ""); + return null; + } else if (useAuthAgent.equals(HostDatabase.AUTHAGENT_CONFIRM)) { + Boolean result = bridge.promptHelper.requestBooleanPrompt(null, + manager.res.getString(R.string.prompt_allow_agent_to_use_key, + nickname)); + if (result == null || !result) + return null; + } + return manager.getKey(nickname); + } + + public boolean addIdentity(Object key, String comment) { + manager.addKey(comment, key); + return true; + } + + public boolean removeAllIdentities() { + manager.loadedKeypairs.clear(); + return true; + } + + public boolean removeIdentity(byte[] publicKey) { + return manager.removeKey(publicKey); + } } diff --git a/src/org/connectbot/util/HostDatabase.java b/src/org/connectbot/util/HostDatabase.java index 8f6fc02..f81d2c7 100644 --- a/src/org/connectbot/util/HostDatabase.java +++ b/src/org/connectbot/util/HostDatabase.java @@ -49,7 +49,7 @@ public class HostDatabase extends SQLiteOpenHelper { public final static String TAG = "ConnectBot.HostDatabase"; public final static String DB_NAME = "hosts"; - public final static int DB_VERSION = 18; + public final static int DB_VERSION = 19; public final static String TABLE_HOSTS = "hosts"; public final static String FIELD_HOST_NICKNAME = "nickname"; @@ -62,6 +62,7 @@ public class HostDatabase extends SQLiteOpenHelper { public final static String FIELD_HOST_LASTCONNECT = "lastconnect"; public final static String FIELD_HOST_COLOR = "color"; public final static String FIELD_HOST_USEKEYS = "usekeys"; + public final static String FIELD_HOST_USEAUTHAGENT = "useauthagent"; public final static String FIELD_HOST_POSTLOGIN = "postlogin"; public final static String FIELD_HOST_PUBKEYID = "pubkeyid"; public final static String FIELD_HOST_WANTSESSION = "wantsession"; @@ -102,6 +103,10 @@ public class HostDatabase extends SQLiteOpenHelper { public final static String DELKEY_DEL = "del"; public final static String DELKEY_BACKSPACE = "backspace"; + public final static String AUTHAGENT_NO = "no"; + public final static String AUTHAGENT_CONFIRM = "confirm"; + public final static String AUTHAGENT_YES = "yes"; + public final static String ENCODING_DEFAULT = Charset.defaultCharset().name(); public final static long PUBKEYID_NEVER = -2; @@ -129,6 +134,7 @@ public class HostDatabase extends SQLiteOpenHelper { + FIELD_HOST_LASTCONNECT + " INTEGER, " + FIELD_HOST_COLOR + " TEXT, " + FIELD_HOST_USEKEYS + " TEXT, " + + FIELD_HOST_USEAUTHAGENT + " TEXT, " + FIELD_HOST_POSTLOGIN + " TEXT, " + FIELD_HOST_PUBKEYID + " INTEGER DEFAULT " + PUBKEYID_ANY + ", " + FIELD_HOST_DELKEY + " TEXT DEFAULT '" + DELKEY_DEL + "', " @@ -226,6 +232,9 @@ public class HostDatabase extends SQLiteOpenHelper { + FIELD_COLOR_BG + " INTEGER)"); db.execSQL("CREATE INDEX " + TABLE_COLOR_DEFAULTS + FIELD_COLOR_SCHEME + "index ON " + TABLE_COLOR_DEFAULTS + " (" + FIELD_COLOR_SCHEME + ");"); + case 18: + db.execSQL("ALTER TABLE " + TABLE_HOSTS + + " ADD COLUMN " + FIELD_HOST_USEAUTHAGENT + " TEXT DEFAULT '" + AUTHAGENT_NO + "'"); } } catch (SQLiteException e) { // The database has entered an unknown state. Try to recover. @@ -374,6 +383,7 @@ public class HostDatabase extends SQLiteOpenHelper { COL_LASTCONNECT = c.getColumnIndexOrThrow(FIELD_HOST_LASTCONNECT), COL_COLOR = c.getColumnIndexOrThrow(FIELD_HOST_COLOR), COL_USEKEYS = c.getColumnIndexOrThrow(FIELD_HOST_USEKEYS), + COL_USEAUTHAGENT = c.getColumnIndexOrThrow(FIELD_HOST_USEAUTHAGENT), COL_POSTLOGIN = c.getColumnIndexOrThrow(FIELD_HOST_POSTLOGIN), COL_PUBKEYID = c.getColumnIndexOrThrow(FIELD_HOST_PUBKEYID), COL_WANTSESSION = c.getColumnIndexOrThrow(FIELD_HOST_WANTSESSION), @@ -393,6 +403,7 @@ public class HostDatabase extends SQLiteOpenHelper { host.setLastConnect(c.getLong(COL_LASTCONNECT)); host.setColor(c.getString(COL_COLOR)); host.setUseKeys(Boolean.valueOf(c.getString(COL_USEKEYS))); + host.setUseAuthAgent(c.getString(COL_USEAUTHAGENT)); host.setPostLogin(c.getString(COL_POSTLOGIN)); host.setPubkeyId(c.getLong(COL_PUBKEYID)); host.setWantSession(Boolean.valueOf(c.getString(COL_WANTSESSION))); diff --git a/src/org/connectbot/util/PubkeyUtils.java b/src/org/connectbot/util/PubkeyUtils.java index 3f4220e..8a1e7eb 100644 --- a/src/org/connectbot/util/PubkeyUtils.java +++ b/src/org/connectbot/util/PubkeyUtils.java @@ -267,6 +267,29 @@ public class PubkeyUtils { throw new InvalidKeyException("Unknown key type"); } + /* + * OpenSSH compatibility methods + */ + + /** + * @param trileadKey + * @return OpenSSH-encoded pubkey + */ + public static byte[] extractOpenSSHPublic(Object trileadKey) { + try { + if (trileadKey instanceof com.trilead.ssh2.signature.RSAPrivateKey) + return RSASHA1Verify.encodeSSHRSAPublicKey( + ((com.trilead.ssh2.signature.RSAPrivateKey) trileadKey).getPublicKey()); + else if (trileadKey instanceof com.trilead.ssh2.signature.DSAPrivateKey) + return DSASHA1Verify.encodeSSHDSAPublicKey( + ((com.trilead.ssh2.signature.DSAPrivateKey) trileadKey).getPublicKey()); + else + return null; + } catch (IOException e) { + return null; + } + } + public static String exportPEM(PrivateKey key, String secret) throws NoSuchAlgorithmException, InvalidParameterSpecException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, InvalidKeySpecException, IllegalBlockSizeException, IOException { StringBuilder sb = new StringBuilder(); |