
Why I deliberately didn't bind my Keystore key to biometric auth (Flutter password manager, now open source)
I've been building a zero-knowledge password manager (PassKeyra) in Flutter for the past year, and I just released it under the GPL-3.0 license.
The most interesting Flutter problem was biometric unlock, and I made a deliberate trade-off I'm not 100% sure about. I'd love this community's take.
The vault is encrypted with a key derived from the master password (PBKDF2-HMAC-SHA256, AES-256-GCM), entirely in Dart. For biometric unlock, I didn't want to keep that vault key in plaintext behind a simple fingerprint, so I wrap it with a hardware AES-256-GCM key stored in the Android Keystore.
Flutter_secure_storage is great for storing secrets, but it doesn't expose the wrap/unwrap primitives I needed. So I wrote a small native Kotlin plugin (BiometricKeywrapPlugin) over a MethodChannel: wrapKeyMaterial / unwrapKeyMaterial / clearWrappingKey. StrongBox is best-effort, with a fallback, because setIsStrongBoxBacked(true) doesn't throw at build time: it throws at generateKey() via StrongBoxUnavailableException on devices that don't have it (a lot of mid-range phones and tablets).
The obvious "secure" choice is setUserAuthenticationRequired(true), so the Keystore key only decrypts after a fresh biometric auth through a CryptoObject. I deliberately chose NOT to do that.
The reason: CryptoObject-bound auth requires STRONG biometric (Class 3). That silently breaks face unlock on a huge number of devices. Many Pixels and Samsung tablets only expose Class 1/2 / WEAK / CONVENIENCE. Users would lose biometric unlock for no visible reason. So the key is non-exportable and hardware-bound, but with setUserAuthenticationRequired(false), and biometric auth is enforced at the app layer via local_auth.authenticate() BEFORE wrap/unwrap is ever called.
Once auth passes, unwrap runs without a second prompt. The honest downside, which I left as a comment in the code: the key isn't cryptographically bound to strong biometric. It's the same model as Bitwarden and 1Password, but I'd rather say it plainly than pretend it's more solid than it is. The code (it's all there, including that comment): https://github.com/Pass-Keyra/PassKeyra
My questions for you:
Would you have forced Class 3 and accepted losing face unlock on a lot of devices?
Is there a cleaner pattern to get both broad device support AND CryptoObject-level binding in Flutter?
Thanks for reading this far, and a special thank you to anyone who takes the time to go look at the code. That's exactly the point of open source: you shouldn't have to take my word for it.
(This is a shipped freemium app, with an ad banner in the free version. I'm posting here for the technical discussion.)