docs: log the encryption decision, the crash it uncovered, and two false greens
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) <noreply@anthropic.com>
This commit is contained in:
parent
17cddb2a61
commit
42a2d543ad
|
|
@ -32,6 +32,83 @@ written and stay true. It is exempt from review for the same reason a receipt is
|
||||||
|
|
||||||
## Entries
|
## 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
|
### 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
|
Privacy: Period Tracker was the only Privacy LLC product without a page on
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue