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:
null 2026-08-19 04:05:53 -05:00
parent fa1cfe5515
commit 5e9184f377
1 changed files with 85 additions and 0 deletions

View File

@ -32,6 +32,91 @@ written and stay true. It is exempt from review for the same reason a receipt is
## Entries ## 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 ### 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. The question was whether the cycle database could be encrypted at rest. It can.