From 9a1a54a6fcfa6fd6cf142f2af9804375255d14cf Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Wed, 2 Oct 2019 11:00:45 +0530 Subject: [PATCH] Initial biometric authentication support (#541) * [WIP] Initial biometric authentication support * Redirect decryption app shortcut to go through LaunchActivity * UserPreference: Clear existing shortcuts when toggling password auth Clears out any auth-bypassed entries that exist * Fix hilarious copypasta derp Signed-off-by: Harsh Shandilya --- app/build.gradle.kts | 4 +- app/src/main/AndroidManifest.xml | 10 ++- .../java/com/zeapo/pwdstore/LaunchActivity.kt | 49 +++++++++++++++ .../com/zeapo/pwdstore/PasswordStore.java | 19 +++--- .../java/com/zeapo/pwdstore/UserPreference.kt | 41 ++++++++++++ .../utils/auth/AuthenticationResult.kt | 14 +++++ .../pwdstore/utils/auth/Authenticator.kt | 63 +++++++++++++++++++ app/src/main/res/values/strings.xml | 8 +++ app/src/main/res/xml/preference.xml | 4 ++ 9 files changed, 202 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt create mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/auth/AuthenticationResult.kt create mode 100644 app/src/main/java/com/zeapo/pwdstore/utils/auth/Authenticator.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c1ea485e..e65cb285 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -71,12 +71,14 @@ android { dependencies { implementation("androidx.appcompat:appcompat:1.1.0") implementation("androidx.cardview:cardview:1.0.0") + implementation("androidx.core:core-ktx:1.2.0-alpha04") implementation("androidx.constraintlayout:constraintlayout:2.0.0-beta2") implementation("androidx.documentfile:documentfile:1.0.1") implementation("androidx.preference:preference:1.1.0") implementation("androidx.recyclerview:recyclerview:1.1.0-beta04") implementation("com.google.android.material:material:1.1.0-alpha10") implementation("androidx.annotation:annotation:1.1.0") + implementation("androidx.biometric:biometric:1.0.0-beta02") implementation("org.sufficientlysecure:openpgp-api:12.0") implementation("org.eclipse.jgit:org.eclipse.jgit:3.7.1.201504261725-r") { exclude(group = "org.apache.httpcomponents", module = "httpclient") @@ -106,7 +108,7 @@ tasks { withType { kotlinOptions { jvmTarget = "1.8" - freeCompilerArgs += "-Xnew-inference" + freeCompilerArgs = freeCompilerArgs + "-Xnew-inference" } } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index abe12820..21131dde 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,9 @@ android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" tools:ignore="ProtectedPermissions" /> + + + + + android:label="@string/app_name" /> + + diff --git a/app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt b/app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt new file mode 100644 index 00000000..8f607ee0 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt @@ -0,0 +1,49 @@ +package com.zeapo.pwdstore + +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.preference.PreferenceManager +import com.zeapo.pwdstore.crypto.PgpActivity +import com.zeapo.pwdstore.utils.auth.AuthenticationResult +import com.zeapo.pwdstore.utils.auth.Authenticator + +class LaunchActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val prefs = PreferenceManager.getDefaultSharedPreferences(this) + if (prefs.getBoolean("biometric_auth", false)) { + Authenticator(this) { + when (it) { + is AuthenticationResult.Success -> { + startTargetActivity() + } + is AuthenticationResult.UnrecoverableError -> { + finish() + } + else -> { + } + } + }.authenticate() + } else { + startTargetActivity() + } + } + + private fun startTargetActivity() { + if (intent?.getStringExtra("OPERATION") == "DECRYPT") { + val decryptIntent = Intent(this, PgpActivity::class.java) + decryptIntent.putExtra("NAME", intent.getStringExtra("NAME")) + decryptIntent.putExtra("FILE_PATH", intent.getStringExtra("FILE_PATH")) + decryptIntent.putExtra("REPO_PATH", intent.getStringExtra("REPO_PATH")) + decryptIntent.putExtra("LAST_CHANGED_TIMESTAMP", intent.getLongExtra("LAST_CHANGED_TIMESTAMP", 0L)) + decryptIntent.putExtra("OPERATION", "DECRYPT") + startActivity(decryptIntent) + }else { + startActivity(Intent(this, PasswordStore::class.java)) + } + overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) + finish() + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java index 469dac32..a0003bb9 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java @@ -457,12 +457,15 @@ public class PasswordStore extends AppCompatActivity { } public void decryptPassword(PasswordItem item) { - Intent intent = new Intent(this, PgpActivity.class); - intent.putExtra("NAME", item.toString()); - intent.putExtra("FILE_PATH", item.getFile().getAbsolutePath()); - intent.putExtra("REPO_PATH", PasswordRepository.getRepositoryDirectory(getApplicationContext()).getAbsolutePath()); - intent.putExtra("LAST_CHANGED_TIMESTAMP", getLastChangedTimestamp(item.getFile().getAbsolutePath())); - intent.putExtra("OPERATION", "DECRYPT"); + Intent decryptIntent = new Intent(this, PgpActivity.class); + Intent authDecryptIntent = new Intent(this, LaunchActivity.class); + for (Intent intent : new Intent[] {decryptIntent, authDecryptIntent}) { + intent.putExtra("NAME", item.toString()); + intent.putExtra("FILE_PATH", item.getFile().getAbsolutePath()); + intent.putExtra("REPO_PATH", PasswordRepository.getRepositoryDirectory(getApplicationContext()).getAbsolutePath()); + intent.putExtra("LAST_CHANGED_TIMESTAMP", getLastChangedTimestamp(item.getFile().getAbsolutePath())); + intent.putExtra("OPERATION", "DECRYPT"); + } // Adds shortcut if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { @@ -470,7 +473,7 @@ public class PasswordStore extends AppCompatActivity { .setShortLabel(item.toString()) .setLongLabel(item.getFullPathToParent() + item.toString()) .setIcon(Icon.createWithResource(this, R.mipmap.ic_launcher)) - .setIntent(intent.setAction("DECRYPT_PASS")) // Needs action + .setIntent(authDecryptIntent.setAction("DECRYPT_PASS")) // Needs action .build(); List shortcuts = shortcutManager.getDynamicShortcuts(); @@ -483,7 +486,7 @@ public class PasswordStore extends AppCompatActivity { shortcutManager.addDynamicShortcuts(Collections.singletonList(shortcut)); } } - startActivityForResult(intent, REQUEST_CODE_DECRYPT_AND_VERIFY); + startActivityForResult(decryptIntent, REQUEST_CODE_DECRYPT_AND_VERIFY); } public void editPassword(PasswordItem item) { diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt index 3384356f..48edf141 100644 --- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt +++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt @@ -4,6 +4,7 @@ import android.accessibilityservice.AccessibilityServiceInfo import android.app.Activity import android.content.Context import android.content.Intent +import android.content.pm.ShortcutManager import android.net.Uri import android.os.Build import android.os.Bundle @@ -15,16 +16,21 @@ import android.view.MenuItem import android.view.accessibility.AccessibilityManager import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.biometric.BiometricManager +import androidx.core.content.getSystemService import androidx.documentfile.provider.DocumentFile import androidx.preference.CheckBoxPreference import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager +import androidx.preference.SwitchPreference import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity import com.zeapo.pwdstore.crypto.PgpActivity import com.zeapo.pwdstore.git.GitActivity import com.zeapo.pwdstore.utils.PasswordRepository +import com.zeapo.pwdstore.utils.auth.AuthenticationResult +import com.zeapo.pwdstore.utils.auth.Authenticator import org.apache.commons.io.FileUtils import org.openintents.openpgp.util.OpenPgpUtils import java.io.File @@ -249,6 +255,41 @@ class UserPreference : AppCompatActivity() { false } } + + findPreference("biometric_auth")?.apply { + val isFingerprintSupported = BiometricManager.from(requireContext()).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS + if (!isFingerprintSupported) { + isEnabled = false + isChecked = false + summary = getString(R.string.biometric_auth_summary_error) + } else { + setOnPreferenceClickListener { + val editor = sharedPreferences.edit() + val checked = isChecked + Authenticator(requireActivity()) { result -> + when (result) { + is AuthenticationResult.Success -> { + // Apply the changes + editor.putBoolean("biometric_auth", checked) + } + else -> { + // If any error occurs, revert back to the previous state. This + // catch-all clause includes the cancellation case. + editor.putBoolean("biometric_auth", !checked) + isChecked = !checked + } + } + }.authenticate() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + requireContext().getSystemService()?.apply { + removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList()) + } + } + editor.apply() + true + } + } + } } override fun onResume() { diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/auth/AuthenticationResult.kt b/app/src/main/java/com/zeapo/pwdstore/utils/auth/AuthenticationResult.kt new file mode 100644 index 00000000..d8530ba3 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/auth/AuthenticationResult.kt @@ -0,0 +1,14 @@ +package com.zeapo.pwdstore.utils.auth + +import androidx.biometric.BiometricPrompt + +internal sealed class AuthenticationResult { + internal data class Success(val cryptoObject: BiometricPrompt.CryptoObject?) : + AuthenticationResult() + internal data class RecoverableError(val code: Int, val message: CharSequence) : + AuthenticationResult() + internal data class UnrecoverableError(val code: Int, val message: CharSequence) : + AuthenticationResult() + internal object Failure : AuthenticationResult() + internal object Cancelled : AuthenticationResult() +} diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/auth/Authenticator.kt b/app/src/main/java/com/zeapo/pwdstore/utils/auth/Authenticator.kt new file mode 100644 index 00000000..5139ef01 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/utils/auth/Authenticator.kt @@ -0,0 +1,63 @@ +package com.zeapo.pwdstore.utils.auth + +import android.os.Handler +import android.util.Log +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricPrompt +import androidx.fragment.app.FragmentActivity +import com.zeapo.pwdstore.R + +internal class Authenticator( + private val fragmentActivity: FragmentActivity, + private val callback: (AuthenticationResult) -> Unit +) { + private val handler = Handler() + private val biometricManager = BiometricManager.from(fragmentActivity) + + private val authCallback = object : BiometricPrompt.AuthenticationCallback() { + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + Log.d(TAG,"Error: $errorCode: $errString") + callback(AuthenticationResult.UnrecoverableError(errorCode, errString)) + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Log.d(TAG, "Failed") + callback(AuthenticationResult.Failure) + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + Log.d(TAG, "Success") + callback(AuthenticationResult.Success(result.cryptoObject)) + } + } + + private val biometricPrompt = BiometricPrompt( + fragmentActivity, + { runnable -> handler.post(runnable) }, + authCallback + ) + + private val promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle(fragmentActivity.getString(R.string.biometric_prompt_title)) + .setNegativeButtonText(fragmentActivity.getString(R.string.biometric_prompt_negative_text)) + .build() + + fun authenticate() { + if (biometricManager.canAuthenticate() != BiometricManager.BIOMETRIC_SUCCESS) { + callback(AuthenticationResult.UnrecoverableError( + 0, + fragmentActivity.getString(R.string.biometric_prompt_no_hardware) + )) + } else { + biometricPrompt.authenticate(promptInfo) + } + } + + companion object { + private const val TAG = "Authenticator" + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e39558b1..ebb4b413 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -263,4 +263,12 @@ SD-Card root selected You have selected the root of your sdcard for the store. This is extremely dangerous and you will lose your data as its content will, eventually, be deleted Abort and Push + Biometric Prompt + Cancel + Retry + Authentication canceled + No Biometric hardware was found + Enable biometric authentication + When enabled, Password Store will prompt you for your fingerprint when launching the app + Fingerprint hardware not accessible or missing diff --git a/app/src/main/res/xml/preference.xml b/app/src/main/res/xml/preference.xml index 194053e6..9124d218 100644 --- a/app/src/main/res/xml/preference.xml +++ b/app/src/main/res/xml/preference.xml @@ -88,6 +88,10 @@ android:entries="@array/sort_order_entries" android:entryValues="@array/sort_order_values" android:persistent="true" /> +