Three ways the lock could fail badly, all in its storage.
hasPin had no catch, unlike the equivalent flow in
UserPreferencesRepository, and it is collected on the startup path in two
places that cannot handle a throw: the gate's stateIn, which launches
outside its own exception handler, and MainActivity's FLAG_SECURE
collector. A corrupt app_lock file crashed the app before any UI existed
to say why.
It now reads as no lock. That is a decision, not a shrug: whoever can
corrupt that file has the app's private storage and therefore reads the
database this lock does not encrypt anyway, while the alternative is an
app that can be neither opened nor erased -- because the way out is behind
the lock that is broken. It is the rule VerifierRecord.decode already
applies to a single corrupt record, extended to the file.
The store also gains a corruption handler. Without one, a single bad write
leaves DataStore unable to read OR write it: the lock can never be set
again, and the "Forgot your PIN?" erase fails too, since it writes here.
And a wrong PIN now costs time even when the counter cannot be signed.
Signing needs the Keystore, which can be briefly unavailable; the write
returned early, so guessing was free for as long as that lasted -- the one
direction this must not fail in. The unwritten counter is held in memory
and read back whenever it is the longer wait. Not persisted: it is the
delay actually earned rather than the maximum a tampered counter earns,
and it is forgotten on process death, which is the bound a reboot already
gives an attacker.
Writing that test found the honest boundary. If the key is gone entirely
the PIN cannot be checked either, so the app reports that it does not know
rather than charging for a guess it could not read; the failure worth
defending against is the counter alone failing while the verifier still
works. The fake now tells those apart by the domain-separation tag the two
uses already carry.
Proved with prove-guard, one red each: dropping the in-memory counter, and
removing the catch.
closes#64
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>