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
This commit is contained in:
parent
94bf103b33
commit
f272e4dde2
6 changed files with 493 additions and 62 deletions
|
@ -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")
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
@ -370,6 +378,16 @@ public class GitActivity extends AppCompatActivity {
|
|||
updateURI();
|
||||
}
|
||||
|
||||
@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.
|
||||
|
@ -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;
|
||||
launchGitOperation(operation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
op.executeAfterAuthentication(connectionMode, settings.getString("git_remote_username", "git"), new File(getFilesDir() + "/.ssh_key"));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
protected void onActivityResult(int requestCode, int resultCode,
|
||||
Intent data) {
|
||||
if (resultCode == RESULT_CANCELED) {
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (resultCode == RESULT_OK) {
|
||||
GitOperation op;
|
||||
|
||||
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"));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
@ -76,6 +80,18 @@ public abstract class GitOperation {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
@ -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");
|
||||
|
|
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue