Browse Source

Fixes #16 edit password

isundil 7 years ago
parent
commit
88fcac146a

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

@@ -1,11 +1,15 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="info.knacki.pass">
+    <!-- For sync -->
     <uses-permission android:name="android.permission.INTERNET"/>
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+
+    <!-- For fingerprint unlock -->
     <uses-permission android:name="android.permission.USE_BIOMETRIC"/>
 
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <!-- For GPG keys -->
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_INTERNAL_STORAGE" />
 
     <application
         android:allowBackup="true"

+ 10 - 0
app/src/main/java/info/knacki/pass/io/FileInterfaceFactory.java

@@ -13,6 +13,16 @@ public class FileInterfaceFactory {
         void WrongPassword();
     }
 
+    public interface ChangePasswordGetter extends PasswordGetter {
+        enum eStep {
+            eOldPassword,
+            eNewPassword,
+            eRetypePassword
+        }
+
+        ChangePasswordGetter SetStep(eStep step);
+    }
+
     public final static String FINGERPRINT_SUFFIX = ".fck";
     public final static String PASSWORD_SUFFIX = ".enc";
     public final static String GPG_SUFFIX = ".gpg";

+ 6 - 275
app/src/main/java/info/knacki/pass/io/GPGFileInterface.java

@@ -2,292 +2,23 @@ package info.knacki.pass.io;
 
 import android.content.Context;
 
-import org.bouncycastle.bcpg.ArmoredOutputStream;
-import org.bouncycastle.bcpg.CompressionAlgorithmTags;
-import org.bouncycastle.openpgp.PGPCompressedData;
-import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
-import org.bouncycastle.openpgp.PGPEncryptedData;
-import org.bouncycastle.openpgp.PGPEncryptedDataGenerator;
-import org.bouncycastle.openpgp.PGPEncryptedDataList;
-import org.bouncycastle.openpgp.PGPException;
-import org.bouncycastle.openpgp.PGPLiteralData;
-import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
-import org.bouncycastle.openpgp.PGPObjectFactory;
-import org.bouncycastle.openpgp.PGPPBEEncryptedData;
-import org.bouncycastle.openpgp.PGPPrivateKey;
-import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData;
-import org.bouncycastle.openpgp.PGPSecretKey;
-import org.bouncycastle.openpgp.PGPSecretKeyRing;
-import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
-import org.bouncycastle.openpgp.PGPUtil;
-import org.bouncycastle.openpgp.operator.bc.BcPBEDataDecryptorFactory;
-import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider;
-import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory;
-import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
-import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
-import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
-import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder;
-import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator;
-import org.bouncycastle.util.io.Streams;
-
-import java.io.ByteArrayOutputStream;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
 import java.nio.charset.Charset;
