From d2721f2d7dd5b9660bba1fe59b91bb5f3122cd76 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Mon, 30 Mar 2020 10:45:49 +0530 Subject: 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 --- build.gradle | 17 ++--- ui/build.gradle | 1 + .../android/fragment/TunnelEditorFragment.kt | 25 ++++++-- .../android/preference/ZipExporterPreference.kt | 25 ++++++-- .../android/util/BiometricAuthenticator.kt | 73 ++++++++++++++++++++++ ui/src/main/res/values/strings.xml | 4 ++ 6 files changed, 127 insertions(+), 18 deletions(-) create mode 100644 ui/src/main/java/com/wireguard/android/util/BiometricAuthenticator.kt diff --git a/build.gradle b/build.gradle index 7710893f..e1bbdf79 100644 --- a/build.gradle +++ b/build.gradle @@ -7,27 +7,28 @@ allprojects { buildscript { ext { + agpVersion = '3.6.1' annotationsVersion = '1.1.0' appcompatVersion = '1.1.0' + bintrayPluginVersion = '1.8.4' + biometricVersion = '1.0.1' cardviewVersion = '1.0.0' collectionVersion = '1.1.0' - coreKtxVersion = '1.2.0' - coroutinesVersion = '1.3.5' constraintLayoutVersion = '1.1.3' coordinatorLayoutVersion = '1.1.0' - agpVersion = '3.6.1' + coreKtxVersion = '1.2.0' + coroutinesVersion = '1.3.5' + eddsaVersion = '0.3.0' fragmentVersion = '1.2.3' - materialComponentsVersion = '1.1.0' jsr305Version = '3.0.2' + junitVersion = '4.13' kotlinVersion = '1.3.71' + materialComponentsVersion = '1.1.0' + mavenPluginVersion = '2.1' preferenceVersion = '1.1.0' streamsupportVersion = '1.7.2' threetenabpVersion = '1.2.3' zxingEmbeddedVersion = '3.6.0' - eddsaVersion = '0.3.0' - bintrayPluginVersion = '1.8.4' - mavenPluginVersion = '2.1' - junitVersion = '4.13' groupName = 'com.wireguard.android' } 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 @@ Saved to “%s” Zip file will be saved to downloads folder Export tunnels to zip file + Authenticate to export tunnels + Authenticate to view private key + Authentication failure + Authentication failure: %s -- cgit v1.2.3-59-g8ed1b