aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorvexofp <vexofp@gmail.com>2019-04-05 18:14:38 -0400
committerMohamed Zenadi <zeapo@users.noreply.github.com>2019-04-06 00:14:38 +0200
commitf272e4dde2c05de64681a0e089387ef9c54bd7c7 (patch)
tree3e75e9c7a3a996c3f6d7762dac644ab3f4e7c700
parent94bf103b337d9df1d6232d85f5edf4673ede651c (diff)
Authentication using OpenKeystore SSH API (#486)
* Implemented OpenKeystore SSH API as a new authentication option * Fix formatting problems Signed-off-by: Harsh Shandilya <msfjarvis@gmail.com> * Addressed review comments. Removed leftover debugging code. Wrapped excessively long lines. Added missing new parameter to Javadoc. * Merge remote-tracking branch 'upstream/master' into gpg-ssh-key
-rw-r--r--app/build.gradle.kts1
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/GitActivity.java150
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/GitOperation.java41
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java358
-rw-r--r--app/src/main/res/values/arrays.xml1
-rw-r--r--app/src/main/res/values/strings.xml4
6 files changed, 493 insertions, 62 deletions
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 5c7a58df..a82de5dc 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -84,6 +84,7 @@ dependencies {
implementation("com.jayway.android.robotium:robotium-solo:5.6.3")
implementation(kotlin("stdlib-jdk8", KotlinCompilerVersion.VERSION))
implementation("androidx.constraintlayout:constraintlayout:1.1.3")
+ implementation("org.sufficientlysecure:sshauthentication-api:1.0")
// Testing-only dependencies
androidTestImplementation("junit:junit:4.12")
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.java b/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.java
index ca451247..e945c973 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.java
+++ b/app/src/main/java/com/zeapo/pwdstore/git/GitActivity.java
@@ -17,11 +17,15 @@ import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
+
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
+
import com.zeapo.pwdstore.R;
import com.zeapo.pwdstore.UserPreference;
+import com.zeapo.pwdstore.git.config.SshApiSessionFactory;
import com.zeapo.pwdstore.utils.PasswordRepository;
+
import org.apache.commons.io.FileUtils;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.RebaseCommand;
@@ -53,6 +57,8 @@ public class GitActivity extends AppCompatActivity {
private File localDir;
private String hostname;
private SharedPreferences settings;
+ private SshApiSessionFactory.IdentityBuilder identityBuilder;
+ private SshApiSessionFactory.ApiIdentity identity;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -117,8 +123,10 @@ public class GitActivity extends AppCompatActivity {
connection_mode_spinner.setEnabled(true);
// however, if we have some saved that, that's more important!
- if (connectionMode.equals("ssh-key")) {
+ if (connectionMode.equalsIgnoreCase("ssh-key")) {
connection_mode_spinner.setSelection(0);
+ } else if (connectionMode.equalsIgnoreCase("OpenKeychain")) {
+ connection_mode_spinner.setSelection(2);
} else {
connection_mode_spinner.setSelection(1);
}
@@ -371,6 +379,16 @@ public class GitActivity extends AppCompatActivity {
}
@Override
+ protected void onDestroy() {
+ // Do not leak the service connection
+ if (identityBuilder != null) {
+ identityBuilder.close();
+ identityBuilder = null;
+ }
+ super.onDestroy();
+ }
+
+ @Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.git_clone, menu);
@@ -556,16 +574,7 @@ public class GitActivity extends AppCompatActivity {
(dialog, id) -> {
try {
FileUtils.deleteDirectory(localDir);
- try {
- new CloneOperation(localDir, activity)
- .setCommand(hostname)
- .executeAfterAuthentication(connectionMode, settings.getString("git_remote_username", "git"), new File(getFilesDir() + "/.ssh_key"));
- } catch (Exception e) {
- //This is what happens when jgit fails :(
- //TODO Handle the diffent cases of exceptions
- e.printStackTrace();
- new AlertDialog.Builder(GitActivity.this).setMessage(e.getMessage()).show();
- }
+ launchGitOperation(REQUEST_CLONE);
} catch (IOException e) {
//TODO Handle the exception correctly if we are unable to delete the directory...
e.printStackTrace();
@@ -590,15 +599,13 @@ public class GitActivity extends AppCompatActivity {
new AlertDialog.Builder(GitActivity.this).setMessage(e.getMessage()).show();
}
}
- new CloneOperation(localDir, activity)
- .setCommand(hostname)
- .executeAfterAuthentication(connectionMode, settings.getString("git_remote_username", "git"), new File(getFilesDir() + "/.ssh_key"));
} catch (Exception e) {
//This is what happens when jgit fails :(
//TODO Handle the diffent cases of exceptions
e.printStackTrace();
new AlertDialog.Builder(this).setMessage(e.getMessage()).show();
}
+ launchGitOperation(REQUEST_CLONE);
}
}
@@ -627,47 +634,45 @@ public class GitActivity extends AppCompatActivity {
else {
// check that the remote origin is here, else add it
PasswordRepository.addRemote("origin", hostname, false);
- GitOperation op;
-
- switch (operation) {
- case REQUEST_PULL:
- op = new PullOperation(localDir, activity).setCommand();
- break;
- case REQUEST_PUSH:
- op = new PushOperation(localDir, activity).setCommand();
- break;
- case REQUEST_SYNC:
- op = new SyncOperation(localDir, activity).setCommands();
- break;
- default:
- Log.e(TAG, "Sync operation not recognized : " + operation);
- return;
- }
-
- try {
- op.executeAfterAuthentication(connectionMode, settings.getString("git_remote_username", "git"), new File(getFilesDir() + "/.ssh_key"));
- } catch (Exception e) {
- e.printStackTrace();
- }
+ launchGitOperation(operation);
}
}
- protected void onActivityResult(int requestCode, int resultCode,
- Intent data) {
- if (resultCode == RESULT_CANCELED) {
- setResult(RESULT_CANCELED);
- finish();
- return;
- }
+ /**
+ * Attempt to launch the requested GIT operation. Depending on the configured auth, it may not
+ * be possible to launch the operation immediately. In that case, this function may launch an
+ * intermediate activity instead, which will gather necessary information and post it back via
+ * onActivityResult, which will then re-call this function. This may happen multiple times,
+ * until either an error is encountered or the operation is successfully launched.
+ *
+ * @param operation The type of GIT operation to launch
+ */
+ protected void launchGitOperation(int operation) {
+ GitOperation op;
+
+ try {
+
+ // Before launching the operation with OpenKeychain auth, we need to issue several requests
+ // to the OpenKeychain API. IdentityBuild will take care of launching the relevant intents,
+ // we just need to keep calling it until it returns a completed ApiIdentity.
+ if (connectionMode.equalsIgnoreCase("OpenKeychain") && identity == null) {
+ // Lazy initialization of the IdentityBuilder
+ if (identityBuilder == null) {
+ identityBuilder = new SshApiSessionFactory.IdentityBuilder(this);
+ }
- if (resultCode == RESULT_OK) {
- GitOperation op;
+ // Try to get an ApiIdentity and bail if one is not ready yet. The builder will ensure
+ // that onActivityResult is called with operation again, which will re-invoke us here
+ identity = identityBuilder.tryBuild(operation);
+ if (identity == null)
+ return;
+ }
- switch (requestCode) {
+ switch (operation) {
case REQUEST_CLONE:
- setResult(RESULT_OK);
- finish();
- return;
+ op = new CloneOperation(localDir, activity).setCommand(hostname);
+ break;
+
case REQUEST_PULL:
op = new PullOperation(localDir, activity).setCommand();
break;
@@ -676,22 +681,57 @@ public class GitActivity extends AppCompatActivity {
op = new PushOperation(localDir, activity).setCommand();
break;
+ case REQUEST_SYNC:
+ op = new SyncOperation(localDir, activity).setCommands();
+ break;
+
case GitOperation.GET_SSH_KEY_FROM_CLONE:
op = new CloneOperation(localDir, activity).setCommand(hostname);
break;
+
+ case SshApiSessionFactory.POST_SIGNATURE:
+ return;
+
default:
- Log.e(TAG, "Operation not recognized : " + resultCode);
+ Log.e(TAG, "Operation not recognized : " + operation);
setResult(RESULT_CANCELED);
finish();
return;
}
- try {
- op.executeAfterAuthentication(connectionMode, settings.getString("git_remote_username", "git"), new File(getFilesDir() + "/.ssh_key"));
- } catch (Exception e) {
- e.printStackTrace();
- }
+ op.executeAfterAuthentication(connectionMode,
+ settings.getString("git_remote_username", "git"),
+ new File(getFilesDir() + "/.ssh_key"),
+ identity);
+ } catch (Exception e) {
+ e.printStackTrace();
+ new AlertDialog.Builder(this).setMessage(e.getMessage()).show();
+ }
+ }
+ protected void onActivityResult(int requestCode, int resultCode,
+ Intent data) {
+
+ // In addition to the pre-operation-launch series of intents for OpenKeychain auth
+ // that will pass through here and back to launchGitOperation, there is one
+ // synchronous operation that happens /after/ the operation has been launched in the
+ // background thread - the actual signing of the SSH challenge. We pass through the
+ // completed signature to the ApiIdentity, which will be blocked in the other thread
+ // waiting for it.
+ if (requestCode == SshApiSessionFactory.POST_SIGNATURE && identity != null)
+ identity.postSignature(data);
+
+ if (resultCode == RESULT_CANCELED) {
+ setResult(RESULT_CANCELED);
+ finish();
+ } else if (resultCode == RESULT_OK) {
+ // If an operation has been re-queued via this mechanism, let the
+ // IdentityBuilder attempt to extract some updated state from the intent before
+ // trying to re-launch the operation.
+ if (identityBuilder != null) {
+ identityBuilder.consume(data);
+ }
+ launchGitOperation(requestCode);
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.java b/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.java
index 51ea92d3..39edf158 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.java
+++ b/app/src/main/java/com/zeapo/pwdstore/git/GitOperation.java
@@ -11,16 +11,20 @@ import android.view.View;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.LinearLayout;
+
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
+
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.KeyPair;
import com.zeapo.pwdstore.R;
import com.zeapo.pwdstore.UserPreference;
import com.zeapo.pwdstore.git.config.GitConfigSessionFactory;
+import com.zeapo.pwdstore.git.config.SshApiSessionFactory;
import com.zeapo.pwdstore.git.config.SshConfigSessionFactory;
import com.zeapo.pwdstore.utils.PasswordRepository;
+
import org.eclipse.jgit.api.GitCommand;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.JschConfigSessionFactory;
@@ -77,6 +81,18 @@ public abstract class GitOperation {
}
/**
+ * Sets the authentication using OpenKeystore scheme
+ *
+ * @param identity The identiy to use
+ * @return the current object
+ */
+ GitOperation setAuthentication(String username, SshApiSessionFactory.ApiIdentity identity) {
+ SshSessionFactory.setInstance(new SshApiSessionFactory(username, identity));
+ this.provider = null;
+ return this;
+ }
+
+ /**
* Executes the GitCommand in an async task
*/
public abstract void execute();
@@ -86,10 +102,14 @@ public abstract class GitOperation {
*
* @param connectionMode the server-connection mode
* @param username the username
- * @param sshKey the ssh-key file
+ * @param sshKey the ssh-key file to use in ssh-key connection mode
+ * @param identity the api identity to use for auth in OpenKeychain connection mode
*/
- public void executeAfterAuthentication(final String connectionMode, final String username, @Nullable final File sshKey) {
- executeAfterAuthentication(connectionMode, username, sshKey, false);
+ public void executeAfterAuthentication(final String connectionMode,
+ final String username,
+ @Nullable final File sshKey,
+ SshApiSessionFactory.ApiIdentity identity) {
+ executeAfterAuthentication(connectionMode, username, sshKey, identity, false);
}
/**
@@ -97,10 +117,15 @@ public abstract class GitOperation {
*
* @param connectionMode the server-connection mode
* @param username the username
- * @param sshKey the ssh-key file
+ * @param sshKey the ssh-key file to use in ssh-key connection mode
+ * @param identity the api identity to use for auth in OpenKeychain connection mode
* @param showError show the passphrase edit text in red
*/
- private void executeAfterAuthentication(final String connectionMode, final String username, @Nullable final File sshKey, final boolean showError) {
+ private void executeAfterAuthentication(final String connectionMode,
+ final String username,
+ @Nullable final File sshKey,
+ SshApiSessionFactory.ApiIdentity identity,
+ final boolean showError) {
if (connectionMode.equalsIgnoreCase("ssh-key")) {
if (sshKey == null || !sshKey.exists()) {
new AlertDialog.Builder(callingActivity)
@@ -153,7 +178,7 @@ public abstract class GitOperation {
setAuthentication(sshKey, username, sshKeyPassphrase).execute();
} else {
// call back the method
- executeAfterAuthentication(connectionMode, username, sshKey, true);
+ executeAfterAuthentication(connectionMode, username, sshKey, identity, true);
}
} else {
new AlertDialog.Builder(callingActivity)
@@ -171,7 +196,7 @@ public abstract class GitOperation {
} else {
settings.edit().putString("ssh_key_passphrase", null).apply();
// call back the method
- executeAfterAuthentication(connectionMode, username, sshKey, true);
+ executeAfterAuthentication(connectionMode, username, sshKey, identity, true);
}
}).setNegativeButton(callingActivity.getResources().getString(R.string.dialog_cancel), (dialog, whichButton) -> {
// Do nothing.
@@ -189,6 +214,8 @@ public abstract class GitOperation {
}).show();
}
}
+ } else if (connectionMode.equalsIgnoreCase("OpenKeychain")) {
+ setAuthentication(username, identity).execute();
} else {
final EditText password = new EditText(callingActivity);
password.setHint("Password");
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java b/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java
new file mode 100644
index 00000000..f3d2f20a
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java
@@ -0,0 +1,358 @@
+package com.zeapo.pwdstore.git.config;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.content.IntentSender;
+
+import androidx.appcompat.app.AlertDialog;
+
+import com.jcraft.jsch.Identity;
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.Session;
+import com.jcraft.jsch.UserInfo;
+import com.zeapo.pwdstore.R;
+
+import org.eclipse.jgit.errors.UnsupportedCredentialItem;
+import org.eclipse.jgit.transport.CredentialItem;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.CredentialsProviderUserInfo;
+import org.eclipse.jgit.transport.OpenSshConfig;
+import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.util.Base64;
+import org.eclipse.jgit.util.FS;
+import org.openintents.ssh.authentication.ISshAuthenticationService;
+import org.openintents.ssh.authentication.SshAuthenticationApi;
+import org.openintents.ssh.authentication.SshAuthenticationApiError;
+import org.openintents.ssh.authentication.SshAuthenticationConnection;
+import org.openintents.ssh.authentication.request.KeySelectionRequest;
+import org.openintents.ssh.authentication.request.Request;
+import org.openintents.ssh.authentication.request.SigningRequest;
+import org.openintents.ssh.authentication.request.SshPublicKeyRequest;
+import org.openintents.ssh.authentication.util.SshAuthenticationApiUtils;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+public class SshApiSessionFactory extends GitConfigSessionFactory {
+ /**
+ * Intent request code indicating a completed signature that should be posted to an outstanding
+ * ApiIdentity
+ */
+ public static final int POST_SIGNATURE = 301;
+ private String username;
+ private Identity identity;
+ public SshApiSessionFactory(String username, Identity identity) {
+ this.username = username;
+ this.identity = identity;
+ }
+
+ @Override
+ protected JSch
+ getJSch(final OpenSshConfig.Host hc, FS fs) throws JSchException {
+ JSch jsch = super.getJSch(hc, fs);
+ jsch.removeAllIdentity();
+ jsch.addIdentity(identity, null);
+ return jsch;
+ }
+
+ @Override
+ protected void configure(OpenSshConfig.Host hc, Session session) {
+ session.setConfig("StrictHostKeyChecking", "no");
+ session.setConfig("PreferredAuthentications", "publickey");
+
+ CredentialsProvider provider = new CredentialsProvider() {
+ @Override
+ public boolean isInteractive() {
+ return false;
+ }
+
+ @Override
+ public boolean supports(CredentialItem... items) {
+ return true;
+ }
+
+ @Override
+ public boolean get(URIish uri, CredentialItem... items) throws UnsupportedCredentialItem {
+ for (CredentialItem item : items) {
+ if (item instanceof CredentialItem.Username) {
+ ((CredentialItem.Username) item).setValue(username);
+ }
+ }
+ return true;
+ }
+ };
+ UserInfo userInfo = new CredentialsProviderUserInfo(session, provider);
+ session.setUserInfo(userInfo);
+ }
+
+ /**
+ * Helper to build up an ApiIdentity via the invocation of several pending intents that
+ * communicate with OpenKeychain. The user of this class must handle onActivityResult and
+ * keep feeding the resulting intents into the IdentityBuilder until it can successfully complete
+ * the build.
+ */
+ public static class IdentityBuilder {
+ private SshAuthenticationConnection connection;
+ private SshAuthenticationApi api;
+ private String keyId, description, alg;
+ private byte[] publicKey;
+ private Activity callingActivity;
+
+ /**
+ * Construct a new IdentityBuilder
+ *
+ * @param callingActivity Activity that will be used to launch pending intents and that will
+ * receive and handle the results.
+ */
+ public IdentityBuilder(Activity callingActivity) {
+ this.callingActivity = callingActivity;
+
+ List<String> providers = SshAuthenticationApiUtils.getAuthenticationProviderPackageNames(callingActivity);
+ if (providers.isEmpty())
+ throw new RuntimeException(callingActivity.getString(R.string.no_ssh_api_provider));
+
+ // TODO: Handle multiple available providers? Are there actually any in practice beyond
+ // OpenKeychain?
+ connection = new SshAuthenticationConnection(callingActivity, providers.get(0));
+ }
+
+ /**
+ * Free any resources associated with this IdentityBuilder
+ */
+ public void close() {
+ if (connection != null && connection.isConnected())
+ connection.disconnect();
+ }
+
+ /**
+ * Helper to invoke an OpenKeyshain SSH API method and correctly interpret the result.
+ *
+ * @param request The request intent to launch
+ * @param requestCode The request code to use if a pending intent needs to be sent
+ * @return The resulting intent if the request completed immediately, or null if we had to
+ * launch a pending intent to interact with the user
+ */
+ private Intent executeApi(Request request, int requestCode) {
+ Intent result = api.executeApi(request.toIntent());
+
+ switch (result.getIntExtra(SshAuthenticationApi.EXTRA_RESULT_CODE, -1)) {
+ case SshAuthenticationApi.RESULT_CODE_ERROR:
+ SshAuthenticationApiError error = result.getParcelableExtra(SshAuthenticationApi.EXTRA_ERROR);
+ throw new RuntimeException(error.getMessage());
+ case SshAuthenticationApi.RESULT_CODE_SUCCESS:
+ break;
+ case SshAuthenticationApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ PendingIntent pendingIntent = result.getParcelableExtra(SshAuthenticationApi.EXTRA_PENDING_INTENT);
+ try {
+ callingActivity.startIntentSenderForResult(pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0);
+ return null;
+ } catch (IntentSender.SendIntentException e) {
+ e.printStackTrace();
+ throw new RuntimeException(callingActivity.getString(R.string.ssh_api_pending_intent_failed));
+ }
+ default:
+ throw new RuntimeException(callingActivity.getString(R.string.ssh_api_unknown_error));
+ }
+
+ return result;
+ }
+
+ /**
+ * Parse a given intent to see if it is the result of an OpenKeychain pending intent. If so,
+ * extract any updated state from it.
+ *
+ * @param intent The intent to inspect
+ */
+ public void consume(Intent intent) {
+ if (intent == null)
+ return;
+
+ if (intent.hasExtra(SshAuthenticationApi.EXTRA_KEY_ID)) {
+ keyId = intent.getStringExtra(SshAuthenticationApi.EXTRA_KEY_ID);
+ description = intent.getStringExtra(SshAuthenticationApi.EXTRA_KEY_DESCRIPTION);
+ }
+
+ if (intent.hasExtra(SshAuthenticationApi.EXTRA_SSH_PUBLIC_KEY)) {
+ String keyStr = intent.getStringExtra(SshAuthenticationApi.EXTRA_SSH_PUBLIC_KEY);
+ String[] keyParts = keyStr.split(" ");
+ alg = keyParts[0];
+ publicKey = Base64.decode(keyParts[1]);
+ }
+ }
+
+ /**
+ * Try to build an ApiIdentity that will perform SSH authentication via OpenKeychain.
+ *
+ * @param requestCode The request code to use if a pending intent needs to be sent
+ * @return The built identity, or null of user interaction is still required (in which case
+ * a pending intent will have already been launched)
+ */
+ public ApiIdentity tryBuild(int requestCode) {
+ // First gate, need to initiate a connection to the service and wait for it to connect.
+ if (api == null) {
+ connection.connect(new SshAuthenticationConnection.OnBound() {
+ @Override
+ public void onBound(ISshAuthenticationService sshAgent) {
+ api = new SshAuthenticationApi(callingActivity, sshAgent);
+ // We can immediately try the next phase without needing to post back
+ // though onActivityResult
+ tryBuild(requestCode);
+ }
+
+ @Override
+ public void onError() {
+ new AlertDialog.Builder(callingActivity).setMessage(callingActivity.getString(
+ R.string.openkeychain_ssh_api_connect_fail)).show();
+ }
+ });
+
+ return null;
+ }
+
+ // Second gate, need the user to select which key they want to use
+ if (keyId == null) {
+ consume(executeApi(new KeySelectionRequest(), requestCode));
+ // If we did not immediately get the result, bail for now and wait to be re-entered
+ if (keyId == null)
+ return null;
+ }
+
+ // Third gate, need to get the public key for the selected key. This one often does not
+ // need use interaction.
+ if (publicKey == null) {
+ consume(executeApi(new SshPublicKeyRequest(keyId), requestCode));
+ // If we did not immediately get the result, bail for now and wait to be re-entered
+ if (publicKey == null)
+ return null;
+ }
+
+ // Have everything we need for now, build the identify
+ return new ApiIdentity(keyId, description, publicKey, alg, callingActivity, api);
+ }
+ }
+
+ /**
+ * A Jsch identity that delegates key operations via the OpenKeychain SSH API
+ */
+ public static class ApiIdentity implements Identity {
+ private String keyId, description, alg;
+ private byte[] publicKey;
+ private Activity callingActivity;
+ private SshAuthenticationApi api;
+ private CountDownLatch latch;
+ private byte[] signature;
+
+ ApiIdentity(String keyId, String description, byte[] publicKey, String alg, Activity callingActivity, SshAuthenticationApi api) {
+ this.keyId = keyId;
+ this.description = description;
+ this.publicKey = publicKey;
+ this.alg = alg;
+ this.callingActivity = callingActivity;
+ this.api = api;
+ }
+
+ @Override
+ public boolean setPassphrase(byte[] passphrase) throws JSchException {
+ // We are not encrypted with a passphrase
+ return true;
+ }
+
+ @Override
+ public byte[] getPublicKeyBlob() {
+ return publicKey;
+ }
+
+ /**
+ * Helper to handle the result of an OpenKeyshain SSH API signing request
+ *
+ * @param result The result intent to handle
+ * @return The signed challenge, or null if it was not immediately available, in which
+ * case the latch has been initialized and the pending intent started
+ */
+ private byte[] handleSignResult(Intent result) {
+ switch (result.getIntExtra(SshAuthenticationApi.EXTRA_RESULT_CODE, -1)) {
+ case SshAuthenticationApi.RESULT_CODE_ERROR:
+ SshAuthenticationApiError error = result.getParcelableExtra(SshAuthenticationApi.EXTRA_ERROR);
+ throw new RuntimeException(error.getMessage());
+ case SshAuthenticationApi.RESULT_CODE_SUCCESS:
+ return result.getByteArrayExtra(SshAuthenticationApi.EXTRA_SIGNATURE);
+ case SshAuthenticationApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ PendingIntent pendingIntent = result.getParcelableExtra(SshAuthenticationApi.EXTRA_PENDING_INTENT);
+ try {
+ latch = new CountDownLatch(1);
+ callingActivity.startIntentSenderForResult(pendingIntent.getIntentSender(), POST_SIGNATURE, null, 0, 0, 0);
+ return null;
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new RuntimeException(callingActivity.getString(R.string.ssh_api_pending_intent_failed));
+ }
+ default:
+ if (result.hasExtra(SshAuthenticationApi.EXTRA_CHALLENGE))
+ return handleSignResult(api.executeApi(result));
+ throw new RuntimeException(callingActivity.getString(R.string.ssh_api_unknown_error));
+ }
+
+ }
+
+ @Override
+ public byte[] getSignature(byte[] data) {
+ Intent request = new SigningRequest(data, keyId, SshAuthenticationApi.SHA1).toIntent();
+ signature = handleSignResult(api.executeApi(request));
+
+ // If we did not immediately get a signature (probable), we will block on a latch until
+ // the main activity gets the intent result and posts to us.
+ if (signature == null) {
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ return signature;
+ }
+
+
+ /**
+ * Post a signature response back to an in-progress operation using this ApiIdentity.
+ *
+ * @param data The signature data (hopefully)
+ */
+ public void postSignature(Intent data) {
+ try {
+ signature = handleSignResult(data);
+ } finally {
+ if (latch != null)
+ latch.countDown();
+ }
+ }
+
+ @Override
+ public boolean decrypt() {
+ return true;
+ }
+
+ @Override
+ public String getAlgName() {
+ return alg;
+ }
+
+ @Override
+ public String getName() {
+ return description;
+ }
+
+ @Override
+ public boolean isEncrypted() {
+ return false;
+ }
+
+ @Override
+ public void clear() {
+
+ }
+ }
+}
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 44698c41..77b93647 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -3,6 +3,7 @@
<string-array name="connection_modes" translatable="false">
<item>ssh-key</item>
<item>username/password</item>
+ <item>OpenKeychain</item>
</string-array>
<string-array name="clone_protocols" translatable="false">
<item>ssh://</item>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ba0fed4c..cc4888ca 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -252,4 +252,8 @@
<string name="crypto_extra_edit_hint">username: something other extra content</string>
<string name="get_last_changed_failed">Failed to get last changed date</string>
<string name="hotp_pending">Tap copy to calculate HOTP</string>
+ <string name="openkeychain_ssh_api_connect_fail">Failed to connect to OpenKeychain SSH API service.</string>
+ <string name="no_ssh_api_provider">No SSH API provider found. Is OpenKeychain installed?</string>
+ <string name="ssh_api_pending_intent_failed">SSH API pending intent failed</string>
+ <string name="ssh_api_unknown_error">Unknown SSH API Error</string>
</resources>