Browse Source

Closes #18 export GPG key

isundil 7 years ago
parent
commit
47e40b50f0

+ 10 - 2
app/src/main/AndroidManifest.xml

@@ -50,7 +50,15 @@
         <activity
             android:name=".ui.EditPasswordActivity"
             android:windowSoftInputMode="adjustResize" />
-        <activity android:name=".ui.GitPullActivity">
-        </activity>
+        <activity android:name=".ui.GitPullActivity"></activity>
+
+        <provider
+            android:authorities="info.knacki.pass.provider"
+            android:name="android.support.v4.content.FileProvider"
+            android:grantUriPermissions="true">
+            <meta-data
+                android:name="android.support.FILE_PROVIDER_PATHS"
+                android:resource="@xml/provider_paths" />
+        </provider>
     </application>
 </manifest>

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

@@ -1,6 +1,9 @@
 package info.knacki.pass.io;
 
 import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
 
 public class FileUtils {
     public static void rmdir(File f) {
@@ -13,4 +16,15 @@ public class FileUtils {
             f.delete();
         }
     }
+
+    public static void pipe(InputStream in, OutputStream out) throws IOException {
+        int len;
+        byte[] buf = new byte[1024];
+        do {
+            len = in.read(buf, 0, 1024);
+            if (len > 0) {
+                out.write(buf, 0, len);
+            }
+        } while (len > 0);
+    }
 }

+ 80 - 49
app/src/main/java/info/knacki/pass/io/GPGUtil.java

