aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/com/zeapo/pwdstore/git/config
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 /app/src/main/java/com/zeapo/pwdstore/git/config
parent94bf103b337d9df1d6232d85f5edf4673ede651c (diff)
downloadHardenedPass-f272e4dde2c05de64681a0e089387ef9c54bd7c7.tar.gz
HardenedPass-f272e4dde2c05de64681a0e089387ef9c54bd7c7.zip
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
Diffstat (limited to 'app/src/main/java/com/zeapo/pwdstore/git/config')
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java358
1 files changed, 358 insertions, 0 deletions
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() {
+
+ }
+ }
+}