Skip to content
This repository

Allow other Android Apps to use ConnectBot as an ssh-agent #13

Open
wants to merge 4 commits into from

7 participants

Roberto Tyley Kaleb Hornsby Leonardo Taglialegne Vadim A. Misbakh-Soloviov Giovanni Toraldo Kenny Root Samuel Tardieu
Roberto Tyley
rtyley commented June 13, 2011

I've recently released Agit, a Git client for Android (https://market.android.com/details?id=com.madgag.agit). In order to support git+ssh functionality, I created this patch of ConnectBot which allows other android apps to use it in the same manner as an ssh-agent. This means that Agit doesn't need to actually manage or even store SSH keys itself, which is pretty cool - as ConnectBot already has the UI & infrastructure to manage SSH keys, I don't want to have to do it again...!

The functionality is exposed using AIDL, defining a small two-method interface, getIdentities() & sign() :

https://github.com/rtyley/madgag-ssh/blob/master/ssh-android/src/main/java/com/madgag/ssh/android/authagent/AndroidAuthAgent.aidl

The patch adds the AndroidAuthAgent.java generated from this AIDL definition to ConnectBot, and exposes it using a AuthAgentService to external Apps with the org.openintents.ssh.permission.ACCESS_SSH_AGENT permission so that they can bind to the agent and use it to provide public-keys and sign the ssh-challenges that they encounter.

Just to go over that again, it's the external app (e.g. Agit) that creates the network connection to the remote ssh server, using it's own ssh libraries, it's just that it binds to ConnectBot in order to get a list of available identities (ie public keys), and then later, when the remote ssh server issues a challenge, Agit will call the auth-agent again to sign the issued ssh cryptographic-challenge.

The bit of Agit's code where it does this is here:

https://github.com/rtyley/agit/blob/agit-parent-1.7/agit/src/main/java/com/madgag/agit/ssh/AndroidSshSessionFactory.java

It works really well - it's a symphony of cryptography and inter-process communication!

Hope you like it,
Roberto

added some commits January 25, 2011
Roberto Tyley Adding capability to act as ssh-agent for other apps on the Android d…
…evice

Got over the TerminalManager binding race-condition by adding some possibly ill-advised Locking with a capital 'L'

Extracting & adding the AndroidAuthAgent.java source file into ConnectBot - just adding the ssh-agent jar to the eclipse project as a jar didn't
work, failed on compile, couldn't find it
0db788d
Roberto Tyley open names for permissions b890a0c
Roberto Tyley Merge branch 'master' of git://github.com/kruton/connectbot into ssh-…
…agent
2d7918e
Roberto Tyley Allow externals apps directing user to Identity admin to solve ssh fa…
…ilure

If an external program suffers an SSH Auth fail when performing a an ssh
op, it's nice to be able to point the user to their keys so that they
unlock the required key.
d91bc58
Kaleb Hornsby
kaleb commented June 29, 2011

Sounds like a great idea. I will have to test this out when I get a chance.

Roberto Tyley
rtyley commented June 29, 2011

Hi @kaleb - there's a bit of documentation on using Agit with this patch of ConnectBot here:

https://github.com/rtyley/agit/wiki/SSH

-there's also a rather scrappy video of the installation process here: http://www.youtube.com/watch?v=6YXR-ZhZ1Qk

Samuel Tardieu

Rather than scaring the user, wouldn't it be a better idea to popup a message box the first time a given key is used with a given application? À la Superuser application?

Leonardo Taglialegne

Hasn't connectbot moved to google code hosting?

Roberto Tyley

@miniBill it was on github when I wrote this pull request! Thanks for the heads-up tho!

I've now moved the patch to Google Code:

http://code.google.com/r/robertotyley-connectbot-ssh-agent/source/list?name=ssh-agent

Vadim A. Misbakh-Soloviov

@miniBill @rtyley afais on googlecode, it exactly moved to github from there.

