Merge remote-tracking branch 'hanscappelle/feature/fingerprint-password-handling' into feature/fingerprint-password-handling

This commit is contained in:
Brian Pellin
2017-10-31 19:12:49 -05:00
14 changed files with 811 additions and 117 deletions

51
.gitignore vendored
View File

@@ -1,8 +1,51 @@
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store .DS_Store
/build /build
/app/build /app/build
/projectFilesBackup /projectFilesBackup
# Built application files
*.apk
*.ap_
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# Intellij
*.iml
.idea/*
# Keystore files
*.jks
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
# Google Services (e.g. APIs or Firebase)
google-services.json

View File

@@ -1,12 +1,12 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
compileSdkVersion = 22 compileSdkVersion = 23
buildToolsVersion = '26.0.2' buildToolsVersion = '26.0.2'
defaultConfig { defaultConfig {
applicationId = "com.android.keepass" applicationId = "com.android.keepass"
minSdkVersion 3 minSdkVersion 4
targetSdkVersion 12 targetSdkVersion 12
versionCode = 155 versionCode = 155
@@ -48,4 +48,6 @@ dependencies {
compile 'com.madgag.spongycastle:core:1.58.0.0' compile 'com.madgag.spongycastle:core:1.58.0.0'
compile 'com.madgag.spongycastle:prov:1.58.0.0' compile 'com.madgag.spongycastle:prov:1.58.0.0'
compile 'joda-time:joda-time:2.9.4' compile 'joda-time:joda-time:2.9.4'
compile 'com.android.support:support-v4:23.0.0'
} }

View File

@@ -8,8 +8,9 @@
android:largeScreens="true" android:largeScreens="true"
android:anyDensity="true" android:anyDensity="true"
/> />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission> <uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<uses-permission android:name="android.permission.VIBRATE"></uses-permission> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<application <application
android:label="@string/app_name" android:label="@string/app_name"
android:icon="@drawable/launcher" android:icon="@drawable/launcher"

View File

@@ -41,6 +41,7 @@ import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.text.method.PasswordTransformationMethod; import android.text.method.PasswordTransformationMethod;
@@ -228,13 +229,17 @@ public class EntryActivity extends LockCloseHideActivity {
private Notification getNotification(String intentText, int descResId) { private Notification getNotification(String intentText, int descResId) {
String desc = getString(descResId); String desc = getString(descResId);
Notification notify = new Notification(R.drawable.notify, desc, System.currentTimeMillis());
Intent intent = new Intent(intentText); Intent intent = new Intent(intentText);
PendingIntent pending = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); PendingIntent pending = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
notify.setLatestEventInfo(this, getString(R.string.app_name), desc, pending); // no longer supported for api level >22
// notify.setLatestEventInfo(this, getString(R.string.app_name), desc, pending);
// so instead using compat builder and create new notification
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
Notification notify = builder.setContentIntent(pending).setContentText(desc).setContentTitle(getString(R.string.app_name))
.setSmallIcon(R.drawable.notify).setTicker(desc).setWhen(System.currentTimeMillis()).build();
return notify; return notify;
} }

View File

@@ -19,21 +19,22 @@
*/ */
package com.keepassdroid; package com.keepassdroid;
import android.annotation.TargetApi;
import android.app.Activity; import android.app.Activity;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnClickListener;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.hardware.fingerprint.FingerprintManager;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.text.Editable;
import android.text.InputType; import android.text.InputType;
import android.text.TextWatcher;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
@@ -58,6 +59,7 @@ import com.keepassdroid.database.edit.LoadDB;
import com.keepassdroid.database.edit.OnFinish; import com.keepassdroid.database.edit.OnFinish;
import com.keepassdroid.dialog.PasswordEncodingDialogHelper; import com.keepassdroid.dialog.PasswordEncodingDialogHelper;
import com.keepassdroid.fileselect.BrowserDialog; import com.keepassdroid.fileselect.BrowserDialog;
import com.keepassdroid.fingerprint.FingerPrintHelper;
import com.keepassdroid.intents.Intents; import com.keepassdroid.intents.Intents;
import com.keepassdroid.settings.AppSettingsActivity; import com.keepassdroid.settings.AppSettingsActivity;
import com.keepassdroid.utils.EmptyUtils; import com.keepassdroid.utils.EmptyUtils;
@@ -68,7 +70,9 @@ import com.keepassdroid.utils.Util;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
public class PasswordActivity extends LockingActivity { import javax.crypto.Cipher;
public class PasswordActivity extends LockingActivity implements FingerPrintHelper.FingerPrintCallback {
public static final String KEY_DEFAULT_FILENAME = "defaultFileName"; public static final String KEY_DEFAULT_FILENAME = "defaultFileName";
private static final String KEY_FILENAME = "fileName"; private static final String KEY_FILENAME = "fileName";
@@ -86,11 +90,25 @@ public class PasswordActivity extends LockingActivity {
private boolean mRememberKeyfile; private boolean mRememberKeyfile;
SharedPreferences prefs; SharedPreferences prefs;
public static void Launch(Activity act, String fileName) throws FileNotFoundException { private FingerPrintHelper fingerPrintHelper;
Launch(act,fileName,""); private int mode;
private static final String PREF_KEY_VALUE_PREFIX = "valueFor_"; // key is a combination of db file name and this prefix
private static final String PREF_KEY_IV_PREFIX = "ivFor_"; // key is a combination of db file name and this prefix
private View fingerprintView;
private TextView confirmationView;
private EditText passwordView;
private Button confirmButton;
public static void Launch(
Activity act,
String fileName) throws FileNotFoundException {
Launch(act, fileName, "");
} }
public static void Launch(Activity act, String fileName, String keyFile) throws FileNotFoundException { public static void Launch(
Activity act,
String fileName,
String keyFile) throws FileNotFoundException {
if (EmptyUtils.isNullOrEmpty(fileName)) { if (EmptyUtils.isNullOrEmpty(fileName)) {
throw new FileNotFoundException(); throw new FileNotFoundException();
} }
@@ -114,52 +132,55 @@ public class PasswordActivity extends LockingActivity {
} }
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { protected void onActivityResult(
int requestCode,
int resultCode,
Intent data) {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) { switch (requestCode) {
case KeePass.EXIT_NORMAL: case KeePass.EXIT_NORMAL:
setEditText(R.id.password, ""); setEditText(R.id.password, "");
App.getDB().clear(); App.getDB().clear();
break; break;
case KeePass.EXIT_LOCK: case KeePass.EXIT_LOCK:
setResult(KeePass.EXIT_LOCK); setResult(KeePass.EXIT_LOCK);
setEditText(R.id.password, ""); setEditText(R.id.password, "");
finish(); finish();
App.getDB().clear(); App.getDB().clear();
break; break;
case FILE_BROWSE: case FILE_BROWSE:
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
String filename = data.getDataString(); String filename = data.getDataString();
if (filename != null) { if (filename != null) {
EditText fn = (EditText) findViewById(R.id.pass_keyfile); EditText fn = (EditText) findViewById(R.id.pass_keyfile);
fn.setText(filename); fn.setText(filename);
mKeyUri = UriUtil.parseDefaultFile(filename); mKeyUri = UriUtil.parseDefaultFile(filename);
}
}
break;
case GET_CONTENT:
case OPEN_DOC:
if (resultCode == RESULT_OK) {
if (data != null) {
Uri uri = data.getData();
if (uri != null) {
if (requestCode==GET_CONTENT) {
uri = UriUtil.translate(this, uri);
}
String path = uri.toString();
if (path != null) {
EditText fn = (EditText) findViewById(R.id.pass_keyfile);
fn.setText(path);
}
mKeyUri = uri;
} }
} }
} break;
break; case GET_CONTENT:
case OPEN_DOC:
if (resultCode == RESULT_OK) {
if (data != null) {
Uri uri = data.getData();
if (uri != null) {
if (requestCode == GET_CONTENT) {
uri = UriUtil.translate(this, uri);
}
String path = uri.toString();
if (path != null) {
EditText fn = (EditText) findViewById(R.id.pass_keyfile);
fn.setText(path);
}
mKeyUri = uri;
}
}
}
break;
} }
} }
@@ -173,7 +194,14 @@ public class PasswordActivity extends LockingActivity {
mRememberKeyfile = prefs.getBoolean(getString(R.string.keyfile_key), getResources().getBoolean(R.bool.keyfile_default)); mRememberKeyfile = prefs.getBoolean(getString(R.string.keyfile_key), getResources().getBoolean(R.bool.keyfile_default));
setContentView(R.layout.password); setContentView(R.layout.password);
confirmButton = (Button) findViewById(R.id.pass_ok);
fingerprintView = findViewById(R.id.fingerprint);
confirmationView = (TextView) findViewById(R.id.fingerprint_label);
passwordView = (EditText) findViewById(R.id.password);
new InitTask().execute(i); new InitTask().execute(i);
initForFingerprint();
} }
@Override @Override
@@ -189,6 +217,9 @@ public class PasswordActivity extends LockingActivity {
// Clear the shutdown flag // Clear the shutdown flag
App.clearShutdown(); App.clearShutdown();
// checks if fingerprint is available, will also start listening for fingerprints when available
checkAvailability();
} }
private void retrieveSettings() { private void retrieveSettings() {
@@ -200,7 +231,7 @@ public class PasswordActivity extends LockingActivity {
} }
private Uri getKeyFile(Uri dbUri) { private Uri getKeyFile(Uri dbUri) {
if ( mRememberKeyfile ) { if (mRememberKeyfile) {
return App.getFileHistory().getFileByName(dbUri); return App.getFileHistory().getFileByName(dbUri);
} else { } else {
@@ -223,15 +254,215 @@ public class PasswordActivity extends LockingActivity {
} }
*/ */
private void errorMessage(int resId) private void errorMessage(int resId) {
{
Toast.makeText(this, resId, Toast.LENGTH_LONG).show(); Toast.makeText(this, resId, Toast.LENGTH_LONG).show();
} }
// fingerprint related code here
private void initForFingerprint() {
fingerPrintHelper = new FingerPrintHelper(this, this);
if (fingerPrintHelper.isFingerprintSupported()) {
// when text entered we can enable the logon/purchase button and if required update encryption/decryption mode
passwordView.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(
final CharSequence s,
final int start,
final int count,
final int after) {
}
@Override
public void onTextChanged(
final CharSequence s,
final int start,
final int before,
final int count) {
}
@Override
public void afterTextChanged(final Editable s) {
final boolean validInput = s.length() > 0;
// encrypt or decrypt mode based on how much input or not
confirmationView.setText(validInput ? R.string.store_with_fingerprint : R.string.scanning_fingerprint);
mode = validInput ? toggleMode(Cipher.ENCRYPT_MODE) : toggleMode(Cipher.DECRYPT_MODE);
}
});
// callback for fingerprint findings
fingerPrintHelper.setAuthenticationCallback(new FingerprintManager.AuthenticationCallback() {
@Override
public void onAuthenticationError(
final int errorCode,
final CharSequence errString) {
// this is triggered on stop/start listening done by helper to switch between modes so don't restart here
// errorCode = 5
// errString = "Fingerprint operation canceled."
//onException();
//confirmationView.setText(errString);
// true false fingerprint readings are handled otherwise with the toast messages, see below in code
}
@Override
public void onAuthenticationHelp(
final int helpCode,
final CharSequence helpString) {
onException();
confirmationView.setText(helpString);
}
@Override
public void onAuthenticationSucceeded(final FingerprintManager.AuthenticationResult result) {
if (mode == Cipher.ENCRYPT_MODE) {
// newly store the entered password in encrypted way
final String password = passwordView.getText().toString();
fingerPrintHelper.encryptData(password);
} else if (mode == Cipher.DECRYPT_MODE) {
// retrieve the encrypted value from preferences
final String encryptedValue = prefs.getString(getPreferenceKeyValue(), null);
if (encryptedValue != null) {
fingerPrintHelper.decryptData(encryptedValue);
}
}
}
@Override
public void onAuthenticationFailed() {
onException();
}
});
}
}
private String getPreferenceKeyValue() {
// makes it possible to store passwords uniqly per database
return PREF_KEY_VALUE_PREFIX + (mDbUri != null ? mDbUri.getPath() : "");
}
private String getPreferenceKeyIvSpec() {
return PREF_KEY_IV_PREFIX + (mDbUri != null ? mDbUri.getPath() : "");
}
private int toggleMode(final int newMode) {
// check if mode is different so we can update fingerprint helper
if (mode != newMode) {
mode = newMode;
switch (mode) {
case Cipher.ENCRYPT_MODE:
fingerPrintHelper.initEncryptData();
break;
case Cipher.DECRYPT_MODE:
final String ivSpecValue = prefs.getString(getPreferenceKeyIvSpec(), null);
fingerPrintHelper.initDecryptData(ivSpecValue);
break;
}
return newMode;
}
// remains in current mode
return mode;
}
@Override
protected void onPause() {
super.onPause();
// stop listening when we go in background
if (fingerPrintHelper != null) {
fingerPrintHelper.stopListening();
}
}
private void checkAvailability() {
// fingerprint not supported (by API level or hardware) so keep option hidden
if (!fingerPrintHelper.isFingerprintSupported()) {
fingerprintView.setVisibility(View.GONE);
// since this is a fingerprint example inform user, don't do this in your app
//confirmationView.setText(R.string.fingerprint_not_supported);
confirmationView.setVisibility(View.GONE);
}
// fingerprint is available but not configured show icon but in disabled state with some information
else if (!fingerPrintHelper.hasEnrolledFingerprints()) {
fingerprintView.setVisibility(View.VISIBLE);
confirmationView.setVisibility(View.VISIBLE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
fingerprintView.setAlpha(0.3f);
}
// This happens when no fingerprints are registered. Listening won't start
confirmationView.setText(R.string.configure_fingerprint);
}
// finally fingerprint available and configured so we can use it
else {
fingerprintView.setVisibility(View.VISIBLE);
confirmationView.setVisibility(View.VISIBLE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
fingerprintView.setAlpha(1f);
}
// fingerprint available but no stored password found yet for this DB so show info don't listen
if (prefs.getString(getPreferenceKeyValue(), null) == null) {
confirmationView.setText(R.string.no_password_stored);
}
// all is set here so we can confirm to user and start listening for fingerprints
else {
confirmationView.setText(R.string.scanning_fingerprint);
// listen for decryption by default
toggleMode(Cipher.DECRYPT_MODE);
}
}
}
@Override
public void handleEncryptedResult(
final String value,
final String ivSpec) {
prefs.edit()
.putString(getPreferenceKeyValue(), value)
.putString(getPreferenceKeyIvSpec(), ivSpec)
.commit();
// and remove visual input to reset UI
passwordView.setText("");
confirmationView.setText(R.string.encrypted_value_stored);
}
@Override
public void handleDecryptedResult(final String value) {
// on decrypt enter it for the purchase/login action
passwordView.setText(value);
confirmButton.performClick();
}
@Override
public void onInvalidKeyException() {
Toast.makeText(this, R.string.fingerprint_invalid_key, Toast.LENGTH_SHORT).show();
checkAvailability(); // restarts listening
}
@Override
public void onException() {
Toast.makeText(this, R.string.fingerprint_error, Toast.LENGTH_SHORT).show();
checkAvailability(); // restarts listening
}
private class DefaultCheckChange implements CompoundButton.OnCheckedChangeListener { private class DefaultCheckChange implements CompoundButton.OnCheckedChangeListener {
@Override @Override
public void onCheckedChanged(CompoundButton buttonView, public void onCheckedChanged(
CompoundButton buttonView,
boolean isChecked) { boolean isChecked) {
String newDefaultFileName; String newDefaultFileName;
@@ -262,13 +493,16 @@ public class PasswordActivity extends LockingActivity {
} }
} }
private void loadDatabase(String pass, String keyfile) { private void loadDatabase(
String pass,
String keyfile) {
loadDatabase(pass, UriUtil.parseDefaultFile(keyfile)); loadDatabase(pass, UriUtil.parseDefaultFile(keyfile));
} }
private void loadDatabase(String pass, Uri keyfile) private void loadDatabase(
{ String pass,
if ( pass.length() == 0 && (keyfile == null || keyfile.toString().length() == 0)) { Uri keyfile) {
if (pass.length() == 0 && (keyfile == null || keyfile.toString().length() == 0)) {
errorMessage(R.string.error_nopass); errorMessage(R.string.error_nopass);
return; return;
} }
@@ -290,9 +524,11 @@ public class PasswordActivity extends LockingActivity {
return Util.getEditText(this, resId); return Util.getEditText(this, resId);
} }
private void setEditText(int resId, String str) { private void setEditText(
TextView te = (TextView) findViewById(resId); int resId,
assert(te == null); String str) {
TextView te = (TextView) findViewById(resId);
assert (te == null);
if (te != null) { if (te != null) {
te.setText(str); te.setText(str);
@@ -311,24 +547,27 @@ public class PasswordActivity extends LockingActivity {
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch ( item.getItemId() ) { switch (item.getItemId()) {
case R.id.menu_about: case R.id.menu_about:
AboutDialog dialog = new AboutDialog(this); AboutDialog dialog = new AboutDialog(this);
dialog.show(); dialog.show();
return true; return true;
case R.id.menu_app_settings: case R.id.menu_app_settings:
AppSettingsActivity.Launch(this); AppSettingsActivity.Launch(this);
return true; return true;
} }
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
private final class AfterLoad extends OnFinish { private final class AfterLoad extends OnFinish {
private Database db; private Database db;
public AfterLoad(Handler handler, Database db) { public AfterLoad(
Handler handler,
Database db) {
super(handler); super(handler);
this.db = db; this.db = db;
@@ -336,17 +575,19 @@ public class PasswordActivity extends LockingActivity {
@Override @Override
public void run() { public void run() {
if ( db.passwordEncodingError) { if (db.passwordEncodingError) {
PasswordEncodingDialogHelper dialog = new PasswordEncodingDialogHelper(); PasswordEncodingDialogHelper dialog = new PasswordEncodingDialogHelper();
dialog.show(PasswordActivity.this, new OnClickListener() { dialog.show(PasswordActivity.this, new OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(
DialogInterface dialog,
int which) {
GroupActivity.Launch(PasswordActivity.this); GroupActivity.Launch(PasswordActivity.this);
} }
}); });
} else if ( mSuccess ) { } else if (mSuccess) {
GroupActivity.Launch(PasswordActivity.this); GroupActivity.Launch(PasswordActivity.this);
} else { } else {
displayMessage(PasswordActivity.this); displayMessage(PasswordActivity.this);
@@ -355,23 +596,24 @@ public class PasswordActivity extends LockingActivity {
} }
private class InitTask extends AsyncTask<Intent, Void, Integer> { private class InitTask extends AsyncTask<Intent, Void, Integer> {
String password = ""; String password = "";
boolean launch_immediately = false; boolean launch_immediately = false;
@Override @Override
protected Integer doInBackground(Intent... args) { protected Integer doInBackground(Intent... args) {
Intent i = args[0]; Intent i = args[0];
String action = i.getAction();; String action = i.getAction();
if ( action != null && action.equals(VIEW_INTENT) ) { ;
if (action != null && action.equals(VIEW_INTENT)) {
Uri incoming = i.getData(); Uri incoming = i.getData();
mDbUri = incoming; mDbUri = incoming;
mKeyUri = ClipDataCompat.getUriFromIntent(i, KEY_KEYFILE); mKeyUri = ClipDataCompat.getUriFromIntent(i, KEY_KEYFILE);
if (incoming == null) { if (incoming == null) {
return R.string.error_can_not_handle_uri; return R.string.error_can_not_handle_uri;
} } else if (incoming.getScheme().equals("file")) {
else if (incoming.getScheme().equals("file")) {
String fileName = incoming.getPath(); String fileName = incoming.getPath();
if (fileName.length() == 0) { if (fileName.length() == 0) {
@@ -385,14 +627,14 @@ public class PasswordActivity extends LockingActivity {
return R.string.FileNotFound; return R.string.FileNotFound;
} }
if(mKeyUri == null) if (mKeyUri == null) {
mKeyUri = getKeyFile(mDbUri); mKeyUri = getKeyFile(mDbUri);
} }
else if (incoming.getScheme().equals("content")) { } else if (incoming.getScheme().equals("content")) {
if(mKeyUri == null) if (mKeyUri == null) {
mKeyUri = getKeyFile(mDbUri); mKeyUri = getKeyFile(mDbUri);
} }
else { } else {
return R.string.error_can_not_handle_uri; return R.string.error_can_not_handle_uri;
} }
password = i.getStringExtra(KEY_PASSWORD); password = i.getStringExtra(KEY_PASSWORD);
@@ -404,7 +646,7 @@ public class PasswordActivity extends LockingActivity {
password = i.getStringExtra(KEY_PASSWORD); password = i.getStringExtra(KEY_PASSWORD);
launch_immediately = i.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false); launch_immediately = i.getBooleanExtra(KEY_LAUNCH_IMMEDIATELY, false);
if ( mKeyUri == null || mKeyUri.toString().length() == 0) { if (mKeyUri == null || mKeyUri.toString().length() == 0) {
mKeyUri = getKeyFile(mDbUri); mKeyUri = getKeyFile(mDbUri);
} }
} }
@@ -412,7 +654,7 @@ public class PasswordActivity extends LockingActivity {
} }
public void onPostExecute(Integer result) { public void onPostExecute(Integer result) {
if(result != null) { if (result != null) {
Toast.makeText(PasswordActivity.this, result, Toast.LENGTH_LONG).show(); Toast.makeText(PasswordActivity.this, result, Toast.LENGTH_LONG).show();
finish(); finish();
return; return;
@@ -420,18 +662,18 @@ public class PasswordActivity extends LockingActivity {
populateView(); populateView();
Button confirmButton = (Button) findViewById(R.id.pass_ok);
confirmButton.setOnClickListener(new OkClickHandler()); confirmButton.setOnClickListener(new OkClickHandler());
CheckBox checkBox = (CheckBox) findViewById(R.id.show_password); CheckBox checkBox = (CheckBox) findViewById(R.id.show_password);
// Show or hide password // Show or hide password
checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() { checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, public void onCheckedChanged(
CompoundButton buttonView,
boolean isChecked) { boolean isChecked) {
TextView password = (TextView) findViewById(R.id.password); TextView password = (TextView) findViewById(R.id.password);
if ( isChecked ) { if (isChecked) {
password.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); password.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
} else { } else {
password.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); password.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
@@ -457,8 +699,7 @@ public class PasswordActivity extends LockingActivity {
i.addCategory(Intent.CATEGORY_OPENABLE); i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("*/*"); i.setType("*/*");
startActivityForResult(i, OPEN_DOC); startActivityForResult(i, OPEN_DOC);
} } else {
else {
Intent i = new Intent(Intent.ACTION_GET_CONTENT); Intent i = new Intent(Intent.ACTION_GET_CONTENT);
i.addCategory(Intent.CATEGORY_OPENABLE); i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("*/*"); i.setType("*/*");
@@ -508,8 +749,9 @@ public class PasswordActivity extends LockingActivity {
retrieveSettings(); retrieveSettings();
if (launch_immediately) if (launch_immediately) {
loadDatabase(password, mKeyUri); loadDatabase(password, mKeyUri);
}
} }
} }
} }

View File

@@ -19,16 +19,21 @@
*/ */
package com.keepassdroid.fileselect; package com.keepassdroid.fileselect;
import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
import android.view.Menu; import android.view.Menu;
@@ -74,6 +79,7 @@ import java.net.URLDecoder;
public class FileSelectActivity extends Activity { public class FileSelectActivity extends Activity {
private static final int MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE = 111;
private ListView mList; private ListView mList;
private ListAdapter mAdapter; private ListAdapter mAdapter;
@@ -218,7 +224,12 @@ public class FileSelectActivity extends Activity {
startActivityForResult(i, OPEN_DOC); startActivityForResult(i, OPEN_DOC);
} }
else { else {
Intent i = new Intent(Intent.ACTION_GET_CONTENT); Intent i;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
i = new Intent(Intent.ACTION_OPEN_DOCUMENT);
} else {
i = new Intent(Intent.ACTION_GET_CONTENT);
}
i.addCategory(Intent.CATEGORY_OPENABLE); i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("*/*"); i.setType("*/*");
@@ -408,6 +419,9 @@ public class FileSelectActivity extends Activity {
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
// check for storage permission
checkStoragePermission();
// Check to see if we need to change modes // Check to see if we need to change modes
if ( fileHistory.hasRecentFiles() != recentMode ) { if ( fileHistory.hasRecentFiles() != recentMode ) {
@@ -421,6 +435,60 @@ public class FileSelectActivity extends Activity {
fnv.updateExternalStorageWarning(); fnv.updateExternalStorageWarning();
} }
private void checkStoragePermission() {
// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(FileSelectActivity.this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// Should we show an explanation?
//if (ActivityCompat.shouldShowRequestPermissionRationale(FileSelectActivity.this,
// Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
// Show an explanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
//} else {
// No explanation needed, we can request the permission.
ActivityCompat.requestPermissions(FileSelectActivity.this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE);
// MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
// app-defined int constant. The callback method gets the
// result of the request.
//}
}
}
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case MY_PERMISSIONS_REQUEST_EXTERNAL_STORAGE: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// permission was granted, yay! Do the
// contacts-related task you need to do.
} else {
// permission denied, boo! Disable the
// functionality that depends on this permission.
}
return;
}
// other 'case' lines to check for other
// permissions this app might request
}
}
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu); super.onCreateOptionsMenu(menu);

View File

@@ -0,0 +1,284 @@
package com.keepassdroid.fingerprint;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.KeyguardManager;
import android.content.Context;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.CancellationSignal;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties;
import android.util.Base64;
import java.security.KeyStore;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
public class FingerPrintHelper {
private static final String ALIAS_KEY = "example-key";
private FingerprintManager fingerprintManager;
private KeyStore keyStore = null;
private KeyGenerator keyGenerator = null;
private Cipher cipher = null;
private KeyguardManager keyguardManager = null;
private FingerprintManager.CryptoObject cryptoObject = null;
private boolean initOk = false;
private FingerPrintCallback fingerPrintCallback;
private CancellationSignal cancellationSignal;
private FingerprintManager.AuthenticationCallback authenticationCallback;
public void setAuthenticationCallback(final FingerprintManager.AuthenticationCallback authenticationCallback) {
this.authenticationCallback = authenticationCallback;
}
@SuppressLint("NewApi")
public void startListening() {
// no need to start listening when not initialised
if (!isFingerprintInitialized()) {
if (fingerPrintCallback != null) {
fingerPrintCallback.onException();
}
return;
}
// starts listening for fingerprints with the initialised crypto object
cancellationSignal = new CancellationSignal();
fingerprintManager.authenticate(
cryptoObject,
cancellationSignal,
0 /* flags */,
authenticationCallback,
null);
}
@SuppressLint("NewApi")
public void stopListening() {
if (!isFingerprintInitialized()) {
return;
}
if (cancellationSignal != null) {
cancellationSignal.cancel();
cancellationSignal = null;
}
}
public interface FingerPrintCallback {
void handleEncryptedResult(String value, String ivSpec);
void handleDecryptedResult(String value);
void onInvalidKeyException();
void onException();
}
@TargetApi(Build.VERSION_CODES.M)
public FingerPrintHelper(
final Context context,
final FingerPrintCallback fingerPrintCallback) {
if (!isFingerprintSupported()) {
// really not much to do when no fingerprint support found
setInitOk(false);
return;
}
this.fingerprintManager = context.getSystemService(FingerprintManager.class);
this.keyguardManager = context.getSystemService(KeyguardManager.class);
this.fingerPrintCallback = fingerPrintCallback;
if (hasEnrolledFingerprints()) {
try {
this.keyStore = KeyStore.getInstance("AndroidKeyStore");
this.keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore");
this.cipher = Cipher.getInstance(
KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7);
this.cryptoObject = new FingerprintManager.CryptoObject(cipher);
setInitOk(true);
} catch (final Exception e) {
setInitOk(false);
fingerPrintCallback.onException();
}
}
}
public boolean isFingerprintInitialized() {
return hasEnrolledFingerprints() && initOk;
}
@SuppressWarnings("NewApi")
public void initEncryptData() {
if (!isFingerprintInitialized()) {
if (fingerPrintCallback != null) {
fingerPrintCallback.onException();
}
return;
}
try {
createNewKeyIfNeeded(false); // no need to keep deleting existing keys
keyStore.load(null);
final SecretKey key = (SecretKey) keyStore.getKey(ALIAS_KEY, null);
cipher.init(Cipher.ENCRYPT_MODE, key);
stopListening();
startListening();
} catch (final KeyPermanentlyInvalidatedException invalidKeyException) {
fingerPrintCallback.onInvalidKeyException();
} catch (final Exception e) {
fingerPrintCallback.onException();
}
}
@SuppressWarnings("NewApi")
public void encryptData(final String value) {
if (!isFingerprintInitialized()) {
if (fingerPrintCallback != null) {
fingerPrintCallback.onException();
}
return;
}
try {
// actual do encryption here
byte[] encrypted = cipher.doFinal(value.getBytes());
final String encryptedValue = Base64.encodeToString(encrypted, 0 /* flags */);
// passes updated iv spec on to callback so this can be stored for decryption
final IvParameterSpec spec = cipher.getParameters().getParameterSpec(IvParameterSpec.class);
final String ivSpecValue = Base64.encodeToString(spec.getIV(), Base64.DEFAULT);
fingerPrintCallback.handleEncryptedResult(encryptedValue, ivSpecValue);
} catch (final Exception e) {
fingerPrintCallback.onException();
}
}
@SuppressWarnings("NewApi")
public void initDecryptData(final String ivSpecValue) {
if (!isFingerprintInitialized()) {
if (fingerPrintCallback != null) {
fingerPrintCallback.onException();
}
return;
}
try {
createNewKeyIfNeeded(false);
keyStore.load(null);
final SecretKey key = (SecretKey) keyStore.getKey(ALIAS_KEY, null);
// important to restore spec here that was used for decryption
final byte[] iv = Base64.decode(ivSpecValue, Base64.DEFAULT);
final IvParameterSpec spec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
stopListening();
startListening();
} catch (final KeyPermanentlyInvalidatedException invalidKeyException) {
fingerPrintCallback.onInvalidKeyException();
} catch (final Exception e) {
fingerPrintCallback.onException();
}
}
@SuppressWarnings("NewApi")
public void decryptData(final String encryptedValue) {
if (!isFingerprintInitialized()) {
if (fingerPrintCallback != null) {
fingerPrintCallback.onException();
}
return;
}
try {
// actual decryption here
final byte[] encrypted = Base64.decode(encryptedValue, 0);
byte[] decrypted = cipher.doFinal(encrypted);
final String decryptedString = new String(decrypted);
//final String encryptedString = Base64.encodeToString(encrypted, 0 /* flags */);
fingerPrintCallback.handleDecryptedResult(decryptedString);
} catch (final Exception e) {
fingerPrintCallback.onException();
}
}
@SuppressLint("NewApi")
private void createNewKeyIfNeeded(final boolean allowDeleteExisting) {
if (!isFingerprintInitialized()) {
return;
}
try {
keyStore.load(null);
if (allowDeleteExisting
&& keyStore.containsAlias(ALIAS_KEY)) {
keyStore.deleteEntry(ALIAS_KEY);
}
// Create new key if needed
if (!keyStore.containsAlias(ALIAS_KEY)) {
// Set the alias of the entry in Android KeyStore where the key will appear
// and the constrains (purposes) in the constructor of the Builder
keyGenerator.init(
new KeyGenParameterSpec.Builder(
ALIAS_KEY,
KeyProperties.PURPOSE_ENCRYPT |
KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
// Require the user to authenticate with a fingerprint to authorize every use
// of the key
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.build());
keyGenerator.generateKey();
}
} catch (final Exception e) {
fingerPrintCallback.onException();
}
}
@SuppressLint("NewApi")
public boolean isHardwareDetected() {
return isFingerprintSupported()
&& fingerprintManager != null
&& fingerprintManager.isHardwareDetected();
}
@SuppressLint("NewApi")
public boolean hasEnrolledFingerprints() {
// fingerprint hardware supported and api level OK
return isHardwareDetected()
// fingerprints enrolled
&& fingerprintManager != null
&& fingerprintManager.hasEnrolledFingerprints()
// and lockscreen configured
&& keyguardManager.isKeyguardSecure();
}
void setInitOk(final boolean initOk) {
this.initOk = initOk;
}
public boolean isFingerprintSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -18,11 +18,12 @@
along with KeePassDroid. If not, see <http://www.gnu.org/licenses/>. along with KeePassDroid. If not, see <http://www.gnu.org/licenses/>.
--> -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent"> android:layout_height="fill_parent"
<TextView android:id="@+id/filename_label" xmlns:tools="http://schemas.android.com/tools">
<TextView android:id="@+id/filename_label"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/pass_filename" /> android:text="@string/pass_filename" />
<ImageView android:id="@+id/divider1" <ImageView android:id="@+id/divider1"
android:layout_width="fill_parent" android:layout_width="fill_parent"
@@ -30,10 +31,10 @@
android:layout_below="@id/filename_label" android:layout_below="@id/filename_label"
android:scaleType="fitXY" android:scaleType="fitXY"
android:src="@android:drawable/divider_horizontal_dark"/> android:src="@android:drawable/divider_horizontal_dark"/>
<TextView android:id="@+id/filename" <TextView android:id="@+id/filename"
style="@style/GroupText" style="@style/GroupText"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/divider1" android:layout_below="@id/divider1"
android:singleLine="true"/> android:singleLine="true"/>
<ImageView android:id="@+id/divider2" <ImageView android:id="@+id/divider2"
@@ -53,28 +54,53 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/default_database" android:layout_below="@id/default_database"
android:text="@string/entry_and_or" /> android:text="@string/entry_and_or" />
<EditText android:id="@+id/password" <EditText android:id="@+id/password"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/password_label" android:layout_below="@id/password_label"
android:layout_toLeftOf="@+id/fingerprint"
android:singleLine="true" android:singleLine="true"
android:inputType="textPassword" android:inputType="textPassword"
android:hint="@string/hint_login_pass"/> android:hint="@string/hint_login_pass"/>
<!-- added these 2 fingerprint related views -->
<ImageView
android:id="@+id/fingerprint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_small"
android:layout_marginRight="@dimen/margin_small"
android:layout_below="@id/password_label"
android:layout_alignParentRight="true"
android:src="@drawable/ic_fp_40px"
android:visibility="gone"
tools:visibility="visible"
/>
<TextView
android:id="@+id/fingerprint_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/fingerprint"
android:text="@string/entry_and_or"
android:visibility="gone"
tools:visibility="visible"
/>
<ImageButton android:id="@+id/browse_button" <ImageButton android:id="@+id/browse_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:src="@drawable/ic_launcher_folder_small" android:src="@drawable/ic_launcher_folder_small"
android:layout_below="@id/password" android:layout_below="@id/fingerprint_label"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
/> />
<EditText android:id="@+id/pass_keyfile" <EditText android:id="@+id/pass_keyfile"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/password" android:layout_below="@id/fingerprint_label"
android:layout_toLeftOf="@id/browse_button" android:layout_toLeftOf="@id/browse_button"
android:singleLine="true" android:singleLine="true"
android:hint="@string/entry_keyfile"/> android:hint="@string/entry_keyfile"/>
<Button android:id="@+id/pass_ok" <Button android:id="@+id/pass_ok"
android:text="@android:string/ok" android:text="@android:string/ok"
android:layout_width="100sp" android:layout_width="100sp"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="margin_tiny">4dp</dimen>
<dimen name="margin_small">8dp</dimen>
<dimen name="margin_medium">16dp</dimen>
<dimen name="margin_large">32dp</dimen>
<dimen name="margin_huge">64dp</dimen>
</resources>

View File

@@ -194,7 +194,14 @@
<string name="warning_read_only">Your sd card is currently read-only. You may not be able to save changes to your database.</string> <string name="warning_read_only">Your sd card is currently read-only. You may not be able to save changes to your database.</string>
<string name="warning_unmounted">Your sd card is not currently mounted on your device. You will not be able to load or create your database.</string> <string name="warning_unmounted">Your sd card is not currently mounted on your device. You will not be able to load or create your database.</string>
<string name="version_label">Version:</string> <string name="version_label">Version:</string>
<string name="configure_fingerprint">Fingerprint supported but not configured for device</string>
<string name="scanning_fingerprint">Listening for fingerprints</string>
<string name="encrypted_value_stored">Encrypted password stored</string>
<string name="fingerprint_invalid_key">Invalid Key problem</string>
<string name="fingerprint_error">Fingerprint problem</string>
<string name="store_with_fingerprint">Use fingerprint to store this password</string>
<string name="no_password_stored">No password stored yet for this database</string>
<string-array name="clipboard_timeout_options"> <string-array name="clipboard_timeout_options">
<item>30 seconds</item> <item>30 seconds</item>
<item>1 minute</item> <item>1 minute</item>

View File

@@ -2,6 +2,10 @@
buildscript { buildscript {
repositories { repositories {
jcenter() jcenter()
maven {
url 'https://maven.google.com/'
name 'Google'
}
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.0.0' classpath 'com.android.tools.build:gradle:3.0.0'
@@ -11,5 +15,9 @@ buildscript {
allprojects { allprojects {
repositories { repositories {
jcenter() jcenter()
maven {
url 'https://maven.google.com/'
name 'Google'
}
} }
} }