[feat] [biometric] Adding the capability of encrypting/decrypting data using biometric authentication on Android (Ref. #2306) (#2454)

* –feat: adding the ability to encrypt/decrypt data based on the biometric auth

Adding the method biometricCipher() that allows to use the biometric authentication to use a generated key from android that can be accessed only through the biometric authentication. The method uses this key to encrypt/decrypt the data provided by the caller in the "options" parameter, with "dataToEncrypt" or "dataToDecrypt" filled.

Changes were done both on Kotlin, Rust and JS sides.

* Improving the way we encrypt data by allowing only if the biometric was successful.

* Adding a warning on the JS side

* Adding doc changes in readme.md

* Adding doc changes in readme.md

* Update README.md - it was updated incomplete

* update docs

* enhance error handling

* update example

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
Charles SchaeferandLucas Nogueira authored and GitHub committed 2026-10-12 10:38:16 -03:00
1 parent b3e52ce036
commit 0374bec1e2
19 files changed
+915 -141

No files matched your search

+47 -10
View File
@@ -1,14 +1,14 @@
![biometric](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/biometric/banner.png)
Prompt the user for biometric authentication on Android, iOS and macOS.
Prompt the user for biometric authentication on Android, iOS and macOS. On Android, it also allows encrypting and decrypting data with keys protected by biometric authentication.
| Platform | Supported |
| -------- | --------- |
| Linux | x |
| Windows | x |
| macOS | ✓ |
| Android | ✓ |
| iOS | ✓ |
| Platform | Supported |
| -------- | ----------------------------- |
| Linux | x |
| Windows | x |
| macOS | ✓ |
| Android | ✓ |
| iOS | ✓ (no biometric cryptography) |
## Install
@@ -61,8 +61,45 @@ 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.isAvailable) {
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
)
}
```
## Permissions
The default permission set (`biometric:default`) only allows `status` and `authenticate`. To use `biometricCipher`, add the `biometric:allow-biometric-cipher` permission to your capability:
```json
{
"permissions": ["biometric:default", "biometric:allow-biometric-cipher"]
}
```
## Contributing
@@ -16,23 +16,131 @@ 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.KeyPermanentlyInvalidatedException
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!!
)
}
cipherOperation = intent.hasExtra(BiometricPlugin.ENCRYPT_DECRYPT_OPERATION)
// null when the activity already finished with an error
val promptInfo = createPromptInfo() ?: return
val prompt = createBiometricPrompt()
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
}
// Fails on malformed encrypted data or a key that is no longer valid
// (e.g. invalidated by a new biometric enrollment).
val cipher = try {
getCipher()
} catch (e: Exception) {
finishActivity(
BiometryResultType.ERROR,
0,
"Failed to initialize the cipher: ${e.message ?: e.javaClass.simpleName}"
)
return
}
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) {
// Decrypting data that was tampered with or encrypted with another key fails here.
val processedData = try {
processCipherData()
} catch (e: Exception) {
finishActivity(
BiometryResultType.ERROR,
0,
"Failed to ${if (cipherType == CipherType.ENCRYPT) "encrypt" else "decrypt"} the data: ${e.message ?: e.javaClass.simpleName}"
)
return
}
intent.putExtra(
prefix + BiometricPlugin.RESULT_ENCRYPT_DECRYPT_DATA,
processedData
)
}
setResult(Activity.RESULT_OK, intent)
finish()
}
/**
* Encrypts or decrypts [encryptDecryptData] with the cipher unlocked by the authentication.
* Encrypted data is returned as Base64 `data;iv`, decrypted data as a UTF-8 string.
*/
private fun processCipherData(): String {
val cryptoObject = requireNotNull(authenticationResult?.cryptoObject)
val cipher = requireNotNull(cryptoObject.cipher)
return if (cipherType == CipherType.ENCRYPT) {
encodeEncryptedData(cipher, cipher.doFinal(encryptDecryptData.toByteArray()))
} else {
val (_, encryptedData) = decodeEncryptedData(encryptDecryptData)
String(cipher.doFinal(encryptedData), Charset.forName("UTF-8"))
}
}
private fun createPromptInfo(): BiometricPrompt.PromptInfo? {
val builder = BiometricPrompt.PromptInfo.Builder()
val intent = intent
var title = intent.getStringExtra(BiometricPlugin.TITLE)
@@ -54,7 +162,7 @@ class BiometricActivity : AppCompatActivity() {
BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL,
getString(R.string.tauri_biometric_no_device_credential)
)
return
return null
}
if (title.isNullOrEmpty()) {
@@ -63,7 +171,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
}
@@ -86,50 +198,119 @@ 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.toString()
)
}
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
super.onAuthenticationSucceeded(result)
finishActivity()
}
}
)
prompt.authenticate(promptInfo)
return builder.build()
}
@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
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!!
)
}
}
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(
errorCode: Int,
errorMessage: CharSequence
) {
super.onAuthenticationError(errorCode, errorMessage)
finishActivity(
BiometryResultType.ERROR,
errorCode,
errorMessage.toString()
)
}
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
super.onAuthenticationSucceeded(result)
authenticationResult = result
finishActivity()
}
}
return BiometricPrompt(this, executor, callback)
}
/**
* 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 deleteEncryptionKey() {
val keyStore = KeyStore.getInstance(KEYSTORE_NAME)
keyStore.load(null)
keyStore.deleteEntry(KEY_ALIAS)
}
private fun getCipher(): Cipher {
val biometricKey = getEncryptionKey()
val cipher = Cipher.getInstance("$ENCRYPTION_ALGORITHM/$ENCRYPTION_BLOCK_MODE/$ENCRYPTION_PADDING")
if (cipherType == CipherType.ENCRYPT) {
try {
cipher.init(Cipher.ENCRYPT_MODE, biometricKey)
} catch (e: KeyPermanentlyInvalidatedException) {
// The key was invalidated by a biometric enrollment change or the lock screen
// being removed. Whatever it encrypted can no longer be decrypted, so replace it.
deleteEncryptionKey()
cipher.init(Cipher.ENCRYPT_MODE, getEncryptionKey())
}
} 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 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] = "passcodeNotSet"
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"
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] = "passcodeNotSet"
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"
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,52 @@ 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)
if (args.dataToEncrypt == null && args.dataToDecrypt == null) {
invoke.reject("Either dataToEncrypt or dataToDecrypt must be provided")
return
}
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 +281,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
View File
@@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_BIOMETRIC__=function(e,i){"use strict";var t;return 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.invoke("plugin:biometric|authenticate",{reason:e,...t})},e.checkStatus=async function(){return await i.invoke("plugin:biometric|status")},e}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"biometric",{value:__TAURI_PLUGIN_BIOMETRIC__})}
if("__TAURI__"in window){var __TAURI_PLUGIN_BIOMETRIC__=function(i,e){"use strict";var t;return 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.invoke("plugin:biometric|authenticate",{reason:i,...t})},i.biometricCipher=async function(i,t){return await e.invoke("plugin:biometric|biometric_cipher",{reason:i,...t})},i.checkStatus=async function(){return await e.invoke("plugin:biometric|status")},i}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"biometric",{value:__TAURI_PLUGIN_BIOMETRIC__})}
+1 -1
View File
@@ -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)
+42
View File
@@ -58,6 +58,10 @@ export interface AuthOptions {
allowDeviceCredential?: boolean
/** Label for the cancel button. Available on Android, iOS and macOS. */
cancelTitle?: string
/** The plain data that must be encrypted after successful biometric authentication. Used by {@linkcode biometricCipher}. **Android only.** */
dataToEncrypt?: string
/** The encrypted data that must be decrypted after successful biometric authentication. Used by {@linkcode biometricCipher}. **Android only.** */
dataToDecrypt?: string
// iOS and macOS options
/** Text displayed on the fallback button if biometric authentication fails. **iOS and macOS only.** */
@@ -115,3 +119,41 @@ 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"]
@@ -5,7 +5,11 @@ biometric features are by default exposed.
#### Granted Permissions
It allows acccess to all biometric commands.
It allows checking the biometric status and
prompting for authentication. Encrypting and
decrypting data with `biometric_cipher` is not
included and must be enabled explicitly with
`biometric:allow-biometric-cipher`.
#### This default permission set includes the following:
@@ -50,6 +54,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>
+5 -1
View File
@@ -6,7 +6,11 @@ biometric features are by default exposed.
#### Granted Permissions
It allows acccess to all biometric commands.
It allows checking the biometric status and
prompting for authentication. Encrypting and
decrypting data with `biometric_cipher` is not
included and must be enabled explicitly with
`biometric:allow-biometric-cipher`.
"""
@@ -306,6 +306,18 @@
"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",
"markdownDescription": "Enables the biometric_cipher command without any pre-configured scope."
},
{
"description": "Denies the biometric_cipher command without any pre-configured scope.",
"type": "string",
"const": "deny-biometric-cipher",
"markdownDescription": "Denies the biometric_cipher command without any pre-configured scope."
},
{
"description": "Enables the status command without any pre-configured scope.",
"type": "string",
@@ -319,10 +331,10 @@
"markdownDescription": "Denies the status command without any pre-configured scope."
},
{
"description": "This permission set configures which\nbiometric features are by default exposed.\n\n#### Granted Permissions\n\nIt allows acccess to all biometric commands.\n\n\n#### This default permission set includes:\n\n- `allow-authenticate`\n- `allow-status`",
"description": "This permission set configures which\nbiometric features are by default exposed.\n\n#### Granted Permissions\n\nIt allows checking the biometric status and\nprompting for authentication. Encrypting and\ndecrypting data with `biometric_cipher` is not\nincluded and must be enabled explicitly with\n`biometric:allow-biometric-cipher`.\n\n\n#### This default permission set includes:\n\n- `allow-authenticate`\n- `allow-status`",
"type": "string",
"const": "default",
"markdownDescription": "This permission set configures which\nbiometric features are by default exposed.\n\n#### Granted Permissions\n\nIt allows acccess to all biometric commands.\n\n\n#### This default permission set includes:\n\n- `allow-authenticate`\n- `allow-status`"
"markdownDescription": "This permission set configures which\nbiometric features are by default exposed.\n\n#### Granted Permissions\n\nIt allows checking the biometric status and\nprompting for authentication. Encrypting and\ndecrypting data with `biometric_cipher` is not\nincluded and must be enabled explicitly with\n`biometric:allow-biometric-cipher`.\n\n\n#### This default permission set includes:\n\n- `allow-authenticate`\n- `allow-status`"
}
]
}
+1
View File
@@ -33,6 +33,7 @@ pub(crate) async fn authenticate<R: Runtime>(
title,
subtitle,
confirmation_required,
..Default::default()
};
// The prompt blocks until the user answers, so keep it off the async runtime workers.
tauri::async_runtime::spawn_blocking(move || app.biometric().authenticate(reason, options))
+13
View File
@@ -53,4 +53,17 @@ impl<R: Runtime> Biometric<R> {
.run_mobile_plugin("authenticate", AuthenticatePayload { reason, options })
.map_err(Into::into)
}
/// Prompts the user for biometric authentication and, on success, encrypts
/// [`AuthOptions::data_to_encrypt`] or decrypts [`AuthOptions::data_to_decrypt`] using a key
/// protected by the device's strong biometrics. **Android only.**
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)
}
}
+11
View File
@@ -12,6 +12,10 @@ pub struct AuthOptions {
pub allow_device_credential: bool,
/// Label for the Cancel button. This feature is available on Android, iOS and macOS.
pub cancel_title: Option<String>,
/// The plain data that must be encrypted after successful biometric authentication. **Android only.**
pub data_to_encrypt: Option<String>,
/// The encrypted data that must be decrypted after successful biometric authentication. **Android only.**
pub data_to_decrypt: Option<String>,
/// Specifies the text displayed on the fallback button if biometric authentication fails. This feature is available on iOS and macOS only.
pub fallback_title: Option<String>,
/// Title indicating the purpose of biometric verification. This feature is available Android only.
@@ -50,3 +54,10 @@ pub struct Status {
/// Only set when [`Self::is_available`] is `false`.
pub error_code: Option<String>,
}
/// The result of [`Biometric::biometric_cipher`](crate::Biometric::biometric_cipher).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CipherResult {
/// The encrypted or decrypted data.
pub data: String,
}