biometric_signature
Hardwareunterstützte biometrische Authentifizierung für Flutter (Android, iOS, macOS, Windows). Erstellen Sie kryptografische Signaturen mithilfe von Secure Enclave, StrongBox und Windows Hello.
Hardwareunterstützte biometrische Authentifizierung für Flutter. Erstellen Sie kryptografische Signaturen mithilfe von Gerätebiometrien mit Schlüsseln, die im Secure Enclave/StrongBox/Windows Hello gespeichert sind.
{"sdk":"flutter"}^2.0.2{"sdk":"flutter"}^5.0.0^25.3.2Englischer Projektschnappschuss. Aktuelle Inhalte auf GitHub.
Stop just unlocking the UI. Start proving the identity.
Standard biometric integrations typically return only a boolean indicating whether authentication succeeded.
biometric_signature provides a complete biometric solution:
Even if an attacker bypasses or hooks biometric APIs, your backend will still reject the request because the attacker cannot forge a hardware-backed signature without the private key.
eciesEncryptionStandardX963SHA256AESGCM)keyAlias parameter.failIfExists option.The plugin supports different operational modes depending on the platform:
Android supports three key modes:
RSA Mode (SignatureType.rsa):
EC Signing-Only (SignatureType.ecdsa, enableDecryption: false):
Hybrid EC Mode (SignatureType.ecdsa, enableDecryption: true):
Apple platforms support two key modes (Secure Enclave only supports EC keys natively):
EC Mode (SignatureType.ecdsa):
eciesEncryptionStandardX963SHA256AESGCM)RSA Mode (SignatureType.rsa) - Hybrid Architecture:
kSecClassGenericPasswordEnrollment
User authenticates → hardware generates a signing key.
Hybrid modes additionally generate a software decryption key, which is then encrypted using secure hardware.
Signing
Biometric prompt is shown
Hardware unlocks the signing key, and a verifiable signature is produced.
Decryption
A biometric prompt is shown again.
Hybrid modes unwrap the software private key using hardware-protected AES-GCM, then decrypt the payload.
Backend Verification
The backend verifies signatures using the registered public key.
Verification must not be performed on the client.
Perform verification on the server. Below are reference implementations.
const crypto = require('crypto');
function verifySignature(publicKeyPem, payload, signatureBase64) {
const verify = crypto.createVerify('SHA256');
verify.update(payload); // The original string you sent to the plugin
verify.end();
// Returns true if valid
return verify.verify(publicKeyPem, Buffer.from(signatureBase64, 'base64'));
}
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import serialization
import base64
def verify_signature(public_key_pem_str, payload_str, signature_base64_str):
public_key = serialization.load_pem_public_key(public_key_pem_str.encode())
signature = base64.b64decode(signature_base64_str)
try:
# Assuming RSA (For EC, use ec.ECDSA(hashes.SHA256()))
public_key.verify(
signature,
payload_str.encode(),
padding.PKCS1v15(),
hashes.SHA256()
)
return True
except Exception:
return False
import (
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
)
func verify(pubPemStr, payload, sigBase64 string) error {
block, _ := pem.Decode([]byte(pubPemStr))
pub, _ := x509.ParsePKIXPublicKey(block.Bytes)
rsaPub := pub.(*rsa.PublicKey)
hashed := sha256.Sum256([]byte(payload))
sig, _ := base64.StdEncoding.DecodeString(sigBase64)
return rsa.VerifyPKCS1v15(rsaPub, crypto.SHA256, hashed[:], sig)
}
To get started with Biometric Signature, follow these steps:
pubspec.yaml file:dependencies:
biometric_signature: ^13.0.0
| Android | iOS | macOS | Windows | |
|---|---|---|---|---|
| Support | SDK 23+ | 13.0+ | 10.15+ | 10+ |
Minimum Flutter SDK: 3.24.5 (Dart 3.5.0).
The plugin transitively depends on androidx.biometric:1.4.0-alpha05, whose AAR
metadata enforces minCompileSdk = 35. Flutter 3.24.5 ships with
flutter.compileSdkVersion = 34 and flutter.ndkVersion = "23.1.7779620" by
default, which are below what this plugin (and other modern AndroidX libraries)
need. Your app's android/app/build.gradle.kts therefore needs to override
those values explicitly:
android {
// androidx.biometric:1.4.0-alpha05 requires compileSdk >= 35.
compileSdk = 35
// Many recent plugins (shared_preferences_android, etc.) require NDK 27.
ndkVersion = "27.0.12077973"
defaultConfig {
// Floor for androidx.biometric BiometricPrompt.
minSdk = 23
// ...
}
}
If you skip those, Gradle fails with:
Dependency 'androidx.biometric:biometric:1.4.0-alpha05' requires librariesand applications that depend on it to compile against version 35 or laterof the Android APIs.
The plugin itself compiles against compileSdk = 35 and ships a buildscript
pinned to AGP 8.6.0. AGP 8.6.0 is one minor above Flutter 3.24.5's
maxKnownAndSupportedAgpVersion (8.4.0), which produces a verbose-only trace
log — the build succeeds normally on default Flutter 3.24.5 tooling.
compileSdk = 35 is a floor for consumers, not a ceiling — your app can
compile and target SDK 36 or 37 against this plugin without changes.
AGP 9 supplies the Kotlin toolchain itself ("built-in Kotlin") and removes
kotlinOptions {} from the Android extension, so a plugin that unconditionally
applies kotlin-android fails to configure. This plugin picks the right path at
configuration time and is verified on:
| Toolchain | Status |
|---|---|
| AGP 8.9.1 / Gradle 8.12 / Flutter 3.24.5 | ✅ |
AGP 9.3.1 / Gradle 9.6.1 / Flutter 3.44.8, android.builtInKotlin=true (AGP 9 default) |
✅ |
AGP 9.3.1 / Gradle 9.6.1 / Flutter 3.44.8, android.builtInKotlin=false (what Flutter's AGP 9 migrator writes) |
✅ |
On AGP 9 the build still prints:
WARNING: Your app uses the following plugins that apply Kotlin Gradle Plugin(KGP): biometric_signature
This warning is safe to ignore. It comes from Flutter searching the plugin's
android/build.gradle for a literal apply plugin: "kotlin-android" line — it
does not reflect what actually runs. That line is guarded by a runtime check and
is skipped under built-in Kotlin, but it has to stay in the file verbatim:
without it, Flutter applies KGP to the plugin itself, which breaks the default
AGP 9 build.
This plugin works with Touch ID or Face ID. To use Face ID in available devices, you need to add:
<dict>
<key>NSFaceIDUsageDescription</key>
<string>This app is using FaceID for authentication</string>
</dict>
to your Info.plist file.
This plugin requires the use of a FragmentActivity instead of Activity. Update your MainActivity.kt to extend FlutterFragmentActivity:
import io.flutter.embedding.android.FlutterFragmentActivity
class MainActivity : FlutterFragmentActivity() {
}
Update your project's AndroidManifest.xml file to include the
USE_BIOMETRIC permission.
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.app">
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
</manifest>
This plugin works with Touch ID on supported Macs. To use Touch ID, you need to:
Open your macOS project's entitlements file (typically located at macos/Runner/DebugProfile.entitlements and macos/Runner/Release.entitlements) and ensure it includes:
<key>com.apple.security.device.usb</key>
<false/>
<key>com.apple.security.device.bluetooth</key>
<false/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.yourdomain.yourapp</string>
</array>
Replace com.yourdomain.yourapp with your actual bundle identifier.
macos/Podfile. The plugin requires macOS 10.15 or later:platform :osx, '10.15'
This plugin uses Windows Hello (Windows.Security.Credentials.KeyCredentialManager) for biometric authentication on Windows 10 and later. Keys are typically backed by the device's TPM (Trusted Platform Module) for hardware-grade security.
Platform Limitations:
enforceBiometric is effectively always true).setInvalidatedByBiometricEnrollment and useDeviceCredentials arguments are ignored on this platform.No additional configuration is required. The plugin will automatically use Windows Hello when available.
import 'package:biometric_signature/biometric_signature.dart';
final biometricSignature = BiometricSignature();
This package simplifies server authentication using biometrics. The following image from Android Developers Blog illustrates the basic use case:
biometric_signature
When a user enrolls in biometrics, a key pair is generated. The private key is securely stored on the device, while the public key is sent to a server for registration. To authenticate, the user is prompted to use their biometrics, unlocking the private key. A cryptographic signature is then generated and sent to the server for verification. If the server successfully verifies the signature, it returns an appropriate response, authorizing the user.
The plugin also supports secure decryption, ensuring that sensitive data transmitted from the server can only be accessed by the authenticated user on their specific device.
Biometric Decryption Lifecycle
Step 3 above has to match the scheme the device decrypts with. For RSA keys that is OAEP with a SHA-256 main digest on every platform — but the MGF1 digest differs, because each platform's crypto API fixes it:
| Platform | Scheme | Main digest | MGF1 digest | Label |
|---|---|---|---|---|
| Android | RSA-OAEP | SHA-256 | SHA-1 | empty |
| iOS / macOS | RSA-OAEP | SHA-256 | SHA-256 | empty |
Android Keystore applies SHA-1 for MGF1, while Apple's SecKeyAlgorithm
.rsaEncryptionOAEPSHA256 uses SHA-256 for both digests. From v12.1.1 the plugin states Android's
parameters explicitly instead of inheriting the provider default, and pins SHA-1 on the key itself
where the platform allows it (API 35+), so a backend can rely on the values above rather than on
whatever the device happens to default to. Select the MGF1 digest from the client platform you
issued the public key to:
# Python (cryptography) — Android
ciphertext = public_key.encrypt(
plaintext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA1()),
algorithm=hashes.SHA256(),
label=None,
),
)
# Python (cryptography) — iOS / macOS
ciphertext = public_key.encrypt(
plaintext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
RSA-2048 with a SHA-256 OAEP digest leaves 190 bytes of plaintext capacity; wrap a symmetric key rather than the payload itself if you need more.
For EC keys the payload is encrypted with ECIES (eciesEncryptionStandardX963SHA256AESGCM) on all
platforms, so no per-platform branch is needed. iOS/macOS keys created with SignatureType.rsa use
the hybrid architecture and still decrypt RSA-OAEP payloads — encrypt against the RSA key returned
in decryptingPublicKey.
Keys created before v11.0.0 authorise PKCS#1 v1.5 instead of OAEP. The plugin still falls back to PKCS#1 v1.5 for those, so existing ciphertext keeps working, but new keys should use OAEP.
This class provides methods to manage and utilize biometric authentication for secure server interactions. It supports both Android and iOS platforms.
createKeys({ keyAlias, config, keyFormat, promptMessage })Generates a new key pair (RSA 2048 or EC) for biometric authentication. The private key is securely stored on the device.
Parameters:
keyAlias: Optional name for this key pair. Different aliases create independent key pairs. When null, the default alias is used.config: CreateKeysConfig with platform options (see below)keyFormat: Output format (KeyFormat.base64, pem, hex)promptMessage: Custom authentication prompt messageReturns: Future<KeyCreationResult>.
publicKey: The formatted public key string (Base64 or PEM).code: BiometricError code (e.g., success, userCanceled, keyAlreadyExists).error: Descriptive error message.| Option | Platforms | Default | Description |
|---|---|---|---|
signatureType |
Android/iOS/macOS | SignatureType.rsa |
SignatureType.rsa or SignatureType.ecdsa |
enforceBiometric |
Android/iOS/macOS | false |
Require biometric during key creation |
setInvalidatedByBiometricEnrollment |
Android/iOS/macOS | true |
Invalidate key on biometric changes |
useDeviceCredentials |
Android/iOS/macOS | false |
Allow PIN/passcode fallback |
requireAuthentication |
Android/iOS/macOS | true |
Require authentication at signing/decryption time |
enableDecryption |
Android | false |
Enable decryption capability |
failIfExists |
All | false |
Fail with keyAlreadyExists if key already exists |
promptSubtitle |
Android | none | Subtitle for biometric prompt |
promptDescription |
Android | none | Description for biometric prompt |
cancelButtonText |
Android | "Cancel" |
Cancel button text |
On setInvalidatedByBiometricEnrollment: the default is true on every platform that
supports it — a key created without the flag is bound to the biometric set enrolled at
creation time, and enrolling or removing a fingerprint/face permanently invalidates it. Your
app must then create a new key and re-enroll its public key with the server; use
getKeyInfo(checkValidity: true) to detect this before signing. Pass false to opt out and
keep keys usable across enrollment changes.
It maps to KeyGenParameterSpec.Builder.setInvalidatedByBiometricEnrollment(...) on Android
(API 23 has no such setter, so keys there are always invalidated) and selects
.biometryCurrentSet vs .biometryAny on the Secure Enclave key for iOS/macOS. It is ignored
on Windows, and ignored when requireAuthentication is false — a key with no
user-authentication constraint is not tied to the enrolled biometric set.
final result = await biometricSignature.createKeys(
keyAlias: 'payment_key', // Optional: named alias
keyFormat: KeyFormat.pem,
promptMessage: 'Authenticate to create keys',
config: CreateKeysConfig(
signatureType: SignatureType.rsa,
enforceBiometric: true,
setInvalidatedByBiometricEnrollment: true,
useDeviceCredentials: false,
enableDecryption: true, // Android only
failIfExists: true, // Prevent overwriting existing key
),
);
if (result.code == BiometricError.success) {
print('Public Key: ${result.publicKey}');
} else if (result.code == BiometricError.keyAlreadyExists) {
print('Key already exists for this alias');
}
createSignature({ payload, keyAlias, config, signatureFormat, keyFormat, promptMessage })Prompts the user for biometric authentication and generates a cryptographic signature.
payload: The data to signkeyAlias: Which key to sign with. Defaults to the default alias.config: CreateSignatureConfig with platform optionssignatureFormat: Output format for signaturekeyFormat: Output format for public keypromptMessage: Custom authentication prompt| Option | Platforms | Description |
|---|---|---|
allowDeviceCredentials |
Android | Allow PIN/pattern fallback |
promptSubtitle |
Android | Subtitle for biometric prompt |
promptDescription |
Android | Description for biometric prompt |
cancelButtonText |
Android | Cancel button text |
Future<SignatureResult>.
signature: The signed payload.publicKey: The public key.code: BiometricError code.final result = await biometricSignature.createSignature(
payload: 'Data to sign',
keyAlias: 'payment_key', // Optional: use named key
promptMessage: 'Please authenticate',
signatureFormat: SignatureFormat.base64,
keyFormat: KeyFormat.base64,
config: CreateSignatureConfig(
allowDeviceCredentials: false,
),
);
createSignatureFromBytes({ payload, keyAlias, config, signatureFormat, keyFormat, promptMessage })Prompts the user for biometric authentication and generates a cryptographic signature over raw binary data. This is ideal for challenge-response authentication flows where a random nonce is generated as raw bytes.
payload: The raw byte data (Uint8List) to signkeyAlias: Which key to sign with. Defaults to the default alias.config: CreateSignatureConfig with platform optionssignatureFormat: Output format for signaturekeyFormat: Output format for public keypromptMessage: Custom authentication prompt| Option | Platforms | Description |
|---|---|---|
allowDeviceCredentials |
Android | Allow PIN/pattern fallback |
promptSubtitle |
Android | Subtitle for biometric prompt |
promptDescription |
Android | Description for biometric prompt |
cancelButtonText |
Android | Cancel button text |
Future<SignatureResult>.
signature: The signed payload.signatureBytes: The raw signature bytes.publicKey: The public key.code: BiometricError code.final random = Random.secure();
final nonceBytes = Uint8List.fromList(
List<int>.generate(32, (_) => random.nextInt(256)),
);
final result = await biometricSignature.createSignatureFromBytes(
payload: nonceBytes,
keyAlias: 'payment_key', // Optional: use named key
promptMessage: 'Please authenticate',
signatureFormat: SignatureFormat.base64,
keyFormat: KeyFormat.base64,
config: CreateSignatureConfig(
allowDeviceCredentials: false,
),
);
decrypt({ payload, payloadFormat, keyAlias, config, promptMessage })Decrypts the given payload using the private key and biometrics.
payload: The encrypted data. See Encrypting a payload for the exact scheme the backend must use.payloadFormat: Format of encrypted data (PayloadFormat.base64, hex)keyAlias: Which key to decrypt with. Defaults to the default alias.config: DecryptConfig with platform optionspromptMessage: Custom authentication prompt| Option | Platforms | Description |
|---|---|---|
allowDeviceCredentials |
Android | Allow PIN/pattern fallback |
promptSubtitle |
Android | Subtitle for biometric prompt |
promptDescription |
Android | Description for biometric prompt |
cancelButtonText |
Android | Cancel button text |
Note: Decryption is not supported on Windows. On iOS, if a legacy v2.x-era unwrapped RSA private key exists in the keychain (default alias, no modern EC key), it is automatically migrated to the Secure Enclave on the first signing/decryption call. v11.x had a
shouldMigrateflag for this; v12 auto-detects.
Future<DecryptResult>.
decryptedData: The plaintext string.code: BiometricError code.final result = await biometricSignature.decrypt(
payload: encryptedBase64,
payloadFormat: PayloadFormat.base64,
keyAlias: 'payment_key', // Optional: use named key
promptMessage: 'Authenticate to decrypt',
config: DecryptConfig(
allowDeviceCredentials: false,
),
);
deleteKeys({ keyAlias })Deletes biometric key material for a specific alias from the device's secure storage.
Parameters:
keyAlias: Which key to delete. When null, deletes the default alias only. Other aliases are not affected.Returns: Future<bool>.
true: Keys were successfully deleted, or no keys existed (idempotent).false: Deletion failed due to a system error.Note: This operation is idempotent—calling
deleteKeys()when no keys exist will still returntrue. This allows safe "logout" or "reset" flows without checking key existence first.
// Delete a specific named key
final deleted = await biometricSignature.deleteKeys(keyAlias: 'payment_key');
// Delete the default key
final defaultDeleted = await biometricSignature.deleteKeys();
deleteAllKeys()Deletes all biometric key material across all aliases. This is a destructive operation — use deleteKeys() with a specific alias for targeted deletion.
Future<bool>.
true: All keys were successfully deleted.false: Deletion failed due to a system error.final deleted = await biometricSignature.deleteAllKeys();
if (deleted) {
print('All biometric keys removed across all aliases');
}
biometricAuthAvailable()Checks if biometric authentication is available on the device and returns a structured response.
Future<BiometricAvailability>.
canAuthenticate: bool indicating if auth is possible.hasEnrolledBiometrics: bool indicating if user has enrolled biometrics.availableBiometrics: List<BiometricType> (e.g., fingerprint, face).reason: String explanation if unavailable.final availability = await biometricSignature.biometricAuthAvailable();
if (availability.canAuthenticate) {
print('Biometrics available: ${availability.availableBiometrics}');
} else {
print('Not available: ${availability.reason}');
}
getKeyInfo({ keyAlias, checkValidity, keyFormat })Retrieves detailed information about existing biometric keys without prompting for authentication.
keyAlias: Which key to query. Defaults to the default alias.checkValidity: Whether to verify the key hasn't been invalidated by biometric changes. Default is false.keyFormat: Output format for public keys (KeyFormat.base64, pem, hex). Default is base64.Future<KeyInfo>.
exists: Whether any biometric key exists.isValid: Key validity status (only populated when checkValidity: true).algorithm: "RSA" or "EC".keySize: Key size in bits (e.g., 2048, 256).isHybridMode: Whether using hybrid signing/decryption keys.publicKey: The signing public key.decryptingPublicKey: Decryption key (hybrid mode only).final info = await biometricSignature.getKeyInfo(
keyAlias: 'payment_key', // Optional: query named key
checkValidity: true,
keyFormat: KeyFormat.pem,
);
if (info.exists && (info.isValid ?? true)) {
print('Algorithm: ${info.algorithm}, Size: ${info.keySize}');
print('Hybrid Mode: ${info.isHybridMode}');
}
biometricKeyExists({ keyAlias, checkValidity })Convenience method that wraps getKeyInfo() and returns a simple boolean.
keyAlias: Which key to check. Defaults to the default alias.checkValidity: Whether to check key validity. Default is false.Future<bool> - true if key exists and is valid.final exists = await biometricSignature.biometricKeyExists(
keyAlias: 'payment_key',
checkValidity: true,
);
simplePrompt({ promptMessage, config })Performs biometric authentication without performing any cryptographic operation. Useful for quick re-authentication or gating sensitive UI.
| Option | Platforms | Description |
|---|---|---|
subtitle |
Android | Subtitle for biometric prompt |
description |
Android | Description for biometric prompt |
cancelButtonText |
Android | Cancel button text |
allowDeviceCredentials |
Android/iOS/macOS | Allow PIN/pattern/passcode fallback |
biometricStrength |
Android | BiometricStrength.strong or BiometricStrength.weak |
final result = await biometricSignature.simplePrompt(
promptMessage: 'Verify your identity',
config: SimplePromptConfig(
subtitle: 'Access secure features',
allowDeviceCredentials: true,
biometricStrength: BiometricStrength.strong,
),
);
if (result.success == true) {
// Authenticated
} else {
print('Failed: ${result.code} - ${result.error}');
}
This section covers breaking changes and migration steps for upgrading between major versions. It assumes familiarity with the plugin’s core concepts (key creation, signing, biometric availability).
v7.0.0 replaced the legacy map-based createSignature() API with typed SignatureOptions.
createSignature() API ChangeBefore (v5/v6):
final signature = await biometricSignature.createSignature(
options: {
'payload': 'data to sign',
'promptMessage': 'Authenticate',
'cancelButtonText': 'Cancel', // Android
'allowDeviceCredentials': 'false', // Android
'shouldMigrate': 'true', // iOS
},
);
After (v7):
final signature = await biometricSignature.createSignature(
SignatureOptions(
payload: 'data to sign',
promptMessage: 'Authenticate',
androidOptions: AndroidSignatureOptions(
cancelButtonText: 'Cancel',
allowDeviceCredentials: false,
),
iosOptions: IosSignatureOptions(
shouldMigrate: true,
),
),
);
[!NOTE] v7 provided a temporary helper
createSignatureFromLegacyOptions()for migration, but this was removed in v8.
v8.0.0 introduced structured return types and configurable key/signature formats.
Before (v7): Methods returned String? or bool?.
After (v8): Methods return structured result objects with metadata.
| Method | v7 Return Type | v8 Return Type |
|---|---|---|
createKeys() |
String? |
KeyCreationResult? |
createSignature() |
String? |
SignatureResult? |
createKeys() ChangesBefore (v7):
final publicKey = await biometricSignature.createKeys(
androidConfig: AndroidConfig(useDeviceCredentials: false),
iosConfig: IosConfig(useDeviceCredentials: false),
);
// publicKey is a String?
After (v8):
final result = await biometricSignature.createKeys(
androidConfig: AndroidConfig(useDeviceCredentials: false),
iosConfig: IosConfig(useDeviceCredentials: false),
keyFormat: KeyFormat.pem, // NEW: choose output format
);
// result.publicKey, result.algorithm, result.keySize available
createSignature() ChangesBefore (v7):
final signature = await biometricSignature.createSignature(options);
// signature is a String?
After (v8):
final result = await biometricSignature.createSignature(
SignatureOptions(
payload: 'data',
promptMessage: 'Sign',
keyFormat: KeyFormat.base64, // NEW: output format
),
);
// result.signature, result.publicKey available
KeyFormat.base64, KeyFormat.pem, KeyFormat.hex, KeyFormat.rawdecrypt()MacosConfigv9.0.0 is a major refactoring that unifies platform configurations and migrates to Pigeon for type-safe platform communication.
AndroidConfig, IosConfig, MacosConfig) consolidated into single config classesBiometricError enum codesgetKeyInfo() for detailed key inspection, deleteKeys() returns Future<bool>createKeys() ChangesBefore (v8):
final result = await biometricSignature.createKeys(
androidConfig: AndroidConfig(
useDeviceCredentials: false,
signatureType: AndroidSignatureType.RSA,
enforceBiometric: true,
setInvalidatedByBiometricEnrollment: true,
enableDecryption: true,
),
iosConfig: IosConfig(
useDeviceCredentials: false,
signatureType: IOSSignatureType.RSA,
enforceBiometric: true,
setInvalidatedByBiometricEnrollment: true,
),
macosConfig: MacosConfig(
useDeviceCredentials: false,
signatureType: MacosSignatureType.RSA,
),
keyFormat: KeyFormat.pem,
);
After (v9):
final result = await biometricSignature.createKeys(
keyFormat: KeyFormat.pem,
promptMessage: 'Authenticate to create keys', // NEW: top-level
config: CreateKeysConfig(
signatureType: SignatureType.rsa, // Unified enum
enforceBiometric: true,
setInvalidatedByBiometricEnrollment: true,
useDeviceCredentials: false,
enableDecryption: true, // Android only
promptSubtitle: 'Subtitle', // Android only
promptDescription: 'Description', // Android only
cancelButtonText: 'Cancel', // Android only
),
);
if (result.code == BiometricError.success) {
print('Public Key: ${result.publicKey}');
} else {
print('Error: ${result.code} - ${result.error}');
}
createSignature() ChangesBefore (v8):
final result = await biometricSignature.createSignature(
SignatureOptions(
payload: 'data to sign',
promptMessage: 'Authenticate',
keyFormat: KeyFormat.base64,
androidOptions: AndroidSignatureOptions(
cancelButtonText: 'Cancel',
allowDeviceCredentials: false,
),
iosOptions: IosSignatureOptions(
shouldMigrate: true,
),
),
);
After (v9):
final result = await biometricSignature.createSignature(
payload: 'data to sign', // Top-level parameter
promptMessage: 'Authenticate', // Top-level parameter
signatureFormat: SignatureFormat.base64, // NEW: separate format
keyFormat: KeyFormat.base64, // Public key format
config: CreateSignatureConfig(
allowDeviceCredentials: false, // Android
promptSubtitle: 'Subtitle', // Android
promptDescription: 'Description', // Android
cancelButtonText: 'Cancel', // Android
shouldMigrate: true, // iOS
),
);
if (result.code == BiometricError.success) {
print('Signature: ${result.signature}');
}
biometricAuthAvailable() ChangesBefore (v8):
final availability = await biometricSignature.biometricAuthAvailable();
// Returns String? like "fingerprint", "face", "none", etc.
After (v9):
final availability = await biometricSignature.biometricAuthAvailable();
// Returns BiometricAvailability object
if (availability.canAuthenticate ?? false) {
print('Biometrics available: ${availability.availableBiometrics}');
// availableBiometrics is List<BiometricType>
} else {
print('Not available: ${availability.reason}');
}
decrypt() Changes (v8.4+ → v9)Before (v8):
final result = await biometricSignature.decrypt(
DecryptionOptions(
payload: encryptedBase64,
promptMessage: 'Decrypt',
androidOptions: AndroidDecryptionOptions(
allowDeviceCredentials: false,
),
iosOptions: IosDecryptionOptions(
shouldMigrate: true,
),
),
);
After (v9):
final result = await biometricSignature.decrypt(
payload: encryptedBase64,
payloadFormat: PayloadFormat.base64, // NEW: explicit format
promptMessage: 'Decrypt',
config: DecryptConfig(
allowDeviceCredentials: false, // Android
shouldMigrate: true, // iOS
),
);
if (result.code == BiometricError.success) {
print('Decrypted: ${result.decryptedData}');
}
getKeyInfo() Methodv9 introduces getKeyInfo() for inspecting existing keys without authentication:
final info = await biometricSignature.getKeyInfo(
checkValidity: true, // Check if key was invalidated
keyFormat: KeyFormat.pem,
);
if (info.exists ?? false) {
print('Algorithm: ${info.algorithm}'); // "RSA" or "EC"
print('Key Size: ${info.keySize}'); // 2048, 256, etc.
print('Hybrid Mode: ${info.isHybridMode}'); // Separate decrypt key?
print('Valid: ${info.isValid}'); // Not invalidated?
}
[!TIP]
biometricKeyExists()is now a convenience wrapper aroundgetKeyInfo().
| Change | v8 | v9 |
|---|---|---|
| Platform configs | AndroidConfig, IosConfig, MacosConfig |
CreateKeysConfig, CreateSignatureConfig, DecryptConfig |
| Signature type enum | AndroidSignatureType.RSA |
SignatureType.rsa |
| Error handling | Check for null |
Check result.code == BiometricError.success |
| biometricAuthAvailable | Returns String? |
Returns BiometricAvailability |
| Platform communication | MethodChannel with maps | Pigeon with typed classes |
| Windows support | ❌ | ✅ (v9.0.0+) |
Before (v8):
import 'package:biometric_signature/biometric_signature.dart';
import 'package:biometric_signature/android_config.dart';
import 'package:biometric_signature/ios_config.dart';
import 'package:biometric_signature/signature_options.dart';
After (v9):
import 'package:biometric_signature/biometric_signature.dart';
// All types exported from single import
v10.0.0 refines error handling and introduces non-cryptographic authentication.
BiometricError ChangesValues Added: New error codes were added to cover more edge cases:
BiometricError.securityUpdateRequiredBiometricError.notSupportedBiometricError.systemCanceledBiometricError.promptErrorsimplePrompt()v10 adds simplePrompt() for scenarios where you only need to verify the user's presence without cryptographic operations. See the Usage section for details.
v11.0.0 adds named key aliases, key overwrite protection, and internal architecture improvements. (Custom fallback options were also introduced in v11 but removed in v12 — see "Migrating from v11 to v12" below.)
All key operations now accept an optional keyAlias parameter:
// Before (v10.2) — single default key
final result = await biometricSignature.createKeys(...);
// After (v11.0) — multiple named keys
final authKey = await biometricSignature.createKeys(keyAlias: 'auth', ...);
final paymentKey = await biometricSignature.createKeys(keyAlias: 'payment', ...);
Methods updated: createKeys, createSignature, decrypt, deleteKeys, getKeyInfo, biometricKeyExists.
final result = await biometricSignature.createKeys(
keyAlias: 'payment',
config: CreateKeysConfig(failIfExists: true),
);
if (result.code == BiometricError.keyAlreadyExists) {
// Key already exists — handle accordingly
}
deleteAllKeys()// Delete all keys across all aliases
await biometricSignature.deleteAllKeys();
BiometricError ValuesBiometricError.keyAlreadyExists — returned when failIfExists: true and key exists.v12.0.0 lowers the minimum Flutter to 3.24.5 so the plugin builds out of the box on a much wider range of host projects. To make this possible, the Android-only custom fallback options feature added in v11 was removed (it required compileSdk = 36 / AGP 8.9.1 via androidx.biometric:1.4.0-alpha06).
Symbols removed:
BiometricFallbackOption classBiometricError.fallbackSelectedfallbackOptions field on CreateKeysConfig, CreateSignatureConfig, DecryptConfig, SimplePromptConfigselectedFallbackIndex / selectedFallbackText on SignatureResult, DecryptResult, SimplePromptResultshouldMigrate field on CreateSignatureConfig and DecryptConfig — the iOS Secure Enclave migration is now auto-detected (issue #65).If you were rendering Android 15+ custom fallback buttons via fallbackOptions, you can replicate the UX in Flutter: catch the cancel/userCanceled outcome and present your own bottom-sheet listing the alternative actions. The standard cancelButtonText and allowDeviceCredentials flow continues to work everywhere it did before.
shouldMigrate removal — what changed and whyIn v11.x and earlier, callers had to opt into the legacy v2.x → Secure Enclave RSA migration by passing shouldMigrate: true on CreateSignatureConfig / DecryptConfig. Two problems:
signatureType: SignatureType.ecdsa) and the caller still passed shouldMigrate: true, the iOS code would search for a non-existent legacy RSA key and fail with RSA private key not found in Keychain. Reported as issue #65.true (risking #1) or never set it (orphaning legacy keys).v12 removes the flag. The iOS plugin now checks the keychain itself: a migration is performed only when (a) keyAlias == nil, (b) no modern EC key exists, and (c) a legacy unwrapped RSA private key is actually present. If any of those is false, the call routes straight to the EC path (or, if a wrapped RSA already exists, the hybrid RSA path). Apps no longer need to know or care.
If you were already passing shouldMigrate: false, deleting the line is a no-op. If you were passing shouldMigrate: true, deleting the line is also safe — the auto-detection will run the migration in exactly the same scenarios where it would have succeeded under v11.x, and skip it cleanly in the scenarios where v11.x would have errored.