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) ![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 | | Platform | Supported |
| -------- | --------- | | -------- | --------- |
@ -8,7 +8,7 @@ Prompt the user for biometric authentication on Android and iOS.
| Windows | x | | Windows | x |
| macOS | x | | macOS | x |
| Android | ✓ | | Android | ✓ |
| iOS | ✓ | | iOS | ✓ (except biometric cryptography ) |
## Install ## Install
@ -70,8 +70,27 @@ fn main() {
Afterwards all the plugin's APIs are available through the JavaScript guest bindings: Afterwards all the plugin's APIs are available through the JavaScript guest bindings:
```javascript ```javascript
import { authenticate } from '@tauri-apps/plugin-biometric' import { checkStatus, authenticate, biometricCipher } from '@tauri-apps/plugin-biometric'
await authenticate('Open your wallet')
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 ## Contributing

@ -16,29 +16,120 @@ import android.os.Handler
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt
import java.util.concurrent.Executor 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() { 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") @SuppressLint("WrongConstant")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.auth_activity) setContentView(R.layout.auth_activity)
val executor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val promptInfo = createPromptInfo()
this.mainExecutor val prompt = createBiometricPrompt()
} else {
Executor { command: Runnable? -> cipherOperation = intent.hasExtra(BiometricPlugin.ENCRYPT_DECRYPT_OPERATION)
Handler(this.mainLooper).post( if (!cipherOperation) {
command!! 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 builder = BiometricPrompt.PromptInfo.Builder()
val intent = intent val intent = intent
var title = intent.getStringExtra(BiometricPlugin.TITLE) var title = intent.getStringExtra(BiometricPlugin.TITLE)
val subtitle = intent.getStringExtra(BiometricPlugin.SUBTITLE) val subtitle = intent.getStringExtra(BiometricPlugin.SUBTITLE)
val description = intent.getStringExtra(BiometricPlugin.REASON) val description = intent.getStringExtra(BiometricPlugin.REASON)
allowDeviceCredential = false 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 // Android docs say we should check if the device is secure before enabling device credential fallback
val manager = getSystemService( val manager = getSystemService(
Context.KEYGUARD_SERVICE Context.KEYGUARD_SERVICE
@ -54,7 +145,11 @@ class BiometricActivity : AppCompatActivity() {
builder.setTitle(title).setSubtitle(subtitle).setDescription(description) builder.setTitle(title).setSubtitle(subtitle).setDescription(description)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 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) { if (allowDeviceCredential) {
authenticators = authenticators or BiometricManager.Authenticators.DEVICE_CREDENTIAL authenticators = authenticators or BiometricManager.Authenticators.DEVICE_CREDENTIAL
} }
@ -76,54 +171,108 @@ class BiometricActivity : AppCompatActivity() {
builder.setConfirmationRequired( builder.setConfirmationRequired(
intent.getBooleanExtra(BiometricPlugin.CONFIRMATION_REQUIRED, true) intent.getBooleanExtra(BiometricPlugin.CONFIRMATION_REQUIRED, true)
) )
val promptInfo = builder.build()
val prompt = BiometricPrompt( return builder.build()
this, }
executor,
object : BiometricPrompt.AuthenticationCallback() { private fun createBiometricPrompt(): BiometricPrompt {
override fun onAuthenticationError( val executor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
errorCode: Int, this.mainExecutor
errorMessage: CharSequence } else {
) { Executor { command: Runnable? ->
super.onAuthenticationError(errorCode, errorMessage) Handler(this.mainLooper).post(
finishActivity( command!!
BiometryResultType.ERROR, )
errorCode,
errorMessage as String
)
}
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
super.onAuthenticationSucceeded(result)
finishActivity()
}
} }
) }
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( * Get the key if exists, or create a new one if not
resultType: BiometryResultType = BiometryResultType.SUCCESS, */
errorCode: Int = 0, private fun getEncryptionKey(): SecretKey {
errorMessage: String? = "" val keyStore = KeyStore.getInstance(KEYSTORE_NAME)
) { keyStore.load(null)
val intent = Intent() keyStore.getKey(KEY_ALIAS, null)?.let { return it as SecretKey }
val prefix = BiometricPlugin.RESULT_EXTRA_PREFIX
intent // from here ahead, we will move only if there is not a key with the provided alias
.putExtra(prefix + BiometricPlugin.RESULT_TYPE, resultType.toString()) val keyGenerator = KeyGenerator.getInstance(ENCRYPTION_ALGORITHM, KEYSTORE_NAME)
.putExtra(prefix + BiometricPlugin.RESULT_ERROR_CODE, errorCode)
.putExtra( val builder = KeyGenParameterSpec.Builder(
prefix + BiometricPlugin.RESULT_ERROR_MESSAGE, KEY_ALIAS,
errorMessage KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
) )
setResult(Activity.RESULT_OK, intent) .setBlockModes(ENCRYPTION_BLOCK_MODE)
finish() .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 { companion object {
var allowDeviceCredential = false 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 SUCCESS, FAILURE, ERROR
} }
enum class CipherType {
ENCRYPT, DECRYPT
}
private const val MAX_ATTEMPTS = "maxAttemps" private const val MAX_ATTEMPTS = "maxAttemps"
private const val BIOMETRIC_FAILURE = "authenticationFailed" private const val BIOMETRIC_FAILURE = "authenticationFailed"
private const val INVALID_CONTEXT_ERROR = "invalidContext" private const val INVALID_CONTEXT_ERROR = "invalidContext"
@ -41,6 +45,8 @@ class AuthOptions {
var cancelTitle: String? = null var cancelTitle: String? = null
var confirmationRequired: Boolean? = null var confirmationRequired: Boolean? = null
var maxAttemps: Int = 3 var maxAttemps: Int = 3
var dataToEncrypt: String? = null
var dataToDecrypt: String? = null
} }
@TauriPlugin @TauriPlugin
@ -58,32 +64,36 @@ class BiometricPlugin(private val activity: Activity): Plugin(activity) {
const val RESULT_ERROR_MESSAGE = "errorMessage" const val RESULT_ERROR_MESSAGE = "errorMessage"
const val DEVICE_CREDENTIAL = "allowDeviceCredential" const val DEVICE_CREDENTIAL = "allowDeviceCredential"
const val CONFIRMATION_REQUIRED = "confirmationRequired" 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 // Maps biometry error numbers to string error codes
private var biometryErrorCodeMap: MutableMap<Int, String> = HashMap() private var biometryErrorCodeMap: MutableMap<Int, String> = HashMap()
private var biometryNameMap: MutableMap<BiometryType, String> = EnumMap(BiometryType::class.java) private var biometryNameMap: MutableMap<BiometryType, String> = EnumMap(BiometryType::class.java)
init { init {
biometryErrorCodeMap[BiometricManager.BIOMETRIC_SUCCESS] = "" biometryErrorCodeMap[BiometricManager.BIOMETRIC_SUCCESS] = ""
biometryErrorCodeMap[BiometricManager.BIOMETRIC_SUCCESS] = "" biometryErrorCodeMap[BiometricManager.BIOMETRIC_SUCCESS] = ""
biometryErrorCodeMap[BiometricPrompt.ERROR_CANCELED] = "systemCancel" biometryErrorCodeMap[BiometricPrompt.ERROR_CANCELED] = "systemCancel"
biometryErrorCodeMap[BiometricPrompt.ERROR_HW_NOT_PRESENT] = "biometryNotAvailable" biometryErrorCodeMap[BiometricPrompt.ERROR_HW_NOT_PRESENT] = "biometryNotAvailable"
biometryErrorCodeMap[BiometricPrompt.ERROR_HW_UNAVAILABLE] = "biometryNotAvailable" biometryErrorCodeMap[BiometricPrompt.ERROR_HW_UNAVAILABLE] = "biometryNotAvailable"
biometryErrorCodeMap[BiometricPrompt.ERROR_LOCKOUT] = "biometryLockout" biometryErrorCodeMap[BiometricPrompt.ERROR_LOCKOUT] = "biometryLockout"
biometryErrorCodeMap[BiometricPrompt.ERROR_LOCKOUT_PERMANENT] = "biometryLockout" biometryErrorCodeMap[BiometricPrompt.ERROR_LOCKOUT_PERMANENT] = "biometryLockout"
biometryErrorCodeMap[BiometricPrompt.ERROR_NEGATIVE_BUTTON] = "userCancel" biometryErrorCodeMap[BiometricPrompt.ERROR_NEGATIVE_BUTTON] = "userCancel"
biometryErrorCodeMap[BiometricPrompt.ERROR_NO_BIOMETRICS] = "biometryNotEnrolled" biometryErrorCodeMap[BiometricPrompt.ERROR_NO_BIOMETRICS] = "biometryNotEnrolled"
biometryErrorCodeMap[BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL] = "noDeviceCredential" biometryErrorCodeMap[BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL] = "noDeviceCredential"
biometryErrorCodeMap[BiometricPrompt.ERROR_NO_SPACE] = "systemCancel" biometryErrorCodeMap[BiometricPrompt.ERROR_NO_SPACE] = "systemCancel"
biometryErrorCodeMap[BiometricPrompt.ERROR_TIMEOUT] = "systemCancel" biometryErrorCodeMap[BiometricPrompt.ERROR_TIMEOUT] = "systemCancel"
biometryErrorCodeMap[BiometricPrompt.ERROR_UNABLE_TO_PROCESS] = "systemCancel" biometryErrorCodeMap[BiometricPrompt.ERROR_UNABLE_TO_PROCESS] = "systemCancel"
biometryErrorCodeMap[BiometricPrompt.ERROR_USER_CANCELED] = "userCancel" biometryErrorCodeMap[BiometricPrompt.ERROR_USER_CANCELED] = "userCancel"
biometryErrorCodeMap[BiometricPrompt.ERROR_VENDOR] = "systemCancel" biometryErrorCodeMap[BiometricPrompt.ERROR_VENDOR] = "systemCancel"
biometryNameMap[BiometryType.NONE] = "No Authentication" biometryNameMap[BiometryType.NONE] = "No Authentication"
biometryNameMap[BiometryType.FINGERPRINT] = "Fingerprint Authentication" biometryNameMap[BiometryType.FINGERPRINT] = "Fingerprint Authentication"
biometryNameMap[BiometryType.FACE] = "Face Authentication" biometryNameMap[BiometryType.FACE] = "Face Authentication"
biometryNameMap[BiometryType.IRIS] = "Iris 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 private fun configBiometricPrompt(args: AuthOptions): Intent {
fun authenticate(invoke: Invoke) {
// The result of an intent is supposed to have the package name as a prefix // The result of an intent is supposed to have the package name as a prefix
RESULT_EXTRA_PREFIX = activity.packageName + "." RESULT_EXTRA_PREFIX = activity.packageName + "."
val intent = Intent( val intent = Intent(
activity, activity,
BiometricActivity::class.java BiometricActivity::class.java
) )
val args = invoke.parseArgs(AuthOptions::class.java)
// Pass the options to the activity // Pass the options to the activity
intent.putExtra( intent.putExtra(
@ -186,9 +193,47 @@ class BiometricPlugin(private val activity: Activity): Plugin(activity) {
val maxAttemptsConfig = args.maxAttemps val maxAttemptsConfig = args.maxAttemps
val maxAttempts = max(maxAttemptsConfig, 1) val maxAttempts = max(maxAttemptsConfig, 1)
intent.putExtra(MAX_ATTEMPTS, maxAttempts) 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") startActivityForResult(invoke, intent, "authenticateResult")
} }
@ActivityCallback @ActivityCallback
private fun authenticateResult(invoke: Invoke, result: ActivityResult) { private fun authenticateResult(invoke: Invoke, result: ActivityResult) {
val resultCode = result.resultCode val resultCode = result.resultCode
@ -231,8 +276,29 @@ class BiometricPlugin(private val activity: Activity): Plugin(activity) {
var errorMessage = data.getStringExtra( var errorMessage = data.getStringExtra(
RESULT_EXTRA_PREFIX + RESULT_ERROR_MESSAGE 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) { 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 BiometryResultType.FAILURE -> // Biometry was successfully presented but was not recognized
invoke.reject(errorMessage, BIOMETRIC_FAILURE) 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: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
const COMMANDS: &[&str] = &["authenticate", "status"]; const COMMANDS: &[&str] = &["authenticate", "status", "biometric_cipher"];
fn main() { fn main() {
let result = tauri_plugin::Builder::new(COMMANDS) let result = tauri_plugin::Builder::new(COMMANDS)

@ -33,15 +33,26 @@ export interface Status {
} }
export interface AuthOptions { export interface AuthOptions {
/** Enables authentication using the device's password. This feature is available on both Android and iOS. */
allowDeviceCredential?: boolean allowDeviceCredential?: boolean
/** Label for the Cancel button. This feature is available on both Android and iOS. */
cancelTitle?: string 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 // iOS options
/** Specifies the text displayed on the fallback button if biometric authentication fails. This feature is available iOS only. */
fallbackTitle?: string fallbackTitle?: string
// android options // android options
/** Title indicating the purpose of biometric verification. This feature is available Android only. */
title?: string title?: string
/** SubTitle providing contextual information of biometric verification. This feature is available Android only. */
subtitle?: string 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 confirmationRequired?: boolean
maxAttemps?: number maxAttemps?: number
} }
@ -75,3 +86,43 @@ export async function authenticate(
...options ...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> <tr>
<td> <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` `biometric:allow-status`
</td> </td>

@ -306,6 +306,16 @@
"const": "deny-authenticate", "const": "deny-authenticate",
"markdownDescription": "Denies the authenticate command without any pre-configured scope." "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.", "description": "Enables the status command without any pre-configured scope.",
"type": "string", "type": "string",

@ -23,6 +23,7 @@ const PLUGIN_IDENTIFIER: &str = "app.tauri.biometric";
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
tauri::ios_plugin_binding!(init_plugin_biometric); tauri::ios_plugin_binding!(init_plugin_biometric);
/// Access to the biometric APIs. /// Access to the biometric APIs.
pub struct Biometric<R: Runtime>(PluginHandle<R>); pub struct Biometric<R: Runtime>(PluginHandle<R>);
@ -43,6 +44,12 @@ impl<R: Runtime> Biometric<R> {
.run_mobile_plugin("authenticate", AuthenticatePayload { reason, options }) .run_mobile_plugin("authenticate", AuthenticatePayload { reason, options })
.map_err(Into::into) .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. /// 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, pub allow_device_credential: bool,
/// Label for the Cancel button. This feature is available on both Android and iOS. /// Label for the Cancel button. This feature is available on both Android and iOS.
pub cancel_title: Option<String>, 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. /// Specifies the text displayed on the fallback button if biometric authentication fails. This feature is available iOS only.
pub fallback_title: Option<String>, pub fallback_title: Option<String>,
/// Title indicating the purpose of biometric verification. This feature is available Android only. /// 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: Option<String>,
pub error_code: Option<String>, pub error_code: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CipherResult {
pub data: String
}
Loading…
Cancel
Save