From 5e9184f3771ff14d9e5676ea7b60009679929b46 Mon Sep 17 00:00:00 2001 From: null Date: Wed, 19 Aug 2026 04:05:53 -0500 Subject: [PATCH] docs: log the app lock, and the three defects testing found MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/history/DEVELOPMENT_LOG.md | 85 +++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/docs/history/DEVELOPMENT_LOG.md b/docs/history/DEVELOPMENT_LOG.md index c900bf8..b6e9505 100644 --- a/docs/history/DEVELOPMENT_LOG.md +++ b/docs/history/DEVELOPMENT_LOG.md @@ -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.