-import java.security.SecureRandom;
-import java.util.Date;
-import java.util.Iterator;
-import java.util.logging.Level;
-import java.util.logging.Logger;
 
 import info.knacki.pass.git.GitInterface;
 import info.knacki.pass.settings.SettingsManager;
 
 class GPGFileInterface implements IFileInterface {
-    private final static Logger log = Logger.getLogger(GPGFileInterface.class.getName());
     private final File fFile;
     private final FileInterfaceFactory.PasswordGetter fPasswordGetter;
-    private final File fKeyFile;
+    private final Context fContext;
 
     GPGFileInterface(Context ctx, FileInterfaceFactory.PasswordGetter passwordGetter, File f) {
         fFile = f;
-        String gpgFile = SettingsManager.GetGPGKeyFile(ctx);
-        fKeyFile = gpgFile == null ? null : new File(gpgFile);
         fPasswordGetter = passwordGetter;
-    }
-
-    private boolean TryPassword(PGPSecretKey pgpSecKey, String pass, GitInterface.OnResponseListener<PGPPrivateKeyAndPass> onResponse) {
-        try {
-            PGPPrivateKey pgpPrivKey = pgpSecKey.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder(new JcaPGPDigestCalculatorProviderBuilder().setProvider("BC").build()).setProvider("BC").build(pass.toCharArray()));
-            PGPPrivateKeyAndPass res = new PGPPrivateKeyAndPass();
-            res.key = pgpPrivKey;
-            res.pass = pass;
-            onResponse.onResponse(res);
-        }
-        catch (PGPException e) {
-            // Wrong password
-            log.log(Level.INFO, e.getMessage() +" (wrong password ?)", e);
-            return false;
-        }
-        return true;
-    }
-
-    private boolean FindSecretKey(PGPSecretKeyRingCollection pgpSec, long keyID, final GitInterface.OnResponseListener<PGPPrivateKeyAndPass> onResponse) throws PGPException {
-        final PGPSecretKey pgpSecKey = pgpSec.getSecretKey(keyID);
-
-        if (pgpSecKey != null) {
-            if (TryPassword(pgpSecKey, "", onResponse))
-                return true;
-            GitInterface.OnResponseListener<String> onResp = new GitInterface.OnResponseListener<String>() {
-                @Override
-                public void onResponse(String result) {
-                    if (result == null) {
-                        onResponse.onError("Invalid password", null);
-                    } else if (!TryPassword(pgpSecKey, result, onResponse)) {
-                        fPasswordGetter.WrongPassword();
-                        fPasswordGetter.GetPassword(this);
-                    }
-                }
-
-                @Override
-                public void onError(String msg, Throwable e) {
-                    onResponse.onError(msg, e);
-                }
-            };
-            fPasswordGetter.GetPassword(onResp);
-        }
-        return false;
-    }
-
-    private InputStream getFirstLiteralDataInputStream(InputStream in, PGPPrivateKeyAndPass pKey) throws IOException, PGPException {
-        PGPObjectFactory pof = new PGPObjectFactory(in, new JcaKeyFingerprintCalculator());
-        Object po;
-
-        while (true) {
-            po = pof.nextObject();
-
-            if (po == null) {
-                break;
-            } else if (po instanceof PGPCompressedData) {
-                InputStream ret = getFirstLiteralDataInputStream(((PGPCompressedData) po).getDataStream(), pKey);
-
-                if (ret != null) return ret;
-            } else if (po instanceof PGPLiteralData) {
-                return ((PGPLiteralData) po).getDataStream();
-            } else if (po instanceof PGPEncryptedDataList) {
-                for (Iterator it = ((PGPEncryptedDataList) po).getEncryptedDataObjects(); it.hasNext();) {
-                    PGPEncryptedData ped = (PGPEncryptedData) it.next();
-
-                    if (ped instanceof PGPPublicKeyEncryptedData) {
-                        InputStream ret = getFirstLiteralDataInputStream(((PGPPublicKeyEncryptedData) ped).getDataStream(new BcPublicKeyDataDecryptorFactory(pKey.key)), pKey);
-
-                        if (ret != null)
-                            return ret;
-                    } else if (ped instanceof PGPPBEEncryptedData) {
-                        PGPPBEEncryptedData pped = (PGPPBEEncryptedData) ped;
-
-                        InputStream ret = getFirstLiteralDataInputStream(pped.getDataStream(new BcPBEDataDecryptorFactory(pKey.pass.toCharArray(), new BcPGPDigestCalculatorProvider())), pKey);
-
-                        if (ret != null) return ret;
-                    } else {
-                        throw new IOException("Unknown encryption packet");
-                    }
-                }
-            }
-        }
-        return null;
-    }
-
-    private class PGPPrivateKeyAndPass {
-        public PGPPrivateKey key;
-        public String pass;
-    }
-
-    private void DecryptFile(final GitInterface.OnResponseListener<byte[]> resp) throws IOException, PGPException {
-        PGPObjectFactory pgpF = new PGPObjectFactory(PGPUtil.getDecoderStream(new FileInputStream(fFile)), new JcaKeyFingerprintCalculator());
-        Object o = pgpF.nextObject();
-        final Iterator<?> it = ((o instanceof PGPEncryptedDataList) ? (PGPEncryptedDataList) o : (PGPEncryptedDataList) pgpF.nextObject()).getEncryptedDataObjects();
-
-        while (it.hasNext()) {
-            final PGPPublicKeyEncryptedData pbe = (PGPPublicKeyEncryptedData) it.next();
-
-            if (FindSecretKey(
-                    new PGPSecretKeyRingCollection(PGPUtil.getDecoderStream(new FileInputStream(fKeyFile)), new JcaKeyFingerprintCalculator()),
-                    pbe.getKeyID(),
-                    new GitInterface.OnResponseListener<PGPPrivateKeyAndPass>() {
-                @Override
-                public void onResponse(PGPPrivateKeyAndPass sKey) {
-                    byte[] output;
-                    try {
-                        BcPublicKeyDataDecryptorFactory publicKeyDataDecryptorFactory = new BcPublicKeyDataDecryptorFactory(sKey.key);
-                        InputStream ss = getFirstLiteralDataInputStream(pbe.getDataStream(publicKeyDataDecryptorFactory), sKey);
-                        if (null == ss) {
-                            resp.onError("No literal GPG data found", null);
-                            return;
-                        }
-                        ByteArrayOutputStream actualOutput = new ByteArrayOutputStream();
-                        Streams.pipeAll(ss, actualOutput);
-                        actualOutput.close();
-                        output = actualOutput.toByteArray();
-                    }
-                    catch (PGPException | IOException e) {
-                        resp.onError(e.getMessage(), e);
-                        return;
-                    }
-                    resp.onResponse(output);
-                }
-
-                @Override
-                public void onError(String msg, Throwable e) {
-                    resp.onError(msg, e);
-                }
-            }))
-                break;
-        }
-    }
-
-    private void FindPassword(final PGPSecretKey pgpSecKey, final GitInterface.OnResponseListener<PGPPrivateKeyAndPass> resp) {
-        if (!TryPassword(pgpSecKey, "", resp)) {
-            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)) {
-                        fPasswordGetter.WrongPassword();
-                        fPasswordGetter.GetPassword(this);
-                    }
-                }
-
-                @Override
-                public void onError(String msg, Throwable e) {
-                    resp.onError(msg, e);
-                }
-            };
-            fPasswordGetter.GetPassword(onResp);
-        }
-    }
-
-    private static PGPSecretKey findSecretKey(InputStream in) throws IOException, PGPException {
-        in = PGPUtil.getDecoderStream(in);
-        PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(in, new JcaKeyFingerprintCalculator());
-        PGPSecretKey key = null;
-        Iterator rIt = pgpSec.getKeyRings();
-
-        while (key == null && rIt.hasNext()) {
-            PGPSecretKeyRing kRing = (PGPSecretKeyRing) rIt.next();
-            Iterator kIt = kRing.getSecretKeys();
-
-            while (key == null && kIt.hasNext()) {
-                PGPSecretKey k = (PGPSecretKey) kIt.next();
-
-                if (k.isSigningKey()) {
-                    key = k;
-                }
-            }
-        }
-
-        if (key == null) {
-            throw new IllegalArgumentException("Can't find signing key in key ring.");
-        }
-        return key;
-    }
-
-    private void CryptFile(final OutputStream fileOutStream, final byte[] data, final GitInterface.OnResponseListener<Void> resp) {
-        try {
-            ByteArrayOutputStream compressedDataStream = new ByteArrayOutputStream();
-            PGPCompressedDataGenerator comData = new PGPCompressedDataGenerator(CompressionAlgorithmTags.UNCOMPRESSED);
-            OutputStream cos = comData.open(compressedDataStream);
-            PGPLiteralDataGenerator lData = new PGPLiteralDataGenerator();
-
-            OutputStream pOut = lData.open(cos, // the compressed output stream
-                    PGPLiteralData.BINARY,
-                    PGPLiteralData.CONSOLE,  // "filename" to store
-                    data.length, // length of clear data
-                    new Date()  // current time
-            );
-
-            pOut.write(data);
-            pOut.close();
-            comData.close();
-
-            final byte[] compressedData = compressedDataStream.toByteArray();
-            final OutputStream out = new ArmoredOutputStream(fileOutStream);
-
-            final PGPSecretKey sKey = findSecretKey(new FileInputStream(fKeyFile));
-            FindPassword(sKey, new GitInterface.OnResponseListener<PGPPrivateKeyAndPass>() {
-                @Override
-                public void onResponse(PGPPrivateKeyAndPass result) {
-                    PGPEncryptedDataGenerator encGen = new PGPEncryptedDataGenerator(new JcePGPDataEncryptorBuilder(PGPEncryptedDataGenerator.AES_128).setWithIntegrityPacket(true).setSecureRandom(new SecureRandom()).setProvider("BC"));
-                    encGen.addMethod(new JcePublicKeyKeyEncryptionMethodGenerator(sKey.getPublicKey()).setProvider("BC"));
-                    try {
-                        OutputStream encOut = encGen.open(fileOutStream, compressedData.length);
-
-                        encOut.write(compressedData);
-                        encOut.close();
-                        out.close();
-                        fileOutStream.close();
-                        resp.onResponse(null);
-                    }
-                    catch (IOException | PGPException e) {
-                        resp.onError(e.getMessage(), e);
-                    }
-                }
-
-                @Override
-                public void onError(String msg, Throwable e) {
-                    resp.onError(msg, e);
-                }
-            });
-        }
-        catch (IOException | PGPException e) {
-            resp.onError(e.getMessage(), e);
-        }
+        fContext = ctx;
     }
 
     @Override
