|
|
@@ -0,0 +1,353 @@
|
|
|
+package info.knacki.pass.ui.passwordPicker;
|
|
|
+
|
|
|
+import android.content.Context;
|
|
|
+import android.os.Build;
|
|
|
+import android.security.keystore.KeyGenParameterSpec;
|
|
|
+import android.security.keystore.KeyProperties;
|
|
|
+import android.support.annotation.RequiresApi;
|
|
|
+import android.support.v4.hardware.fingerprint.FingerprintManagerCompat;
|
|
|
+import android.support.v4.os.CancellationSignal;
|
|
|
+import android.util.Base64;
|
|
|
+import android.util.Base64DataException;
|
|
|
+import android.widget.Toast;
|
|
|
+
|
|
|
+import java.io.File;
|
|
|
+import java.io.FileWriter;
|
|
|
+import java.io.IOException;
|
|
|
+import java.io.UnsupportedEncodingException;
|
|
|
+import java.io.Writer;
|
|
|
+import java.security.InvalidAlgorithmParameterException;
|
|
|
+import java.security.InvalidKeyException;
|
|
|
+import java.security.Key;
|
|
|
+import java.security.KeyStore;
|
|
|
+import java.security.KeyStoreException;
|
|
|
+import java.security.NoSuchAlgorithmException;
|
|
|
+import java.security.NoSuchProviderException;
|
|
|
+import java.security.UnrecoverableKeyException;
|
|
|
+import java.security.cert.CertificateException;
|
|
|
+import java.util.ArrayDeque;
|
|
|
+import java.util.Queue;
|
|
|
+import java.util.logging.Level;
|
|
|
+import java.util.logging.Logger;
|
|
|
+
|
|
|
+import javax.crypto.BadPaddingException;
|
|
|
+import javax.crypto.Cipher;
|
|
|
+import javax.crypto.IllegalBlockSizeException;
|
|
|
+import javax.crypto.KeyGenerator;
|
|
|
+import javax.crypto.NoSuchPaddingException;
|
|
|
+import javax.crypto.spec.IvParameterSpec;
|
|
|
+
|
|
|
+import info.knacki.pass.R;
|
|
|
+import info.knacki.pass.io.FileInterfaceFactory;
|
|
|
+import info.knacki.pass.io.FileUtils;
|
|
|
+import info.knacki.pass.io.OnErrorListener;
|
|
|
+import info.knacki.pass.io.OnResponseListener;
|
|
|
+import info.knacki.pass.io.PathUtils;
|
|
|
+import info.knacki.pass.ui.alertPrompt.AlertPrompt;
|
|
|
+import info.knacki.pass.ui.alertPrompt.AlertPromptGenerator;
|
|
|
+import info.knacki.pass.ui.alertPrompt.views.FingerprintView;
|
|
|
+
|
|
|
+@RequiresApi(api = Build.VERSION_CODES.M)
|
|
|
+class FingerprintPicker extends PasswordPicker {
|
|
|
+ private static final Logger log = Logger.getLogger(FingerprintPicker.class.getName());
|
|
|
+ private static final String KEY_STORE_PROVIDER = "AndroidKeyStore";
|
|
|
+ private static final String KEY_NAME = "AndroidPassFingerprintKey";
|
|
|
+ private static final String KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
|
|
|
+ private static final String KEY_BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC;
|
|
|
+ private static final String KEY_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7;
|
|
|
+
|
|
|
+ private final File fFile;
|
|
|
+ private IvAndPayload fEncrypted;
|
|
|
+ private Queue<String> fPassword;
|
|
|
+ private Queue<String> fPasswordIterator;
|
|
|
+ private AlertPrompt fDisplayingPrompt;
|
|
|
+
|
|
|
+ private static final class IvAndPayload {
|
|
|
+ static final char SEPARATOR = '-';
|
|
|
+ final byte[] fIV;
|
|
|
+ final byte[] fPayload;
|
|
|
+
|
|
|
+ IvAndPayload(Cipher c, byte[] payload) {
|
|
|
+ fIV = c.getIV();
|
|
|
+ fPayload = payload;
|
|
|
+ }
|
|
|
+
|
|
|
+ IvAndPayload(String raw) throws Base64DataException {
|
|
|
+ int sep = raw.indexOf(SEPARATOR);
|
|
|
+ if (sep < 0)
|
|
|
+ throw new Base64DataException("Cannot find separator");
|
|
|
+ fIV = Base64.decode(raw.substring(0, sep), Base64.NO_WRAP);
|
|
|
+ fPayload = Base64.decode(raw.substring(sep + 1), Base64.NO_WRAP);
|
|
|
+ }
|
|
|
+
|
|
|
+ public String toString() {
|
|
|
+ return Base64.encodeToString(fIV, Base64.NO_WRAP) + SEPARATOR + Base64.encodeToString(fPayload, Base64.NO_WRAP);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ FingerprintPicker(Context ctx, AlertPromptGenerator alertFactory) {
|
|
|
+ super(ctx, alertFactory);
|
|
|
+ fFile = new File(PathUtils.GetFingerprintFile(ctx));
|
|
|
+ fPassword = fPasswordIterator = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private FingerprintManagerCompat GetFingerprintManager() {
|
|
|
+ return FingerprintManagerCompat.from(fContext);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Key GetKey(KeyStore keyStore) {
|
|
|
+ try {
|
|
|
+ return keyStore.getKey(KEY_NAME, null);
|
|
|
+ }
|
|
|
+ catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
|
|
|
+ log.log(Level.SEVERE, e.getMessage(), e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Key TryGetKey(OnErrorListener resp) {
|
|
|
+ KeyStore keyStore;
|
|
|
+ try {
|
|
|
+ keyStore = KeyStore.getInstance(KEY_STORE_PROVIDER);
|
|
|
+ keyStore.load(null);
|
|
|
+ } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) {
|
|
|
+ final String msg = "Cannot get android keystore";
|
|
|
+ resp.onError(msg, e);
|
|
|
+ log.log(Level.SEVERE, msg, e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return GetKey(keyStore);
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean GenerateKey(OnErrorListener resp) {
|
|
|
+ log.info("Generating new key");
|
|
|
+ try {
|
|
|
+ KeyGenerator keyGenerator;
|
|
|
+ keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM, KEY_STORE_PROVIDER);
|
|
|
+ keyGenerator.init(new KeyGenParameterSpec.Builder(KEY_NAME, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
|
|
|
+ .setBlockModes(KEY_BLOCK_MODE)
|
|
|
+ .setEncryptionPaddings(KEY_PADDING)
|
|
|
+ .build());
|
|
|
+ keyGenerator.generateKey();
|
|
|
+ } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
|
|
|
+ final String msg = "Cannot create Key generator";
|
|
|
+ resp.onError(msg, e);
|
|
|
+ log.log(Level.SEVERE, msg, e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private FingerprintManagerCompat.CryptoObject InitCryptoObject(IvAndPayload ivForDecoding, OnErrorListener resp) {
|
|
|
+ Key key = TryGetKey(resp);
|
|
|
+ if (key == null) {
|
|
|
+ if (!GenerateKey(resp)) {
|
|
|
+ // Unrecoverable failure, error already handled
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ key = TryGetKey(resp);
|
|
|
+ if (key == null) {
|
|
|
+ // Unrecoverable failure
|
|
|
+ final String msg = "Cannot get android keystore";
|
|
|
+ log.log(Level.SEVERE, msg);
|
|
|
+ resp.onError(msg, null);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Cipher c;
|
|
|
+ try {
|
|
|
+ c = InitCipher(key, ivForDecoding);
|
|
|
+ }
|
|
|
+ catch (InvalidKeyException e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
|
|
|
+ final String msg = "Cannot create Cipher";
|
|
|
+ resp.onError(msg, e);
|
|
|
+ log.log(Level.SEVERE, msg, e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return new FingerprintManagerCompat.CryptoObject(c);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Cipher InitCipher(Key key, IvAndPayload ivForDecoding) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
|
|
|
+ Cipher c = Cipher.getInstance(KEY_ALGORITHM +"/" +KEY_BLOCK_MODE +"/" +KEY_PADDING);
|
|
|
+ if (ivForDecoding == null)
|
|
|
+ c.init(Cipher.ENCRYPT_MODE, key);
|
|
|
+ else
|
|
|
+ c.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(ivForDecoding.fIV));
|
|
|
+ return c;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void DoWriteFile(FingerprintManagerCompat.CryptoObject cryptoObject, String content) {
|
|
|
+ try {
|
|
|
+ Cipher cipher = cryptoObject.getCipher();
|
|
|
+ if (cipher == null) {
|
|
|
+ log.severe("Cannot write content: cipher is null");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ byte[] encrypted = cipher.doFinal(content.getBytes());
|
|
|
+ Writer writer = new FileWriter(fFile);
|
|
|
+ writer.write((new IvAndPayload(cipher, encrypted)).toString());
|
|
|
+ writer.close();
|
|
|
+ }
|
|
|
+ catch (IOException | BadPaddingException | IllegalBlockSizeException | NullPointerException e) {
|
|
|
+ log.log(Level.SEVERE, e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void DisplayPrompt(IvAndPayload ivForDecoding, OnResponseListener<FingerprintManagerCompat.CryptoObject> resp) {
|
|
|
+ final CancellationSignal signal = new CancellationSignal();
|
|
|
+ final FingerprintView fingerprintView = new FingerprintView(fContext);
|
|
|
+ fDisplayingPrompt = fAlertFactory.Generate(fContext)
|
|
|
+ .setView(fingerprintView)
|
|
|
+ .setTitle(R.string.pushfinger)
|
|
|
+ .setNegativeButton(R.string.cancel, (dialogInterface, view) -> {
|
|
|
+ fDisplayingPrompt = null;
|
|
|
+ signal.cancel();
|
|
|
+ resp.onError(fContext.getString(R.string.cancelled), null);
|
|
|
+ })
|
|
|
+ .show();
|
|
|
+ final FingerprintManagerCompat.CryptoObject cryptoObject = InitCryptoObject(ivForDecoding, resp);
|
|
|
+ if (cryptoObject == null)
|
|
|
+ return; // Error already handled
|
|
|
+
|
|
|
+ GetFingerprintManager().authenticate(cryptoObject, 0, signal, new FingerprintManagerCompat.AuthenticationCallback() {
|
|
|
+ @Override
|
|
|
+ public void onAuthenticationError(int errMsgId, CharSequence errString) {
|
|
|
+ ClosePrompt();
|
|
|
+ if (errMsgId != 5) // cancelled
|
|
|
+ resp.onError(errString.toString(), null);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
|
|
|
+ log.severe("Authentification help " +helpString);
|
|
|
+ super.onAuthenticationHelp(helpMsgId, helpString);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
|
|
|
+ ClosePrompt();
|
|
|
+ resp.onResponse(cryptoObject);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onAuthenticationFailed() {
|
|
|
+ ((FingerprintView)(fDisplayingPrompt.getView())).onAuthFailed();
|
|
|
+ }
|
|
|
+ }, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ClosePrompt() {
|
|
|
+ if (fDisplayingPrompt != null) {
|
|
|
+ fDisplayingPrompt.close();
|
|
|
+ fDisplayingPrompt = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ReadAllPasswords(final FileInterfaceFactory.OnPasswordEnteredListener onResp) {
|
|
|
+ try {
|
|
|
+ String raw = FileUtils.ReadAllFile(fFile);
|
|
|
+ fEncrypted = new IvAndPayload(raw);
|
|
|
+ } catch (IOException e) {
|
|
|
+ // Fallback password
|
|
|
+ SuperGetPassword(true, onResp);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ DisplayPrompt(fEncrypted, new OnResponseListener<FingerprintManagerCompat.CryptoObject>() {
|
|
|
+ @Override
|
|
|
+ public void onResponse(FingerprintManagerCompat.CryptoObject cryptoObject) {
|
|
|
+ try {
|
|
|
+ Cipher cipher = cryptoObject.getCipher();
|
|
|
+ if (cipher == null) {
|
|
|
+ log.severe("Cannot read content: cipher is null");
|
|
|
+ Toast.makeText(fContext, R.string.fingerprint_init_error, Toast.LENGTH_LONG).show();
|
|
|
+ SuperGetPassword(false, onResp);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ byte[] decoded = cipher.doFinal(fEncrypted.fPayload);
|
|
|
+ String[] passwords = (new String(decoded, "UTF-8")).split("\n");
|
|
|
+ fPassword = new ArrayDeque<>();
|
|
|
+ fPasswordIterator = new ArrayDeque<>();
|
|
|
+ // Try all password in keystore
|
|
|
+ for (String i: passwords)
|
|
|
+ if (i.length() > 0)
|
|
|
+ fPassword.add(i);
|
|
|
+ fPasswordIterator.addAll(fPassword);
|
|
|
+ if (fPasswordIterator.isEmpty())
|
|
|
+ SuperGetPassword(true, onResp);
|
|
|
+ else
|
|
|
+ onResp.onResponse(fPasswordIterator.remove());
|
|
|
+ }
|
|
|
+ catch (BadPaddingException | IllegalBlockSizeException | UnsupportedEncodingException e) {
|
|
|
+ onError(e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onError(String msg, Throwable e) {
|
|
|
+ // FIXME Toast error
|
|
|
+ // Fallback password
|
|
|
+ fPassword = new ArrayDeque<>();
|
|
|
+ fPasswordIterator = new ArrayDeque<>();
|
|
|
+ SuperGetPassword(false, onResp);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private void SuperGetPassword(boolean savePassword, FileInterfaceFactory.OnPasswordEnteredListener onResp) {
|
|
|
+ ClosePrompt();
|
|
|
+ FingerprintPicker.super.GetPassword(savePassword ? new FileInterfaceFactory.OnPasswordEnteredListener() {
|
|
|
+ @Override
|
|
|
+ public boolean onResponse(String result) {
|
|
|
+ // TODO checkbox save password ?
|
|
|
+ if (result != null) {
|
|
|
+ AddPassword(result);
|
|
|
+ }
|
|
|
+ return onResp.onResponse(result);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onError(String msg, Throwable e) {
|
|
|
+ onResp.onError(msg, e);
|
|
|
+ }
|
|
|
+ } : onResp);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void GetPassword(FileInterfaceFactory.OnPasswordEnteredListener onResp) {
|
|
|
+ if (fPassword == null)
|
|
|
+ ReadAllPasswords(onResp);
|
|
|
+ else if (fPasswordIterator.isEmpty())
|
|
|
+ SuperGetPassword(true, onResp);
|
|
|
+ else
|
|
|
+ onResp.onResponse(fPasswordIterator.remove());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void WrongPassword() {
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ private void AddPassword(String newPass) {
|
|
|
+ fEncrypted = null;
|
|
|
+ DisplayPrompt(null, new OnResponseListener<FingerprintManagerCompat.CryptoObject>() {
|
|
|
+ @Override
|
|
|
+ public void onResponse(FingerprintManagerCompat.CryptoObject cryptoObject) {
|
|
|
+ StringBuilder prev = new StringBuilder();
|
|
|
+ if (fPassword != null)
|
|
|
+ for (String i: fPassword)
|
|
|
+ prev.append(i).append('\n');
|
|
|
+ prev.append(newPass);
|
|
|
+ DoWriteFile(cryptoObject, prev.toString());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onError(String msg, Throwable e) {
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+}
|