Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

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

Open
wants to merge 4 commits into from

7 participants

@rtyley

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

rtyley added some commits
@rtyley rtyley 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
@rtyley rtyley open names for permissions
b890a0c
@rtyley rtyley Merge branch 'master' of git://github.com/kruton/connectbot into ssh-…
…agent
2d7918e
@rtyley rtyley 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

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

@rtyley

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

@samueltardieu

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?

@miniBill

Hasn't connectbot moved to google code hosting?

@rtyley

@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

@msva

@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
Closed

support for SFTP public key authentication #108

@gionn

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

@kruton
Owner

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

@rtyley

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
Commits on Jan 25, 2011
  1. @rtyley

    Adding capability to act as ssh-agent for other apps on the Android d…

    rtyley authored
    …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
Commits on Feb 26, 2011
  1. @rtyley

    open names for permissions

    rtyley authored
Commits on Jun 6, 2011
  1. @rtyley
Commits on Jun 13, 2011
  1. @rtyley

    Allow externals apps directing user to Identity admin to solve ssh fa…

    rtyley authored
    …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.
This page is out of date. Refresh to see the latest.
View
24 AndroidManifest.xml
@@ -33,7 +33,12 @@
</intent-filter>
</activity>
- <activity android:name=".PubkeyListActivity" android:configChanges="keyboardHidden|orientation" />
+ <activity android:name=".PubkeyListActivity" android:configChanges="keyboardHidden|orientation" >
+ <intent-filter>
+ <action android:name="org.openintents.ssh.agent.IDENTITY_ADMIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
<activity android:name=".GeneratePubkeyActivity" android:configChanges="keyboardHidden|orientation" />
<activity android:name=".HostEditorActivity" android:configChanges="keyboardHidden|orientation" />
<activity android:name=".PortForwardListActivity" android:configChanges="keyboardHidden|orientation" />
@@ -46,6 +51,14 @@
<service android:name="org.connectbot.service.TerminalManager"
android:configChanges="keyboardHidden|orientation"
android:description="@string/service_desc" />
+
+ <service android:name="org.connectbot.service.AuthAgentService"
+ android:description="@string/auth_agent_service_desc"
+ android:permission="org.openintents.ssh.permission.ACCESS_SSH_AGENT">
+ <intent-filter>
+ <action android:name="org.openintents.ssh.BIND_SSH_AGENT_SERVICE" />
+ </intent-filter>
+ </service>
<activity android:name=".ConsoleActivity" android:configChanges="keyboardHidden|orientation"
android:theme="@style/NoTitle" android:windowSoftInputMode="stateAlwaysVisible|adjustResize"
@@ -72,4 +85,13 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<supports-screens />
+
+ <permission
+ android:name="org.openintents.ssh.permission.ACCESS_SSH_AGENT"
+ android:protectionLevel="dangerous"
+ android:permissionGroup="android.permission-group.PERSONAL_INFO"
+ android:label="@string/ssh_agent_permission_label"
+ android:description="@string/ssh_agent_permission_desc"
+ android:icon="@drawable/pubkey">
+ </permission>
</manifest>
View
5 res/values/strings.xml
@@ -21,6 +21,11 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_desc">"Simple, powerful, open-source SSH client."</string>
<string name="service_desc">"Maintains SSH connections and loaded pubkeys"</string>
+ <string name="auth_agent_service_desc">"Acts as an ssh-agent for other Android Apps on the device"</string>
+
+ <string name="ssh_agent_permission_label">use SSH keys stored in your SSH Agent</string>
+ <string name="ssh_agent_permission_desc">Allows the application to authenticate SSH transactions using your private keys stored in ConnectBot.
+ VERY DANGEROUS if used by a malicious application.</string>
<!-- Window title for the Host List -->
<string name="title_hosts_list">"Hosts"</string>
View
126 src/com/madgag/ssh/android/authagent/AndroidAuthAgent.java
@@ -0,0 +1,126 @@
+/*
+ * This file is auto-generated. DO NOT MODIFY.
+ * Original file: /home/roberto/development/madgag-ssh/ssh-android/src/main/java/com/madgag/ssh/android/authagent/AndroidAuthAgent.aidl
+ */
+package com.madgag.ssh.android.authagent;
+public interface AndroidAuthAgent extends android.os.IInterface
+{
+/** Local-side IPC implementation stub class. */
+public static abstract class Stub extends android.os.Binder implements com.madgag.ssh.android.authagent.AndroidAuthAgent
+{
+private static final java.lang.String DESCRIPTOR = "com.madgag.ssh.android.authagent.AndroidAuthAgent";
+/** Construct the stub at attach it to the interface. */
+public Stub()
+{
+this.attachInterface(this, DESCRIPTOR);
+}
+/**
+ * Cast an IBinder object into an com.madgag.ssh.android.authagent.AndroidAuthAgent interface,
+ * generating a proxy if needed.
+ */
+public static com.madgag.ssh.android.authagent.AndroidAuthAgent asInterface(android.os.IBinder obj)
+{
+if ((obj==null)) {
+return null;
+}
+android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR);
+if (((iin!=null)&&(iin instanceof com.madgag.ssh.android.authagent.AndroidAuthAgent))) {
+return ((com.madgag.ssh.android.authagent.AndroidAuthAgent)iin);
+}
+return new com.madgag.ssh.android.authagent.AndroidAuthAgent.Stub.Proxy(obj);
+}
+public android.os.IBinder asBinder()
+{
+return this;
+}
+@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
+{
+switch (code)
+{
+case INTERFACE_TRANSACTION:
+{
+reply.writeString(DESCRIPTOR);
+return true;
+}
+case TRANSACTION_getIdentities:
+{
+data.enforceInterface(DESCRIPTOR);
+java.util.Map _result = this.getIdentities();
+reply.writeNoException();
+reply.writeMap(_result);
+return true;
+}
+case TRANSACTION_sign:
+{
+data.enforceInterface(DESCRIPTOR);
+byte[] _arg0;
+_arg0 = data.createByteArray();
+byte[] _arg1;
+_arg1 = data.createByteArray();
+byte[] _result = this.sign(_arg0, _arg1);
+reply.writeNoException();
+reply.writeByteArray(_result);
+return true;
+}
+}
+return super.onTransact(code, data, reply, flags);
+}
+private static class Proxy implements com.madgag.ssh.android.authagent.AndroidAuthAgent
+{
+private android.os.IBinder mRemote;
+Proxy(android.os.IBinder remote)
+{
+mRemote = remote;
+}
+public android.os.IBinder asBinder()
+{
+return mRemote;
+}
+public java.lang.String getInterfaceDescriptor()
+{
+return DESCRIPTOR;
+}
+public java.util.Map getIdentities() throws android.os.RemoteException
+{
+android.os.Parcel _data = android.os.Parcel.obtain();
+android.os.Parcel _reply = android.os.Parcel.obtain();
+java.util.Map _result;
+try {
+_data.writeInterfaceToken(DESCRIPTOR);
+mRemote.transact(Stub.TRANSACTION_getIdentities, _data, _reply, 0);
+_reply.readException();
+java.lang.ClassLoader cl = (java.lang.ClassLoader)this.getClass().getClassLoader();
+_result = _reply.readHashMap(cl);
+}
+finally {
+_reply.recycle();
+_data.recycle();
+}
+return _result;
+}
+public byte[] sign(byte[] publicKey, byte[] data) throws android.os.RemoteException
+{
+android.os.Parcel _data = android.os.Parcel.obtain();
+android.os.Parcel _reply = android.os.Parcel.obtain();
+byte[] _result;
+try {
+_data.writeInterfaceToken(DESCRIPTOR);
+_data.writeByteArray(publicKey);
+_data.writeByteArray(data);
+mRemote.transact(Stub.TRANSACTION_sign, _data, _reply, 0);
+_reply.readException();
+_result = _reply.createByteArray();
+}
+finally {
+_reply.recycle();
+_data.recycle();
+}
+return _result;
+}
+}
+static final int TRANSACTION_getIdentities = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
+static final int TRANSACTION_sign = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
+}
+public java.util.Map getIdentities() throws android.os.RemoteException;
+public byte[] sign(byte[] publicKey, byte[] data) throws android.os.RemoteException;
+}
View
161 src/org/connectbot/service/AuthAgentService.java
@@ -0,0 +1,161 @@
+package org.connectbot.service;
+
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.connectbot.service.TerminalManager.KeyHolder;
+
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.madgag.ssh.android.authagent.AndroidAuthAgent;
+import com.trilead.ssh2.signature.DSAPrivateKey;
+import com.trilead.ssh2.signature.DSAPublicKey;
+import com.trilead.ssh2.signature.DSASHA1Verify;
+import com.trilead.ssh2.signature.DSASignature;
+import com.trilead.ssh2.signature.RSAPrivateKey;
+import com.trilead.ssh2.signature.RSAPublicKey;
+import com.trilead.ssh2.signature.RSASHA1Verify;
+import com.trilead.ssh2.signature.RSASignature;
+
+public class AuthAgentService extends Service {
+ private static final String TAG = "ConnectBot.AuthAgentService";
+ protected TerminalManager manager;
+ final Lock lock = new ReentrantLock();
+ final Condition managerReady = lock.newCondition();
+
+ private ServiceConnection connection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ Log.d(TAG, "Terminal manager available! Hurrah");
+ manager = ((TerminalManager.TerminalBinder) service).getService();
+ lock.lock();
+ try {
+ managerReady.signal();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ manager = null;
+ Log.d(TAG, "Terminal manager gone...");
+ }
+ };
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ Log.d(TAG, "onBind() called");
+ bindService(new Intent(this, TerminalManager.class), connection, BIND_AUTO_CREATE);
+ return mBinder;
+ }
+
+ private final AndroidAuthAgent.Stub mBinder = new AndroidAuthAgent.Stub() {
+
+ public Map getIdentities() throws RemoteException {
+ Log.d(TAG, "getIdentities() called");
+ waitForTerminalManager();
+ Log.d(TAG, "getIdentities() manager.loadedKeypairs : " + manager.loadedKeypairs);
+
+ return sshEncodedPubKeysFrom(manager.loadedKeypairs);
+ }
+
+ public byte[] sign(byte[] publicKey, byte[] data) throws RemoteException {
+ Log.d(TAG, "sign() called");
+ waitForTerminalManager();
+ Object trileadKey = keyPairFor(publicKey);
+ Log.d(TAG, "sign() - signing keypair found : "+trileadKey);
+
+ if (trileadKey == null) {
+ return null;
+ }
+
+ if (trileadKey instanceof RSAPrivateKey) {
+ return sshEncodedSignatureFor(data, (RSAPrivateKey) trileadKey);
+ } else if (trileadKey instanceof DSAPrivateKey) {
+ return sshEncodedSignatureFor(data, (DSAPrivateKey) trileadKey);
+ }
+ return null;
+ }
+
+
+ private void waitForTerminalManager() throws RemoteException {
+ lock.lock();
+ try {
+ while (manager == null) {
+ Log.d(TAG, "Waiting for TerminalManager...");
+ managerReady.await();
+ }
+ } catch (InterruptedException e) {
+ throw new RemoteException();
+ } finally {
+ lock.unlock();
+ }
+ Log.d(TAG, "Got TerminalManager : "+manager);
+ }
+
+ private Map<String, byte[]> sshEncodedPubKeysFrom(Map<String, KeyHolder> keypairs) {
+ Map<String, byte[]> encodedPubKeysByName = new HashMap<String, byte[]>(keypairs.size());
+
+ for (Entry<String, KeyHolder> entry : keypairs.entrySet()) {
+ byte[] encodedKey = sshEncodedPubKeyFrom(entry.getValue().trileadKey);
+ if (encodedKey != null) {
+ encodedPubKeysByName.put(entry.getKey(), encodedKey);
+ }
+ }
+ return encodedPubKeysByName;
+ }
+
+ private byte[] sshEncodedPubKeyFrom(Object trileadKey) {
+ try {
+ if (trileadKey instanceof RSAPrivateKey) {
+ RSAPublicKey pubkey = ((RSAPrivateKey) trileadKey).getPublicKey();
+ return RSASHA1Verify.encodeSSHRSAPublicKey(pubkey);
+ } else if (trileadKey instanceof DSAPrivateKey) {
+ DSAPublicKey pubkey = ((DSAPrivateKey) trileadKey).getPublicKey();
+ return DSASHA1Verify.encodeSSHDSAPublicKey(pubkey);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Couldn't encode " + trileadKey, e);
+ }
+ return null;
+ }
+
+ private byte[] sshEncodedSignatureFor(byte[] data, RSAPrivateKey trileadKey) {
+ try {
+ RSASignature signature = RSASHA1Verify.generateSignature(data, trileadKey);
+ return RSASHA1Verify.encodeSSHRSASignature(signature);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private byte[] sshEncodedSignatureFor(byte[] data, DSAPrivateKey dsaPrivateKey) {
+ DSASignature signature = DSASHA1Verify.generateSignature(data, dsaPrivateKey, new SecureRandom());
+ return DSASHA1Verify.encodeSSHDSASignature(signature);
+ }
+
+ private Object keyPairFor(byte[] publicKey) {
+ String nickname = manager.getKeyNickname(publicKey);
+
+ if (nickname == null) {
+ Log.w(TAG, "No key-pair found for public-key.");
+ return null;
+ }
+
+ // check manager.loadedKeypairs.get(nickname).bean.isConfirmUse() and promptForPubkeyUse(nickname) ?
+ return manager.getKey(nickname);
+ }
+
+ };
+}
Something went wrong with that request. Please try again.