@@ -297,9 +28,9 @@ class GPGFileInterface implements IFileInterface {
             return;
         }
         try {
-            if (fKeyFile == null)
+            if (!SettingsManager.HasGPGKey(fContext))
                 throw new FileNotFoundException("GPG key not set");
-            DecryptFile(new GitInterface.OnResponseListener<byte[]>() {
+            GPGUtil.DecryptFile(fContext, fPasswordGetter, fFile, new GitInterface.OnResponseListener<byte[]>() {
                 @Override
                 public void onResponse(byte[] result) {
                     resp.onResponse(new String(result, Charset.defaultCharset()));
@@ -318,9 +49,9 @@ class GPGFileInterface implements IFileInterface {
     @Override
     public void WriteFile(String content, GitInterface.OnResponseListener<Void> resp) {
         try {
-            if (fKeyFile == null)
+            if (!SettingsManager.HasGPGKey(fContext))
                 throw new FileNotFoundException("GPG key not set");
-            CryptFile(new FileOutputStream(fFile), content.getBytes(), resp);
+            GPGUtil.CryptFile(fContext, fPasswordGetter, new FileOutputStream(fFile), content.getBytes(), resp);
         }
         catch (Throwable e) {
             resp.onError(e.getMessage(), e);

+ 306 - 0
app/src/main/java/info/knacki/pass/io/GPGUtil.java

@@ -0,0 +1,306 @@
+package info.knacki.pass.io;
+
+import android.content.Context;
+
+import org.bouncycastle.bcpg.ArmoredOutputStream;
+import org.bouncycastle.bcpg.CompressionAlgorithmTags;
+import org.bouncycastle.bcpg.HashAlgorithmTags;
+import org.bouncycastle.openpgp.PGPCompressedData;
+import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
+import org.bouncycastle.openpgp.PGPEncryptedData;
+import org.bouncycastle.openpgp.PGPEncryptedDataGenerator;
+import org.bouncycastle.openpgp.PGPEncryptedDataList;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPKeyPair;
+import org.bouncycastle.openpgp.PGPKeyRingGenerator;
+import org.bouncycastle.openpgp.PGPLiteralData;
+import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
+import org.bouncycastle.openpgp.PGPObjectFactory;
+import org.bouncycastle.openpgp.PGPPBEEncryptedData;
+import org.bouncycastle.openpgp.PGPPrivateKey;
+import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData;
+import org.bouncycastle.openpgp.PGPSecretKey;
+import org.bouncycastle.openpgp.PGPSecretKeyRing;
+import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.PGPUtil;
+import org.bouncycastle.openpgp.operator.bc.BcPBEDataDecryptorFactory;
+import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider;
+import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory;
+import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
+import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
+import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
+import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
+import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder;
+import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder;
+import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator;
+import org.bouncycastle.util.io.Streams;
+
+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;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import info.knacki.pass.git.GitInterface;
+import info.knacki.pass.settings.SettingsManager;
+
+public class GPGUtil {
+    private final static Logger log = Logger.getLogger(GPGUtil.class.getName());
+
+    private static class PGPPrivateKeyAndPass {
+        public final PGPPrivateKey fPrivateKey;
+        public final String fPass;
+
+        public PGPPrivateKeyAndPass(PGPPrivateKey priv, String key) {
+            fPrivateKey = priv;
+            fPass = key;
+        }
+    }
+
+    private static boolean TryPassword(PGPSecretKey pgpSecKey, String pass, GitInterface.OnResponseListener<PGPPrivateKeyAndPass> onResponse) {
+        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);
+        } catch (PGPException e) {
+            // Wrong password
+            log.log(Level.INFO, e.getMessage() + " (wrong password ?)", e);
+            return false;
+        }
+        return true;
+    }
+
+    private static void FindPassword(final FileInterfaceFactory.PasswordGetter passwordGetter, final PGPSecretKey pgpSecKey, final GitInterface.OnResponseListener<PGPPrivateKeyAndPass> resp) {
+        if (!TryPassword(pgpSecKey, "", resp)) {
+            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);
+                    }
+                }
+
+                @Override
+                public void onError(String msg, Throwable e) {
+                    resp.onError(msg, e);
+                }
+            };
+            passwordGetter.GetPassword(onResp);
+        }
+    }
+
+    private static InputStream getFirstLiteralDataInputStream(InputStream in, PGPPrivateKeyAndPass pKey) throws IOException, PGPException {
+        PGPObjectFactory pof = new PGPObjectFactory(in, new JcaKeyFingerprintCalculator());
+        Object po;
+
+        while (true) {
+            po = pof.nextObject();
+
+            if (po == null) {
+                break;
+            } else if (po instanceof PGPCompressedData) {
+                InputStream ret = getFirstLiteralDataInputStream(((PGPCompressedData) po).getDataStream(), pKey);
+                if (ret != null)
+                    return ret;
+            } else if (po instanceof PGPLiteralData) {
+                return ((PGPLiteralData) po).getDataStream();
+            } else if (po instanceof PGPEncryptedDataList) {
+                for (Iterator it = ((PGPEncryptedDataList) po).getEncryptedDataObjects(); it.hasNext(); ) {
+                    PGPEncryptedData ped = (PGPEncryptedData) it.next();
+
+                    if (ped instanceof PGPPublicKeyEncryptedData) {
+                        InputStream ret = getFirstLiteralDataInputStream(((PGPPublicKeyEncryptedData) ped).getDataStream(new BcPublicKeyDataDecryptorFactory(pKey.fPrivateKey)), pKey);
+
+                        if (ret != null)
+                            return ret;
+                    } else if (ped instanceof PGPPBEEncryptedData) {
+                        InputStream ret = getFirstLiteralDataInputStream(((PGPPBEEncryptedData) ped).getDataStream(new BcPBEDataDecryptorFactory(pKey.fPass.toCharArray(), new BcPGPDigestCalculatorProvider())), pKey);
+                        if (ret != null)
+                            return ret;
+                    } else {
+                        throw new IOException("Unknown encryption packet");
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    private static InputStream getKeyInputStream(Context ctx) throws FileNotFoundException {
+        return SettingsManager.GetGPGKeyContent(ctx);
+    }
+
+    private static PGPSecretKey findSecretKey(InputStream in) throws IOException, PGPException {
+        in = PGPUtil.getDecoderStream(in);
+        PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(in, new JcaKeyFingerprintCalculator());
+        Iterator rIt = pgpSec.getKeyRings();
+
+        while (rIt.hasNext()) {
+            PGPSecretKeyRing kRing = (PGPSecretKeyRing) rIt.next();
+            Iterator kIt = kRing.getSecretKeys();
+
+            while (kIt.hasNext()) {
+                PGPSecretKey k = (PGPSecretKey) kIt.next();
+
+                if (k.isSigningKey()) {
+                    return k;
+                }
+            }
+        }
+        throw new IllegalArgumentException("Can't find signing key in key ring.");
+    }
+
+    private static void DoDecryptFile(PGPPrivateKeyAndPass sKey, InputStream dataStream, GitInterface.OnResponseListener<byte[]> resp) {
+        byte[] output;
+        try {
+            InputStream ss = getFirstLiteralDataInputStream(dataStream, sKey);
+            if (null == ss) {
+                resp.onError("No literal GPG data found", null);
+                return;
+            }
+            ByteArrayOutputStream actualOutput = new ByteArrayOutputStream();
+            Streams.pipeAll(ss, actualOutput);
+            actualOutput.close();
+            output = actualOutput.toByteArray();
+        } catch (PGPException | IOException e) {
+            resp.onError(e.getMessage(), e);
+            return;
+        }
+        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 {
+        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();
+
+        while (it.hasNext()) {
+            final PGPPublicKeyEncryptedData pbe = (PGPPublicKeyEncryptedData) it.next();
+
+            FindPassword(
+                    passwordGetter,
+                    findSecretKey(getKeyInputStream(ctx)),
+                    new GitInterface.OnResponseListener<PGPPrivateKeyAndPass>() {
+                        @Override
+                        public void onResponse(PGPPrivateKeyAndPass sKey) {
+                            try {
+                                DoDecryptFile(sKey, pbe.getDataStream(new BcPublicKeyDataDecryptorFactory(sKey.fPrivateKey)), resp);
+                            } catch (PGPException e) {
+                                onError(e.getMessage(), e);
+                            }
+                        }
+
+                        @Override
+                        public void onError(String msg, Throwable e) {
+                            resp.onError(msg, e);
+                        }
+                    });
+        }
+    }
+
+    public static void CryptFile(final Context ctx, final FileInterfaceFactory.PasswordGetter passwordGetter, final OutputStream fileOutStream, final byte[] data, final GitInterface.OnResponseListener<Void> resp) {
+        try {
+            ByteArrayOutputStream compressedDataStream = new ByteArrayOutputStream();
+            PGPCompressedDataGenerator comData = new PGPCompressedDataGenerator(CompressionAlgorithmTags.UNCOMPRESSED);
+            OutputStream cos = comData.open(compressedDataStream);
+            PGPLiteralDataGenerator lData = new PGPLiteralDataGenerator();
+
+            OutputStream pOut = lData.open(cos, // the compressed output stream
+                    PGPLiteralData.BINARY,
+                    PGPLiteralData.CONSOLE,  // "filename" to store
+                    data.length, // length of clear data
+                    new Date()  // current time
+            );
+
+            pOut.write(data);
+            pOut.close();
+            comData.close();
+
+            final byte[] compressedData = compressedDataStream.toByteArray();
+            final OutputStream out = new ArmoredOutputStream(fileOutStream);
+            final PGPSecretKey sKey = findSecretKey(getKeyInputStream(ctx));
+
+            PGPEncryptedDataGenerator encGen = new PGPEncryptedDataGenerator(new JcePGPDataEncryptorBuilder(PGPEncryptedDataGenerator.AES_128).setWithIntegrityPacket(true).setSecureRandom(new SecureRandom()).setProvider("BC"));
+            encGen.addMethod(new JcePublicKeyKeyEncryptionMethodGenerator(sKey.getPublicKey()).setProvider("BC"));
+            try {
+                OutputStream encOut = encGen.open(fileOutStream, compressedData.length);
+
+                encOut.write(compressedData);
+                encOut.close();
+                out.close();
+                fileOutStream.close();
+                resp.onResponse(null);
+            } catch (IOException | PGPException e) {
+                resp.onError(e.getMessage(), e);
+            }
+        } catch (IOException | PGPException e) {
+            resp.onError(e.getMessage(), e);
+        }
+    }
+
+    private static PGPSecretKeyRing DoChangePassword(PGPSecretKey sec, PGPPrivateKeyAndPass sKey, String newPassword) throws PGPException {
+        return new PGPKeyRingGenerator(
+                PGPSignature.POSITIVE_CERTIFICATION,
+                new PGPKeyPair(sec.getPublicKey(), sKey.fPrivateKey),
+                "" + sec.getKeyID(),
+                new BcPGPDigestCalculatorProvider().get(HashAlgorithmTags.SHA1),
+                null,
+                null,
+                new JcaPGPContentSignerBuilder(sec.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA1),
+                new JcePBESecretKeyEncryptorBuilder(PGPEncryptedData.AES_256).setProvider("BC").build(newPassword.toCharArray()))
+                .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) {
+                                    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);
+                                    }
+                                }
+
+                                @Override
+                                public void onError(String msg, Throwable e) {
+                                    onDone.run();
+                                }
+                            });
+                        }
+
+                        @Override
+                        public void onError(String msg, Throwable e) {
+                        }
+                    });
+        } catch (PGPException e) {
+
+        }
+    }
+}

