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:
parent
3095e19e13
commit
d2721f2d7d
17
build.gradle
17
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'
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user