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 { buildscript {
ext { ext {
agpVersion = '3.6.1'
annotationsVersion = '1.1.0' annotationsVersion = '1.1.0'
appcompatVersion = '1.1.0' appcompatVersion = '1.1.0'
bintrayPluginVersion = '1.8.4'
biometricVersion = '1.0.1'
cardviewVersion = '1.0.0' cardviewVersion = '1.0.0'
collectionVersion = '1.1.0' collectionVersion = '1.1.0'
coreKtxVersion = '1.2.0'
coroutinesVersion = '1.3.5'
constraintLayoutVersion = '1.1.3' constraintLayoutVersion = '1.1.3'
coordinatorLayoutVersion = '1.1.0' coordinatorLayoutVersion = '1.1.0'
agpVersion = '3.6.1' coreKtxVersion = '1.2.0'
coroutinesVersion = '1.3.5'
eddsaVersion = '0.3.0'
fragmentVersion = '1.2.3' fragmentVersion = '1.2.3'
materialComponentsVersion = '1.1.0'
jsr305Version = '3.0.2' jsr305Version = '3.0.2'
junitVersion = '4.13'
kotlinVersion = '1.3.71' kotlinVersion = '1.3.71'
materialComponentsVersion = '1.1.0'
mavenPluginVersion = '2.1'
preferenceVersion = '1.1.0' preferenceVersion = '1.1.0'
streamsupportVersion = '1.7.2' streamsupportVersion = '1.7.2'
threetenabpVersion = '1.2.3' threetenabpVersion = '1.2.3'
zxingEmbeddedVersion = '3.6.0' zxingEmbeddedVersion = '3.6.0'
eddsaVersion = '0.3.0'
bintrayPluginVersion = '1.8.4'
mavenPluginVersion = '2.1'
junitVersion = '4.13'
groupName = 'com.wireguard.android' groupName = 'com.wireguard.android'
} }

View File

@ -68,6 +68,7 @@ dependencies {
implementation "androidx.cardview:cardview:$cardviewVersion" implementation "androidx.cardview:cardview:$cardviewVersion"
implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion" implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$coordinatorLayoutVersion" implementation "androidx.coordinatorlayout:coordinatorlayout:$coordinatorLayoutVersion"
implementation "androidx.biometric:biometric:$biometricVersion"
implementation "androidx.core:core-ktx:$coreKtxVersion" implementation "androidx.core:core-ktx:$coreKtxVersion"
implementation "androidx.databinding:databinding-runtime:$agpVersion" implementation "androidx.databinding:databinding-runtime:$agpVersion"
implementation "androidx.fragment:fragment:$fragmentVersion" 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.databinding.TunnelEditorFragmentBinding
import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener
import com.wireguard.android.model.ObservableTunnel import com.wireguard.android.model.ObservableTunnel
import com.wireguard.android.util.BiometricAuthenticator
import com.wireguard.android.util.ErrorMessages import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.viewmodel.ConfigProxy import com.wireguard.android.viewmodel.ConfigProxy
import com.wireguard.android.widget.EdgeToEdge.setUpRoot import com.wireguard.android.widget.EdgeToEdge.setUpRoot
@ -233,13 +234,27 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
if (!isFocused) return if (!isFocused) return
val edit = view as? EditText ?: return val edit = view as? EditText ?: return
if (!haveShownKeys && edit.text.isNotEmpty()) { if (!haveShownKeys && edit.text.isNotEmpty()) {
if (true /* TODO: do biometric auth prompt */) { BiometricAuthenticator.authenticate(R.string.biometric_prompt_private_key_title, requireActivity()) {
haveShownKeys = true when (it) {
} else { is BiometricAuthenticator.Result.Success, is BiometricAuthenticator.Result.HardwareUnavailableOrDisabled -> {
/* Unauthorized, so return and don't change visibility. */ haveShownKeys = true
return 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) requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
edit.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD 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.Application
import com.wireguard.android.R import com.wireguard.android.R
import com.wireguard.android.model.ObservableTunnel import com.wireguard.android.model.ObservableTunnel
import com.wireguard.android.util.BiometricAuthenticator
import com.wireguard.android.util.DownloadsFileSaver import com.wireguard.android.util.DownloadsFileSaver
import com.wireguard.android.util.ErrorMessages import com.wireguard.android.util.ErrorMessages
import com.wireguard.android.util.FragmentUtils 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 getTitle() = context.getString(R.string.zip_export_title)
override fun onClick() { override fun onClick() {
FragmentUtils.getPrefActivity(this) val prefActivity = FragmentUtils.getPrefActivity(this)
.ensurePermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults -> BiometricAuthenticator.authenticate(R.string.biometric_prompt_zip_exporter_title, prefActivity) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { when (it) {
isEnabled = false // When we have successful authentication, or when there is no biometric hardware available.
exportZip() 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 { 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_success">Saved to “%s”</string>
<string name="zip_export_summary">Zip file will be saved to downloads folder</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="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> </resources>