+ 50 - 4
app/src/main/java/info/knacki/pass/settings/SettingsManager.java

@@ -2,16 +2,21 @@ package info.knacki.pass.settings;
 
 import android.content.Context;
 import android.content.SharedPreferences;
+import android.util.Base64;
 import android.util.JsonReader;
 import android.util.MalformedJsonException;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.StringReader;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
 public class SettingsManager {
     private static final Logger log = Logger.getLogger(SettingsManager.class.getName());
+    public static final String GPG_GENERATED = "BouncyCastle gpg key";
 
     public static class EncryptionType {
         public static final EncryptionType TYPE_RAW = new EncryptionType(0);
@@ -220,11 +225,52 @@ public class SettingsManager {
             prefs.remove(VCS.class.getSimpleName()).commit();
     }
 
-    public static void SetGPGKeyFile(Context ctx, String filename) {
-        GetPrefManager(ctx).edit().putString("GPGKeyFile", filename).commit();
+    private static void doSetGpgKeyContent(Context ctx, String filename, String content) {
+        GetPrefManager(ctx).edit().putString("GPGKeyFile", filename + '@' + content).commit();
     }
 
-    public static String GetGPGKeyFile(Context ctx) {
-        return GetPrefManager(ctx).getString("GPGKeyFile", null);
+    public static void RemoveGpgKey(Context ctx) {
+        GetPrefManager(ctx).edit().remove("GPGKeyFile").commit();
+    }
+
+    public static boolean SetGPGKeyContent(Context ctx, String filename, InputStream stream) {
+        if (stream == null) {
+            GetPrefManager(ctx).edit().remove("GPGKeyFile").commit();
+            return false;
+        }
+        try {
+            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+            final int bufLen = 1024;
+            byte[] buffer = new byte[bufLen];
+            int len;
+            do {
+                len = stream.read(buffer);
+                bytes.write(buffer, 0, len);
+            } while (len == bufLen);
+            doSetGpgKeyContent(ctx, filename, Base64.encodeToString(bytes.toByteArray(), 0));
+        } catch (IOException e) {
+            log.log(Level.SEVERE, "Cannot read file", e);
+            RemoveGpgKey(ctx);
+            return false;
+        }
+        return true;
+    }
+
+    public static InputStream GetGPGKeyContent(Context ctx) {
+        String content = GetPrefManager(ctx).getString("GPGKeyFile", null);
+        if (content != null)
+            return new ByteArrayInputStream(Base64.decode(content.substring(content.indexOf('@') + 1), 0));
+        return null;
+    }
+
+    public static String GetGPGKeyName(Context ctx) {
+        String content = GetPrefManager(ctx).getString("GPGKeyFile", null);
+        if (content != null)
+            return content.substring(0, content.indexOf('@'));
+        return null;
+    }
+
+    public static boolean HasGPGKey(Context ctx) {
+        return GetPrefManager(ctx).contains("GPGKeyFile");
     }
 }

+ 48 - 14
app/src/main/java/info/knacki/pass/settings/ui/SettingsActivity.java

@@ -5,6 +5,7 @@ import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.res.Configuration;
+import android.database.Cursor;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
@@ -14,6 +15,7 @@ import android.preference.Preference;
 import android.preference.PreferenceActivity;
 import android.preference.PreferenceFragment;
 import android.preference.SwitchPreference;
+import android.provider.MediaStore;
 import android.support.v4.app.NavUtils;
 import android.support.v7.app.ActionBar;
 import android.util.SparseArray;
@@ -22,6 +24,8 @@ import android.view.View;
 import android.widget.Toast;
 
 import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.util.List;
 import java.util.logging.Logger;
 
@@ -30,11 +34,13 @@ import info.knacki.pass.git.GitInterface;
 import info.knacki.pass.git.GitInterfaceFactory;
 import info.knacki.pass.git.entities.GitRef;
 import info.knacki.pass.io.FileUtils;
+import info.knacki.pass.io.GPGUtil;
 import info.knacki.pass.io.PathUtils;
 import info.knacki.pass.settings.SettingsManager;
 import info.knacki.pass.ui.GitPullActivity;
 import info.knacki.pass.ui.alertPrompt.AlertPrompt;
 import info.knacki.pass.ui.alertPrompt.views.TextView;
+import info.knacki.pass.ui.passwordPicker.PasswordPicker;
 
 /**
  * A {@link PreferenceActivity} that presents a set of application settings. On
@@ -472,36 +478,64 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
                 @Override
                 public boolean onPreferenceClick(Preference preference) {
                     Intent i = new Intent(Intent.ACTION_GET_CONTENT);
-                    i.setType("file/*");
+                    i.setType("*/*");
+                    i.addCategory(Intent.CATEGORY_OPENABLE);
                     GPGPreferenceFragment.this.startActivityForResult(i, ACTIVITY_REQUEST_CODE_BROWSEGPG);
                     return true;
                 }
             });
