The launch-time auto-open received boUnavailable and wiped the cache, seconds after the write path had proved the device could authenticate. The diagnosis was wrong and the reaction irreversible. An automatic action now never clears the vault: it logs the cause and leaves the user on the unlock screen. Only an explicit tap on Open, followed by a clean refusal from the system, may clear it. PMA.Biometric keeps the reason in LastBiometricError, and a Java-layer exception now maps to boFailed rather than boUnavailable - an exception says nothing about the device. The log memo also wraps: it was cutting off exactly the end of the message, which is where the cause is named.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A pattern lock reported "no authentication here", so the cache was written
under the plain key.
FMX builds its authenticator mask from FBiometricStrengths, and that field
starts as the EMPTY SET - the constructor never initialises it. On API 30+
CanAuthenticate therefore calls canAuthenticate(0), which never answers
"available", whatever the device can actually do. Asking before setting the
strengths cannot succeed.
Both entry points now go through AcquireService, which sets Strong +
DeviceCredential and only then hands back the service. DeviceCredential is
the point: a phone with a pattern or a PIN and no enrolled fingerprint is
the common case, not an edge one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Opening the vault threw java.lang.SecurityException: USE_BIOMETRIC is not in
Delphi default permission list, and CanAuthenticate raises instead of
answering false. So the fault showed up neither at compile time nor at
startup, but on the first unlock - after the cache had already been written
under an auth-protected key.
The permission is the shallow half. The deep half is that SaveVault was
called with a hard-coded True: we asked for a key guarded by a prompt that
could not be shown, producing a file that was unreadable forever. It now
receives BiometricAvailable, evaluated on the main thread before the worker
starts.
BiometricAvailable and AuthenticateThen also swallow what comes up from the
Java layer and map it to boUnavailable. The caller has a path for every
TBioOutcome and none for an exception.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Keystore key now requires authentication, valid for 30 s afterwards - short enough that an unattended phone is not exploitable, long enough to decrypt and display. Time-bound rather than a per-use CryptoObject: BiometricPrompt's callback would need a Java class compiled into a jar, while FMX already bridges it.
Two aliases, because the auth constraint is fixed when the key is created and cannot be added later. The file header records which key wrote it, so a read knows whether to authenticate first.
A device with neither fingerprint nor lock screen cannot hold such a key, so SaveVault falls back to the plain key rather than giving up the cache. And a failed authentication no longer wipes the cache: the vault is intact, it is the user who cancelled - erasing it would cost a download and 4 s of derivation for a dismissed prompt.