From 42a2d543ad56fa51835bc820595a9664fe7569ba Mon Sep 17 00:00:00 2001 From: null Date: Wed, 19 Aug 2026 03:09:26 -0500 Subject: [PATCH] docs: log the encryption decision, the crash it uncovered, and two false greens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 6 of WORK_CYCLE. Next action is #34, the app lock — the recovery decision is settled and the design is on the issue. Co-Authored-By: Claude Opus 5 (1M context) --- docs/history/DEVELOPMENT_LOG.md | 77 +++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/docs/history/DEVELOPMENT_LOG.md b/docs/history/DEVELOPMENT_LOG.md index 207e113..c900bf8 100644 --- a/docs/history/DEVELOPMENT_LOG.md +++ b/docs/history/DEVELOPMENT_LOG.md @@ -32,6 +32,83 @@ written and stay true. It is exempt from review for the same reason a receipt is ## Entries +### 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. +It is not, and the reasoning is now in `SECURITY.md` under *Deliberately out of +scope* rather than in anybody's head. + +**The architecture answers it.** `ReminderCoordinator` collects Room flows at +every process start — including processes WorkManager starts after a reboot with +no Activity — and `ReminderWorker` reads the forecast on a schedule. So a +database key would have to be readable with no user present, which is the same +availability condition Android's own file-based encryption key already has. A +key anything running as this app can reach unattended defends a **file**, not a +**process**. Against every adversary `SECURITY.md` lists — someone holding an +unlocked phone, someone reading a lock screen, a rooted device — it adds +nothing. What it would add is narrow and real: a file copied off the device and +read elsewhere, a phone with no screen lock, and crypto-shred on delete. + +Costed rather than argued: about +1 MB downloaded and +2 MB installed on a 2 MB +app, and a new class of total loss, since a Keystore key can be lost in the +field, platform backup is off, and no telemetry would tell us. The reversing +condition is written down — a key bound to the app lock's secret, once export +gives the user a copy they control. + +**Looking for the encryption seam found a live defect instead.** The +application scope was built with `SupervisorJob` and no +`CoroutineExceptionHandler`, and two Room flows are collected on it. +`SupervisorJob` stops a failing child cancelling its siblings; it does not stop +the exception, which reaches the thread's default handler and ends the process — +headless, at every boot, with nothing on screen to explain it. Both ViewModels +already had a handler. The one scope that runs with nobody watching did not. The +trigger is not hypothetical: `repository.forecast` runs the prediction engine +inside the flow, and `Prediction`'s init block enforces its window invariants +with `require`. Filed as #45, fixed in `424a513`, closed with its evidence. + +**`prove-guard.sh` told me it had caught something it had not.** The first proof +ran `./gradlew :app:test --tests …`. `:app:test` is AGP's lifecycle task and +takes no `--tests` option, so Gradle failed in 544 ms with `Unknown +command-line option`, the mutation was never compiled, no test ran — and the +script reported *"the guard caught it"*. It decides from the exit code and +cannot tell a broken test from a broken command. + +Pulling that thread: **the three documented boundary-guard proofs in +`architecture/README.md` have never exited 0.** Gradle prints no test-style +summary for those tasks, so `prove-guard` counts log lines, and its default +pattern also matches `FAILURE:` and `BUILD FAILED` — one caught violation reads +as three, exit 3, "not a pass". They now carry a `PROVE_GUARD_FAIL_PATTERN` and +were re-run at 0, 0, 0. Both lessons are `GUARDS.md` §8, which is exactly that +document's review trigger. + +**And a false claim that was one drawing away from shipping.** +`BRAND_GUIDE.md` carried an artwork brief reading *"explaining encrypted local +data storage"* — on the single subject where a padlock reads as a promise. +Replaced with what is true and is the better story anyway: the data never +leaves, so there is no server to breach and nothing to sell. `SECURITY_CHECKLIST` +gained the row that would have caught it. Nothing shipped ever claimed it; grep +over `app`, `core` and `domain` comes back empty. + +Also registered the Forgejo webhook (#9), verified with a signed replay that +returned `applied:true` — so a `P0` now raises an alert immediately instead of +waiting for the next sweep. + +**What this proved:** 207 tests, 0 failures, 0 skipped; `checkModuleBoundaries`, +`checkNoHealthLogging` and `checkPermissions` all exit 0, the last against a +freshly built release manifest; removing the new handler fails exactly one test, +and removing either `catch` fails exactly its own. + +**Next action:** #34, the biometric and PIN app lock. The recovery decision is +settled — none — and the full design is on the issue. Start with `:core:security` +and remember both rows it needs in the root `build.gradle.kts`, since +`checkNoHealthLogging` reads a hardcoded module list and a missing row fails +silently in the one module that holds key material. + +**Blockers:** none. #34 needs `androidx.biometric`, which fails `checkPermissions` +until `USE_BIOMETRIC` and `USE_FINGERPRINT` are added to the allowlist with their +reasons, and needs `MainActivity` to become a `FragmentActivity` — both known, +neither blocking. + ### 2026-08-19 — Eleven screenshots, and the day the seeding chose a bad hero Privacy: Period Tracker was the only Privacy LLC product without a page on