although, unfortunately, author ignores some issues on the both sides :(

chrysn chrysn referenced this pull request in BrandroidTools/OpenExplorer October 30, 2013
Open

support for SFTP public key authentication #108

Giovanni Toraldo

@kruton there is any chance to get this PR merged?

Kenny Root
Owner

Instead of a permission, it should use a PendingIntent and request user confirmation and the button accepting confirmation should have android:filterTouchesWhenObscured set.

Roberto Tyley

Instead of a permission, it should use a PendingIntent and request user confirmation and the button accepting confirmation should have android:filterTouchesWhenObscured set.

Thanks for the feedback @kruton - that does sound like quite a nice alteration (ensuring that users don't fall victim to apps they blithely granted permissions to, I guess) but it might need some tweaking for Agit's pattern of use - the first time someone does a clone in Agit, it's reasonable that the user might get prompted for confirmation - but subsequently, Agit is supposed to keep the Git repo up to date with periodic polling and fetching - that won't work well if the user is getting prompted each time?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 4 unique commits by 1 author.

Jan 25, 2011
Roberto Tyley Adding capability to act as ssh-agent for other apps on the Android d…
…evice

Got over the TerminalManager binding race-condition by adding some possibly ill-advised Locking with a capital 'L'

Extracting & adding the AndroidAuthAgent.java source file into ConnectBot - just adding the ssh-agent jar to the eclipse project as a jar didn't
work, failed on compile, couldn't find it
0db788d
Feb 26, 2011
Roberto Tyley open names for permissions b890a0c
Jun 06, 2011
Roberto Tyley Merge branch 'master' of git://github.com/kruton/connectbot into ssh-…
…agent
2d7918e
Jun 13, 2011
Roberto Tyley Allow externals apps directing user to Identity admin to solve ssh fa…
…ilure

If an external program suffers an SSH Auth fail when performing a an ssh
op, it's nice to be able to point the user to their keys so that they
unlock the required key.
d91bc58
This page is out of date. Refresh to see the latest.
24  AndroidManifest.xml
@@ -33,7 +33,12 @@
33 33
 			</intent-filter>
34 34
 		</activity>
35 35
 
36  
-		<activity android:name=".PubkeyListActivity" android:configChanges="keyboardHidden|orientation" />
  36
+		<activity android:name=".PubkeyListActivity" android:configChanges="keyboardHidden|orientation" >
  37
+			<intent-filter>
  38
+				<action android:name="org.openintents.ssh.agent.IDENTITY_ADMIN" />
  39
+				<category android:name="android.intent.category.DEFAULT" />
  40
+			</intent-filter>
  41
+		</activity>
37 42
 		<activity android:name=".GeneratePubkeyActivity" android:configChanges="keyboardHidden|orientation" />
38 43
 		<activity android:name=".HostEditorActivity" android:configChanges="keyboardHidden|orientation" />
39 44
 		<activity android:name=".PortForwardListActivity" android:configChanges="keyboardHidden|orientation" />
@@ -46,6 +51,14 @@
46 51
 		<service android:name="org.connectbot.service.TerminalManager"
47 52
 			android:configChanges="keyboardHidden|orientation"
48 53
 			android:description="@string/service_desc" />
  54
+			
  55
+		<service android:name="org.connectbot.service.AuthAgentService"
  56
+			android:description="@string/auth_agent_service_desc"
  57
+			android:permission="org.openintents.ssh.permission.ACCESS_SSH_AGENT">
  58
+			<intent-filter>
  59
+				<action android:name="org.openintents.ssh.BIND_SSH_AGENT_SERVICE" />
  60
+			</intent-filter>
  61
+		</service>
49 62
 
50 63
 		<activity android:name=".ConsoleActivity" android:configChanges="keyboardHidden|orientation"
51 64
 			android:theme="@style/NoTitle" android:windowSoftInputMode="stateAlwaysVisible|adjustResize"
@@ -72,4 +85,13 @@
72 85
 	<uses-permission android:name="android.permission.WAKE_LOCK" />
73 86
 
74 87
 	<supports-screens />
  88
+	
  89
+	<permission 
  90
+		android:name="org.openintents.ssh.permission.ACCESS_SSH_AGENT"
  91
+		android:protectionLevel="dangerous"
  92
+		android:permissionGroup="android.permission-group.PERSONAL_INFO"
  93
+		android:label="@string/ssh_agent_permission_label"
  94
+		android:description="@string/ssh_agent_permission_desc"
  95
+		android:icon="@drawable/pubkey">
  96
+	</permission>
75 97
 </manifest>
5  res/values/strings.xml
@@ -21,6 +21,11 @@
21 21
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
22 22
 	<string name="app_desc">"Simple, powerful, open-source SSH client."</string>
23 23
 	<string name="service_desc">"Maintains SSH connections and loaded pubkeys"</string>
  24
+	<string name="auth_agent_service_desc">"Acts as an ssh-agent for other Android Apps on the device"</string>
  25
+	
  26
+	<string name="ssh_agent_permission_label">use SSH keys stored in your SSH Agent</string>
  27
+    <string name="ssh_agent_permission_desc">Allows the application to authenticate SSH transactions using your private keys stored in ConnectBot.
  28
+    	VERY DANGEROUS if used by a malicious application.</string>	
24 29
 
25 30
 	<!-- Window title for the Host List -->
26 31
 	<string name="title_hosts_list">"Hosts"</string>
126  src/com/madgag/ssh/android/authagent/AndroidAuthAgent.java
... ...
@@ -0,0 +1,126 @@
  1
+/*
  2
+ * This file is auto-generated.  DO NOT MODIFY.
  3
+ * Original file: /home/roberto/development/madgag-ssh/ssh-android/src/main/java/com/madgag/ssh/android/authagent/AndroidAuthAgent.aidl
  4
+ */
  5
+package com.madgag.ssh.android.authagent;
  6
+public interface AndroidAuthAgent extends android.os.IInterface
  7
+{
  8
+/** Local-side IPC implementation stub class. */
  9
+public static abstract class Stub extends android.os.Binder implements com.madgag.ssh.android.authagent.AndroidAuthAgent
  10
+{
  11
+private static final java.lang.String DESCRIPTOR = "com.madgag.ssh.android.authagent.AndroidAuthAgent";
  12
+/** Construct the stub at attach it to the interface. */
  13
+public Stub()
  14
+{
  15
+this.attachInterface(this, DESCRIPTOR);
  16
+}
  17
+/**
  18
+ * Cast an IBinder object into an com.madgag.ssh.android.authagent.AndroidAuthAgent interface,
  19
+ * generating a proxy if needed.
  20
+ */
  21
+public static com.madgag.ssh.android.authagent.AndroidAuthAgent asInterface(android.os.IBinder obj)
  22
+{
  23
+if ((obj==null)) {
  24
+return null;
  25
+}
  26
+android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR);
  27
+if (((iin!=null)&&(iin instanceof com.madgag.ssh.android.authagent.AndroidAuthAgent))) {
  28
+return ((com.madgag.ssh.android.authagent.AndroidAuthAgent)iin);
  29
+}
  30
+return new com.madgag.ssh.android.authagent.AndroidAuthAgent.Stub.Proxy(obj);
  31
+}
  32
+public android.os.IBinder asBinder()
  33
+{
  34
+return this;
  35
+}
  36
+@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
  37
+{
  38
+switch (code)
  39
+{
  40
+case INTERFACE_TRANSACTION:
  41
+{
  42
+reply.writeString(DESCRIPTOR);
  43
+return true;
  44
+}
  45
+case TRANSACTION_getIdentities:
  46
+{
  47
+data.enforceInterface(DESCRIPTOR);
  48
+java.util.Map _result = this.getIdentities();
  49
+reply.writeNoException();
  50
+reply.writeMap(_result);
  51
+return true;
  52
+}
  53
+case TRANSACTION_sign:
  54
+{
  55
+data.enforceInterface(DESCRIPTOR);
  56
+byte[] _arg0;
  57
+_arg0 = data.createByteArray();
  58
+byte[] _arg1;
  59
+_arg1 = data.createByteArray();
  60
+byte[] _result = this.sign(_arg0, _arg1);
  61
+reply.writeNoException();
  62
+reply.writeByteArray(_result);
  63
+return true;
  64
+}
  65
+}
  66
+return super.onTransact(code, data, reply, flags);
  67
+}
  68
+private static class Proxy implements com.madgag.ssh.android.authagent.AndroidAuthAgent
  69
+{
  70
+private android.os.IBinder mRemote;
  71
+Proxy(android.os.IBinder remote)
  72
+{
  73
+mRemote = remote;
  74
+}
  75
+public android.os.IBinder asBinder()
  76
+{
  77
+return mRemote;
  78
+}
  79
+public java.lang.String getInterfaceDescriptor()
  80
+{
  81
+return DESCRIPTOR;
  82
+}
  83
+public java.util.Map getIdentities() throws android.os.RemoteException
  84
+{
  85
+android.os.Parcel _data = android.os.Parcel.obtain();
  86
+android.os.Parcel _reply = android.os.Parcel.obtain();
  87
+java.util.Map _result;
  88
+try {
  89
+_data.writeInterfaceToken(DESCRIPTOR);
  90
+mRemote.transact(Stub.TRANSACTION_getIdentities, _data, _reply, 0);
  91
+_reply.readException();
  92
+java.lang.ClassLoader cl = (java.lang.ClassLoader)this.getClass().getClassLoader();
  93
+_result = _reply.readHashMap(cl);
  94
+}
  95
+finally {
  96
+_reply.recycle();
  97
+_data.recycle();
  98
+}
  99
+return _result;
  100
+}
  101
+public byte[] sign(byte[] publicKey, byte[] data) throws android.os.RemoteException
  102
+{
  103
+android.os.Parcel _data = android.os.Parcel.obtain();
  104
+android.os.Parcel _reply = android.os.Parcel.obtain();
  105
+byte[] _result;
  106
+try {
  107
+_data.writeInterfaceToken(DESCRIPTOR);
  108
+_data.writeByteArray(publicKey);
  109
+_data.writeByteArray(data);
  110
+mRemote.transact(Stub.TRANSACTION_sign, _data, _reply, 0);
  111
+_reply.readException();
  112
+_result = _reply.createByteArray();
  113
+}
  114
+finally {
  115
+_reply.recycle();
  116
+_data.recycle();
  117
+}
  118
+return _result;
  119
+}
  120
+}
  121
+static final int TRANSACTION_getIdentities = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
  122
+static final int TRANSACTION_sign = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
  123
+}
  124
+public java.util.Map getIdentities() throws android.os.RemoteException;
  125
+public byte[] sign(byte[] publicKey, byte[] data) throws android.os.RemoteException;
  126
+}
161  src/org/connectbot/service/AuthAgentService.java
... ...
@@ -0,0 +1,161 @@
  1
+package org.connectbot.service;
  2
+
  3
+import java.io.IOException;
  4
+import java.security.SecureRandom;
  5
+import java.util.HashMap;
  6
+import java.util.Map;
  7
+import java.util.Map.Entry;
  8
+import java.util.concurrent.locks.Condition;
  9
+import java.util.concurrent.locks.Lock;
  10
+import java.util.concurrent.locks.ReentrantLock;
  11
+
  12
+import org.connectbot.service.TerminalManager.KeyHolder;
  13
+
  14
+import android.app.Service;
  15
+import android.content.ComponentName;
  16
+import android.content.Intent;
  17
+import android.content.ServiceConnection;
  18
+import android.os.IBinder;
  19
+import android.os.RemoteException;
  20
+import android.util.Log;
  21
+
  22
+import com.madgag.ssh.android.authagent.AndroidAuthAgent;
  23
+import com.trilead.ssh2.signature.DSAPrivateKey;
  24
+import com.trilead.ssh2.signature.DSAPublicKey;
  25
+import com.trilead.ssh2.signature.DSASHA1Verify;
  26
+import com.trilead.ssh2.signature.DSASignature;
  27
+import com.trilead.ssh2.signature.RSAPrivateKey;
  28
+import com.trilead.ssh2.signature.RSAPublicKey;
  29
+import com.trilead.ssh2.signature.RSASHA1Verify;
  30
+import com.trilead.ssh2.signature.RSASignature;
  31
+
  32
+public class AuthAgentService extends Service {
  33
+	private static final String TAG = "ConnectBot.AuthAgentService";
  34
+	protected TerminalManager manager;
  35
+	final Lock lock = new ReentrantLock();
  36
+	final Condition managerReady = lock.newCondition();
  37
+
  38
+	private ServiceConnection connection = new ServiceConnection() {
  39
+		public void onServiceConnected(ComponentName className, IBinder service) {
  40
+			Log.d(TAG, "Terminal manager available! Hurrah");
  41
+			manager = ((TerminalManager.TerminalBinder) service).getService();
  42
+			lock.lock();
  43
+			try {
  44
+				managerReady.signal();
  45
+			} finally {
  46
+				lock.unlock();
  47
+			}
  48
+		}
  49
+
  50
+		public void onServiceDisconnected(ComponentName className) {
  51
+			manager = null;
  52
+			Log.d(TAG, "Terminal manager gone...");
  53
+		}
  54
+	};
  55
+
  56
+	@Override
  57
+	public IBinder onBind(Intent intent) {
  58
+		Log.d(TAG, "onBind() called");
  59
+		bindService(new Intent(this, TerminalManager.class), connection, BIND_AUTO_CREATE);
  60
+		return mBinder;
  61
+	}
  62
+
  63
+	private final AndroidAuthAgent.Stub mBinder = new AndroidAuthAgent.Stub() {
  64
+
  65
+		public Map getIdentities() throws RemoteException {
  66
+			Log.d(TAG, "getIdentities() called");
  67
+			waitForTerminalManager();
  68
+			Log.d(TAG, "getIdentities() manager.loadedKeypairs : " + manager.loadedKeypairs);
  69
+
  70
+			return sshEncodedPubKeysFrom(manager.loadedKeypairs);
  71
+		}
  72
+
  73
+		public byte[] sign(byte[] publicKey, byte[] data) throws RemoteException {
  74
+			Log.d(TAG, "sign() called");
  75
+			waitForTerminalManager();
  76
+			Object trileadKey = keyPairFor(publicKey);
  77
+			Log.d(TAG, "sign() - signing keypair found : "+trileadKey);
  78
+
  79
+			if (trileadKey == null) {
  80
+				return null;
  81
+			}
  82
+
  83
+			if (trileadKey instanceof RSAPrivateKey) {
  84
+				return sshEncodedSignatureFor(data, (RSAPrivateKey) trileadKey);
  85
+			} else if (trileadKey instanceof DSAPrivateKey) {
  86
+				return sshEncodedSignatureFor(data, (DSAPrivateKey) trileadKey);
  87
+			}
  88
+			return null;
  89
+		}
  90
+
  91
+
  92
+		private void waitForTerminalManager() throws RemoteException {
  93
+			lock.lock();
  94
+			try {
  95
+				while (manager == null) {
  96
+					Log.d(TAG, "Waiting for TerminalManager...");
  97
+					managerReady.await();
  98
+				}
  99
+			} catch (InterruptedException e) {
  100
+				throw new RemoteException();
  101
+			} finally {
  102
+				lock.unlock();
  103
+			}
  104
+			Log.d(TAG, "Got TerminalManager : "+manager);
  105
+		}
  106
+
  107
+		private Map<String, byte[]> sshEncodedPubKeysFrom(Map<String, KeyHolder> keypairs) {
  108
+			Map<String, byte[]> encodedPubKeysByName = new HashMap<String, byte[]>(keypairs.size());
  109
+
  110
+			for (Entry<String, KeyHolder> entry : keypairs.entrySet()) {
  111
+				byte[] encodedKey = sshEncodedPubKeyFrom(entry.getValue().trileadKey);
  112
+				if (encodedKey != null) {
  113
+					encodedPubKeysByName.put(entry.getKey(), encodedKey);
  114
+				}
  115
+			}
  116
+			return encodedPubKeysByName;
  117
+		}
  118
+
  119
+		private byte[] sshEncodedPubKeyFrom(Object trileadKey) {
  120
+			try {
  121
+				if (trileadKey instanceof RSAPrivateKey) {
  122
+					RSAPublicKey pubkey = ((RSAPrivateKey) trileadKey).getPublicKey();
  123
+					return RSASHA1Verify.encodeSSHRSAPublicKey(pubkey);
  124
+				} else if (trileadKey instanceof DSAPrivateKey) {
  125
+					DSAPublicKey pubkey = ((DSAPrivateKey) trileadKey).getPublicKey();
  126
+					return DSASHA1Verify.encodeSSHDSAPublicKey(pubkey);
  127
+				}
  128
+			} catch (IOException e) {
  129
+				Log.e(TAG, "Couldn't encode " + trileadKey, e);
  130
+			}
  131
+			return null;
  132
+		}
  133
+
  134
+		private byte[] sshEncodedSignatureFor(byte[] data, RSAPrivateKey trileadKey) {
  135
+			try {
  136
+				RSASignature signature = RSASHA1Verify.generateSignature(data, trileadKey);
  137
+				return RSASHA1Verify.encodeSSHRSASignature(signature);
  138
+			} catch (IOException e) {
  139
+				throw new RuntimeException(e);
  140
+			}
  141
+		}
  142
+
  143
+		private byte[] sshEncodedSignatureFor(byte[] data, DSAPrivateKey dsaPrivateKey) {
  144
+			DSASignature signature = DSASHA1Verify.generateSignature(data, dsaPrivateKey, new SecureRandom());
  145
+			return DSASHA1Verify.encodeSSHDSASignature(signature);
  146
+		}
  147
+
  148
+		private Object keyPairFor(byte[] publicKey) {
  149
+			String nickname = manager.getKeyNickname(publicKey);
  150
+
  151
+			if (nickname == null) {
  152
+				Log.w(TAG, "No key-pair found for public-key.");
  153
+				return null;
  154
+			}
  155
+
  156
+			// check manager.loadedKeypairs.get(nickname).bean.isConfirmUse() and promptForPubkeyUse(nickname) ?
  157
+			return manager.getKey(nickname);
  158
+		}
  159
+
  160
+	};
  161
+}
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.