|
|
|
@ -5,19 +5,248 @@
|
|
|
|
|
package app.tauri.biometric
|
|
|
|
|
|
|
|
|
|
import android.app.Activity
|
|
|
|
|
import android.content.Intent
|
|
|
|
|
import android.content.pm.PackageManager
|
|
|
|
|
import android.os.Build
|
|
|
|
|
import android.webkit.WebView
|
|
|
|
|
import androidx.activity.result.ActivityResult
|
|
|
|
|
import androidx.biometric.BiometricManager
|
|
|
|
|
import androidx.biometric.BiometricPrompt
|
|
|
|
|
import app.tauri.annotation.ActivityCallback
|
|
|
|
|
import app.tauri.annotation.Command
|
|
|
|
|
import app.tauri.annotation.TauriPlugin
|
|
|
|
|
import app.tauri.plugin.Invoke
|
|
|
|
|
import app.tauri.plugin.JSArray
|
|
|
|
|
import app.tauri.plugin.JSObject
|
|
|
|
|
import app.tauri.plugin.Plugin
|
|
|
|
|
import app.tauri.plugin.Invoke
|
|
|
|
|
import java.util.EnumMap
|
|
|
|
|
import java.util.HashMap
|
|
|
|
|
import kotlin.math.max
|
|
|
|
|
|
|
|
|
|
enum class BiometryResultType {
|
|
|
|
|
SUCCESS, FAILURE, ERROR
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private const val MAX_ATTEMPTS = "androidMaxAttempts"
|
|
|
|
|
private const val DEFAULT_MAX_ATTEMPTS = 3
|
|
|
|
|
private const val BIOMETRIC_FAILURE = "authenticationFailed"
|
|
|
|
|
private const val INVALID_CONTEXT_ERROR = "invalidContext"
|
|
|
|
|
|
|
|
|
|
@TauriPlugin
|
|
|
|
|
class BiometricPlugin(private val activity: Activity): Plugin(activity) {
|
|
|
|
|
private var biometryTypes: ArrayList<BiometryType> = arrayListOf()
|
|
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
|
var RESULT_EXTRA_PREFIX = ""
|
|
|
|
|
const val TITLE = "title"
|
|
|
|
|
const val SUBTITLE = "subtitle"
|
|
|
|
|
const val REASON = "reason"
|
|
|
|
|
const val CANCEL_TITLE = "cancelTitle"
|
|
|
|
|
const val RESULT_TYPE = "type"
|
|
|
|
|
const val RESULT_ERROR_CODE = "errorCode"
|
|
|
|
|
const val RESULT_ERROR_MESSAGE = "errorMessage"
|
|
|
|
|
const val DEVICE_CREDENTIAL = "allowDeviceCredential"
|
|
|
|
|
const val CONFIRMATION_REQUIRED = "confirmationRequired"
|
|
|
|
|
|
|
|
|
|
// Maps biometry error numbers to string error codes
|
|
|
|
|
private var biometryErrorCodeMap: MutableMap<Int, String> = HashMap()
|
|
|
|
|
private var biometryNameMap: MutableMap<BiometryType, String> = EnumMap(BiometryType::class.java)
|
|
|
|
|
|
|
|
|
|
init {
|
|
|
|
|
biometryErrorCodeMap[BiometricManager.BIOMETRIC_SUCCESS] = ""
|
|
|
|
|
biometryErrorCodeMap[BiometricManager.BIOMETRIC_SUCCESS] = ""
|
|
|
|
|
biometryErrorCodeMap[BiometricPrompt.ERROR_CANCELED] = "systemCancel"
|
|
|
|
|
biometryErrorCodeMap[BiometricPrompt.ERROR_HW_NOT_PRESENT] = "biometryNotAvailable"
|
|
|
|
|
biometryErrorCodeMap[BiometricPrompt.ERROR_HW_UNAVAILABLE] = "biometryNotAvailable"
|
|
|
|
|
biometryErrorCodeMap[BiometricPrompt.ERROR_LOCKOUT] = "biometryLockout"
|
|
|
|
|
biometryErrorCodeMap[BiometricPrompt.ERROR_LOCKOUT_PERMANENT] = "biometryLockout"
|
|
|
|
|
biometryErrorCodeMap[BiometricPrompt.ERROR_NEGATIVE_BUTTON] = "userCancel"
|
|
|
|
|
biometryErrorCodeMap.put(
|
|
|
|
|
BiometricPrompt.ERROR_NO_BIOMETRICS,
|
|
|
|
|
"biometryNotEnrolled"
|
|
|
|
|
)
|
|
|
|
|
biometryErrorCodeMap[BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL] = "noDeviceCredential"
|
|
|
|
|
biometryErrorCodeMap[BiometricPrompt.ERROR_NO_SPACE] = "systemCancel"
|
|
|
|
|
biometryErrorCodeMap[BiometricPrompt.ERROR_TIMEOUT] = "systemCancel"
|
|
|
|
|
biometryErrorCodeMap[BiometricPrompt.ERROR_UNABLE_TO_PROCESS] = "systemCancel"
|
|
|
|
|
biometryErrorCodeMap[BiometricPrompt.ERROR_USER_CANCELED] = "userCancel"
|
|
|
|
|
biometryErrorCodeMap[BiometricPrompt.ERROR_VENDOR] = "systemCancel"
|
|
|
|
|
|
|
|
|
|
biometryNameMap[BiometryType.NONE] = "No Authentication"
|
|
|
|
|
biometryNameMap[BiometryType.FINGERPRINT] = "Fingerprint Authentication"
|
|
|
|
|
biometryNameMap[BiometryType.FACE] = "Face Authentication"
|
|
|
|
|
biometryNameMap[BiometryType.IRIS] = "Iris Authentication"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun load(webView: WebView) {
|
|
|
|
|
super.load(webView)
|
|
|
|
|
|
|
|
|
|
biometryTypes = ArrayList()
|
|
|
|
|
val manager = activity.packageManager
|
|
|
|
|
if (manager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
|
|
|
|
|
biometryTypes.add(BiometryType.FINGERPRINT)
|
|
|
|
|
}
|
|
|
|
|
if (manager.hasSystemFeature(PackageManager.FEATURE_FACE)) {
|
|
|
|
|
biometryTypes.add(BiometryType.FACE)
|
|
|
|
|
}
|
|
|
|
|
if (manager.hasSystemFeature(PackageManager.FEATURE_IRIS)) {
|
|
|
|
|
biometryTypes.add(BiometryType.IRIS)
|
|
|
|
|
}
|
|
|
|
|
if (biometryTypes.size == 0) {
|
|
|
|
|
biometryTypes.add(BiometryType.NONE)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check the device's availability and type of biometric authentication.
|
|
|
|
|
*/
|
|
|
|
|
@Command
|
|
|
|
|
fun ping(invoke: Invoke) {
|
|
|
|
|
val value = invoke.getString("value") ?: ""
|
|
|
|
|
fun getStatus(invoke: Invoke) {
|
|
|
|
|
val manager = BiometricManager.from(activity)
|
|
|
|
|
val biometryResult = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
|
|
|
manager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
|
|
|
|
|
} else {
|
|
|
|
|
manager.canAuthenticate()
|
|
|
|
|
}
|
|
|
|
|
val ret = JSObject()
|
|
|
|
|
ret.put("value", value)
|
|
|
|
|
|
|
|
|
|
val available = biometryResult == BiometricManager.BIOMETRIC_SUCCESS
|
|
|
|
|
ret.put(
|
|
|
|
|
"isAvailable",
|
|
|
|
|
available
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
ret.put("biometryType", biometryTypes[0].type)
|
|
|
|
|
|
|
|
|
|
if (!available) {
|
|
|
|
|
var reason = ""
|
|
|
|
|
when (biometryResult) {
|
|
|
|
|
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> reason =
|
|
|
|
|
"Biometry unavailable."
|
|
|
|
|
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> reason =
|
|
|
|
|
"Biometrics not enrolled."
|
|
|
|
|
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> reason =
|
|
|
|
|
"No biometric on this device."
|
|
|
|
|
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> reason =
|
|
|
|
|
"A security update is required."
|
|
|
|
|
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> reason =
|
|
|
|
|
"Unsupported biometry."
|
|
|
|
|
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> reason =
|
|
|
|
|
"Unknown biometry state."
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var errorCode = biometryErrorCodeMap[biometryResult]
|
|
|
|
|
if (errorCode == null) {
|
|
|
|
|
errorCode = "biometryNotAvailable"
|
|
|
|
|
}
|
|
|
|
|
ret.put("error", reason)
|
|
|
|
|
ret.put("errorCode", errorCode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
invoke.resolve(ret)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Prompt the user for biometric authentication.
|
|
|
|
|
*/
|
|
|
|
|
@Command
|
|
|
|
|
fun authenticate(invoke: Invoke) {
|
|
|
|
|
// The result of an intent is supposed to have the package name as a prefix
|
|
|
|
|
RESULT_EXTRA_PREFIX = activity.packageName + "."
|
|
|
|
|
val intent = Intent(
|
|
|
|
|
activity,
|
|
|
|
|
BiometricActivity::class.java
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Pass the options to the activity
|
|
|
|
|
intent.putExtra(
|
|
|
|
|
TITLE,
|
|
|
|
|
invoke.getString(TITLE, biometryNameMap[biometryTypes[0]] ?: "")
|
|
|
|
|
)
|
|
|
|
|
intent.putExtra(SUBTITLE, invoke.getString(SUBTITLE))
|
|
|
|
|
intent.putExtra(REASON, invoke.getString(REASON))
|
|
|
|
|
intent.putExtra(CANCEL_TITLE, invoke.getString(CANCEL_TITLE))
|
|
|
|
|
intent.putExtra(
|
|
|
|
|
DEVICE_CREDENTIAL,
|
|
|
|
|
invoke.getBoolean(DEVICE_CREDENTIAL, false)
|
|
|
|
|
)
|
|
|
|
|
if (invoke.hasOption(CONFIRMATION_REQUIRED)) {
|
|
|
|
|
intent.putExtra(
|
|
|
|
|
CONFIRMATION_REQUIRED,
|
|
|
|
|
invoke.getBoolean(CONFIRMATION_REQUIRED, true)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val maxAttemptsConfig = invoke.getInt(MAX_ATTEMPTS, DEFAULT_MAX_ATTEMPTS)
|
|
|
|
|
val maxAttempts = max(
|
|
|
|
|
maxAttemptsConfig,
|
|
|
|
|
1
|
|
|
|
|
)
|
|
|
|
|
intent.putExtra(MAX_ATTEMPTS, maxAttempts)
|
|
|
|
|
startActivityForResult(invoke, intent, "authenticateResult")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ActivityCallback
|
|
|
|
|
private fun authenticateResult(invoke: Invoke, result: ActivityResult) {
|
|
|
|
|
val resultCode = result.resultCode
|
|
|
|
|
|
|
|
|
|
// If the system canceled the activity, we might get RESULT_CANCELED in resultCode.
|
|
|
|
|
// In that case return that immediately, because there won't be any data.
|
|
|
|
|
if (resultCode == Activity.RESULT_CANCELED) {
|
|
|
|
|
invoke.reject(
|
|
|
|
|
"The system canceled authentication",
|
|
|
|
|
biometryErrorCodeMap[BiometricPrompt.ERROR_CANCELED]
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Convert the string result type to an enum
|
|
|
|
|
val data = result.data
|
|
|
|
|
val resultTypeName = data?.getStringExtra(
|
|
|
|
|
RESULT_EXTRA_PREFIX + RESULT_TYPE
|
|
|
|
|
)
|
|
|
|
|
if (resultTypeName == null) {
|
|
|
|
|
invoke.reject(
|
|
|
|
|
"Missing data in the result of the activity",
|
|
|
|
|
INVALID_CONTEXT_ERROR
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
val resultType = try {
|
|
|
|
|
BiometryResultType.valueOf(resultTypeName)
|
|
|
|
|
} catch (e: IllegalArgumentException) {
|
|
|
|
|
invoke.reject(
|
|
|
|
|
"Invalid data in the result of the activity",
|
|
|
|
|
INVALID_CONTEXT_ERROR
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
val errorCode = data.getIntExtra(
|
|
|
|
|
RESULT_EXTRA_PREFIX + RESULT_ERROR_CODE,
|
|
|
|
|
0
|
|
|
|
|
)
|
|
|
|
|
var errorMessage = data.getStringExtra(
|
|
|
|
|
RESULT_EXTRA_PREFIX + RESULT_ERROR_MESSAGE
|
|
|
|
|
)
|
|
|
|
|
when (resultType) {
|
|
|
|
|
BiometryResultType.SUCCESS -> invoke.resolve()
|
|
|
|
|
BiometryResultType.FAILURE -> // Biometry was successfully presented but was not recognized
|
|
|
|
|
invoke.reject(errorMessage, BIOMETRIC_FAILURE)
|
|
|
|
|
|
|
|
|
|
BiometryResultType.ERROR -> {
|
|
|
|
|
// The user cancelled, the system cancelled, or some error occurred.
|
|
|
|
|
// If the user cancelled, errorMessage is the text of the "negative" button,
|
|
|
|
|
// which is not especially descriptive.
|
|
|
|
|
if (errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON) {
|
|
|
|
|
errorMessage = "Cancel button was pressed"
|
|
|
|
|
}
|
|
|
|
|
invoke.reject(errorMessage, biometryErrorCodeMap[errorCode])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal enum class BiometryType(val type: Int) {
|
|
|
|
|
NONE(0), FINGERPRINT(1), FACE(2), IRIS(3);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|