summaryrefslogtreecommitdiffhomepage
path: root/ui
diff options
context:
space:
mode:
authorHarsh Shandilya <me@msfjarvis.dev>2020-03-30 10:45:49 +0530
committerJason A. Donenfeld <Jason@zx2c4.com>2020-03-30 03:23:32 -0600
commitd2721f2d7dd5b9660bba1fe59b91bb5f3122cd76 (patch)
treebc9894a8d176969443658a56e29f540efb819654 /ui
parent3095e19e13e282771caf69627bc43310b8e8462c (diff)
BiometricAuthenticator: implement biometric authentication for sensitive operations
When biometric hardware is available, it will be used to authenticate the user before private keys are shown on screen or when zip exports are executed. Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
Diffstat (limited to 'ui')
-rw-r--r--ui/build.gradle1
-rw-r--r--ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt25
-rw-r--r--ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt25
-rw-r--r--ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt73
-rw-r--r--ui/src/main/res/values/strings.xml4
5 files changed, 118 insertions, 10 deletions
diff --git a/ui/build.gradle b/ui/build.gradle
index b5b47c51..009a717f 100644
--- a/ui/build.gradle
+++ b/ui/build.gradle
@@ -68,6 +68,7 @@ dependencies {
implementation "androidx.cardview:cardview:$cardviewVersion"
implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$coordinatorLayoutVersion"
+ implementation "androidx.biometric:biometric:$biometricVersion"
implementation "androidx.core:core-ktx:$coreKtxVersion"
implementation "androidx.databinding:databinding-runtime:$agpVersion"
implementation "androidx.fragment:fragment:$fragmentVersion"
diff --git a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt
index 9ac2473c..1b8af50e 100644
--- a/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt
+++ b/ui/src/main/java/com/wireguard/android/fragment/TunnelEditorFragment.kt
@@ -25,6 +25,7 @@ import com.wireguard.android.backend.Tunnel
import com.wireguard.android.databinding.TunnelEditorFragmentBinding
import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener
import com.wireguard.android.model.ObservableTunnel
+import com.wireguard.android.util.BiometricAuthenticator
import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.viewmodel.ConfigProxy
import com.wireguard.android.widget.EdgeToEdge.setUpRoot
@@ -233,13 +234,27 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
if (!isFocused) return
val edit = view as? EditText ?: return
if (!haveShownKeys && edit.text.isNotEmpty()) {
- if (true /* TODO: do biometric auth prompt */) {
- haveShownKeys = true
- } else {
- /* Unauthorized, so return and don't change visibility. */
- return
+ BiometricAuthenticator.authenticate(R.string.biometric_prompt_private_key_title, requireActivity()) {
+ when (it) {
+ is BiometricAuthenticator.Result.Success, is BiometricAuthenticator.Result.HardwareUnavailableOrDisabled -> {
+ haveShownKeys = true
+ showPrivateKey(edit)
+ }
+ is BiometricAuthenticator.Result.Failure -> {
+ Snackbar.make(
+ binding!!.mainContainer,
+ it.message,
+ Snackbar.LENGTH_SHORT
+ ).show()
+ }
+ }
}
+ } else {
+ showPrivateKey(edit)
}
+ }
+
+ private fun showPrivateKey(edit: EditText) {
requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
edit.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
}
diff --git a/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt b/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt
index d0cb02f0..c83c1cca 100644
--- a/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt
+++ b/ui/src/main/java/com/wireguard/android/preference/ZipExporterPreference.kt
@@ -14,6 +14,7 @@ import com.google.android.material.snackbar.Snackbar
import com.wireguard.android.Application
import com.wireguard.android.R
import com.wireguard.android.model.ObservableTunnel
+import com.wireguard.android.util.BiometricAuthenticator
import com.wireguard.android.util.DownloadsFileSaver
import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.util.FragmentUtils
@@ -81,13 +82,27 @@ class ZipExporterPreference(context: Context, attrs: AttributeSet?) : Preference
override fun getTitle() = context.getString(R.string.zip_export_title)
override fun onClick() {
- FragmentUtils.getPrefActivity(this)
- .ensurePermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults ->
- if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- isEnabled = false
- exportZip()
+ val prefActivity = FragmentUtils.getPrefActivity(this)
+ BiometricAuthenticator.authenticate(R.string.biometric_prompt_zip_exporter_title, prefActivity) {
+ when (it) {
+ // When we have successful authentication, or when there is no biometric hardware available.
+ is BiometricAuthenticator.Result.Success, is BiometricAuthenticator.Result.HardwareUnavailableOrDisabled -> {
+ prefActivity.ensurePermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults ->
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ isEnabled = false
+ exportZip()
+ }
}
}
+ is BiometricAuthenticator.Result.Failure -> {
+ Snackbar.make(
+ prefActivity.findViewById(android.R.id.content),
+ it.message,
+ Snackbar.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
}
companion object {
diff --git a/ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt b/ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt
new file mode 100644
index 00000000..cf81f768
--- /dev/null
+++ b/ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright © 2020 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.wireguard.android.util
+
+import android.os.Handler
+import android.util.Log
+import androidx.annotation.StringRes
+import androidx.biometric.BiometricConstants
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricPrompt
+import androidx.fragment.app.FragmentActivity
+import com.wireguard.android.R
+
+object BiometricAuthenticator {
+ private const val TAG = "WireGuard/BiometricAuthenticator"
+ private val handler = Handler()
+
+ sealed class Result {
+ data class Success(val cryptoObject: BiometricPrompt.CryptoObject?) : Result()
+ data class Failure(val code: Int?, val message: CharSequence) : Result()
+ object HardwareUnavailableOrDisabled : Result()
+ object Cancelled : Result()
+ }
+
+ fun authenticate(
+ @StringRes dialogTitleRes: Int,
+ fragmentActivity: FragmentActivity,
+ callback: (Result) -> Unit
+ ) {
+ val biometricManager = BiometricManager.from(fragmentActivity)
+ val authCallback = object : BiometricPrompt.AuthenticationCallback() {
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
+ super.onAuthenticationError(errorCode, errString)
+ Log.d(TAG, "BiometricAuthentication error: errorCode=$errorCode, msg=$errString")
+ callback(when (errorCode) {
+ BiometricConstants.ERROR_CANCELED, BiometricConstants.ERROR_USER_CANCELED,
+ BiometricConstants.ERROR_NEGATIVE_BUTTON -> {
+ Result.Cancelled
+ }
+ BiometricConstants.ERROR_HW_NOT_PRESENT, BiometricConstants.ERROR_HW_UNAVAILABLE,
+ BiometricConstants.ERROR_NO_BIOMETRICS, BiometricConstants.ERROR_NO_DEVICE_CREDENTIAL -> {
+ Result.HardwareUnavailableOrDisabled
+ }
+ else -> Result.Failure(errorCode, fragmentActivity.getString(R.string.biometric_auth_error_reason, errString))
+ })
+ }
+
+ override fun onAuthenticationFailed() {
+ super.onAuthenticationFailed()
+ callback(Result.Failure(null, fragmentActivity.getString(R.string.biometric_auth_error)))
+ }
+
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
+ super.onAuthenticationSucceeded(result)
+ callback(Result.Success(result.cryptoObject))
+ }
+ }
+ val biometricPrompt = BiometricPrompt(fragmentActivity, { handler.post(it) }, authCallback)
+ val promptInfo = BiometricPrompt.PromptInfo.Builder()
+ .setTitle(fragmentActivity.getString(dialogTitleRes))
+ .setDeviceCredentialAllowed(true)
+ .build()
+
+ if (biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS) {
+ biometricPrompt.authenticate(promptInfo)
+ } else {
+ callback(Result.HardwareUnavailableOrDisabled)
+ }
+ }
+}
diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml
index 8e5c28d1..50bc40b8 100644
--- a/ui/src/main/res/values/strings.xml
+++ b/ui/src/main/res/values/strings.xml
@@ -191,4 +191,8 @@
<string name="zip_export_success">Saved to “%s”</string>
<string name="zip_export_summary">Zip file will be saved to downloads folder</string>
<string name="zip_export_title">Export tunnels to zip file</string>
+ <string name="biometric_prompt_zip_exporter_title">Authenticate to export tunnels</string>
+ <string name="biometric_prompt_private_key_title">Authenticate to view private key</string>
+ <string name="biometric_auth_error">Authentication failure</string>
+ <string name="biometric_auth_error_reason">Authentication failure: %s</string>
</resources>