pull/2454/merge
Charles Schaefer 4 days ago committed by GitHub
commit 2f3c53e3c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,6 +1,6 @@
![biometric](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/biometric/banner.png)
Prompt the user for biometric authentication on Android and iOS.
Prompt the user for biometric authentication on Android and iOS. Also allows to use assymetric cryptography with the keys protected by biometric authentication.
| Platform | Supported |
| -------- | --------- |
@ -8,7 +8,7 @@ Prompt the user for biometric authentication on Android and iOS.
| Windows | x |
| macOS | x |
| Android | ✓ |
| iOS | ✓ |
| iOS | ✓ (except biometric cryptography ) |
## Install
@ -70,8 +70,27 @@ fn main() {
Afterwards all the plugin's APIs are available through the JavaScript guest bindings:
```javascript
import { authenticate } from '@tauri-apps/plugin-biometric'
await authenticate('Open your wallet')
import { checkStatus, authenticate, biometricCipher } from '@tauri-apps/plugin-biometric'
const status = await checkStatus();
if (status.isAvailble) {
await authenticate('Open your wallet')
}
// ... or using biometric-protected cryptography:
if (status.isAvailable) {
const encryptOptions = {
dataToEncrypt: getData(),
};
const encrypted = await biometricCipher('Login without typing password', encryptOptions);
const decryptOptions = {
dataToDecrypt: encrypted.data,
};
const originalData = await biometricCipher('Login without typing password', decryptOptions);
}
```
## Contributing

@ -16,29 +16,120 @@ import android.os.Handler
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricPrompt
import java.util.concurrent.Executor
import android.util.Base64
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
//import javax.crypto.spec.IvParameterSpec
import java.nio.charset.Charset
import app.tauri.biometric.CipherType
class BiometricActivity : AppCompatActivity() {
private var ENCRYPTION_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
private val ENCRYPTION_BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM
private val ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_NONE
private val KEYSTORE_NAME = "AndroidKeyStore"
private val KEY_ALIAS = "tauri-plugin-biometric-key"
@SuppressLint("WrongConstant")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.auth_activity)
val executor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
this.mainExecutor
} else {
Executor { command: Runnable? ->
Handler(this.mainLooper).post(
command!!
val promptInfo = createPromptInfo()
val prompt = createBiometricPrompt()
cipherOperation = intent.hasExtra(BiometricPlugin.ENCRYPT_DECRYPT_OPERATION)
if (!cipherOperation) {
prompt.authenticate(promptInfo)
return
}
intent.getStringExtra(BiometricPlugin.ENCRYPT_DECRYPT_DATA)?.let {
encryptDecryptData = it
}
try {
val type = CipherType.values()[intent.getIntExtra(BiometricPlugin.CIPHER_OPERATION_TYPE, CipherType.ENCRYPT.ordinal)]
cipherType = type
} catch (e: ArrayIndexOutOfBoundsException) {
finishActivity(
BiometryResultType.ERROR,
0,
"Couldn't identify the cipher operation type (encrypt/decrypt)!"
)
return
}
val cipher = getCipher()
prompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
}
@JvmOverloads
fun finishActivity(
resultType: BiometryResultType = BiometryResultType.SUCCESS,
errorCode: Int = 0,
errorMessage: String? = ""
) {
val intent = Intent()
val prefix = BiometricPlugin.RESULT_EXTRA_PREFIX
intent
.putExtra(prefix + BiometricPlugin.RESULT_TYPE, resultType.toString())
.putExtra(prefix + BiometricPlugin.RESULT_ERROR_CODE, errorCode)
.putExtra(
prefix + BiometricPlugin.RESULT_ERROR_MESSAGE,
errorMessage
)
.putExtra(
prefix + BiometricPlugin.ENCRYPT_DECRYPT_OPERATION,
cipherOperation
)
if (resultType == BiometryResultType.SUCCESS && cipherOperation) {
val cryptoObject = requireNotNull(authenticationResult?.cryptoObject)
val cipher = requireNotNull(cryptoObject.cipher)
var dataToProcess = if (cipherType == CipherType.ENCRYPT) {
encryptDecryptData.toByteArray()
} else {
val (_, encryptedData) = decodeEncryptedData(encryptDecryptData)
encryptedData
}
var processedData: ByteArray = cipher.doFinal(dataToProcess)
if (cipherType == CipherType.ENCRYPT) {
// Converts the encrypted data to Base64 string
val encodedString = encodeEncryptedData(cipher, processedData)
intent.putExtra(
prefix + BiometricPlugin.RESULT_ENCRYPT_DECRYPT_DATA,
encodedString
)
} else {
// For decryption, return the decrypted string
intent.putExtra(
prefix + BiometricPlugin.RESULT_ENCRYPT_DECRYPT_DATA,
String(processedData, Charset.forName("UTF-8"))
)
}
}
setResult(Activity.RESULT_OK, intent)
finish()
}
private fun createPromptInfo(): BiometricPrompt.PromptInfo {
val builder = BiometricPrompt.PromptInfo.Builder()
val intent = intent
var title = intent.getStringExtra(BiometricPlugin.TITLE)
val subtitle = intent.getStringExtra(BiometricPlugin.SUBTITLE)
val description = intent.getStringExtra(BiometricPlugin.REASON)
allowDeviceCredential = false
cipherOperation = intent.hasExtra(BiometricPlugin.ENCRYPT_DECRYPT_OPERATION)
// Android docs say we should check if the device is secure before enabling device credential fallback
val manager = getSystemService(
Context.KEYGUARD_SERVICE
@ -54,7 +145,11 @@ class BiometricActivity : AppCompatActivity() {
builder.setTitle(title).setSubtitle(subtitle).setDescription(description)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
var authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK
var authenticators = if (cipherOperation) {
BiometricManager.Authenticators.BIOMETRIC_STRONG
} else {
BiometricManager.Authenticators.BIOMETRIC_WEAK
}
if (allowDeviceCredential) {
authenticators = authenticators or BiometricManager.Authenticators.DEVICE_CREDENTIAL
}
@ -76,54 +171,108 @@ class BiometricActivity : AppCompatActivity() {
builder.setConfirmationRequired(
intent.getBooleanExtra(BiometricPlugin.CONFIRMATION_REQUIRED, true)
)
val promptInfo = builder.build()
val prompt = BiometricPrompt(
this,
executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(
errorCode: Int,
errorMessage: CharSequence
) {
super.onAuthenticationError(errorCode, errorMessage)
finishActivity(
BiometryResultType.ERROR,
errorCode,
errorMessage as String
)
}
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
super.onAuthenticationSucceeded(result)
finishActivity()
}
return builder.build()
}
private fun createBiometricPrompt(): BiometricPrompt {
val executor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
this.mainExecutor
} else {
Executor { command: Runnable? ->
Handler(this.mainLooper).post(
command!!
)
}
)
prompt.authenticate(promptInfo)
}
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(
errorCode: Int,
errorMessage: CharSequence
) {
super.onAuthenticationError(errorCode, errorMessage)
finishActivity(
BiometryResultType.ERROR,
errorCode,
errorMessage as String
)
}
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
super.onAuthenticationSucceeded(result)
authenticationResult = result
finishActivity()
}
}
return BiometricPrompt(this, executor, callback)
}
@JvmOverloads
fun finishActivity(
resultType: BiometryResultType = BiometryResultType.SUCCESS,
errorCode: Int = 0,
errorMessage: String? = ""
) {
val intent = Intent()
val prefix = BiometricPlugin.RESULT_EXTRA_PREFIX
intent
.putExtra(prefix + BiometricPlugin.RESULT_TYPE, resultType.toString())
.putExtra(prefix + BiometricPlugin.RESULT_ERROR_CODE, errorCode)
.putExtra(
prefix + BiometricPlugin.RESULT_ERROR_MESSAGE,
errorMessage
/**
* Get the key if exists, or create a new one if not
*/
private fun getEncryptionKey(): SecretKey {
val keyStore = KeyStore.getInstance(KEYSTORE_NAME)
keyStore.load(null)
keyStore.getKey(KEY_ALIAS, null)?.let { return it as SecretKey }
// from here ahead, we will move only if there is not a key with the provided alias
val keyGenerator = KeyGenerator.getInstance(ENCRYPTION_ALGORITHM, KEYSTORE_NAME)
val builder = KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
setResult(Activity.RESULT_OK, intent)
finish()
.setBlockModes(ENCRYPTION_BLOCK_MODE)
.setEncryptionPaddings(ENCRYPTION_PADDING)
.setKeySize(256)
.setRandomizedEncryptionRequired(true)
.setUserAuthenticationRequired(true) // Forces to use the biometric authentication to create/retrieve the key
keyGenerator.init(builder.build())
return keyGenerator.generateKey()
}
private fun getCipher(): Cipher {
val biometricKey = getEncryptionKey()
val cipher = Cipher.getInstance("$ENCRYPTION_ALGORITHM/$ENCRYPTION_BLOCK_MODE/$ENCRYPTION_PADDING")
if (cipherType == CipherType.ENCRYPT) {
cipher.init(Cipher.ENCRYPT_MODE, biometricKey)
} else {
// Decodes the Base64 string to get the encrypted data and IV
val (iv, _) = decodeEncryptedData(encryptDecryptData)
cipher.init(Cipher.DECRYPT_MODE, biometricKey, GCMParameterSpec(128, iv))
}
return cipher
}
private fun encodeEncryptedData(cipher: Cipher, rawEncryptedData: ByteArray): String {
val encodedData = Base64.encodeToString(rawEncryptedData, Base64.NO_WRAP)
val encodedIv = Base64.encodeToString(cipher.iv, Base64.NO_WRAP)
val encodedString = encodedData + ";" + encodedIv
return encodedString
}
private fun decodeEncryptedData(encryptedDataEncoded: String): List<ByteArray> {
val (data, iv) = encryptedDataEncoded.split(";")
val decodedData = Base64.decode(data, Base64.NO_WRAP)
val decodedIv = Base64.decode(iv, Base64.NO_WRAP)
val ret = listOf(decodedIv, decodedData)
return ret
}
companion object {
var allowDeviceCredential = false
var cipherOperation = false
var encryptDecryptData: String = ""
var authenticationResult: BiometricPrompt.AuthenticationResult? = null
var cipherType: CipherType = CipherType.ENCRYPT
}
}

@ -28,6 +28,10 @@ enum class BiometryResultType {
SUCCESS, FAILURE, ERROR
}
enum class CipherType {
ENCRYPT, DECRYPT
}
private const val MAX_ATTEMPTS = "maxAttemps"
private const val BIOMETRIC_FAILURE = "authenticationFailed"
private const val INVALID_CONTEXT_ERROR = "invalidContext"
@ -41,6 +45,8 @@ class AuthOptions {
var cancelTitle: String? = null
var confirmationRequired: Boolean? = null
var maxAttemps: Int = 3
var dataToEncrypt: String? = null
var dataToDecrypt: String? = null
}
@TauriPlugin
@ -58,32 +64,36 @@ class BiometricPlugin(private val activity: Activity): Plugin(activity) {
const val RESULT_ERROR_MESSAGE = "errorMessage"
const val DEVICE_CREDENTIAL = "allowDeviceCredential"
const val CONFIRMATION_REQUIRED = "confirmationRequired"
const val ENCRYPT_DECRYPT_OPERATION = "cipherOperation"
const val ENCRYPT_DECRYPT_DATA = "encryptDecryptData"
const val CIPHER_OPERATION_TYPE = "cipherOperationType"
const val RESULT_ENCRYPT_DECRYPT_DATA = "resultEncryptDecryptData"
// 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[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"
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[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"
}
}
@ -157,18 +167,15 @@ class BiometricPlugin(private val activity: Activity): Plugin(activity) {
}
/**
* Prompt the user for biometric authentication.
* sets up the options for BiometricPrompt.
*/
@Command
fun authenticate(invoke: Invoke) {
private fun configBiometricPrompt(args: AuthOptions): Intent {
// 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
)
val args = invoke.parseArgs(AuthOptions::class.java)
// Pass the options to the activity
intent.putExtra(
@ -186,9 +193,47 @@ class BiometricPlugin(private val activity: Activity): Plugin(activity) {
val maxAttemptsConfig = args.maxAttemps
val maxAttempts = max(maxAttemptsConfig, 1)
intent.putExtra(MAX_ATTEMPTS, maxAttempts)
return intent
}
/**
* Prompt the user for biometric authentication.
*/
@Command
fun authenticate(invoke: Invoke) {
val args: AuthOptions = invoke.parseArgs(AuthOptions::class.java)
val intent = configBiometricPrompt(args)
startActivityForResult(invoke, intent, "authenticateResult")
}
/**
* Prompt the user for biometric authentication to encrypt/decrypt some provided data]
*/
@Command
fun biometricCipher(invoke: Invoke) {
val args: AuthOptions = invoke.parseArgs(AuthOptions::class.java)
val intent = configBiometricPrompt(args)
var operationType: CipherType
val data = if (args.dataToEncrypt != null) {
operationType = CipherType.ENCRYPT
args.dataToEncrypt
} else {
operationType = CipherType.DECRYPT
args.dataToDecrypt
}
intent.putExtra(ENCRYPT_DECRYPT_DATA, data)
intent.putExtra(ENCRYPT_DECRYPT_OPERATION, true)
intent.putExtra(CIPHER_OPERATION_TYPE, operationType.ordinal)
startActivityForResult(invoke, intent, "authenticateResult")
}
@ActivityCallback
private fun authenticateResult(invoke: Invoke, result: ActivityResult) {
val resultCode = result.resultCode
@ -231,8 +276,29 @@ class BiometricPlugin(private val activity: Activity): Plugin(activity) {
var errorMessage = data.getStringExtra(
RESULT_EXTRA_PREFIX + RESULT_ERROR_MESSAGE
)
val cipherOperation = data.getBooleanExtra(RESULT_EXTRA_PREFIX + ENCRYPT_DECRYPT_OPERATION, false)
var resolvedData = JSObject()
if (cipherOperation) {
val processedData = data.getStringExtra(
RESULT_EXTRA_PREFIX + RESULT_ENCRYPT_DECRYPT_DATA
)
resolvedData.put(
"data",
processedData
)
}
when (resultType) {
BiometryResultType.SUCCESS -> invoke.resolve()
BiometryResultType.SUCCESS -> {
if (cipherOperation) {
invoke.resolve(resolvedData)
} else {
invoke.resolve()
}
}
BiometryResultType.FAILURE -> // Biometry was successfully presented but was not recognized
invoke.reject(errorMessage, BIOMETRIC_FAILURE)

@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_BIOMETRIC__=function(e){"use strict";async function i(e,i={},t){return window.__TAURI_INTERNALS__.invoke(e,i,t)}var t;return"function"==typeof SuppressedError&&SuppressedError,e.BiometryType=void 0,(t=e.BiometryType||(e.BiometryType={}))[t.None=0]="None",t[t.TouchID=1]="TouchID",t[t.FaceID=2]="FaceID",t[t.Iris=3]="Iris",e.authenticate=async function(e,t){await i("plugin:biometric|authenticate",{reason:e,...t})},e.checkStatus=async function(){return await i("plugin:biometric|status")},e}({});Object.defineProperty(window.__TAURI__,"biometric",{value:__TAURI_PLUGIN_BIOMETRIC__})}
if("__TAURI__"in window){var __TAURI_PLUGIN_BIOMETRIC__=function(i){"use strict";async function e(i,e={},t){return window.__TAURI_INTERNALS__.invoke(i,e,t)}var t;return"function"==typeof SuppressedError&&SuppressedError,i.BiometryType=void 0,(t=i.BiometryType||(i.BiometryType={}))[t.None=0]="None",t[t.TouchID=1]="TouchID",t[t.FaceID=2]="FaceID",t[t.Iris=3]="Iris",i.authenticate=async function(i,t){await e("plugin:biometric|authenticate",{reason:i,...t})},i.biometricCipher=async function(i,t){return await e("plugin:biometric|biometric_cipher",{reason:i,...t})},i.checkStatus=async function(){return await e("plugin:biometric|status")},i}({});Object.defineProperty(window.__TAURI__,"biometric",{value:__TAURI_PLUGIN_BIOMETRIC__})}

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
const COMMANDS: &[&str] = &["authenticate", "status"];
const COMMANDS: &[&str] = &["authenticate", "status", "biometric_cipher"];
fn main() {
let result = tauri_plugin::Builder::new(COMMANDS)

@ -33,15 +33,26 @@ export interface Status {
}
export interface AuthOptions {
/** Enables authentication using the device's password. This feature is available on both Android and iOS. */
allowDeviceCredential?: boolean
/** Label for the Cancel button. This feature is available on both Android and iOS. */
cancelTitle?: string
/** The plain data that must be encrypted after successfull biometric authentication */
dataToEncrypt?: string
/** The encrypted data that must be decrypted after successfull biometric authentication */
dataToDecrypt?: string
// iOS options
/** Specifies the text displayed on the fallback button if biometric authentication fails. This feature is available iOS only. */
fallbackTitle?: string
// android options
/** Title indicating the purpose of biometric verification. This feature is available Android only. */
title?: string
/** SubTitle providing contextual information of biometric verification. This feature is available Android only. */
subtitle?: string
/** Specifies whether additional user confirmation is required, such as pressing a button after successful biometric authentication. This feature is available Android only. */
confirmationRequired?: boolean
maxAttemps?: number
}
@ -75,3 +86,43 @@ export async function authenticate(
...options
})
}
/**
* Encrypts/Decrypts some payload using biometric authentication. It will Prompt the user to authenticate
* using the system interface (touchID, faceID or Android Iris).
* Rejects if the authentication fails.
*
* Warning: consider that if the data is encrypted and the user changes the biometric settings, the key needed
* to decrypt the data will be lost. So, it's recommended to use this only to data that the user can get back
* by other means (i.e. storing their password in a secure way so they can login passwordless with biometric authentication).
*
* ```javascript
* import { biometricCipher } from "@tauri-apps/plugin-biometric";
*
* // how to encrypt some data
* const options = {
* dataToEncrypt: "...", // if not empty, will encrypt this data
* };
* const encryptedData = await biometricCipher('Open your wallet', options);
*
* // how to decrypt the encrypted data
* const options = {
* dataToDecrypt: encryptedData // if not empty, will decrypt the data (that was previously encrypted by this method)
* };
* const decryptedData = await biometricCipher('Open your wallet', options);
* ```
* @param reason
* @param options
* @returns
*/
export async function biometricCipher(
reason: string,
options?: AuthOptions
): Promise<{data: string}> {
return await invoke<{data: string}>('plugin:biometric|biometric_cipher', {
reason,
...options
});
}

@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-biometric-cipher"
description = "Enables the biometric_cipher command without any pre-configured scope."
commands.allow = ["biometric_cipher"]
[[permission]]
identifier = "deny-biometric-cipher"
description = "Denies the biometric_cipher command without any pre-configured scope."
commands.deny = ["biometric_cipher"]

@ -50,6 +50,32 @@ Denies the authenticate command without any pre-configured scope.
<tr>
<td>
`biometric:allow-biometric-cipher`
</td>
<td>
Enables the biometric_cipher command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`biometric:deny-biometric-cipher`
</td>
<td>
Denies the biometric_cipher command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`biometric:allow-status`
</td>

@ -306,6 +306,16 @@
"const": "deny-authenticate",
"markdownDescription": "Denies the authenticate command without any pre-configured scope."
},
{
"description": "Enables the biometric_cipher command without any pre-configured scope.",
"type": "string",
"const": "allow-biometric-cipher"
},
{
"description": "Denies the biometric_cipher command without any pre-configured scope.",
"type": "string",
"const": "deny-biometric-cipher"
},
{
"description": "Enables the status command without any pre-configured scope.",
"type": "string",

@ -23,6 +23,7 @@ const PLUGIN_IDENTIFIER: &str = "app.tauri.biometric";
#[cfg(target_os = "ios")]
tauri::ios_plugin_binding!(init_plugin_biometric);
/// Access to the biometric APIs.
pub struct Biometric<R: Runtime>(PluginHandle<R>);
@ -43,6 +44,12 @@ impl<R: Runtime> Biometric<R> {
.run_mobile_plugin("authenticate", AuthenticatePayload { reason, options })
.map_err(Into::into)
}
pub fn biometric_cipher(&self, reason: String, options: AuthOptions) -> crate::Result<CipherResult> {
self.0
.run_mobile_plugin("biometricCipher", AuthenticatePayload { reason, options })
.map_err(Into::into)
}
}
/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the biometric APIs.

@ -11,6 +11,10 @@ pub struct AuthOptions {
pub allow_device_credential: bool,
/// Label for the Cancel button. This feature is available on both Android and iOS.
pub cancel_title: Option<String>,
/// The plain data that must be encrypted after successfull biometric authentication
pub data_to_encrypt: Option<String>,
/// The encrypted data that must be decrypted after successfull biometric authentication
pub data_to_decrypt: Option<String>,
/// Specifies the text displayed on the fallback button if biometric authentication fails. This feature is available iOS only.
pub fallback_title: Option<String>,
/// Title indicating the purpose of biometric verification. This feature is available Android only.
@ -37,3 +41,8 @@ pub struct Status {
pub error: Option<String>,
pub error_code: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CipherResult {
pub data: String
}
Loading…
Cancel
Save