docs: log the app lock, and the three defects testing found
Step 6 of WORK_CYCLE. Next action is #35, export with the file encrypted — the only artifact that leaves the sandbox, and the precondition SECURITY.md records for ever revisiting database encryption. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fa1cfe5515
commit
5e9184f377
|
|
@ -32,6 +32,91 @@ written and stay true. It is exempt from review for the same reason a receipt is
|
|||
|
||||
## Entries
|
||||
|
||||
### 2026-08-19 — The app lock, and three defects only testing found
|
||||
|
||||
#34 is closed. The app asks for a PIN before it opens, and a forgotten PIN
|
||||
cannot be reset by anybody — which was the decision the issue had been waiting
|
||||
on since it was filed.
|
||||
|
||||
**The gate wraps the composition, not a screen.** Today, Calendar and Insights
|
||||
each start collecting from `CycleRepository` the moment they compose, so a lock
|
||||
built as a navigation destination — even the start destination — would have read
|
||||
the history before the user proved anything. `content()` is invoked only in the
|
||||
unlocked branch. There is no lazy tab to hide behind.
|
||||
|
||||
**What is stored is not the PIN.** `HMAC(keystoreKey, 0x01 || salt ||
|
||||
PBKDF2(pin, salt, 210k))`. The Keystore MAC is what makes a six-digit PIN safe
|
||||
at all — a million candidates is nothing to an attacker who can compute the
|
||||
hash, and impossible for one who cannot get the key off the device. PBKDF2
|
||||
underneath is for the day that assumption breaks.
|
||||
|
||||
**The omissions in the key spec are the design.** `AndroidKeyStoreMacProvider`
|
||||
sets three builder options and deliberately omits six, and its KDoc names every
|
||||
one. `setUserAuthenticationRequired` is the important absence: it would bind the
|
||||
key to the device lock, so changing a phone passcode would destroy it — and with
|
||||
no recovery, that is somebody's entire history gone for an unrelated reason. It
|
||||
would also be a bypass, since `SECURITY.md` already lists "someone who knows the
|
||||
unlock PIN" as an adversary this app cannot stop.
|
||||
|
||||
**Wrong PINs cost time and never cost data.** Four free attempts, then
|
||||
30s→1m→2m→5m→15m, capped forever, with no attempt limit and no auto-wipe. Under
|
||||
no-recovery an auto-wipe would hand a partner — or a child, or a pocket — the
|
||||
power to destroy a cycle history while knowing nothing at all. Both clock
|
||||
bypasses are closed: the wait is the longer of a wall-clock and a monotonic
|
||||
deadline, and a reboot re-applies it in full, detected by `elapsedRealtime`
|
||||
going backwards.
|
||||
|
||||
**Two writes had to move.** Tapping "Not yet" on a reminder writes a
|
||||
`NotYetObservation`, and that button sits on the phone's own lock screen where
|
||||
anybody can reach it — so the action is parked and applied only after an unlock,
|
||||
dropped if the session never unlocks. And the erase behind "Forgot your PIN?"
|
||||
deletes health data, then the Keystore key, then the lock store; skipping the
|
||||
middle step leaves the user erased **and still locked out**.
|
||||
|
||||
**Three defects, none found by reading the code.**
|
||||
|
||||
1. A fresh install began in a fifteen-minute lockout. "No counter yet" and
|
||||
"counter was tampered with" were the same value — `null` — and the policy
|
||||
treats tampering as maximum backoff. Caught the moment
|
||||
`AppLockRepositoryTest` ran, and it failed five tests at once.
|
||||
2. Setting a PIN locked you out of the session you set it in: the gate shut the
|
||||
instant `hasPin` turned true. Found by driving the emulator, not by any test,
|
||||
and it would have been the first thing a user saw of a feature they had just
|
||||
been warned was unrecoverable.
|
||||
3. A stray NUL byte inside a char literal made `AppLockViewModel.kt` **binary to
|
||||
git**. It compiled, and 244 tests passed. The only symptom was
|
||||
`Bin 0 -> 7069 bytes` in the commit summary where every other file showed a
|
||||
line count — and a source file git treats as binary produces no diff, so the
|
||||
one file that decides whether the app is locked would have been unreviewable
|
||||
in every future change.
|
||||
|
||||
**What this proved:** 244 JVM tests, 0 skipped. `KeystoreVerifierTest` ran on
|
||||
`PeriodMinSdk26` and `PeriodQA`, confirming `PBKDF2WithHmacSHA256` exists at API
|
||||
26 — the one algorithm choice here with no margin — and that the key is not
|
||||
auth-bound at either level. Removing the key-deletion line from the erase path
|
||||
fails exactly one test, prove-guard exit 0. On device: wrong PIN refused,
|
||||
correct PIN opens, `am kill` then reopen lands on the lock screen, turning the
|
||||
lock off requires the current PIN, and `adb exec-out screencap` returns
|
||||
`mean=0 stddev=0` — `FLAG_SECURE` proved with a number rather than asserted.
|
||||
|
||||
`androidx.biometric` is at 1.1.0 because that is the newest **stable**; 1.4.0 is
|
||||
alpha-only and `biometric-ktx` never shipped a stable release, both checked
|
||||
against Google's Maven index. A full audit of the catalog the same day found
|
||||
every other artifact, all four plugins and Gradle itself already at current
|
||||
stable.
|
||||
|
||||
**Next action:** #35, Export My Data, with the export file encrypted. It is the
|
||||
only artifact that leaves the sandbox, which is exactly where the platform's own
|
||||
file encryption stops protecting — and it is also the precondition recorded in
|
||||
`SECURITY.md` for ever revisiting database encryption, because a user who can
|
||||
hold their own copy is a user for whom a lost key is not a lost history.
|
||||
|
||||
**Blockers:** none. Six device checks from #34's own list were not run and are
|
||||
now rows in `SECURITY_CHECKLIST.md` rather than memory: a fingerprint enrolled on
|
||||
API 26/27, rotation and fontScale 2.0 while unlocked, a reminder action tapped
|
||||
while locked on hardware, an OEM biometric overlay, a `FLAG_SECURE` flicker on
|
||||
first launch, and TalkBack on the lock screen.
|
||||
|
||||
### 2026-08-19 — Encryption, declined for a reason; and the crash it uncovered
|
||||
|
||||
The question was whether the cycle database could be encrypted at rest. It can.
|
||||
|
|
|
|||
Loading…
Reference in New Issue