+            findPreference(getResources().getString(R.string.id_gpg_password)).setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+                @Override
+                public boolean onPreferenceClick(Preference preference) {
+                    try {
+                        GPGUtil.ChangePassword(getActivity(), new PasswordPicker.ChangePasswordPicker(getActivity()), new Runnable() {
+                            @Override
+                            public void run() {
+                                updateGpgFileLabel();
+                            }
+                        });
+                    } catch (IOException e) {
+
+                    }
+                    return true;
+                }
+            });
             updateGpgFileLabel();
         }
 
+        private String GetFileName(Uri uri) {
+            String filename = uri.getLastPathSegment();
+            Cursor cu = getActivity().getContentResolver().query(uri, null, null, null, null);
+            if (cu != null) {
+                if (cu.moveToFirst()) {
+                    filename = cu.getString(cu.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME));
+                }
+                cu.close();
+            }
+            return filename;
+        }
+
         @Override
         public void onActivityResult(int requestCode, int resultCode, Intent data) {
-            if (requestCode == ACTIVITY_REQUEST_CODE_BROWSEGPG) {
-                if (resultCode == RESULT_OK) {
-                    Uri uriData = data.getData();
-                    if (null != uriData) {
-                        String path = uriData.getPath();
-                        SettingsManager.SetGPGKeyFile(getActivity(), path);
-                        updateGpgFileLabel();
-                    }
+            if (requestCode == ACTIVITY_REQUEST_CODE_BROWSEGPG && resultCode == RESULT_OK) {
+                try {
+                    boolean gpgImportStatus = SettingsManager.SetGPGKeyContent(getActivity(), GetFileName(data.getData()), getActivity().getContentResolver().openInputStream(data.getData()));
+                    Toast.makeText(getActivity(), getResources().getString(gpgImportStatus ? R.string.gpg_import_ok : R.string.gpg_import_ko), Toast.LENGTH_LONG).show();
+                } catch (FileNotFoundException e) {
+                    Toast.makeText(getActivity(), getResources().getString(R.string.filenotfound), Toast.LENGTH_LONG).show();
+                    SettingsManager.RemoveGpgKey(getActivity());
                 }
+                updateGpgFileLabel();
             }
         }
 
         protected void updateGpgFileLabel() {
-            String gpgFileName = SettingsManager.GetGPGKeyFile(getActivity());
-            File gpgFile = gpgFileName == null ? null : new File(gpgFileName);
-
-            if (gpgFile != null && gpgFile.exists()) {
-                findPreference(getResources().getString(R.string.id_gpg_keyfile)).setSummary(gpgFile.getName());
+            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);
             } 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);
             }
         }
     }

