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

View file

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

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) { 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("NAME", item.toString());
intent.putExtra("FILE_PATH", item.getFile().getAbsolutePath()); intent.putExtra("FILE_PATH", item.getFile().getAbsolutePath());
intent.putExtra("REPO_PATH", PasswordRepository.getRepositoryDirectory(getApplicationContext()).getAbsolutePath()); intent.putExtra("REPO_PATH", PasswordRepository.getRepositoryDirectory(getApplicationContext()).getAbsolutePath());
intent.putExtra("LAST_CHANGED_TIMESTAMP", getLastChangedTimestamp(item.getFile().getAbsolutePath())); intent.putExtra("LAST_CHANGED_TIMESTAMP", getLastChangedTimestamp(item.getFile().getAbsolutePath()));
intent.putExtra("OPERATION", "DECRYPT"); 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) {

View file

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

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

View file

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