Selaa lähdekoodia

Refs #47 init ssh connection, settings

isundil 7 vuotta sitten
vanhempi
commit
323ef8c4c1

+ 6 - 1
app/build.gradle

@@ -3,7 +3,7 @@ apply plugin: 'com.android.application'
 android {
     compileSdkVersion 28
     defaultConfig {
-        applicationId "info.knacki.pass"
+        applicationId "info.knacki.pass2"
         minSdkVersion 15
         targetSdkVersion 28
         versionCode 1
@@ -16,6 +16,9 @@ android {
             minifyEnabled false
             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
         }
+        debug {
+            applicationIdSuffix ".debug"
+        }
     }
     buildToolsVersion '28.0.3'
     compileOptions {
@@ -37,4 +40,6 @@ dependencies {
     implementation 'commons-http:commons-http:1.1'
     implementation 'org.bouncycastle:bcpg-jdk15on:1.60'
     implementation 'org.bouncycastle:bcprov-jdk15on:1.60'
+    implementation 'org.bouncycastle:bcprov-jdk15on:1.60'
+    implementation 'com.jcraft:jsch:0.1.55'
 }

+ 1 - 1
app/src/main/AndroidManifest.xml

@@ -59,7 +59,7 @@
 
         <provider
             android:name="android.support.v4.content.FileProvider"
-            android:authorities="info.knacki.pass.provider"
+            android:authorities="${applicationId}.provider"
             android:grantUriPermissions="true">
             <meta-data
                 android:name="android.support.FILE_PROVIDER_PATHS"

+ 3 - 1
app/src/main/java/info/knacki/pass/git/GitInterfaceFactory.java

@@ -4,8 +4,10 @@ import info.knacki.pass.settings.SettingsManager;
 
 public class GitInterfaceFactory {
     public static GitInterface factory(SettingsManager.Git config) {
+        if (config == null)
+            return null;
         if (config.GetUrl().startsWith("http://") || config.GetUrl().startsWith("https://"))
             return new HttpGitProtocol(config);
-        return null;
+        return new SSHGitProtocol(config);
     }
 }

+ 70 - 0
app/src/main/java/info/knacki/pass/git/SSHGitProtocol.java

@@ -0,0 +1,70 @@
+package info.knacki.pass.git;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.logging.Logger;
+
+import info.knacki.pass.git.entities.GitCommit;
+import info.knacki.pass.git.entities.GitObject;
+import info.knacki.pass.git.entities.GitRef;
+import info.knacki.pass.io.AppendableInputStream;
+import info.knacki.pass.io.CharsetHelper;
+import info.knacki.pass.io.OnResponseListener;
+import info.knacki.pass.io.OnStreamResponseListener;
+import info.knacki.pass.io.ssh.SSHConnection;
+import info.knacki.pass.io.ssh.SSHFactory;
+import info.knacki.pass.settings.SettingsManager;
+
+public class SSHGitProtocol implements GitInterface {
+    private final static Logger log = Logger.getLogger(SSHGitProtocol.class.getName());
+    private final SettingsManager.Git fConfig;
+
+    SSHGitProtocol(SettingsManager.Git config) {
+        fConfig = config;
+    }
+
+    private void GetRefs(SSHConnection con, OutputStream stdin, InputStream stdout) {
+    }
+
+    private void WriteStdErrToLogger(byte[] stderr) {
+        String err = CharsetHelper.ByteArrayToString(stderr);
+        if (!err.isEmpty())
+            log.severe(err);
+    }
+
+    @Override
+    public void GetRefs(OnResponseListener<GitRef[]> callback) {
+        AppendableInputStream stdin_in = new AppendableInputStream();
+        OutputStream stdin = stdin_in.GetWriter();
+
+        SSHFactory
+            .createInstance(fConfig, "git-upload-pack isundil/config") // FIXME tmp
+            .SetOnConnectionReadyListener((connection, stdout, stderr) -> GetRefs(connection, stdin, stdout))
+            .connect(stdin_in);
+    }
+
+    @Override
+    public void FetchCommit(GitRef ref, OnResponseListener<GitCommit> response) {
+
+    }
+
+    @Override
+    public void FetchTree(GitCommit ci, OnStreamResponseListener<GitObject.GitTree> response) {
+
+    }
+
+    @Override
+    public void FetchHead(OnStreamResponseListener<GitCommit> response) {
+
+    }
+
+    @Override
+    public void FetchBlob(GitObject.GitBlob blob, OnResponseListener<byte[]> response) {
+
+    }
+
+    @Override
+    public void PushBlobs(GitCommit.Builder commitBuilder, OnStreamResponseListener<Void> resp) {
+
+    }
+}

+ 33 - 0
app/src/main/java/info/knacki/pass/io/AppendableInputStream.java

@@ -0,0 +1,33 @@
+package info.knacki.pass.io;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayDeque;
+
+public class AppendableInputStream extends InputStream {
+    private ArrayDeque<Integer> buffer;
+    private OutputStream fWriter = new OutputStream() {
+        @Override
+        public void write(int b) {
+            buffer.push(b);
+        }
+    };
+
+    public AppendableInputStream() {
+        buffer = new ArrayDeque<>(256);
+    }
+
+    public OutputStream GetWriter() {
+        return fWriter;
+    }
+
+    @Override
+    public int read() {
+        return buffer.isEmpty() ? 0 : buffer.pop();
+    }
+
+    @Override
+    public int available() {
+        return buffer.size();
+    }
+}

+ 0 - 1
app/src/main/java/info/knacki/pass/io/FileUtils.java

@@ -1,7 +1,6 @@
 package info.knacki.pass.io;
 
 import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileReader;

+ 12 - 11
app/src/main/java/info/knacki/pass/io/NetworkUtils.java

@@ -21,24 +21,25 @@ import java.util.zip.InflaterInputStream;
 public class NetworkUtils {
     private final static Logger log = Logger.getLogger(NetworkUtils.class.getName());
 
-    public interface NetworkConfig {
+    public interface AuthConfig {
         boolean HasAuthentication();
         String GetUser();
         String GetPassword();
+        String GetPrivateKey();
     }
 
     static class DownloaderTask extends AsyncTask<Void, Void, Integer> {
         private final URL fUrl;
         private OnResponseListener<byte[]> fResp;
-        private final NetworkConfig fConfig;
+        private final AuthConfig fConfig;
 
-        DownloaderTask(URL url, NetworkConfig config, OnResponseListener<byte[]> resp) {
+        DownloaderTask(URL url, AuthConfig config, OnResponseListener<byte[]> resp) {
             fUrl = url;
             fResp = resp;
             fConfig = config;
         }
 
-        DownloaderTask(URL url, NetworkConfig config) {
+        DownloaderTask(URL url, AuthConfig config) {
             fUrl = url;
             fConfig = config;
         }
@@ -84,11 +85,11 @@ public class NetworkUtils {
     static class PostTask extends AsyncTask<Void, Void, Integer> {
         private final URL fUrl;
         private final OnResponseListener<byte[]> fResp;
-        private final NetworkConfig fConfig;
+        private final AuthConfig fConfig;
         private final OnResponseListener<OutputStream> fOnReadyWrite;
         private final Map<String, String> fHeaders;
 
-        PostTask(URL url, NetworkConfig config, Map<String, String> headers, OnResponseListener<byte[]> resp, OnResponseListener<OutputStream> onReadyWrite) {
+        PostTask(URL url, AuthConfig config, Map<String, String> headers, OnResponseListener<byte[]> resp, OnResponseListener<OutputStream> onReadyWrite) {
             fUrl = url;
             fResp = resp;
             fConfig = config;
@@ -167,7 +168,7 @@ public class NetworkUtils {
     }
 
     static class DownloaderWithInflaterTask extends DownloaderTask {
-        DownloaderWithInflaterTask(URL url, NetworkConfig config, OnResponseListener<byte[]> resp) {
+        DownloaderWithInflaterTask(URL url, AuthConfig config, OnResponseListener<byte[]> resp) {
             super(url, config, resp);
         }
 
@@ -196,7 +197,7 @@ public class NetworkUtils {
     static class StringDownloaderTask extends DownloaderTask {
         private final OnResponseListener<String> onResp;
 
-        StringDownloaderTask(URL url, NetworkConfig config, OnResponseListener<String> resultHandler) {
+        StringDownloaderTask(URL url, AuthConfig config, OnResponseListener<String> resultHandler) {
             super(url, config);
             SetResultHandler(new OnResponseListener<byte[]>() {
                 @Override
@@ -213,15 +214,15 @@ public class NetworkUtils {
         }
     }
 
-    public static void protoInflateGet(final URL url, NetworkConfig config, final OnResponseListener<byte[]> callback) {
+    public static void protoInflateGet(final URL url, AuthConfig config, final OnResponseListener<byte[]> callback) {
         new DownloaderWithInflaterTask(url, config, callback).execute();
     }
 
-    public static void ProtoPost(URL url, Map<String, String> headers, NetworkConfig config, OnResponseListener<byte[]> callback, OnResponseListener<OutputStream> onReadyWrite) {
+    public static void ProtoPost(URL url, Map<String, String> headers, AuthConfig config, OnResponseListener<byte[]> callback, OnResponseListener<OutputStream> onReadyWrite) {
         new PostTask(url, config, headers, callback, onReadyWrite).execute();
     }
 
-    public static void protoGet(final URL url, NetworkConfig config, final OnResponseListener<String> callback) {
+    public static void protoGet(final URL url, AuthConfig config, final OnResponseListener<String> callback) {
         new StringDownloaderTask(url, config, callback).execute();
     }
 }

+ 5 - 0
app/src/main/java/info/knacki/pass/io/PathUtils.java

@@ -7,6 +7,7 @@ public class PathUtils {
     @SuppressWarnings("SpellCheckingInspection")
     private static final String DATA_GIT_LOCAL = "gitfiles";
     private static final String DATA_FINGER_LOCAL = "fingerprint";
+    private static final String DATA_PRIVATE_SSH_KEY = "id_rsa";
     private static final String DATA_GPG_FILE = "key";
 
     private static String GetAppRootDir(Context ctx) {
@@ -29,6 +30,10 @@ public class PathUtils {
         return GetAppRootDir(ctx) +"/" +DATA_GPG_FILE;
     }
 
+    public static String GetPrivateRSAKey(Context ctx) {
+        return GetAppRootDir(ctx) +"/" +DATA_PRIVATE_SSH_KEY;
+    }
+
     public static final String TRASH_SUFFIX = ".trash";
 
     public static boolean IsHidden(String path) {

+ 115 - 0
app/src/main/java/info/knacki/pass/io/ssh/JSchWrapper.java

@@ -0,0 +1,115 @@
+package info.knacki.pass.io.ssh;
+
+import android.os.AsyncTask;
+
+import com.jcraft.jsch.ChannelExec;
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.Session;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import info.knacki.pass.io.FileUtils;
+
+public class JSchWrapper extends AsyncTask<InputStream, Void, Void> implements SSHConnection {
+    private static final Logger log = Logger.getLogger(JSchWrapper.class.getName());
+    private final String fCommand;
+    private final ConnectionParams fAuth;
+    private OnConnectionReady fReadyListener;
+
+    private final PipedOutputStream stdout_out = new PipedOutputStream();
+    private final PipedOutputStream stderr_out = new PipedOutputStream();
+    private final PipedInputStream stdout = new PipedInputStream();
+    private final PipedInputStream stderr = new PipedInputStream();
+
+    JSchWrapper(ConnectionParams config, String command) {
+        fCommand = command;
+        fAuth = config;
+    }
+
+    @Override
+    public SSHConnection SetOnConnectionReadyListener(OnConnectionReady listener) {
+        fReadyListener = listener;
+        return this;
+    }
+
+    @Override
+    public byte[] GetOutputStd() throws IOException {
+        return FileUtils.ReadAllStream(stdout);
+    }
+
+    @Override
+    public byte[] GetOutputErr() throws IOException {
+        return FileUtils.ReadAllStream(stderr);
+    }
+
+    @Override
+    public SSHConnection connect() {
+        execute();
+        return this;
+    }
+
+    @Override
+    public SSHConnection connect(InputStream stdin) {
+        execute(stdin);
+        return this;
+    }
+
+    @Override
+    protected Void doInBackground(InputStream... in) {
+        ChannelExec chan = InitConnection(in == null && in.length > 0 ? null : in[0]);
+        if (chan != null) {
+            try {
+                stderr.connect(stdout_out);
+                stdout.connect(stderr_out);
+            }
+            catch (IOException e) {
+                log.log(Level.SEVERE, "Cannot read from output: " +e.getMessage(), e);
+            }
+            if (fReadyListener != null)
+                fReadyListener.OnConnectionReady(this, stdout, stderr);
+        }
+        return null;
+    }
+
+    private ChannelExec InitConnection(InputStream in) {
+        ChannelExec chan;
+
+        try {
+            JSch connection = new JSch();
+            String privKey = fAuth.GetPrivateKey();
+            Session session = connection.getSession(fAuth.GetUser(), fAuth.GetUrl()); // FIXME only host !
+            session.setConfig("StrictHostKeyChecking", "no"); // FIXME
+            if (privKey != null && !privKey.isEmpty()) {
+                connection.addIdentity(privKey); // FIXME passphrase
+            } else {
+                session.setPassword(fAuth.GetPassword());
+            }
+            session.connect();
+            chan = (ChannelExec) session.openChannel("exec");
+            if (in != null)
+                chan.setInputStream(in);
+        }
+        catch (JSchException e) {
+            log.log(Level.SEVERE, "Cannot Initiate ssh command: " +e.getMessage(), e);
+            return null;
+        }
+        try {
+            chan.setErrStream(stderr_out);
+            chan.setOutputStream(stdout_out);
+
+            chan.setCommand(fCommand);
+            chan.connect();
+        }
+        catch (JSchException e) {
+            log.log(Level.SEVERE, "Cannot Initiate ssh command: " +e.getMessage(), e);
+            return null;
+        }
+        return chan;
+    }
+}

+ 22 - 0
app/src/main/java/info/knacki/pass/io/ssh/SSHConnection.java

@@ -0,0 +1,22 @@
+package info.knacki.pass.io.ssh;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import info.knacki.pass.io.NetworkUtils;
+
+public interface SSHConnection {
+    interface OnConnectionReady {
+        void OnConnectionReady(SSHConnection connection, InputStream stdout, InputStream stderr);
+    }
+
+    interface ConnectionParams extends NetworkUtils.AuthConfig {
+        String GetUrl();
+    }
+
+    SSHConnection SetOnConnectionReadyListener(OnConnectionReady listener);
+    byte[] GetOutputStd() throws IOException;
+    byte[] GetOutputErr() throws IOException;
+    SSHConnection connect();
+    SSHConnection connect(InputStream input);
+}

+ 7 - 0
app/src/main/java/info/knacki/pass/io/ssh/SSHFactory.java

@@ -0,0 +1,7 @@
+package info.knacki.pass.io.ssh;
+
+public class SSHFactory {
+    public static SSHConnection createInstance(SSHConnection.ConnectionParams sshParams, String command) {
+        return new JSchWrapper(sshParams, command);
+    }
+}

+ 38 - 10
app/src/main/java/info/knacki/pass/settings/SettingsManager.java

@@ -5,12 +5,15 @@ import android.content.SharedPreferences;
 import android.util.JsonReader;
 import android.util.MalformedJsonException;
 
+import java.io.File;
 import java.io.IOException;
 import java.io.StringReader;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import info.knacki.pass.io.NetworkUtils;
+import info.knacki.pass.io.PathUtils;
+import info.knacki.pass.io.ssh.SSHConnection;
 
 public class SettingsManager {
     private static final Logger log = Logger.getLogger(SettingsManager.class.getName());
@@ -47,9 +50,9 @@ public class SettingsManager {
     }
 
     public static abstract class VCS {
-        static VCS factory(String system, String data) {
+        static VCS factory(Context ctx, String system, String data) {
             if ("git".equals(system))
-                return new Git(data);
+                return new Git(ctx, data);
             return null;
         }
 
@@ -57,15 +60,18 @@ public class SettingsManager {
         public abstract String GetData();
     }
 
-    public static class Git extends VCS implements NetworkUtils.NetworkConfig {
+    public static class Git extends VCS implements NetworkUtils.AuthConfig, SSHConnection.ConnectionParams {
         String fUrl;
         String fUser;
         String fPassword;
+        boolean fPrivKey;
         String fBranch;
         String fUsername;
         String fUserEmail;
+        private final Context fContext;
 
-        Git(String data) {
+        Git(Context ctx, String data) {
+            fContext = ctx;
             if (null == data || data.isEmpty()) {
                 reset();
                 return;
@@ -99,21 +105,21 @@ public class SettingsManager {
                         case "email":
                             fUserEmail = reader.nextString();
                             break;
+                        case "privKey":
+                            fPrivKey = reader.nextBoolean();
+                            break;
                     }
                 }
                 reader.close();
             }
-            catch (MalformedJsonException e) {
-                log.log(Level.SEVERE, e.getMessage(), e);
-                reset();
-            }
             catch (IOException e) {
                 log.log(Level.SEVERE, e.getMessage(), e);
                 reset();
             }
         }
 
-        public Git() {
+        public Git(Context ctx) {
+            fContext = ctx;
             reset();
         }
 
@@ -124,6 +130,7 @@ public class SettingsManager {
             fPassword = "";
             fUsername = "";
             fUserEmail = "";
+            fPrivKey = false;
         }
 
         public String GetName() {
@@ -136,6 +143,7 @@ public class SettingsManager {
             return fUrl;
         }
 
+        @Override
         public String GetUrl() {
             return fUrl;
         }
@@ -144,14 +152,32 @@ public class SettingsManager {
             fPassword = pass;
         }
 
+        @Override
         public String GetPassword() {
             return fPassword;
         }
 
+        public void SetPrivateKey(boolean usePrivateKey) {
+            fPrivKey = usePrivateKey;
+        }
+
+        @Override
+        public String GetPrivateKey() {
+            if (!fPrivKey)
+                return null;
+            String privKeyFile = PathUtils.GetPrivateRSAKey(fContext);
+            return new File(privKeyFile).exists() ? privKeyFile : null;
+        }
+
+        public boolean HasPrivateKey() {
+            return fPrivKey;
+        }
+
         public void SetUser(String user) {
             fUser = user;
         }
 
+        @Override
         public String GetUser() {
             return fUser;
         }
@@ -172,6 +198,7 @@ public class SettingsManager {
             return fBranch;
         }
 
+        @Override
         public boolean HasAuthentication() {
             return !"".equals(fUser);
         }
@@ -185,6 +212,7 @@ public class SettingsManager {
                     "\"url\":\"" +jsonEscape(fUrl) +"\"," +
                     "\"user\":\"" +jsonEscape(fUser) +"\"," +
                     "\"pass\":\"" +jsonEscape(fPassword) +"\"," +
+                    "\"privKey\":" +(fPrivKey ? "true" : "false") +"," +
                     "\"branch\":\"" +jsonEscape(fBranch) +"\"," +
                     "\"username\":\"" +jsonEscape(fUsername) +"\"," +
                     "\"email\":\"" +jsonEscape(fUserEmail) +"\"" +
@@ -210,7 +238,7 @@ public class SettingsManager {
 
     public static VCS GetVCS(Context ctx) {
         SharedPreferences prefs = GetPrefManager(ctx);
-        return VCS.factory(prefs.getString(VCS.class.getSimpleName(), ""), prefs.getString(VCS.class.getSimpleName()+"_data", ""));
+        return VCS.factory(ctx, prefs.getString(VCS.class.getSimpleName(), ""), prefs.getString(VCS.class.getSimpleName()+"_data", ""));
     }
 
     public static void SetVCS(Context ctx, VCS vcs) {

+ 58 - 6
app/src/main/java/info/knacki/pass/settings/ui/SettingsActivity.java

@@ -45,9 +45,10 @@ import info.knacki.pass.git.entities.GitRef;
 import info.knacki.pass.io.FileInterfaceFactory;
 import info.knacki.pass.io.FileMigratoryUtils;
 import info.knacki.pass.io.FileUtils;
+import info.knacki.pass.io.OnResponseListener;
+import info.knacki.pass.io.PathUtils;
 import info.knacki.pass.io.pgp.GPGStorageEngine;
 import info.knacki.pass.io.pgp.GPGUtil;
-import info.knacki.pass.io.OnResponseListener;
 import info.knacki.pass.settings.SettingsManager;
 import info.knacki.pass.ui.GitPullActivity;
 import info.knacki.pass.ui.alertPrompt.AlertPromptGenerator;
@@ -234,6 +235,8 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
     }
 
     public static class VCSPreferenceFragment extends PreferenceFragment {
+        private static final int ACTIVITY_REQUEST_CODE_BROWSE_PRIVATE_KEY = 2;
+
         @Override
         public void onCreate(Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
@@ -254,10 +257,8 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
             findPreference(getResources().getString(R.string.id_vcs_enable)).setOnPreferenceChangeListener(new PrefListener() {
                 @Override
                 void savePref(Preference preference, Object o) {
-                    if ((Boolean) o)
-                        SettingsManager.SetVCS(getActivity(), new SettingsManager.Git());
-                    else
-                        SettingsManager.SetVCS(getActivity(), null);
+                    Context ctx = getActivity();
+                    SettingsManager.SetVCS(ctx, (boolean) o ? new SettingsManager.Git(ctx) : null);
                 }
             });
             findPreference(getResources().getString(R.string.id_vcs_list)).setOnPreferenceChangeListener(new PrefListener() {
@@ -268,7 +269,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
 
                     switch (Integer.parseInt((String) o)) {
                         case 0:
-                            newVcs = new SettingsManager.Git();
+                            newVcs = new SettingsManager.Git(getActivity());
                             break;
                         default:
                             return;
@@ -300,9 +301,20 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
                 void savePref(Preference preference, Object o) {
                     SettingsManager.Git git = (SettingsManager.Git) SettingsManager.GetVCS(getActivity());
                     git.SetPassword(((String) o).trim());
+                    git.SetPrivateKey(false);
                     SettingsManager.SetVCS(getActivity(), git);
                 }
             });
+            findPreference(getResources().getString(R.string.id_vcs_git_private_key)).setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+                @Override
+                public boolean onPreferenceClick(Preference preference) {
+                    Intent i = new Intent(Intent.ACTION_GET_CONTENT);
+                    i.setType("*/*");
+                    i.addCategory(Intent.CATEGORY_OPENABLE);
+                    VCSPreferenceFragment.this.startActivityForResult(i, ACTIVITY_REQUEST_CODE_BROWSE_PRIVATE_KEY);
+                    return true;
+                }
+            });
             findPreference(getResources().getString(R.string.id_vcs_git_ci_username)).setOnPreferenceChangeListener(new PrefListener() {
                 @Override
                 void savePref(Preference preference, Object o) {
@@ -381,6 +393,9 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
                     gitPass.setSummary(((SettingsManager.Git) versioning).GetPassword().length() == 0 ? "" : "******");
                     gitPass.setText(((SettingsManager.Git) versioning).GetPassword());
 
+                    Preference sshKey = FindPreference(R.string.id_vcs_git_private_key);
+                    sshKey.setSummary(((SettingsManager.Git) versioning).HasPrivateKey() ? R.string.pref_vcs_git_private_key_change : R.string.pref_vcs_git_private_key_set);
+
                     FindPreference(R.string.id_vcs_git_commit_info_category);
                     ListPreference gitBranches = (ListPreference) FindPreference(R.string.id_vcs_git_branches);
                     populateBranches(gitBranches, (SettingsManager.Git) versioning);
@@ -397,6 +412,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
                 } else {
                     RemovePrefs(R.string.id_vcs_git_url,
                             R.string.id_vcs_git_user,
+                            R.string.id_vcs_git_private_key,
                             R.string.id_vcs_git_pass,
                             R.string.id_vcs_git_branches,
                             R.string.id_vcs_git_pull,
@@ -414,6 +430,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
                 RemovePrefs(R.string.id_vcs_git_url,
                     R.string.id_vcs_git_user,
                     R.string.id_vcs_git_pass,
+                    R.string.id_vcs_git_private_key,
                     R.string.id_vcs_git_branches,
                     R.string.id_vcs_git_pull,
                     R.string.id_vcs_git_ci_username,
@@ -472,6 +489,41 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
             }
             return super.onOptionsItemSelected(item);
         }
+
+        private boolean SetPrivateKeyFile(InputStream in) {
+            SettingsManager.Git git = (SettingsManager.Git) SettingsManager.GetVCS(getActivity());
+            if (in != null) {
+                try {
+                    File fout = new File(PathUtils.GetPrivateRSAKey(getActivity()));
+                    if (!fout.exists() && !fout.createNewFile()) {
+                        throw new FileNotFoundException();
+                    }
+                    FileUtils.pipe(in, new FileOutputStream(fout));
+                } catch (IOException e) {
+                    log.log(Level.SEVERE, "Cannot import private key: " + e.getMessage(), e);
+                    return false;
+                }
+            }
+            git.SetPrivateKey(in != null);
+            git.SetPassword("");
+            SettingsManager.SetVCS(getActivity(), git);
+            VCSPreferenceFragment.this.reload();
+            return true;
+        }
+
+        @Override
+        public void onActivityResult(int requestCode, int resultCode, Intent data) {
+            if (requestCode == ACTIVITY_REQUEST_CODE_BROWSE_PRIVATE_KEY && resultCode == RESULT_OK) {
+                try {
+                    final Uri intentData = data.getData();
+                    boolean importStatus = SetPrivateKeyFile(intentData == null ? null : getActivity().getContentResolver().openInputStream(intentData));
+                    Toast.makeText(getActivity(), getResources().getString(importStatus ? R.string.gpg_import_ok : R.string.gpg_import_ko), Toast.LENGTH_LONG).show();
+                } catch (FileNotFoundException e) {
+                    Toast.makeText(getActivity(), getResources().getString(R.string.file_not_found), Toast.LENGTH_LONG).show();
+                    GPGStorageEngine.GetDefaultEngine(getActivity()).RemoveGpgKey();
+                }
+            }
+        }
     }
 
     public static class GPGPreferenceFragment extends PreferenceFragment {

+ 18 - 0
app/src/main/java/info/knacki/pass/ui/MainActivity.java

@@ -20,6 +20,9 @@ import java.util.logging.Logger;
 import info.knacki.pass.R;
 import info.knacki.pass.generator.PasswordGenerator;
 import info.knacki.pass.generator.ui.PasswordGeneratorWizard;
+import info.knacki.pass.git.GitInterface;
+import info.knacki.pass.git.GitInterfaceFactory;
+import info.knacki.pass.git.entities.GitRef;
 import info.knacki.pass.io.FileInterfaceFactory;
 import info.knacki.pass.io.FileUtils;
 import info.knacki.pass.io.IFileInterface;
@@ -124,6 +127,21 @@ public class MainActivity extends AppCompatActivity implements PasswordEditListe
         ((ScrollView)findViewById(R.id.passwordListContainer)).addView(fPasswordListView);
 
         requestPermissions();
+
+        GitInterface gitInterface = GitInterfaceFactory.factory((SettingsManager.Git) SettingsManager.GetVCS(this));
+        if (gitInterface != null) {
+            gitInterface.GetRefs(new OnResponseListener<GitRef[]>() {
+                @Override
+                public void OnResponse(GitRef[] result) {
+
+                }
+
+                @Override
+                public void OnError(String msg, Throwable e) {
+
+                }
+            });
+        }
     }
 
     @Override

+ 3 - 0
app/src/main/res/values-fr/lang.xml

@@ -29,6 +29,9 @@
     <string name="pref_vcs_url">Addresse du dépot git</string>
     <string name="pref_vcs_git_user_title">Nom d\'utilisateur</string>
     <string name="pref_vcs_git_pass_title">Mot de passe</string>
+    <string name="pref_vcs_git_private_key_title">Clée privée pour les connections SSH</string>
+    <string name="pref_vcs_git_private_key_set">Choisir sa clée privée</string>
+    <string name="pref_vcs_git_private_key_change">Changer sa clée privée</string>
     <string name="pref_vcs_branch_title">Branche GIT</string>
     <string name="pref_vcs_git_pull">Git pull</string>
     <string name="pref_vcs_git_auth_category">Authentication</string>

+ 3 - 0
app/src/main/res/values/lang.xml

@@ -35,6 +35,9 @@
     <string name="pref_vcs_url">Git repo address</string>
     <string name="pref_vcs_git_user_title">Username</string>
     <string name="pref_vcs_git_pass_title">Password</string>
+    <string name="pref_vcs_git_private_key_title">Private key for SSH connections</string>
+    <string name="pref_vcs_git_private_key_set">Set RSA private key</string>
+    <string name="pref_vcs_git_private_key_change">Change RSA private key</string>
     <string name="pref_vcs_branch_title">Git branch</string>
     <string name="pref_vcs_git_pull">Git pull</string>
     <string name="pref_vcs_git_auth_category">Authentication</string>

+ 2 - 1
app/src/main/res/values/strings.xml

@@ -1,5 +1,5 @@
 <resources>
-    <string name="app_name" translatable="false">pass</string>
+    <string name="app_name" translatable="false">pass 1.1</string>
     <string-array name="pref_vcs_list_values">
         <item>0</item>
     </string-array>
@@ -11,6 +11,7 @@
     <string name="id_vcs_git_url" translatable="false">id_vcs_git_url</string>
     <string name="id_vcs_git_user" translatable="false">id_vcs_git_user</string>
     <string name="id_vcs_git_pass" translatable="false">id_vcs_git_pass</string>
+    <string name="id_vcs_git_private_key" translatable="false">id_vcs_git_private_key</string>
     <string name="id_vcs_git_branches" translatable="false">id_vcs_git_branches</string>
     <string name="id_vcs_git_pull" translatable="false">id_vcs_git_pull</string>
     <string name="id_vcs_git_ci_username" translatable="false">id_vcs_git_ci_username</string>

+ 3 - 0
app/src/main/res/xml/pref_vcs.xml

@@ -28,6 +28,9 @@
         android:key="@string/id_vcs_git_pass"
         android:title="@string/pref_vcs_git_pass_title"
         android:inputType="textPassword"/>
+    <Preference
+        android:key="@string/id_vcs_git_private_key"
+        android:title="@string/pref_vcs_git_private_key_title"/>
 
     <PreferenceCategory
         android:title="@string/pref_vcs_git_commit_info_category"