+ 80 - 1
app/src/main/java/info/knacki/pass/ui/passwordPicker/PasswordPicker.java

@@ -22,6 +22,10 @@ public class PasswordPicker implements FileInterfaceFactory.PasswordGetter {
         return new AlertPrompt(fContext);
     }
 
+    public int GetTitle() {
+        return R.string.enter_password;
+    }
+
     @Override
     public void GetPassword(final GitInterface.OnResponseListener<String> onPassword) {
         AlertFactory()
@@ -39,7 +43,7 @@ public class PasswordPicker implements FileInterfaceFactory.PasswordGetter {
                     onPassword.onResponse(null);
                 }
             })
-            .setTitle(R.string.enter_password)
+                .setTitle(GetTitle())
             .show();
     }
 
@@ -47,4 +51,79 @@ public class PasswordPicker implements FileInterfaceFactory.PasswordGetter {
     public void WrongPassword() {
         Toast.makeText(fContext, fContext.getResources().getString(R.string.wrongPassword), Toast.LENGTH_LONG).show();
     }
+
+    public static class ChangePasswordPicker extends PasswordPicker implements FileInterfaceFactory.ChangePasswordGetter {
+        protected FileInterfaceFactory.ChangePasswordGetter.eStep fCurrentStep = FileInterfaceFactory.ChangePasswordGetter.eStep.eOldPassword;
+        private String fFirstPassword = "";
+        private GitInterface.OnResponseListener<String> fListener;
+
+        public ChangePasswordPicker(Context context) {
+            super(context);
+        }
+
+        @Override
+        public int GetTitle() {
+            switch (fCurrentStep) {
+                case eOldPassword:
+                    return R.string.enter_old_password;
+                case eNewPassword:
+                    return R.string.type_new_password;
+                case eRetypePassword:
+                    return R.string.retype_new_password;
+            }
+            return R.string.enter_password;
+        }
+
+        private GitInterface.OnResponseListener<String> fOnFirstPassEntered = new GitInterface.OnResponseListener<String>() {
+            @Override
+            public void onResponse(String result) {
+                fFirstPassword = result;
+                SetStep(eStep.eRetypePassword).GetPassword(fListener);
+            }
+
+            @Override
+            public void onError(String msg, Throwable e) {
+                fListener.onError(msg, e);
+            }
+        };
+
+        private GitInterface.OnResponseListener<String> fOnRetypePassEntered = new GitInterface.OnResponseListener<String>() {
+            @Override
+            public void onResponse(String result) {
+                if (fFirstPassword.equals(result)) {
+                    fListener.onResponse(result);
+                } else {
+                    Toast.makeText(fContext, fContext.getResources().getString(R.string.password_missmatch), Toast.LENGTH_LONG).show();
+                    SetStep(eStep.eNewPassword).GetPassword(fListener);
+                }
+            }
+
+            @Override
+            public void onError(String msg, Throwable e) {
+                fListener.onError(msg, e);
+            }
+        };
+
+        @Override
+        public void GetPassword(final GitInterface.OnResponseListener<String> onPassword) {
+            fListener = onPassword;
+            switch (fCurrentStep) {
+                case eOldPassword:
+                    super.GetPassword(onPassword);
+                    break;
+                case eNewPassword:
+                    super.GetPassword(fOnFirstPassEntered);
+                    break;
+                case eRetypePassword:
+                    super.GetPassword(fOnRetypePassEntered);
+                    break;
+            }
+        }
+
+        @Override
+        public FileInterfaceFactory.ChangePasswordGetter SetStep(FileInterfaceFactory.ChangePasswordGetter.eStep step) {
+            fCurrentStep = step;
+            return this;
+        }
+    }
 }

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

