diff options
author | Harsh Shandilya <me@msfjarvis.dev> | 2020-03-30 10:45:49 +0530 |
---|---|---|
committer | Jason A. Donenfeld <Jason@zx2c4.com> | 2020-03-30 03:23:32 -0600 |
commit | d2721f2d7dd5b9660bba1fe59b91bb5f3122cd76 (patch) | |
tree | bc9894a8d176969443658a56e29f540efb819654 /ui/src/main | |
parent | 3095e19e13e282771caf69627bc43310b8e8462c (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/src/main')
4 files changed, 117 insertions, 10 deletions
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> |