diff options
author | Harsh Shandilya <msfjarvis@gmail.com> | 2019-10-02 11:00:45 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-10-02 11:00:45 +0530 |
commit | 9a1a54a6fcfa6fd6cf142f2af9804375255d14cf (patch) | |
tree | 5535d9be1196a6a1f98719e2f92e0e26cd92cc5d /app/src/main | |
parent | 27592dde1087804442ea37e17c2e13aad5c72b01 (diff) |
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 <msfjarvis@gmail.com>
Diffstat (limited to 'app/src/main')
-rw-r--r-- | app/src/main/AndroidManifest.xml | 10 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt | 49 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/PasswordStore.java | 19 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/UserPreference.kt | 41 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/utils/auth/AuthenticationResult.kt | 14 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/utils/auth/Authenticator.kt | 63 | ||||
-rw-r--r-- | app/src/main/res/values/strings.xml | 8 | ||||
-rw-r--r-- | app/src/main/res/xml/preference.xml | 4 |
8 files changed, 199 insertions, 9 deletions
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" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> + <uses-permission android:name="android.permission.USE_BIOMETRIC" /> + <!--suppress DeprecatedClassUsageInspection --> + <uses-permission android:name="android.permission.USE_FINGERPRINT" /> <application android:allowBackup="false" @@ -19,10 +22,15 @@ android:supportsRtl="true" android:theme="@style/AppTheme" tools:ignore="GoogleAppIndexingWarning"> + <activity android:name=".PasswordStore" android:configChanges="orientation|screenSize" - android:label="@string/app_name"> + android:label="@string/app_name" /> + + <activity android:name=".LaunchActivity" + android:label="@string/app_name" + android:configChanges="orientation|screenSize"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> 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<ShortcutInfo> 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<SwitchPreference>("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<ShortcutManager>()?.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 @@ <string name="sdcard_root_warning_title">SD-Card root selected</string> <string name="sdcard_root_warning_message">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</string> <string name="git_abort_and_push_title">Abort and Push</string> + <string name="biometric_prompt_title">Biometric Prompt</string> + <string name="biometric_prompt_negative_text">Cancel</string> + <string name="biometric_prompt_retry">Retry</string> + <string name="biometric_prompt_cancelled">Authentication canceled</string> + <string name="biometric_prompt_no_hardware">No Biometric hardware was found</string> + <string name="biometric_auth_title">Enable biometric authentication</string> + <string name="biometric_auth_summary">When enabled, Password Store will prompt you for your fingerprint when launching the app</string> + <string name="biometric_auth_summary_error">Fingerprint hardware not accessible or missing</string> </resources> 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" /> + <androidx.preference.SwitchPreference + android:key="biometric_auth" + android:title="@string/biometric_auth_title" + android:summary="@string/biometric_auth_summary" /> </androidx.preference.PreferenceCategory> <androidx.preference.PreferenceCategory android:title="@string/pref_autofill_title"> |