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>
This commit is contained in:
parent
27592dde10
commit
9a1a54a6fc
9 changed files with 202 additions and 10 deletions
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
|
|
49
app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt
Normal file
49
app/src/main/java/com/zeapo/pwdstore/LaunchActivity.kt
Normal file
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -457,12 +457,15 @@ public class PasswordStore extends AppCompatActivity {
|
|||
}
|
||||
|
||||
public void decryptPassword(PasswordItem item) {
|
||||
Intent intent = new Intent(this, PgpActivity.class);
|
||||
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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in a new issue