Added biometric authentication

pull/157/head
M66B 5 years ago
parent 02433e0bc1
commit 0611cd66d1

@ -203,6 +203,7 @@ The following Android permissions are needed:
* *in-app billing* (BILLING): to allow in-app purchases
* Optional: *read your contacts* (READ_CONTACTS): to autocomplete addresses and to show photos
* Optional: *read the contents of your SD card* (READ_EXTERNAL_STORAGE): to accept files from other, outdated apps, see also [this FAQ](#user-content-faq49)
* Optional: *use fingerprint hardware* (USE_FINGERPRINT) and use *biometric hardware* (USE_BIOMETRIC): to use biometric authentication
The following permissions are needed to show the count of unread messages as a badge (see also [this FAQ](#user-content-faq106)):

@ -65,6 +65,7 @@ All pro features are convenience or advanced features.
* Filter rules ([instructions](https://github.com/M66B/open-source-email/blob/master/FAQ.md#user-content-faq71))
* Search on device or server ([instructions](https://github.com/M66B/open-source-email/blob/master/FAQ.md#user-content-faq13))
* Keyword management
* Biometric authentication
* Encryption/decryption ([OpenPGP](https://www.openpgp.org/)) ([instructions](https://github.com/M66B/open-source-email/blob/master/FAQ.md#user-content-faq12))
* Export settings

@ -138,6 +138,7 @@ dependencies {
def preference_version = "1.1.0-rc01"
def work_version = "2.1.0-rc01"
def exif_version = "1.1.0-beta01"
def biometric_version = "1.0.0-alpha04"
def billingclient_version = "2.0.1"
def javamail_version = "1.6.3"
def jsoup_version = "1.12.1"
@ -193,6 +194,10 @@ dependencies {
// https://mvnrepository.com/artifact/androidx.exifinterface/exifinterface
implementation "androidx.exifinterface:exifinterface:$exif_version"
// https://mvnrepository.com/artifact/androidx.biometric/biometric
// https://developer.android.com/jetpack/androidx/releases/biometric
implementation "androidx.biometric:biometric:$biometric_version"
// https://developer.android.com/google/play/billing/billing_library_releases_notes
implementation "com.android.billingclient:billing:$billingclient_version"

@ -10,6 +10,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<!-- https://developer.android.com/guide/topics/manifest/uses-feature-element#features-reference -->
<uses-feature
@ -21,6 +22,9 @@
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.hardware.fingerprint"
android:required="false" />
<application
android:name=".ApplicationEx"

@ -113,6 +113,9 @@ abstract class ActivityBase extends AppCompatActivity implements SharedPreferenc
Log.i("Contacts permission=" + contacts);
finish();
startActivity(getIntent());
} else if (!this.getClass().equals(ActivityMain.class) && Helper.shouldAuthenticate(this)) {
finish();
startActivity(new Intent(this, ActivityMain.class));
}
super.onResume();
@ -122,6 +125,9 @@ abstract class ActivityBase extends AppCompatActivity implements SharedPreferenc
protected void onPause() {
Log.i("Pause " + this.getClass().getName());
super.onPause();
if (!this.getClass().equals(ActivityMain.class) && Helper.shouldAuthenticate(this))
finish();
}
@Override

@ -47,19 +47,24 @@ public class ActivityMain extends AppCompatActivity implements FragmentManager.O
}
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
boolean eula = prefs.getBoolean("eula", false);
prefs.registerOnSharedPreferenceChangeListener(this);
if (prefs.getBoolean("eula", false)) {
if (eula) {
super.onCreate(savedInstanceState);
new Handler().postDelayed(new Runnable() {
final SimpleTask start = new SimpleTask<Boolean>() {
@Override
public void run() {
getWindow().setBackgroundDrawableResource(R.drawable.splash);
protected void onPreExecute(Bundle args) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
getWindow().setBackgroundDrawableResource(R.drawable.splash);
}
}, 1500);
}
}, 1500);
new SimpleTask<Boolean>() {
@Override
protected Boolean onExecute(Context context, Bundle args) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
@ -83,7 +88,6 @@ public class ActivityMain extends AppCompatActivity implements FragmentManager.O
ServiceSend.boot(ActivityMain.this);
} else
startActivity(new Intent(ActivityMain.this, ActivitySetup.class));
finish();
}
@ -91,7 +95,24 @@ public class ActivityMain extends AppCompatActivity implements FragmentManager.O
protected void onException(Bundle args, Throwable ex) {
Helper.unexpectedError(getSupportFragmentManager(), ex);
}
}.execute(this, new Bundle(), "main:accounts");
};
if (Helper.shouldAuthenticate(this))
Helper.authenticate(ActivityMain.this, null,
new Runnable() {
@Override
public void run() {
start.execute(ActivityMain.this, new Bundle(), "main:accounts");
}
},
new Runnable() {
@Override
public void run() {
finish();
}
});
else
start.execute(this, new Bundle(), "main:accounts");
} else {
// Enable compact view on small screens
if (!getResources().getConfiguration().isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_NORMAL))

@ -201,6 +201,14 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac
}
}));
menus.add(new NavMenuItem(R.drawable.baseline_fingerprint_24, R.string.title_setup_biometrics, new Runnable() {
@Override
public void run() {
drawerLayout.closeDrawer(drawerContainer);
onMenuBiometrics();
}
}));
menus.add(new NavMenuItem(R.drawable.baseline_person_24, R.string.menu_contacts, new Runnable() {
@Override
public void run() {
@ -405,6 +413,29 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac
fragmentTransaction.commit();
}
private void onMenuBiometrics() {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ActivitySetup.this);
final boolean biometrics = prefs.getBoolean("biometrics", false);
final boolean pro = Helper.isPro(this);
Helper.authenticate(this, biometrics, new Runnable() {
@Override
public void run() {
if (pro)
prefs.edit().putBoolean("biometrics", !biometrics).apply();
Toast.makeText(ActivitySetup.this,
pro ? R.string.title_setup_done : R.string.title_pro_feature,
Toast.LENGTH_LONG).show();
}
}, new Runnable() {
@Override
public void run() {
// Do nothing
}
});
}
private void onMenuContacts() {
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED))
getSupportFragmentManager().popBackStack("contacts", FragmentManager.POP_BACK_STACK_INCLUSIVE);

@ -21,9 +21,11 @@ package eu.faircode.email;
import android.app.Dialog;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
@ -33,7 +35,9 @@ import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcel;
import android.provider.Settings;
import android.text.Spannable;
import android.text.Spanned;
import android.text.format.DateUtils;
@ -54,10 +58,12 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.biometric.BiometricPrompt;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import androidx.exifinterface.media.ExifInterface;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.preference.PreferenceManager;
@ -82,10 +88,13 @@ import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
@ -129,6 +138,8 @@ public class Helper {
}
};
final static ExecutorService executor = Executors.newSingleThreadExecutor();
// Features
static boolean hasPermission(Context context, String name) {
@ -644,6 +655,92 @@ public class Helper {
return Objects.equals(signed, expected);
}
static boolean shouldAuthenticate(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean biometrics = prefs.getBoolean("biometrics", false);
if (!biometrics)
return false;
ContentResolver resolver = context.getContentResolver();
int screen_timeout = Settings.System.getInt(resolver, Settings.System.SCREEN_OFF_TIMEOUT, -1);
Log.i("Screen timeout=" + screen_timeout);
long now = new Date().getTime();
long last_authentication = prefs.getLong("last_authentication", 0);
prefs.edit().putLong("last_authentication", now).apply();
return (last_authentication + screen_timeout < now);
}
static void authenticate(final FragmentActivity activity,
Boolean enabled, final
Runnable authenticated, final Runnable cancelled) {
final Handler handler = new Handler();
BiometricPrompt.PromptInfo.Builder info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(activity.getString(enabled == null ? R.string.app_name : R.string.title_setup_biometrics))
.setNegativeButtonText(activity.getString(android.R.string.cancel));
if (enabled != null)
info.setSubtitle(activity.getString(enabled
? R.string.title_setup_biometrics_disable
: R.string.title_setup_biometrics_enable));
BiometricPrompt prompt = new BiometricPrompt(activity, executor,
new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationError(final int errorCode, @NonNull final CharSequence errString) {
Log.w("Biometric error " + errorCode + ": " + errString);
handler.post(new Runnable() {
@Override
public void run() {
if (errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON ||
errorCode == BiometricPrompt.ERROR_CANCELED ||
errorCode == BiometricPrompt.ERROR_USER_CANCELED)
cancelled.run();
else
Toast.makeText(activity,
errString + " (" + errorCode + ")",
Toast.LENGTH_LONG).show();
}
});
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
Log.i("Biometric succeeded");
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
prefs.edit().putLong("last_authentication", new Date().getTime()).apply();
handler.post(new Runnable() {
@Override
public void run() {
authenticated.run();
}
});
}
@Override
public void onAuthenticationFailed() {
Log.w("Biometric failed");
handler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(activity,
R.string.title_unexpected_error,
Toast.LENGTH_LONG).show();
cancelled.run();
}
});
}
});
prompt.authenticate(info.build());
}
// Miscellaneous
static String sanitizeKeyword(String keyword) {

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M17.81,4.47c-0.08,0 -0.16,-0.02 -0.23,-0.06C15.66,3.42 14,3 12.01,3c-1.98,0 -3.86,0.47 -5.57,1.41 -0.24,0.13 -0.54,0.04 -0.68,-0.2 -0.13,-0.24 -0.04,-0.55 0.2,-0.68C7.82,2.52 9.86,2 12.01,2c2.13,0 3.99,0.47 6.03,1.52 0.25,0.13 0.34,0.43 0.21,0.67 -0.09,0.18 -0.26,0.28 -0.44,0.28zM3.5,9.72c-0.1,0 -0.2,-0.03 -0.29,-0.09 -0.23,-0.16 -0.28,-0.47 -0.12,-0.7 0.99,-1.4 2.25,-2.5 3.75,-3.27C9.98,4.04 14,4.03 17.15,5.65c1.5,0.77 2.76,1.86 3.75,3.25 0.16,0.22 0.11,0.54 -0.12,0.7 -0.23,0.16 -0.54,0.11 -0.7,-0.12 -0.9,-1.26 -2.04,-2.25 -3.39,-2.94 -2.87,-1.47 -6.54,-1.47 -9.4,0.01 -1.36,0.7 -2.5,1.7 -3.4,2.96 -0.08,0.14 -0.23,0.21 -0.39,0.21zM9.75,21.79c-0.13,0 -0.26,-0.05 -0.35,-0.15 -0.87,-0.87 -1.34,-1.43 -2.01,-2.64 -0.69,-1.23 -1.05,-2.73 -1.05,-4.34 0,-2.97 2.54,-5.39 5.66,-5.39s5.66,2.42 5.66,5.39c0,0.28 -0.22,0.5 -0.5,0.5s-0.5,-0.22 -0.5,-0.5c0,-2.42 -2.09,-4.39 -4.66,-4.39 -2.57,0 -4.66,1.97 -4.66,4.39 0,1.44 0.32,2.77 0.93,3.85 0.64,1.15 1.08,1.64 1.85,2.42 0.19,0.2 0.19,0.51 0,0.71 -0.11,0.1 -0.24,0.15 -0.37,0.15zM16.92,19.94c-1.19,0 -2.24,-0.3 -3.1,-0.89 -1.49,-1.01 -2.38,-2.65 -2.38,-4.39 0,-0.28 0.22,-0.5 0.5,-0.5s0.5,0.22 0.5,0.5c0,1.41 0.72,2.74 1.94,3.56 0.71,0.48 1.54,0.71 2.54,0.71 0.24,0 0.64,-0.03 1.04,-0.1 0.27,-0.05 0.53,0.13 0.58,0.41 0.05,0.27 -0.13,0.53 -0.41,0.58 -0.57,0.11 -1.07,0.12 -1.21,0.12zM14.91,22c-0.04,0 -0.09,-0.01 -0.13,-0.02 -1.59,-0.44 -2.63,-1.03 -3.72,-2.1 -1.4,-1.39 -2.17,-3.24 -2.17,-5.22 0,-1.62 1.38,-2.94 3.08,-2.94 1.7,0 3.08,1.32 3.08,2.94 0,1.07 0.93,1.94 2.08,1.94s2.08,-0.87 2.08,-1.94c0,-3.77 -3.25,-6.83 -7.25,-6.83 -2.84,0 -5.44,1.58 -6.61,4.03 -0.39,0.81 -0.59,1.76 -0.59,2.8 0,0.78 0.07,2.01 0.67,3.61 0.1,0.26 -0.03,0.55 -0.29,0.64 -0.26,0.1 -0.55,-0.04 -0.64,-0.29 -0.49,-1.31 -0.73,-2.61 -0.73,-3.96 0,-1.2 0.23,-2.29 0.68,-3.24 1.33,-2.79 4.28,-4.6 7.51,-4.6 4.55,0 8.25,3.51 8.25,7.83 0,1.62 -1.38,2.94 -3.08,2.94s-3.08,-1.32 -3.08,-2.94c0,-1.07 -0.93,-1.94 -2.08,-1.94s-2.08,0.87 -2.08,1.94c0,1.71 0.66,3.31 1.87,4.51 0.95,0.94 1.86,1.46 3.27,1.85 0.27,0.07 0.42,0.35 0.35,0.61 -0.05,0.23 -0.26,0.38 -0.47,0.38z"/>
</vector>

@ -150,6 +150,9 @@
<string name="title_setup_reorder_accounts">Order accounts</string>
<string name="title_setup_reorder_folders">Order folders</string>
<string name="title_reset_order">Reset order</string>
<string name="title_setup_biometrics">Biometric authentication</string>
<string name="title_setup_biometrics_enable">Enable</string>
<string name="title_setup_biometrics_disable">Disable</string>
<string name="title_setup_theme">Select theme</string>
<string name="title_setup_light_theme">Light theme</string>
<string name="title_setup_dark_theme">Dark theme</string>

Loading…
Cancel
Save