@@ -54,6 +54,8 @@
     </array>
     <string name="pref_header_gpg_keyfile">Clé GPG</string>
     <string name="pref_summary_gpg_keyfile">Rechercher une clé GPG</string>
+    <string name="pref_header_gpg_password">Mot de passe de la clef GPG</string>
+    <string name="pref_summary_gpg_password">Changer le mot de passe GPG</string>
     <string name="enter_password">Mot de passe</string>
     <string name="wrongPassword">Mot de passe erroné</string>
     <string name="ok">OK</string>
@@ -67,4 +69,11 @@
     <string name="remove">Supprimer</string>
     <string name="copy_clipboard">Copier dans le presse-papier</string>
     <string name="copied_clipboard">Mot de passe dans le presse-papier</string>
+    <string name="gpg_import_ok">Clé GPG importée avec succès</string>
+    <string name="gpg_import_ko">Erreur lors de l\'import de la clé GPG</string>
+    <string name="filenotfound">Fichier introuvable</string>
+    <string name="password_missmatch">Les mots de passes ne correspondent pas. Veuillez rééssayer</string>
+    <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>
 </resources>

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

@@ -54,6 +54,8 @@
     </array>
     <string name="pref_header_gpg_keyfile">GPG key</string>
     <string name="pref_summary_gpg_keyfile">Browse to find GPG exported key</string>
+    <string name="pref_header_gpg_password">GPG key password</string>
+    <string name="pref_summary_gpg_password">Change GPG password</string>
     <string name="enter_password">Enter password</string>
     <string name="wrongPassword">Wrong password</string>
     <string name="ok">OK</string>
@@ -67,4 +69,11 @@
     <string name="remove">Remove</string>
     <string name="copy_clipboard">Copy to clipboard</string>
     <string name="copied_clipboard">Password copied to Clipboard</string>
+    <string name="gpg_import_ok">Done importing GPG Key</string>
+    <string name="gpg_import_ko">Error while importing GPG Key</string>
+    <string name="filenotfound">Error: file not found</string>
+    <string name="password_missmatch">Password missmatch. Please try again</string>
+    <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>
 </resources>

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

@@ -23,6 +23,7 @@
     <string name="id_vcs_git_commitinfocategory">id_vcs_git_commitinfocategory</string>
     <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_removeall">id_removeall</string>
     <integer name="id_keyboard_numbers">-2</integer>
     <integer name="id_keyboard_shift">-1</integer>

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

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