@@ -40,7 +40,6 @@ import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -57,6 +56,24 @@ import info.knacki.pass.settings.SettingsManager;
 public class GPGUtil {
     private final static Logger log = Logger.getLogger(GPGUtil.class.getName());
 
+    public static class MalformedKeyException extends IOException {
+        public final Throwable fCause;
+
+        public MalformedKeyException(Throwable e) {
+            fCause = e;
+        }
+
+        @Override
+        public synchronized Throwable getCause() {
+            return fCause;
+        }
+
+        @Override
+        public String getMessage() {
+            return fCause.getMessage();
+        }
+    }
+
     private static class PGPPrivateKeyAndPass {
         public final PGPPrivateKey fPrivateKey;
         public final String fPass;
@@ -67,29 +84,37 @@ public class GPGUtil {
         }
     }
 
-    private static boolean TryPassword(PGPSecretKey pgpSecKey, String pass, GitInterface.OnResponseListener<PGPPrivateKeyAndPass> onResponse) {
+    private static PGPPrivateKeyAndPass TryPassword(PGPSecretKey pgpSecKey, String pass) {
         try {
             PGPPrivateKey pgpPrivKey = pgpSecKey.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder(new JcaPGPDigestCalculatorProviderBuilder().setProvider("BC").build()).setProvider("BC").build(pass.toCharArray()));
             PGPPrivateKeyAndPass res = new PGPPrivateKeyAndPass(pgpPrivKey, pass);
-            onResponse.onResponse(res);
+            return res;
         } catch (PGPException e) {
             // Wrong password
             log.log(Level.INFO, e.getMessage() + " (wrong password ?)", e);
-            return false;
+            return null;
         }
-        return true;
     }
 
     private static void FindPassword(final FileInterfaceFactory.PasswordGetter passwordGetter, final PGPSecretKey pgpSecKey, final GitInterface.OnResponseListener<PGPPrivateKeyAndPass> resp) {
-        if (!TryPassword(pgpSecKey, "", resp)) {
+        PGPPrivateKeyAndPass priv = TryPassword(pgpSecKey, "");
+
+        if (priv != null) {
+            resp.onResponse(priv);
+        } else {
             GitInterface.OnResponseListener<String> onResp = new GitInterface.OnResponseListener<String>() {
                 @Override
                 public void onResponse(String result) {
                     if (result == null) {
                         resp.onError("Invalid password", null);
-                    } else if (!TryPassword(pgpSecKey, result, resp)) {
-                        passwordGetter.WrongPassword();
-                        passwordGetter.GetPassword(this);
+                    } else {
+                        PGPPrivateKeyAndPass priv = TryPassword(pgpSecKey, result);
+                        if (priv != null) {
+                            resp.onResponse(priv);
+                        } else {
+                            passwordGetter.WrongPassword();
+                            passwordGetter.GetPassword(this);
+                        }
                     }
                 }
 
@@ -139,13 +164,18 @@ public class GPGUtil {
         return null;
     }
 
-    private static InputStream getKeyInputStream(Context ctx) throws FileNotFoundException {
+    private static InputStream getKeyInputStream(Context ctx) {
         return SettingsManager.GetGPGKeyContent(ctx);
     }
 
-    private static PGPSecretKey findSecretKey(InputStream in) throws IOException, PGPException {
+    private static PGPSecretKey findSecretKey(InputStream in) throws IOException {
         in = PGPUtil.getDecoderStream(in);
-        PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(in, new JcaKeyFingerprintCalculator());
+        PGPSecretKeyRingCollection pgpSec;
+        try {
+            pgpSec = new PGPSecretKeyRingCollection(in, new JcaKeyFingerprintCalculator());
+        } catch (PGPException e) {
+            throw new MalformedKeyException(e);
+        }
         Iterator rIt = pgpSec.getKeyRings();
 
         while (rIt.hasNext()) {
@@ -182,7 +212,7 @@ public class GPGUtil {
         resp.onResponse(output);
     }
 
-    public static void DecryptFile(final Context ctx, final FileInterfaceFactory.PasswordGetter passwordGetter, final File file, final GitInterface.OnResponseListener<byte[]> resp) throws IOException, PGPException {
+    public static void DecryptFile(final Context ctx, final FileInterfaceFactory.PasswordGetter passwordGetter, final File file, final GitInterface.OnResponseListener<byte[]> resp) throws IOException {
         PGPObjectFactory pgpF = new PGPObjectFactory(PGPUtil.getDecoderStream(new FileInputStream(file)), new JcaKeyFingerprintCalculator());
         Object o = pgpF.nextObject();
         final Iterator<?> it = ((o instanceof PGPEncryptedDataList) ? (PGPEncryptedDataList) o : (PGPEncryptedDataList) pgpF.nextObject()).getEncryptedDataObjects();
@@ -246,7 +276,7 @@ public class GPGUtil {
             } catch (IOException | PGPException e) {
                 resp.onError(e.getMessage(), e);
             }
-        } catch (IOException | PGPException e) {
+        } catch (IOException e) {
             resp.onError(e.getMessage(), e);
         }
     }
@@ -264,45 +294,46 @@ public class GPGUtil {
                 .generateSecretKeyRing();
     }
 
-    public static void ChangePassword(final Context ctx, final FileInterfaceFactory.ChangePasswordGetter passwordGetter, final Runnable onDone) throws IOException {
-        try {
-            final PGPSecretKey secretKey = findSecretKey(getKeyInputStream(ctx));
-            FindPassword(
-                    passwordGetter.SetStep(FileInterfaceFactory.ChangePasswordGetter.eStep.eOldPassword),
-                    secretKey,
-                    new GitInterface.OnResponseListener<PGPPrivateKeyAndPass>() {
-                        @Override
-                        public void onResponse(final PGPPrivateKeyAndPass sKey) {
-                            passwordGetter.SetStep(FileInterfaceFactory.ChangePasswordGetter.eStep.eNewPassword).GetPassword(new GitInterface.OnResponseListener<String>() {
-                                @Override
-                                public void onResponse(String result) {
-                                    if (result != null) {
-                                        try {
-                                            ArrayList<PGPSecretKeyRing> keyringCollection = new ArrayList<>();
-                                            keyringCollection.add(DoChangePassword(secretKey, sKey, result));
-                                            ByteArrayOutputStream stream = new ByteArrayOutputStream();
-                                            new PGPSecretKeyRingCollection(keyringCollection).encode(stream);
-                                            SettingsManager.SetGPGKeyContent(ctx, SettingsManager.GPG_GENERATED, new ByteArrayInputStream(stream.toByteArray()));
-                                            onDone.run();
-                                        } catch (PGPException | IOException e) {
-                                            onError(e.getMessage(), e);
-                                        }
+    public static void ChangePassword(final Context ctx, final FileInterfaceFactory.ChangePasswordGetter passwordGetter, final GitInterface.OnResponseListener<Void> onDone) throws IOException {
+        final PGPSecretKey secretKey = findSecretKey(getKeyInputStream(ctx));
+        FindPassword(
+                passwordGetter.SetStep(FileInterfaceFactory.ChangePasswordGetter.eStep.eOldPassword),
+                secretKey,
+                new GitInterface.OnResponseListener<PGPPrivateKeyAndPass>() {
+                    @Override
+                    public void onResponse(final PGPPrivateKeyAndPass sKey) {
+                        passwordGetter.SetStep(FileInterfaceFactory.ChangePasswordGetter.eStep.eNewPassword).GetPassword(new GitInterface.OnResponseListener<String>() {
+                            @Override
+                            public void onResponse(String result) {
+                                if (result != null) {
+                                    try {
+                                        ArrayList<PGPSecretKeyRing> keyringCollection = new ArrayList<>();
+                                        keyringCollection.add(DoChangePassword(secretKey, sKey, result));
+                                        ByteArrayOutputStream stream = new ByteArrayOutputStream();
+                                        new PGPSecretKeyRingCollection(keyringCollection).encode(stream);
+                                        SettingsManager.SetGPGKeyContent(ctx, SettingsManager.GPG_GENERATED, new ByteArrayInputStream(stream.toByteArray()));
+                                        onDone.onResponse(null);
+                                    } catch (PGPException | IOException e) {
+                                        onError(e.getMessage(), e);
                                     }
                                 }
+                            }
 
-                                @Override
-                                public void onError(String msg, Throwable e) {
-                                    onDone.run();
-                                }
-                            });
-                        }
+                            @Override
+                            public void onError(String msg, Throwable e) {
+                                onDone.onError(msg, e);
+                            }
+                        });
+                    }
 
-                        @Override
-                        public void onError(String msg, Throwable e) {
-                        }
-                    });
-        } catch (PGPException e) {
+                    @Override
+                    public void onError(String msg, Throwable e) {
+                    }
+                });
+    }
 
-        }
+    public static boolean CheckIsPasswordProtected(Context ctx) throws IOException {
+        final PGPSecretKey secretKey = findSecretKey(getKeyInputStream(ctx));
+        return TryPassword(secretKey, "") == null;
     }
 }

+ 67 - 3
app/src/main/java/info/knacki/pass/settings/ui/SettingsActivity.java

@@ -4,6 +4,8 @@ import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.content.res.Configuration;
 import android.database.Cursor;
 import android.net.Uri;
@@ -17,6 +19,8 @@ import android.preference.PreferenceFragment;
 import android.preference.SwitchPreference;
 import android.provider.MediaStore;
 import android.support.v4.app.NavUtils;
+import android.support.v4.app.ShareCompat;
+import android.support.v4.content.FileProvider;
 import android.support.v7.app.ActionBar;
 import android.util.SparseArray;
 import android.view.MenuItem;
@@ -25,8 +29,10 @@ import android.widget.Toast;
 
 import java.io.File;
 import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.util.List;
+import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import info.knacki.pass.R;
@@ -488,21 +494,77 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
                 @Override
                 public boolean onPreferenceClick(Preference preference) {
                     try {
-                        GPGUtil.ChangePassword(getActivity(), new PasswordPicker.ChangePasswordPicker(getActivity()), new Runnable() {
+                        GPGUtil.ChangePassword(getActivity(), new PasswordPicker.ChangePasswordPicker(getActivity()), new GitInterface.OnResponseListener<Void>() {
                             @Override
-                            public void run() {
+                            public void onResponse(Void result) {
                                 updateGpgFileLabel();
                             }
+
+                            @Override
+                            public void onError(String msg, Throwable e) {
+                                Toast.makeText(getActivity(), "Error: " + msg, Toast.LENGTH_LONG).show();
+                            }
                         });
                     } catch (IOException e) {
-
+                        Toast.makeText(getActivity(), "Error: " + e.getMessage(), Toast.LENGTH_LONG).show();
                     }
                     return true;
                 }
             });
+            findPreference(getResources().getString(R.string.id_gpg_export)).setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+                @Override
+                public boolean onPreferenceClick(Preference preference) {
+                    try {
+                        if (!GPGUtil.CheckIsPasswordProtected(getActivity())) {
+                            new AlertPrompt(getActivity())
+                                    .setCancelable(true)
+                                    .setPositiveButton(R.string.yes, new AlertPrompt.OnClickListener() {
+                                        @Override
+                                        public void onClick(DialogInterface dialogInterface, View view) {
+                                            DoExportKey();
+                                        }
+                                    })
+                                    .setNegativeButton(R.string.no, null)
+                                    .setTitle(R.string.are_you_sure)
+                                    .setView(new TextView(getActivity()).SetText(R.string.unprotected_key))
+                                    .show();
+                        } else {
+                            DoExportKey();
+                        }
+                        return true;
+                    } catch (IOException e) {
+                        return false;
+                    }
+                }
+            });
             updateGpgFileLabel();
         }
 
+        private void DoExportKey() {
+            final File outFile = new File(getActivity().getCacheDir().getAbsolutePath() + "/secretkey.gpg");
+            try {
+                outFile.createNewFile();
+                FileOutputStream fout = new FileOutputStream(outFile);
+                FileUtils.pipe(SettingsManager.GetGPGKeyContent(getActivity()), fout);
+                fout.close();
+            } catch (IOException e) {
+                Toast.makeText(getActivity(), "Cannot prepare key for sharing: " + e.getMessage(), Toast.LENGTH_LONG).show();
+                log.log(Level.SEVERE, "Cannot write key to cache", e);
+                return;
+            }
+            final Uri fileUri = FileProvider.getUriForFile(getActivity(), getActivity().getApplicationContext().getPackageName() + ".provider", outFile);
+            final Intent shareIntent = ShareCompat.IntentBuilder.from(getActivity())
+                    .setStream(fileUri)
+                    .setType("application/octet-stream")
+                    .getIntent()
+                    .setData(fileUri)
+                    .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+            for (ResolveInfo resolveInfo : getActivity().getPackageManager().queryIntentActivities(shareIntent, PackageManager.MATCH_DEFAULT_ONLY))
+                getActivity().grantUriPermission(resolveInfo.activityInfo.packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
+            startActivity(Intent.createChooser(shareIntent, getResources().getText(R.string.id_gpg_export)));
+        }
+
         private String GetFileName(Uri uri) {
             String filename = uri.getLastPathSegment();
             Cursor cu = getActivity().getContentResolver().query(uri, null, null, null, null);
@@ -533,9 +595,11 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
             if (SettingsManager.HasGPGKey(getActivity())) {
                 findPreference(getResources().getString(R.string.id_gpg_keyfile)).setSummary(SettingsManager.GetGPGKeyName(getActivity()));
                 findPreference(getResources().getString(R.string.id_gpg_password)).setEnabled(true);
+                findPreference(getResources().getString(R.string.id_gpg_export)).setEnabled(true);
             } else {
                 findPreference(getResources().getString(R.string.id_gpg_keyfile)).setSummary(R.string.pref_summary_gpg_keyfile);
                 findPreference(getResources().getString(R.string.id_gpg_password)).setEnabled(false);
+                findPreference(getResources().getString(R.string.id_gpg_export)).setEnabled(false);
             }
         }
     }

+ 4 - 2
app/src/main/java/info/knacki/pass/ui/alertPrompt/AlertPrompt.java

@@ -20,12 +20,14 @@ public class AlertPrompt {
 
         @Override
         public void onClick(DialogInterface dialog, int which) {
-            fListener.onClick(dialog, fView);
+            if (fListener != null)
+                fListener.onClick(dialog, fView);
         }
 
         @Override
         public void onCancel(DialogInterface dialog) {
-            fListener.onClick(dialog, fView);
+            if (fListener != null)
+                fListener.onClick(dialog, fView);
         }
 
         void SetListener(OnClickListener listener) {

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

@@ -76,4 +76,8 @@
     <string name="enter_old_password">Saisissez l\'ancien mot de passe</string>
     <string name="type_new_password">Saisissez le nouveau mot de passe</string>
     <string name="retype_new_password">Saisissez de nouveau le nouveau mot de passe</string>
+    <string name="yes">Oui</string>
+    <string name="no">Non</string>
+    <string name="unprotected_key">Cette clé n\'est pas protégée par un mot de passe, Voulez-vous continuer ?</string>
+    <string name="pref_header_gpg_export">Export GPG key</string>
 </resources>

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

@@ -76,4 +76,8 @@
     <string name="enter_old_password">Enter old password</string>
     <string name="type_new_password">Enter new password</string>
     <string name="retype_new_password">Retype new password</string>
+    <string name="yes">Yes</string>
+    <string name="no">No</string>
+    <string name="unprotected_key">This key is not protected with a password, do you want to continue ?</string>
+    <string name="pref_header_gpg_export">Export GPG key</string>
 </resources>

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

@@ -24,6 +24,7 @@
     <string name="id_vcs_git_authcategory">id_vcs_git_authcategory</string>
     <string name="id_gpg_keyfile">id_gpg_keyfile</string>
     <string name="id_gpg_password">id_gpg_password</string>
+    <string name="id_gpg_export">id_gpg_export</string>
     <string name="id_removeall">id_removeall</string>
     <integer name="id_keyboard_numbers">-2</integer>
     <integer name="id_keyboard_shift">-1</integer>

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

@@ -8,4 +8,7 @@
         android:key="@string/id_gpg_password"
         android:title="@string/pref_header_gpg_password"
         android:summary="@string/pref_summary_gpg_password" />
+    <Preference
+        android:key="@string/id_gpg_export"
+        android:title="@string/pref_header_gpg_export" />
 </PreferenceScreen>

+ 6 - 0
app/src/main/res/xml/provider_paths.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<paths>
+    <cache-path
+        name="cache"
+        path="."></cache-path>
+</paths>