diff options
author | Harsh Shandilya <msfjarvis@gmail.com> | 2019-10-02 11:00:45 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-10-02 11:00:45 +0530 |
commit | 9a1a54a6fcfa6fd6cf142f2af9804375255d14cf (patch) | |
tree | 5535d9be1196a6a1f98719e2f92e0e26cd92cc5d /app/src/main/java | |
parent | 27592dde1087804442ea37e17c2e13aad5c72b01 (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>
Diffstat (limited to 'app/src/main/java')
5 files changed, 178 insertions, 8 deletions
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" + } +} |