biometric_signature 13.3.0
biometric_signature: ^13.3.0 copied to clipboard
Hardware-backed biometric authentication for Flutter (Android, iOS, macOS, Windows). Create cryptographic signatures using Secure Enclave, StrongBox, and Windows Hello.
13.3.0 - 2026-10-24 #
Added #
- Where a key lives, and its effective policy. New
KeySecurityLevelenum (strongBox,trustedEnvironment,secureEnclave,secureHardware,software).KeyInfoandKeyCreationResultgainsecurityLevel, so apps can tell whether a key ended up in StrongBox, the TEE, the Secure Enclave or software. Before, that depended on the device and wasn't reported. Android reads it from the keystore'sKeyInfo. Android 9–11 can't tell StrongBox from the TEE, so keys on devices with StrongBox reportsecureHardwarethere. iOS/macOS reportsecureEnclavefor EC keys andsoftwarefor RSA keys, whose private key is stored encrypted by the Secure Enclave but signs in app memory. Windows reportsnull.KeyInfoalso gainsrequiresAuthentication,invalidatedByBiometricEnrollmentandallowsDeviceCredentials: the key's effective policy, which can differ from whatCreateKeysConfigasked for. For example, Android 10 and older ignoreuseDeviceCredentials, and an iOS/macOS key that accepts the passcode is never invalidated by enrollment changes.
listKeyAliases()lists the aliases that have a key, without a prompt, with the default alias asnull. Use it to find keys your app's records have lost, e.g. iOS/macOS keychain keys that survived an uninstall. On Windows it only finds aliases tracked in the app's settings, which an uninstall deletes, so after a reinstall only the default key is found.cancelButtonTextnow works on iOS and macOS forcreateKeys(withenforceBiometric),createSignature,createSignatureFromBytes,decryptandsimplePrompt, throughLAContext.localizedCancelTitle. It used to be Android-only.biometricAuthAvailable(biometricStrength:). Android checked only for strong (Class 3) biometrics, so a device whose only biometric is Class 2 reportedcanAuthenticate: falseeven thoughsimplePromptwithBiometricStrength.weakworks there. The default staysstrong; iOS, macOS and Windows ignore it. Useweakonly beforesimplePrompt: a Class 2 biometric can never unlock a key, so signing, decryption and key creation withenforceBiometricalways need a strong one.- Platform interface:
BiometricSignaturePlatformgainsbiometricAuthAvailableWithStrengthandlistKeyAliases. Both have default implementations, so implementations that extend it keep compiling and working:biometricAuthAvailableWithStrengthfalls back tobiometricAuthAvailable(), andlistKeyAliasesthrowsUnimplementedErroruntil implemented. Classes that implement the interface instead, such as mocks usingMockPlatformInterfaceMixin, need to add both, as with any new platform method.
Fixed #
- Android: a key created with
requireAuthentication: falsecouldn't sign or decrypt without a foreground activity.createSignature,createSignatureFromBytesanddecryptreturnedunknown("Foreground activity required") as soon as no activity was attached, although such a key never shows aBiometricPrompt. So a backgroundFlutterEngine, such as one handling a notification action while the app is closed, couldn't sign with the app's silent device key. The activity check now runs only before a prompt, so these keys work with no activity attached. Keys that need authentication still return "Foreground activity required" without an activity, andcreateKeysandsimplePromptstill need one. Because the check runs later, a missing key (keyNotFound) is now reported ahead of a missing activity, and so is an empty payload (invalidInput) when signing. (#86) - Android: decrypting with a
requireAuthentication: falseRSA key now uses the same OAEP parameters as other keys. That path built its own cipher, which named only the SHA-256 main digest and left the MGF1 digest to an undocumented AndroidKeyStore default, the reliance 13.0.0 removed from decryption with keys that prompt. It now pins SHA-256 and MGF1 SHA-1 too, and falls back the same way when a key doesn't accept that. The parameters are the ones AndroidKeyStore applied before, so existing ciphertext still decrypts. - Windows: a key under the alias
__default__was mistaken for the default key. The plugin records each alias that has a key in the app's settings, and it recorded the default key as__default__, the same as a named alias of that name. SodeleteAllKeysnever deleted a key named__default__,deleteKeyson the default key stopped tracking it, andlistKeyAliasesreported it as the default alias. The default key is no longer recorded, because it is always checked directly, so every alias,__default__included, is handled as itself. Settings written by earlier versions keep working.
Examples #
- The example app has an Android instrumented test,
BackgroundSigningTest, that createsrequireAuthentication: falsekeys with an activity attached, then signs with an EC key and decrypts with an RSA key from a secondFlutterEnginethat never had one. Run it on a device or emulator with./gradlew :app:connectedDebugAndroidTestfromexample/android. - The iOS example apps no longer crash at launch on iOS 27. iOS 27 doesn't launch an app built with Xcode 27 unless it adopts the UIScene life cycle, and the examples hadn't. Flutter's own scene support (
FlutterSceneDelegate) isn't available in Flutter 3.24.5, which the examples also build with, so each app'sAppDelegate.swiftnow runs aFlutterEnginethat aSceneDelegateshows, with the scene declared inInfo.plist. This works with Flutter 3.24.5 and newer versions alike.
13.2.0 - 2026-09-30 #
Added #
- Optional key attestation. New
CreateKeysConfig.attestationModedecides whatcreateKeysdoes whenattestationChallengeis set but the key can't be attested:enforceOnChallenge(default): fails, as in 13.1.0.enforceOnChallengeIfSupported: creates an unattested key when the device or platform can't attest keys (notSupported, including iOS, macOS and Windows), but still fails on transient keystore failures (notAvailable). Android 7–12 report transient failures asnotSupported, so this mode falls back there.preferred: creates an unattested key on any attestation failure.disabled: ignores the challenge.- When a mode falls back, the key is created in the same call, with no second prompt, and the new
KeyCreationResult.attestationErrorCode(notSupportedornotAvailable) andattestationErrorsay why. Every mode exceptdisabledstill returnsinvalidInputfor an empty or over-128-byte challenge.
Changed #
- An invalid
attestationChallengenow returnsinvalidInputon every platform. An empty or over-128-byte challenge used to returnnotSupportedon Android 6 (API 23), iOS, macOS and Windows. The challenge length is now checked first, in every mode exceptdisabled.
13.1.0 - 2026-09-27 #
Added #
- Android hardware key attestation. Fixes #69.
- New
CreateKeysConfig.attestationChallenge(Uint8List, 1–128 bytes, Android 7.0+). When set,createKeyspasses it toKeyGenParameterSpec.Builder.setAttestationChallenge, and returns the keystore's X.509 attestation chain for the new signing key in the newKeyCreationResult.attestationCertificateChain(List<Uint8List>, DER, leaf first). The plugin doesn't parse the chain; verify it on your server — see the README's "Hardware Key Attestation" section. getKeyInforeturns the same chain in the newKeyInfo.attestationCertificateChainfor keys created with a challenge. The chain is recognised by the key attestation extension (OID1.3.6.1.4.1.11129.2.1.17) on its leaf; unattested keys reportnull.- Attestation is an explicit opt-in, so
createKeysfails instead of returning an unattested key:invalidInputfor an empty or over-128-byte challenge, andnotSupportedon Android 6 (API 23). Both are checked before any existing key is touched.notSupportedwhen the keystore can't attest the key, andnotAvailablefor a transient keystore failure (e.g. attestation keys not provisioned yet — retry later with a fresh challenge). The transient case is detected on Android 13+; older versions reportnotSupported. As with any key-creation failure, the existing key under the alias has already been deleted by then; usefailIfExistsor a fresh alias to keep it.- If StrongBox key generation fails, the plugin retries once in the TEE. The chain's
attestationSecurityLevelshows which one produced the key.
- In hybrid mode (
ecdsa+enableDecryption) only the keystore EC signing key is attested; the software decryption key cannot be. - iOS/macOS/Windows: setting
attestationChallengereturnsnotSupportedwithout touching existing keys, instead of being ignored like other platform-specific fields — ignoring it would hand back an unattested key the caller believes is attested. Apple has no public API to attest an individual Secure Enclave key, and Windows attestation is not implemented. - No new
BiometricErrorvalues.
- New
Fixed #
-
Android: a failed hybrid-mode
createKeyscould leave a signing-only key behind. If the AES master key failed to generate after the EC signing key was created (ecdsa+enableDecryption),createKeysreturned an error but left the EC key under the alias, sogetKeyInforeported a non-hybrid EC key there (with an attestation chain, if a challenge was set). The partially created keys are now deleted before the error is returned, as they already were for failures later in key creation, and also if the Flutter engine detaches while the keys are being generated. -
Android: a missing key was reported as
unknowninstead ofkeyNotFound. Signing or decrypting with an alias that has no key (or whose hybrid-mode decryption key is gone) threw a generic exception that fell through tounknown. These cases now returnkeyNotFound, as they already did on iOS, macOS and Windows. -
iOS/macOS:
getKeyInfocouldn't return the public key of an RSA key, and reported it as hybrid. In RSA mode the RSA private key is stored wrapped by the Secure Enclave key, and its public key wasn't persisted, sogetKeyInforeturnedpublicKey: null(reading it back would have needed a prompt). The public key is now stored at creation, sogetKeyInforeturns the same keycreateKeysdid. Keys created by earlier versions store it on their next successful sign or decrypt.isHybridModeis nowfalsefor these keys in bothcreateKeys(previously unset) andgetKeyInfo(previouslytrue). The same RSA key signs and decrypts, so this isn't hybrid mode (separate signing and decryption keys) as the field is defined, and it now matches Android's RSA mode. Encrypt RSA-OAEP payloads againstpublicKey; the README previously saiddecryptingPublicKey, which isnullfor these keys.
-
iOS/macOS: a key invalidated by a biometric enrollment change was never reported as
keyInvalidated. Signing or decrypting with a.biometryCurrentSetkey after a face/fingerprint was enrolled or removed surfaced whatever the Secure Enclave returned, usuallyunknownorauthenticationFailed, so apps couldn't tell that the key had to be re-created. The plugin now compares the enrollment snapshot saved at key creation before prompting, and returnskeyInvalidatedwithout showing a prompt, as Android does.getKeyInfo(checkValidity: true)uses the same check, which also fixes two false reports of an invalid key:- A key created with
useDeviceCredentials: trueuses.userPresence, which the device passcode can always satisfy, so an enrollment change never invalidates it. It was nevertheless recorded as invalidatable and reportedisValid: falseafter one. Such keys are no longer recorded or reported as invalidatable. - Biometric lockout, and keys created before v8.1.0 (always
.biometryAnyor.userPresence), no longer reportisValid: false.
- A key created with
-
createSignatureanddecryptnow reject an empty payload before any prompt.- iOS/macOS
createSignaturesigned an empty payload.createSignature(payload: '')went on to sign zero bytes, where Android and Windows returninvalidInput. It now returnsinvalidInput("Payload is required") before any key lookup or prompt, ascreateSignatureFromBytesalready did. Android still also rejects a whitespace-only string; iOS, macOS and Windows sign it. - iOS/macOS
decryptchecked the payload only after accessing the key, in RSA mode after the user had authenticated. An empty or whitespace-only payload then failed withunknownorinvalidInput("Invalid payload"), depending on the format. Both now returninvalidInput("Payload is required") before any key lookup or prompt, as on Android. - Android
decryptreturnedunknownfor an empty payload when no activity was in the foreground, because it checked for the activity first. It now checks the payload first, ascreateSignaturedoes. - Windows
decryptis unchanged: it returnsnotAvailable, since Windows doesn't support decryption.
- iOS/macOS
-
iOS/macOS: RSA-mode
decryptcould return garbage as a successful result. When RSA-OAEP decryption failed,decryptfell back to PKCS#1 v1.5, for ciphertext made for keys created before v11.0.0. Current iOS and macOS releases decrypt PKCS#1 v1.5 with implicit rejection: invalid ciphertext yields pseudorandom bytes instead of an error. So corrupted or tampered OAEP ciphertext fell through to the fallback, and roughly once in 100 to 300 attemptsdecryptreturnedsuccesswith a meaningless, often empty, string (measured on iOS 26.5, iOS 27 and macOS 27).- Keys created with v11.1.0 or later now decrypt RSA-OAEP only, as Android keys created since v11.0.0 always have. If your server still encrypts PKCS#1 v1.5 for such keys, switch it to RSA-OAEP with SHA-256 and MGF1-SHA-256 (see "Encrypting a payload" in the README); PKCS#1 v1.5 ciphertext now fails with
unknown. - Keys created before v11.1.0, and keys migrated from v2.x, keep the PKCS#1 v1.5 fallback, so their existing ciphertext still decrypts. Keys from 11.0.x can't be told apart from older ones, so they keep it too. The fallback now checks the padding itself (RFC 8017), so invalid ciphertext is rejected instead of turning into garbage.
- Keys created with v11.1.0 or later now decrypt RSA-OAEP only, as Android keys created since v11.0.0 always have. If your server still encrypts PKCS#1 v1.5 for such keys, switch it to RSA-OAEP with SHA-256 and MGF1-SHA-256 (see "Encrypting a payload" in the README); PKCS#1 v1.5 ciphertext now fails with
-
decryptreported a malformed payload only after the user had authenticated. In RSA mode on iOS/macOS, and for every key that needs authentication on Android, the payload was decoded only after the prompt. It's now decoded first: input that isn't valid forpayloadFormatreturnsinvalidInputbefore any key access or prompt. So does input that can't be an ECIES payload (too short, or not starting with an uncompressed ephemeral key) for Android hybrid keys and iOS/macOS EC keys, once the key checks pass. It used to fail only later, sometimes withunknown.- Base64 is decoded strictly: spaces, tabs and line breaks are still ignored, but any other character outside the standard alphabet makes the payload invalid. Both platforms used to drop such characters and decode the rest, so
A!Q==decoded asAQ==and....as nothing.
- Base64 is decoded strictly: spaces, tabs and line breaks are still ignored, but any other character outside the standard alphabet makes the payload invalid. Both platforms used to drop such characters and decode the rest, so
-
README: the ECIES section said no per-platform branch was needed. It is: Android derives a 16-byte AES key and a 12-byte GCM IV from the shared secret with empty shared info, while Apple's
eciesEncryptionStandardX963SHA256AESGCMuses the ephemeral public key as shared info, derives only the key, and uses a 16-byte zero IV. A payload encrypted for one platform fails on the other. "Encrypting a payload" now gives both, and notes thatdecryptreturns UTF-8 text.
Examples #
- The example apps are rebuilt around what the plugin does beyond a biometric prompt; see EXAMPLES.md.
example/is now an API Explorer covering every method, option, format and error code, and the scenario apps (passwordless_login,banking_app, andsecure_vault, formerlydocument_signer) verify signatures and Android attestation chains in an in-process mock server instead of only checking that a signature is non-empty. - The example apps now target iOS 15.0 and macOS 12.0, the oldest versions Xcode 27 builds for. The plugin itself still supports iOS 13.0 and macOS 10.15.
- Xcode 27 also fails on pods that declare an older minimum, including this plugin's, so the examples' Podfiles raise pod targets to the app's in
post_install. Flutter 3.47 and later do this for you; on older versions, an app that builds with Xcode 27 through CocoaPods needs the same step.
- Xcode 27 also fails on pods that declare an older minimum, including this plugin's, so the examples' Podfiles raise pod targets to the app's in
13.0.0 - 2026-07-25 #
Added #
- Two new descriptive
BiometricErrorcauses:authenticationFailed(an attempt that didn't succeed — unrecognised biometric or the platform couldn't process it; retrying usually works) andnotInteractive(the prompt couldn't be shown, e.g. the app is backgrounded). Whether to retry or degrade is left to the consumer.
Changed (breaking) #
- Errors that used to surface as
unknownare now classified more precisely:- iOS:
authenticationFailedand the observed-1000code →authenticationFailed;notInteractive→notInteractive; app-cancel →systemCanceled;-1018and keychainerrSecInteractionNotAllowed→notAvailable; keychainerrSecAuthFailed→authenticationFailed. - Android:
UNABLE_TO_PROCESS(2) and "key user not authenticated" →authenticationFailed. Pruned keystore ops andTIMEOUT(3) stayunknownwith the enriched message; the retry-once for pruned ops still applies.
- iOS:
- Adding enum values is breaking for exhaustive
switches onBiometricError. CreateKeysConfig.setInvalidatedByBiometricEnrollmentnow defaults totrueon every platform. The default diverged per platform: Android usedtrue, iOS/macOS usedfalse, so the samecreateKeyscall produced keys with different lifetimes on each platform. All platforms now default totrue, matching AndroidKeyStore's own default for auth-bound keys (mInvalidatedByBiometricEnrollment = true) and the flag's documented security intent. Fixes #70.- Android: unchanged.
- iOS/macOS: a
createKeyscall that leaves the flag unset now produces a.biometryCurrentSetSecure Enclave key instead of.biometryAny, so the key is permanently invalidated when a face/fingerprint is enrolled or removed and the app must create a new key and re-enroll its public key. PasssetInvalidatedByBiometricEnrollment: falseto keep the previous behaviour. UsegetKeyInfo(checkValidity: true)to detect an invalidated key before signing. - The flag remains ignored on Windows, and is ignored when
requireAuthenticationisfalse— a key created without a user-authentication constraint carries no biometry binding, so it is neither invalidated nor reported as invalid on enrollment changes.
Fixed #
-
Android: an explicit
setInvalidatedByBiometricEnrollment: falsewas silently ignored.KeyManager.configureInvalidationonly ever calledKeyGenParameterSpec.Builder.setInvalidatedByBiometricEnrollment(true)and skipped the call entirely forfalse. Since AndroidKeyStore's own default istrue, opting out left the platform default in place and the key was still permanently invalidated on the next biometric enrollment — with no error to indicate the request had been dropped. The setter is now always called with the caller's value (API 24+; API 23 has no setter and always invalidates). -
Android: the plugin failed to configure under Android Gradle Plugin 9. AGP 9 ships "built-in Kotlin" — it supplies the Kotlin toolchain itself — and rejects
kotlin-androidwith "The 'org.jetbrains.kotlin.android' plugin is no longer required for Kotlin support since AGP 9.0", soandroid/build.gradleaborted atapply plugin: "kotlin-android". AGP 9 also removedkotlinOptions {}from the Android extension. The plugin now applieskotlin-androidonly when AGP has not already registered akotlinextension, and sets the JVM target throughkotlin { compilerOptions { jvmTarget } }, which works on both toolchains. Verified building on AGP 8.9.1/Gradle 8.12/Flutter 3.24.5, and on AGP 9.3.1/Gradle 9.6.1/Flutter 3.44.8 withandroid.builtInKotlinbothtrue(the AGP 9 default) andfalse(what Flutter's AGP 9 migrator writes). Fixes #78.compileSdkstays at35: a library'scompileSdkis a floor for its consumers, not a ceiling, so apps oncompileSdk36/37 already build against it. Raising it would force every consuming app to raise theirs.- On AGP 9, Flutter prints
WARNING: ... plugins that apply Kotlin Gradle Plugin (KGP): biometric_signature. It is a text match on this plugin's build file, not a reflection of what runs, and is safe to ignore — see the README.
-
lintOptions {}(deprecated) replaced with the equivalentlint {}block.
Fixed(android): pin RSA-OAEP MGF1 digest to SHA-1 for decryption #
AndroidKeyStore defaults to SHA-1 for the MGF1 digest in RSA-OAEP, even when SHA-256 is used as the main digest. To prevent future platform changes from making existing ciphertexts undecryptable, this change explicitly pins the RSA-OAEP parameters.
12.1.0 - 2026-06-24 #
Changed #
- Signing failures now report a meaningful error code instead of
unknown(iOS/macOS).createSignaturepreviously hard-codedcode: .unknownon everySecKeyCreateSignaturefailure, discarding the real cause.classifySigningErrornow walks theCFError'sNSUnderlyingErrorKeychain and maps the first layer it recognises —LAErrorDomainviamapLAError,NSOSStatusErrorDomain(e.g.errSecUserCanceled/ -128) viamapSecError— so a cancelled or unavailable signing prompt surfacesBiometricError.userCanceled/BiometricError.notAvailableregardless of whether the cancel arrives as the top-levelCFError, an OSStatus layer, or a nestedLAError(e.g. a localized"Authentifizierung abgebrochen."cancel). The"Signing Error: …"message is additionally enriched with the fulldomain:codechain (e.g.… [CryptoTokenKit:-4 -> com.apple.LocalAuthentication:-2]) so any still-unmapped failure is self-describing in logs. Both the RSA and EC signing paths are covered. - Android
UNKNOWNerrors are no longer flattened to a constant string.ErrorMapper.safeErrorMessageused to collapse every unmapped failure to"Biometric operation failed", throwing away both the numericBiometricPromptcode and the originalerrString. TheUNKNOWNbranch now appends whatever context is available, e.g."Biometric operation failed (code=3 msg=…)", keeping failures diagnosable in logs without an API/schema change.
Added #
-
Non-interactive (no user authentication) keys via
CreateKeysConfig.requireAuthentication. Defaults totrue(existing behaviour). When set tofalse, the key pair is created without a use-time user-authentication constraint and can be used to sign/decrypt without any biometric or device-credential prompt — useful for a device-bound key that lives alongside an interactive (biometric) key under a differentkeyAlias.- Android: the keystore key is generated without
setUserAuthenticationRequired(true)(and without per-operation auth / invalidation), andcreateSignature/decryptdetect the key'sKeyInfo.isUserAuthenticationRequiredand skip theBiometricPromptentirely. - iOS/macOS: the Secure Enclave key is created with only
.privateKeyUsageaccess control (no.biometryAny/.userPresence) andkSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, so signing/decryption never prompt while the device is unlocked. - Windows: ignored — Windows Hello always authenticates.
- Security note: a non-interactive key provides device binding ("something you have") only; it does not verify user presence and cannot satisfy inherence-based SCA requirements.
- Android: the keystore key is generated without
-
New
createSignatureFromBytesmethod: Introduced a high-level API to request biometric signatures directly over raw binary payloads (Uint8List), supporting secure challenge-response and transaction validation patterns across Android, iOS/macOS and Windows. -
Binary Challenge-Response Demo Card: Added a demonstration card inside the example app showcasing the complete secure random nonce generation and direct signing workflow.
-
Android: three more
BiometricPrompterror codes are classified.ErrorMapper.mapToBiometricErrornow mapsERROR_NO_BIOMETRICS(11) →notEnrolled,ERROR_HW_NOT_PRESENT(12) →notAvailable, andERROR_SECURITY_UPDATE_REQUIRED(15) →securityUpdateRequired.ERROR_TIMEOUT(3) andERROR_VENDOR(8) intentionally remain enriched-UNKNOWNso their volume can be observed before assigning a dedicated bucket. -
Android: error-source markers. Failures now carry a short source tag so a "couldn't even show the prompt" failure isn't mistaken for a signing failure:
[src=prompt cred=<allowDeviceCredentials>]for realonAuthenticationErrorcallbacks,[src=availability]forcanAuthenticatepre-checks, and[src=launch]for setup/launch failures (e.g. the host is not aFragmentActivity).CancellationExceptionis passed through unchanged so coroutine cancellation semantics and itsuserCanceledclassification are preserved.
Fixed #
- Android: transient Keystore operation pruning during signing is now retried. The crypto-bound signing operation is opened at
prepareSignature()and held open across the entireBiometricPromptinteraction. While a device-credential (PIN/pattern) prompt is showing, the app is backgrounded and its operation has the lowest pruning resistance, sokeystore2can evict it (INVALID_OPERATION_HANDLE/"outcome: Pruned") whenever another operation needs a slot — which previously surfaced as a hard"Biometric operation failed". Since the key is intact,createSignaturenow detects this specific transient failure (ErrorMapper.isPrunedOperationError) and transparently re-runs begin → authenticate → sign once before giving up. Pruning is contention-based, not a timeout, so it can strike no matter how long the user takes on the credential screen.
12.0.1 - 2026-05-28 #
Fixed #
- Android prompt customization fields are now honored on
createKeys,createSignature, anddecrypt. The Dart API and Pigeon schema have always exposedpromptSubtitle,promptDescription, andcancelButtonTextonCreateKeysConfig/CreateSignatureConfig/DecryptConfig, and the README documented them, but the Android plugin was silently dropping them —createKeysanddecrypthard-codednull, null, "Cancel"into theBiometricPrompt, andcreateSignaturehard-codednullfor the description. All three are now threaded through toBiometricPrompt.PromptInfo.Builder. Fixes #67. - Release-build biometric type detection on Android. Added ProGuard/R8 keep rules for
androidx.biometric.BiometricManager#getStrings(int)andBiometricManager$Strings#getButtonLabel()/getPromptMessage()/getSettingButtonLabel(), which are invoked via reflection byBiometricPromptHelper.detectBiometricTypesto disambiguate face/fingerprint/iris on devices that advertise multiple BIOMETRIC_STRONG modalities. Without these rules, R8 would rename or strip the methods in release builds and the label-matching path would silently fall back to feature-flag-only detection.
12.0.0 - 2026-05-09 #
Changed #
- Lowered minimum Flutter to
3.24.5/ Dart to3.5.0. The plugin resolves on Flutter 3.24.5 with a small Android build-config override in the consuming app — Flutter 3.24.5's defaults (flutter.compileSdkVersion = 34,flutter.ndkVersion = "23.1.7779620",flutter.minSdkVersion = 21) are below whatandroidx.biometric:1.4.0-alpha05and modern AndroidX plugins require. SetcompileSdk = 35,ndkVersion = "27.0.12077973", andminSdk = 23in your app'sandroid/app/build.gradle.kts. See README → "Required Android build configuration" for the exact snippet. - Android
minSdklowered to 23, the floor required byandroidx.biometricforBiometricPrompt. - Android
compileSdklowered to 35 (from 36). The plugin no longer requires Android SDK Platform 36 or AGP 8.9.1 —compileSdk = 35and AGP8.6.0are sufficient. androidx.biometric:biometricdowngraded from1.4.0-alpha06→1.4.0-alpha05, which only requirescompileSdk 35and AGP8.6.0. Plugin's buildscript classpath also dropped to AGP8.6.0to match.- Pigeon dev dependency loosened from
25.3.2→^25.3.2.
Fixed #
- iOS
shouldMigrate: trueagainst an existing EC key no longer errors. Previously, settingshouldMigrate: trueonCreateSignatureConfig/DecryptConfigagainst a v10+ EC-only key (no legacy v2.x RSA in the keychain) triggered a misfired migration that returnedRSA private key not found in Keychain. The migration is now auto-detected from keychain state instead of a caller-supplied flag, so the misfire scenario is structurally unreachable. Fixes #65.
Removed (Breaking) #
- Custom fallback options on the biometric prompt (Android 15+). The
androidx.biometricAPI forFallback.CustomOption,Fallback.ICON_TYPE_*, andAuthenticationResult.CustomFallbackSelectedonly exists in1.4.0-alpha06+, which forcedcompileSdk = 36. Removed entirely so the plugin can run on broader tooling.- Removed
BiometricFallbackOptionclass. - Removed
BiometricError.fallbackSelectedenum value. - Removed
fallbackOptionsfield fromCreateKeysConfig,CreateSignatureConfig,DecryptConfig, andSimplePromptConfig. - Removed
selectedFallbackIndexandselectedFallbackTextfields fromSignatureResult,DecryptResult, andSimplePromptResult.
- Removed
shouldMigrateflag onCreateSignatureConfigandDecryptConfig. The iOS Secure Enclave migration from v2.x unwrapped RSA keys is now auto-detected (see Fixed above). Apps no longer need to opt in — and no longer can opt in incorrectly. Pigeon-bridged removal, so existing call sites that passshouldMigrate: trueorshouldMigrate: falsewill fail to compile until the parameter is dropped.
Migration from 11.x #
- If you were using
BiometricFallbackOptionto render Android 15+ custom fallback buttons, you now need to drive that fallback behaviour from your own UI (e.g. catch the negative-button cancel, then present a Flutter sheet listing the alternatives). The plugin's standardcancelButtonTextandallowDeviceCredentialsflow still work everywhere they did before. - If you were branching on
BiometricError.fallbackSelectedor readingselectedFallback*fields, those code paths can be deleted — they will never fire from the new plugin version. - Delete the
shouldMigrate:line from anyCreateSignatureConfig(...)/DecryptConfig(...)constructor calls. The plugin auto-detects whether a legacy v2.x RSA key exists and migrates it on the first sign/decrypt call when appropriate.
11.1.0 - 2026-04-17 #
Added #
isDeviceLockSet()API: New method onBiometricSignatureto check whether a device-lock credential is configured. Android usesKeyguardManager.isDeviceSecure()(authoritative). iOS/macOS evaluateLAPolicy.deviceOwnerAuthentication;truemeans "set or indeterminate" — a stronger guarantee surfaces via the reactiveBiometricError.passcodeNotSetduring the next operation. Windows reports Windows Hello availability viaKeyCredentialManager.IsSupportedAsync(), not a generic screen-lock state — see the dartdoc for details.AuthenticationTypereporting: NewAuthenticationTypeenum (credential,biometric,unknown) plus anauthenticationTypefield onKeyCreationResult,SignatureResult,DecryptResult, andSimplePromptResult. Authoritative on Android (fromBiometricPrompt.AuthenticationResult). Inferred on Apple platforms from the key's storeduseDeviceCredentialsflag and biometric hardware availability; returns.unknownwhen the stored flag is unavailable rather than falsely reporting.biometric. Always.unknownon Windows.BiometricError.passcodeNotSet: Dedicated error code for "device has no screen lock / passcode configured", distinct fromnotAvailable.
Fixed #
- iOS/macOS
authenticationTypeinference: TheuseDeviceCredentialsflag is now persisted in the keychain at key-creation time and read during sign/decrypt, replacing the previous signing-time heuristic that could not produce an accurate result. - iOS/macOS keychain accessibility: The
DeviceCredentialsSettingkeychain item is now created withkSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, matching the lifetime of the Secure Enclave key it accompanies and keeping the flag device-local. - https://github.com/chamodanethra/biometric_signature/issues/62 was fixed.
Changed #
- Breaking (behavioural) — Android error mapping:
ERROR_NO_DEVICE_CREDENTIAL(cause code 14) now maps toBiometricError.passcodeNotSetinstead ofBiometricError.notAvailable. Consumers that were pattern-matching onBiometricError.notAvailableto drive a "no screen lock" UX must update their switch statements to handleBiometricError.passcodeNotSet. This is a minor-version bump because the Dart API surface is unchanged; only the runtime error value differs. - Breaking (behavioural) — iOS/macOS error mapping:
kLAErrorPasscodeNotSetnow maps toBiometricError.passcodeNotSetinstead ofBiometricError.notAvailable. Same migration guidance as above.
11.0.2 - 2026-04-02 #
- Documentation updates.
- Bug fixes.
11.0.0 - 2026-03-28 #
Added #
- Named Key Aliases: All key operations (
createKeys,createSignature,decrypt,deleteKeys,getKeyInfo,biometricKeyExists) now accept an optionalkeyAliasparameter, allowing apps to manage multiple independent key pairs (e.g., one for auth, one for payment signing). - Key Overwrite Protection:
CreateKeysConfig.failIfExistsprevents accidental key replacement. Whentrue,createKeys()fails withBiometricError.keyAlreadyExistsif a key with the specified alias already exists. - Delete All Keys: New
deleteAllKeys()method removes all plugin-managed keys across all aliases. - Custom Fallback Options (Android 15+): All config classes (
CreateKeysConfig,CreateSignatureConfig,DecryptConfig,SimplePromptConfig) now supportfallbackOptions— a list ofBiometricFallbackOptionitems that appear as custom buttons on the biometric prompt. When the user taps a fallback option, the result containsBiometricError.fallbackSelectedwithselectedFallbackIndexandselectedFallbackText. - OAEP Padding for RSA: RSA encryption now uses OAEP padding with PKCS#1 v1.5 fallback for improved security.
- Atomic File Writing: Sensitive key material writes use atomic file operations to prevent data corruption.
Fixed #
KEY_INVALIDATEDError Mapping:BiometricError.keyInvalidatedis now correctly returned when keys have been invalidated by biometric enrollment changes on Android.- Android Coroutine Dispatching: Key management operations now use proper coroutine dispatchers (IO for file/crypto operations).
- CancellationException Handling: Structured concurrency is preserved —
CancellationExceptionis always rethrown, preventing callbacks to a detached Flutter engine. - Hybrid Key Cleanup: If the second biometric authentication fails during hybrid key creation, partially-created keys are now cleaned up.
Changed #
- Modularized Android Architecture: Extracted Android plugin into focused helper classes (
BiometricPromptHelper,CryptoOperations,ErrorMapper,FileIOHelper,FormatUtils,KeyManager). - Unified macOS/iOS Plugin: Consolidated Swift plugin code using conditional compilation (
#if os(macOS)) to support both platforms from a single source file. - Refined ProGuard Rules: Updated Android ProGuard/R8 rules for better compatibility.
10.2.0 - 2026-03-03 #
- Enhance biometric key security on iOS/macOS.
- Enforce CryptoObject usage for Android signatures.
- Add Android ProGuard rules.
10.1.0 - 2026-02-19 #
- Added Swift Package Manager (SPM) support for iOS and macOS plugin integration.
- Migrated iOS/macOS native source layout to
Package.swift+Sources/<plugin_name>/. - Updated Pigeon generation output paths for Darwin host code to match the SPM layout.
- Updated CocoaPods podspecs to remain compatible alongside SPM.
- Updated package and platform version references/documentation.
- Fix: Enhanced biometric type detection for android.
10.0.0 - 2026-02-06 #
- Breaking: Added new
BiometricErrorenum values; consumers using exhaustive switches must handle the new cases(security update required, not supported, system canceled, prompt error). - Feature: Added
simplePrompt()for lightweight biometric authentication without cryptographic operations. - Fix: Improved Android error handling and prompt robustness.
- Fix: Aligned iOS/macOS error mapping with Android.
- Docs: Updated README and usage examples.
9.0.3 - 2026-01-25 #
- Reduced published package size by ~45%.
9.0.2 - 2026-01-24 #
- Package Optimization: Reduced published package size significantly:
- Converted
assets/logo.png(1.0 MB) toassets/logo.jpeg(120 KB), reducing image size by ~90% - Added
.pubignoreto exclude example applications (banking_app,document_signer,passwordless_login) from published package - Total package size reduced from ~2 MB to ~535 KB
- Converted
- Maintenance: Updated version references across all platform files and documentation.
- Minor bug fix.
- Enhanced the passwordless login example.
- Added "Migration Guide" section to the README.md.
9.0.1 - 2025-12-21 #
- Feature: Added "Biometric Decryption" section to
README.mdwith a detailed lifecycle diagram (usecase-2.png) and process description. - Improved: Enhanced Windows platform documentation to clarify
KeyCredentialManagerusage, TPM backing, RSA-2048 constraints, and lack of decryption support. - Metadata: Updated
pubspec.yamldescription to explicitly include supported platforms and Windows Hello. - Maintenance: Updated Android native dependency.
9.0.0 - 2025-12-18 #
-
Breaking: Method signature changes:
createKeys()now takesconfig,keyFormat,promptMessageparameterscreateSignature()now takespayload,config,signatureFormat,keyFormat,promptMessageparametersdecrypt()now takespayload,payloadFormat,config,promptMessageparameters
-
Moved cross-platform parameters into unified config objects:
signatureType,enforceBiometric,setInvalidatedByBiometricEnrollment,useDeviceCredentialsnow inCreateKeysConfig- Each field is documented with which platform(s) it applies to
Architecture - Type-safe Communication with Pigeon #
- Breaking: Migrated entire platform communication layer to Pigeon.
- Breaking: Replaced raw string/map returns with structured strongly-typed objects:
KeyCreationResult: ContainspublicKey,error, andcode.SignatureResult: Containssignature,publicKey,error, andcode.DecryptResult: ContainsdecryptedData,error, andcode.BiometricAvailability: detailed availability status including enrolled biometric types and error reasons.
- Breaking: Standardized
BiometricErrorenum across all platforms.
API Improvements #
- Breaking:
biometricAuthAvailable()now returns aBiometricAvailabilityobject instead of a raw string. - Removed legacy
signature_options.dart,decryption_options.dartand old config classes. - Enhanced error handling with specific error codes (e.g.,
userCanceled,notEnrolled,lockedOut) instead of generic strings. - New
getKeyInfo()method: Retrieve detailed information about existing biometric keys without creating a signature.- Returns
KeyInfoobject with:exists,isValid,algorithm,keySize,isHybridMode,publicKey,decryptingPublicKey. - Accepts
checkValidityparameter to verify key hasn't been invalidated by biometric changes. - Accepts
keyFormatparameter to specify output format (base64, pem, hex).
- Returns
- New
KeyInfoclass: Exported via Pigeon for type-safe key metadata. biometricKeyExists()is now a convenience wrapper aroundgetKeyInfo().
Improved #
- Cleaner, simpler API with fewer method parameters
- Better documentation of platform-specific options
- Updated all example projects to use new API
8.5.0 - 2025-12-09 #
Added - macOS Platform Support #
Platform Integration
- Full macOS support for biometric authentication using Touch ID.
- Native macOS implementation via
BiometricSignaturePlugin.swift. - Support for macOS 10.15 (Catalina) and later.
- CocoaPods integration for seamless dependency management.
API and Configuration
- New
MacosConfigclass for platform-specific configuration:useDeviceCredentials: Enable device credentials (passcode) fallbacksignatureType: Support for bothMacosSignatureType.RSAandMacosSignatureType.ECDSAbiometryCurrentSet: Bind keys to current Touch ID enrollment state
- New Parameter: Added optional
promptMessageparameter tocreateKeys()method across all platforms- Allows customization of the authentication prompt when
enforceBiometricistrue - Defaults to
"Authenticate to create keys"for backward compatibility - Provides context-specific instructions to users during key generation
- Allows customization of the authentication prompt when
Security Features
- App-specific keychain isolation: Keychain identifiers now incorporate bundle identifier to prevent cross-app conflicts on macOS
- Each app's keys are completely isolated:
{bundleId}.eckey,{bundleId}.biometric_key, etc. - Solves the issue where multiple apps using the plugin would share the same keychain items
- iOS implementation remains unchanged as it already has proper sandboxing
- Each app's keys are completely isolated:
- Secure Enclave integration for EC key storage and operations
- Hardware-backed cryptographic operations using macOS Security framework
- Domain state tracking for biometric enrollment changes
Cryptographic Features
- RSA Mode:
- RSA-2048 hardware-backed signing
- Hybrid mode with software RSA decryption key wrapped via ECIES
- EC Mode:
- P-256 (secp256r1) hardware-backed signing in Secure Enclave
- Native ECIES decryption using
SecKeyAlgorithm.eciesEncryptionStandardX963SHA256AESGCM - Support for EC-only mode and hybrid EC mode
Implementation Details
- Biometry change detection via
LAContext.evaluatedPolicyDomainState - Automatic key invalidation when Touch ID enrollment changes (when
biometryCurrentSetistrue) - Support for all key formats: BASE64, PEM, RAW, HEX
- Consistent error handling and Flutter method channel integration
Changed #
- Updated platform interface to distinguish macOS from iOS
- Enhanced
BiometricSignaturePlatformto properly handle macOS-specific parameters - Updated documentation with macOS integration steps and examples
- Added macOS to platform support table (macOS 10.15+)
8.4.0 - 2025-11-28 #
Added #
- ECIES decryption on Android and iOS.
- X9.63-SHA256 KDF and AES-128-GCM support for elliptic-curve decryption.
- RSA decryption support via
decrypt()on Android and iOS. enableDecryptionoption inAndroidConfigto generate RSA keys with decryption capability.- Cross-platform ECIES support for P-256 (secp256r1) keys.
Android #
- Manual ECIES implementation using ECDH, X9.63 KDF, and AES-GCM.
- Software EC private key for decryption is encrypted using a biometric-protected AES-256 master key (Keystore/StrongBox).
- Wrapped EC private key blob is stored in app-private files with MODE_PRIVATE permissions.
- All sensitive key material is zeroized after use.
iOS #
- Native ECIES support through SecKeyAlgorithm.eciesEncryptionStandardX963SHA256AESGCM.
- Hybrid RSA mode: software RSA key for decryption encrypted via ECIES with Secure Enclave EC public key.
Architecture #
- Updated hybrid EC design:
- Android: hardware EC signing key + AES-wrapped software EC decryption key
- iOS: hardware EC signing key + ECIES-wrapped software RSA key
Misc #
- Expanded documentation and updated examples.
- Improved test coverage across decryption and hybrid modes.
8.3.1 - 2025-11-20 #
- Optimize iOS createKeys implementation.
- ReadMe.md was updated.
8.3.0 - 2025-11-20 #
- Added
enforceBiometricparameter tocreateKeys()method to require biometric authentication before generating the key-pair. - Added an optional subtitle parameter to Android biometric prompts via
AndroidSignatureOptions. - ReadMe.md and example updates.
8.2.0 - 2025-11-13 #
- Upgraded Flutter from 3.32.8 to 3.35.7
- Upgraded Dart SDK from ^3.8.1 to ^3.9.2
- iOS minimum deployment target upgraded from 12.0 to 13.0
- Android minimum SDK upgraded from 23 to 24
- Upgraded Android Gradle Plugin from 8.7.3 to 8.9.1
- Upgraded Android compileSdk from 35 to 36
- Refactored Android native code to use internal objects for error constants and key aliases
- Code quality improvements: formatting and style consistency updates across example projects
8.1.0 - 2025-11-09 #
- Added an optional parameter to configure whether the key should be invalidated on new biometric enrollment when creating the key.
8.0.0 - 2025-10-15 #
- Breaking:
createKeysnow returns aKeyCreationResultinstead of a plain base64 string, enabling configurable output formats. - Breaking:
createSignaturereturns aSignatureResultthat includes both the formatted signature and public key metadata. - Added
KeyFormatsupport across Dart, Android, and iOS with BASE64, PEM, RAW (DER/bytes), and HEX representations. - Android: refactored native layer to emit structured maps, generate PEM blocks directly, and expose raw DER bytes when requested.
- iOS: aligned public key formatting with SubjectPublicKeyInfo, added PEM/RAW/HEX conversions, and unified signature responses.
- Updated documentation, examples, and helper classes to illustrate working with
FormattedValueutilities. - Removed
createSignatureFromLegacyOptionshelper.
7.0.4 - 2025-10-03 #
- ReadMe.md updates.
- Reverting back to previous iOS IPHONEOS_DEPLOYMENT_TARGET(12.0).
- Added 3 practical-world examples.
7.0.3 - 2025-09-28 #
- Updating documentations.
- Minor bug fixes.
7.0.2 - 2025-09-26 #
- Fix formatting errors.
7.0.1 - 2025-09-26 #
- Updating documentations.
7.0.0 - 2025-09-26 #
- Breaking: Replace the map-based
createSignatureAPI with typedSignatureOptions, plus platform-specific option classes. - Added
createSignatureFromLegacyOptionshelper to ease migration from the legacy API. - Fixed Android
allowDeviceCredentialsparsing so boolean values are honoured. - Updated the iOS plugin to accept native booleans for
shouldMigrate. - Improved Android native Kotlin coroutines implementation.
- Updated native dependencies.
6.4.2 - 2025-09-21 #
- The migrate path for iOS from 5.x is preserved.
- ReadMe.md updates.
6.3.1 - 2025-09-02 #
- fix dart formatting errors.
6.3.0 - 2025-09-02 #
- Upgrading Flutter from 3.27.2 to 3.32.8.
- Updating the README.md file descriptions.
- Adding ECDSA Key support for cryptographic operations.
- Suggesting a fix for issue.
6.2.0 - 2025-01-15 #
- Upgrading Flutter from 3.27.0 to 3.27.2.
- Updating the README.md file descriptions.
- Device Credentials' fallback support for compatible devices can be configured.
6.1.0 - 2025-01-06 #
- Feature - Allow Device Credentials as a fallback for biometric authentication.
6.0.0 - 2024-12-29 #
- Upgrading Flutter from 3.19.6 to 3.27.0
5.1.1 - 2024-09-20 #
- ReadMe.md updates.
5.1.0 - 2024-09-19 #
- Feature Secure Enclave migration from Key Chain.
5.0.0 - 2024-09-15 #
- Secure Enclave integration in iOS.
4.2.0 - 2024-09-14 #
4.1.1 - 2024-08-26 #
- Fix linting issues.
4.1.0 - 2024-08-25 #
- Feature Use StrongBox in compatible android devices.
- Refactor key creation to use AndroidConfig object.
4.0.3 - 2024-07-27 #
- fix Local Authentication bypass in iOS when calling createSignature().
4.0.2 - 2024-07-22 #
- fix Biometric portal not coming up in iOS simulators when calling createSignature().
- General improvements.
4.0.1 - 2024-06-30 #
- A crash on Android devices below API level 28 was fixed.
- General improvements.
4.0.0 - 2024-06-12 #
- Fixed a bug in createKeys() for iOS.
- Fixed a bug in createSignature() for android.
- Error codes were updated to maintain consistency.
- Updated README.md and Licence content.
- Hardcoded default payload was removed.
- Improved error handling.
3.0.0 - 2024-06-02 #
New Features: #
- The plugin offers more flexibility for advanced use cases, such as handling different biometric modalities and customizing the signature generation process.
Bug Fixes: #
- Improved the handling of biometric prompt cancellations.
- Enhanced the accuracy of biometric authentication on some devices.
Other Changes: #
- Updated the plugin's documentation to reflect the new features and improvements.
- Migrated the plugin to use the latest Flutter development tools.
- Improved the overall performance and stability of the plugin.
- This version is now compatible with AGP >=7.3 including 8.x support.
Breaking Changes: #
- The minimum supported Flutter version has been increased to 3.3.0.
2.1.2 - 2024-06-14 #
2.1.1 - 2024-05-25 #
- Removes a redundant code push in Android native code.
- Updates README.md and the Example.
2.1.0 - 2024-05-24 #
- Returns "biometric" for Android devices with multiple BIOMETRIC_STRONG options when called biometricAuthAvailable().
- Let createSignature() accept a "payload" keyValue pair in options arg.
- updates dependencies.
- updates README.md and the Example.
2.0.0 - 2023-04-29 #
- Consistent Platform error handling.
- Upgrade dependencies.
1.0.5 - 2023-04-17 #
- improved documentation.
1.0.4 - 2023-04-16 #
- upgrading flutter sdk to 3.7.11.
- improved documentation.
1.0.3 - 2023-03-15 #
- upgrading dependencies.
- refactoring.
1.0.2 - 2023-02-07 #
- fixing createSignature's options param.
1.0.1 - 2023-01-29 #
- downgrade min Dart Sdk.
1.0.0 - 2023-01-29 #
- improved documentation.
0.0.1 - 2023-01-29 #
- initial release.