summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHarsh Shandilya <msfjarvis@gmail.com>2019-10-02 11:00:45 +0530
committerGitHub <noreply@github.com>2019-10-02 11:00:45 +0530
commit9a1a54a6fcfa6fd6cf142f2af9804375255d14cf (patch)
tree5535d9be1196a6a1f98719e2f92e0e26cd92cc5d
parent27592dde1087804442ea37e17c2e13aad5c72b01 (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>
-rw-r--r--app/build.gradle.kts4
-rw-r--r--app/src/main/AndroidManifest.xml10
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt49
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/PasswordStore.java19
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/UserPreference.kt41
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/auth/AuthenticationResult.kt14
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/auth/Authenticator.kt63
-rw-r--r--app/src/main/res/values/strings.xml8
-rw-r--r--app/src/main/res/xml/preference.xml4
9 files changed, 202 insertions, 10 deletions
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<KotlinCompile> {
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" />
<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">