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 {
|
dependencies {
|
||||||
implementation("androidx.appcompat:appcompat:1.1.0")
|
implementation("androidx.appcompat:appcompat:1.1.0")
|
||||||
implementation("androidx.cardview:cardview:1.0.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.constraintlayout:constraintlayout:2.0.0-beta2")
|
||||||
implementation("androidx.documentfile:documentfile:1.0.1")
|
implementation("androidx.documentfile:documentfile:1.0.1")
|
||||||
implementation("androidx.preference:preference:1.1.0")
|
implementation("androidx.preference:preference:1.1.0")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.1.0-beta04")
|
implementation("androidx.recyclerview:recyclerview:1.1.0-beta04")
|
||||||
implementation("com.google.android.material:material:1.1.0-alpha10")
|
implementation("com.google.android.material:material:1.1.0-alpha10")
|
||||||
implementation("androidx.annotation:annotation:1.1.0")
|
implementation("androidx.annotation:annotation:1.1.0")
|
||||||
|
implementation("androidx.biometric:biometric:1.0.0-beta02")
|
||||||
implementation("org.sufficientlysecure:openpgp-api:12.0")
|
implementation("org.sufficientlysecure:openpgp-api:12.0")
|
||||||
implementation("org.eclipse.jgit:org.eclipse.jgit:3.7.1.201504261725-r") {
|
implementation("org.eclipse.jgit:org.eclipse.jgit:3.7.1.201504261725-r") {
|
||||||
exclude(group = "org.apache.httpcomponents", module = "httpclient")
|
exclude(group = "org.apache.httpcomponents", module = "httpclient")
|
||||||
|
@ -106,7 +108,7 @@ tasks {
|
||||||
withType<KotlinCompile> {
|
withType<KotlinCompile> {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = "1.8"
|
||||||
freeCompilerArgs += "-Xnew-inference"
|
freeCompilerArgs = freeCompilerArgs + "-Xnew-inference"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,9 @@
|
||||||
android:name="android.permission.BIND_ACCESSIBILITY_SERVICE"
|
android:name="android.permission.BIND_ACCESSIBILITY_SERVICE"
|
||||||
tools:ignore="ProtectedPermissions" />
|
tools:ignore="ProtectedPermissions" />
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
<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
|
<application
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
|
@ -19,10 +22,15 @@
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
tools:ignore="GoogleAppIndexingWarning">
|
tools:ignore="GoogleAppIndexingWarning">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".PasswordStore"
|
android:name=".PasswordStore"
|
||||||
android:configChanges="orientation|screenSize"
|
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>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<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) {
|
public void decryptPassword(PasswordItem item) {
|
||||||
Intent intent = new Intent(this, PgpActivity.class);
|
Intent decryptIntent = new Intent(this, PgpActivity.class);
|
||||||
intent.putExtra("NAME", item.toString());
|
Intent authDecryptIntent = new Intent(this, LaunchActivity.class);
|
||||||
intent.putExtra("FILE_PATH", item.getFile().getAbsolutePath());
|
for (Intent intent : new Intent[] {decryptIntent, authDecryptIntent}) {
|
||||||
intent.putExtra("REPO_PATH", PasswordRepository.getRepositoryDirectory(getApplicationContext()).getAbsolutePath());
|
intent.putExtra("NAME", item.toString());
|
||||||
intent.putExtra("LAST_CHANGED_TIMESTAMP", getLastChangedTimestamp(item.getFile().getAbsolutePath()));
|
intent.putExtra("FILE_PATH", item.getFile().getAbsolutePath());
|
||||||
intent.putExtra("OPERATION", "DECRYPT");
|
intent.putExtra("REPO_PATH", PasswordRepository.getRepositoryDirectory(getApplicationContext()).getAbsolutePath());
|
||||||
|
intent.putExtra("LAST_CHANGED_TIMESTAMP", getLastChangedTimestamp(item.getFile().getAbsolutePath()));
|
||||||
|
intent.putExtra("OPERATION", "DECRYPT");
|
||||||
|
}
|
||||||
|
|
||||||
// Adds shortcut
|
// Adds shortcut
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||||
|
@ -470,7 +473,7 @@ public class PasswordStore extends AppCompatActivity {
|
||||||
.setShortLabel(item.toString())
|
.setShortLabel(item.toString())
|
||||||
.setLongLabel(item.getFullPathToParent() + item.toString())
|
.setLongLabel(item.getFullPathToParent() + item.toString())
|
||||||
.setIcon(Icon.createWithResource(this, R.mipmap.ic_launcher))
|
.setIcon(Icon.createWithResource(this, R.mipmap.ic_launcher))
|
||||||
.setIntent(intent.setAction("DECRYPT_PASS")) // Needs action
|
.setIntent(authDecryptIntent.setAction("DECRYPT_PASS")) // Needs action
|
||||||
.build();
|
.build();
|
||||||
List<ShortcutInfo> shortcuts = shortcutManager.getDynamicShortcuts();
|
List<ShortcutInfo> shortcuts = shortcutManager.getDynamicShortcuts();
|
||||||
|
|
||||||
|
@ -483,7 +486,7 @@ public class PasswordStore extends AppCompatActivity {
|
||||||
shortcutManager.addDynamicShortcuts(Collections.singletonList(shortcut));
|
shortcutManager.addDynamicShortcuts(Collections.singletonList(shortcut));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
startActivityForResult(intent, REQUEST_CODE_DECRYPT_AND_VERIFY);
|
startActivityForResult(decryptIntent, REQUEST_CODE_DECRYPT_AND_VERIFY);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void editPassword(PasswordItem item) {
|
public void editPassword(PasswordItem item) {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.accessibilityservice.AccessibilityServiceInfo
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.ShortcutManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -15,16 +16,21 @@ import android.view.MenuItem
|
||||||
import android.view.accessibility.AccessibilityManager
|
import android.view.accessibility.AccessibilityManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.preference.CheckBoxPreference
|
import androidx.preference.CheckBoxPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.preference.SwitchPreference
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity
|
import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity
|
||||||
import com.zeapo.pwdstore.crypto.PgpActivity
|
import com.zeapo.pwdstore.crypto.PgpActivity
|
||||||
import com.zeapo.pwdstore.git.GitActivity
|
import com.zeapo.pwdstore.git.GitActivity
|
||||||
import com.zeapo.pwdstore.utils.PasswordRepository
|
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.apache.commons.io.FileUtils
|
||||||
import org.openintents.openpgp.util.OpenPgpUtils
|
import org.openintents.openpgp.util.OpenPgpUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -249,6 +255,41 @@ class UserPreference : AppCompatActivity() {
|
||||||
false
|
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() {
|
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_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="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="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>
|
</resources>
|
||||||
|
|
|
@ -88,6 +88,10 @@
|
||||||
android:entries="@array/sort_order_entries"
|
android:entries="@array/sort_order_entries"
|
||||||
android:entryValues="@array/sort_order_values"
|
android:entryValues="@array/sort_order_values"
|
||||||
android:persistent="true" />
|
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>
|
||||||
|
|
||||||
<androidx.preference.PreferenceCategory android:title="@string/pref_autofill_title">
|
<androidx.preference.PreferenceCategory android:title="@string/pref_autofill_title">
|
||||||
|
|
Loading…
Reference in a new issue