docs: record why the database is not encrypted, and drop the claim that it is

Asked whether encryption at rest could be added. It can, and it should not be
yet — so the reasoning goes in the document that owns the claim, before it is
forgotten and re-derived from scratch.

The architecture decides it. ReminderCoordinator collects Room flows at every
process start, including processes started with no Activity after a reboot,
and ReminderWorker reads the forecast on a schedule. The key would have to be
readable with no user present, which is the availability condition the
platform key already has. A key anything running as this app can reach
unattended defends a file, not a process — and against every adversary this
document already lists, it adds nothing.

What it would genuinely add is narrow: a file copied off the device and read
elsewhere, a phone with no screen lock, and crypto-shredding on delete. The
cost is about +1 MB downloaded and +2 MB installed on a 2 MB app, and a new
class of total loss — a Keystore key can be lost in the field, platform backup
is off, and there is no telemetry that would tell us it happened. Today a
database that will not open is a bug. Afterwards it would be somebody's
history.

The condition that reverses it is written down too: a key bound to the app
lock's user secret, once export has given the user a copy they control.

BRAND_GUIDE carried an artwork brief reading "explaining encrypted local data
storage" — a false claim waiting for somebody to draw it, on the one subject
where a padlock reads as a promise. Replaced with what is true and is a better
story anyway: the data never leaves, so there is no server to breach and
nothing to sell. SECURITY_CHECKLIST gains the row that would have caught it.

Nothing shipped ever claimed encryption; grep over app, core and domain for
strings and illustrations comes back empty. This closes the gap between that
and the documents.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
null 2026-08-19 03:08:22 -05:00
parent 424a513336
commit 17cddb2a61
3 changed files with 40 additions and 1 deletions

View File

@ -731,7 +731,18 @@ Example:
or: or:
**“Create an onboarding illustration explaining encrypted local data storage using this visual system.”** **“Create an onboarding illustration explaining that cycle history stays on this device using this visual system.”**
Note the wording, because it changed. This example used to say *encrypted local
data storage*, and the database is **not** encrypted — see *Deliberately out of
scope* in [`../security/SECURITY.md`](../security/SECURITY.md), which owns that
claim and explains why.
A prompt is a claim the moment somebody draws it. A padlock on an onboarding
screen is a promise, and this one would have been a promise the product does not
keep — which is worse than saying nothing, because the true story here is
genuinely good: the data never leaves, so there is no server to breach and
nothing to sell. Draw that instead.
--- ---

View File

@ -185,6 +185,33 @@ Written down so an unknown gap becomes a known one:
- **A rooted or compromised device.** App-private storage is not a defence - **A rooted or compromised device.** App-private storage is not a defence
against root, and pretending otherwise would justify complexity that buys against root, and pretending otherwise would justify complexity that buys
nothing. nothing.
- **Encryption of the cycle database at rest.** Evaluated on 2026-08-19 and
deliberately not built. Android's file-based encryption already covers the
case people picture: a phone that is off, or that has not been unlocked since
boot, has app-private storage that cannot be read at all. What app-level
encryption would add is narrower — a file *copied off* the device and read
elsewhere, a phone with no screen lock set at all, and crypto-shredding on
delete.
This app's own architecture decides the rest. `ReminderCoordinator` collects
Room flows at **every process start**, including processes started with no
Activity after a reboot, and `ReminderWorker` reads the forecast on a
schedule. So the key would have to be available with no user present — which
is the same availability condition the platform key already has. A key that
anything running as this app can reach unattended defends a **file**, not a
**process**, and against every adversary listed above it adds nothing.
The cost is recorded so nobody has to re-derive it: roughly +1 MB downloaded
and +2 MB installed on a 2 MB app. The risk matters more than the size. A key
held in the Android Keystore can be lost in the field, and with platform
backup off there is no second copy and no telemetry that would tell us it
happened. Today, failing to open the database is a bug. Afterwards it would be
somebody's history, gone, silently.
**What would reverse this:** a key bound to the app lock's own user secret,
once export exists and has given the user a copy they control. That version
does defend a seized, unlocked phone — and it is only defensible once losing
the key is not the same as losing everything.
- **Forensic recovery of deleted rows.** Delete My Data removes the data through - **Forensic recovery of deleted rows.** Delete My Data removes the data through
the database; it does not overwrite flash. the database; it does not overwrite flash.
- **Someone who knows the unlock PIN.** Biometric/PIN gating raises the bar over - **Someone who knows the unlock PIN.** Biometric/PIN gating raises the bar over

View File

@ -51,6 +51,7 @@ The one group that is not generic. Every item proves part of
### What the device and the lock screen expose ### What the device and the lock screen expose
- [ ] Cycle history is in app-private storage only — proves nothing landed in shared or external storage - [ ] Cycle history is in app-private storage only — proves nothing landed in shared or external storage
- [ ] No shipped string, illustration, screenshot or store listing says the cycle database is **encrypted** — proves the product does not claim a control [`SECURITY.md`](SECURITY.md) records as deliberately out of scope. It is on this list because the claim is attractive, easy to draw, and was already sitting in an artwork brief before anybody checked whether it was true
- [ ] Lock-screen text in **Discreet** and **Maximum privacy** modes contains no menstrual detail, checked on a real lock screen at every mode — proves the notification privacy feature actually works, which is the breach most likely to happen - [ ] Lock-screen text in **Discreet** and **Maximum privacy** modes contains no menstrual detail, checked on a real lock screen at every mode — proves the notification privacy feature actually works, which is the breach most likely to happen
- [ ] The health database's inclusion in platform auto-backup is a **recorded decision**, not a default — proves the local-first promise is not undone by the OS - [ ] The health database's inclusion in platform auto-backup is a **recorded decision**, not a default — proves the local-first promise is not undone by the OS
- [ ] Release build has verbose logging off and no raw cycle date in any crash payload — proves §45's logging rule survived the build type - [ ] Release build has verbose logging off and no raw cycle date in any crash payload — proves §45's logging rule survived the build type