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>
This commit is contained in:
Harsh Shandilya 2020-03-30 10:45:49 +05:30 committed by Jason A. Donenfeld
parent 3095e19e13
commit d2721f2d7d
6 changed files with 127 additions and 18 deletions

View File

@ -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'
}

View File

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

View File

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

View File

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

View File

@ -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)
}
}
}

View File

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