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:
Harsh Shandilya 2019-10-02 11:00:45 +05:30 committed by GitHub
parent 27592dde10
commit 9a1a54a6fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 202 additions and 10 deletions

View file

@ -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"
}
}
}

View file

@ -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" />

View 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()
}
}

View file

@ -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) {

View file

@ -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() {

View file

@ -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()
}

View file

@ -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"
}
}

View file

@ -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>

View file

@ -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">