docs: stop describing a biometric key that does not exist

SECURITY.md and AndroidKeyStoreMacProvider's KDoc both described a second,
biometric-bound Keystore key taking "the opposite policy" and invalidating
on enrolment. There is no such key: grep -rn BiometricKey found only those
two sentences. A successful fingerprint flips an in-memory session flag --
no CryptoObject, no key unwrap -- so the biometric path is a gate, not a
second secret.

A security document that describes protection the code does not have is
worse than one admitting the gap, because the next reader trusts it.

The absent version is not obviously wrong, so both places now say why it
is filed rather than built (#63): binding a key to biometric enrolment
means a partner adding their own fingerprint invalidates it, which is
exactly the protection somebody would want -- but it only degrades safely
where a PIN sits behind it. In a fingerprint-only lock, a key destroyed by
an ordinary enrolment is the no-recovery policy turned against its owner.

Also adds the six device rows the development log has claimed since Batch
06 were "rows in SECURITY_CHECKLIST.md rather than memory". They were
never added: a grep for biometric, fingerprint, TalkBack, rotation,
fontScale and flicker over that file returned nothing. A release gate that
silently skips half its checks is the failure the file exists to prevent.

closes #66

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
null 2026-08-21 00:13:01 -05:00
parent f43d580cc7
commit b81a4fb43b
3 changed files with 25 additions and 5 deletions

View File

@ -28,8 +28,11 @@ import javax.crypto.SecretKey
* - **`setUserAuthenticationParameters`** meaningless without the above. * - **`setUserAuthenticationParameters`** meaningless without the above.
* - **`setInvalidatedByBiometricEnrollment`** a no-op without user * - **`setInvalidatedByBiometricEnrollment`** a no-op without user
* authentication, and named here so nobody adds it thinking it does something. * authentication, and named here so nobody adds it thinking it does something.
* The *biometric* key in [BiometricKey] does set it, correctly, because * There is no second, biometric-bound key that does set it: this KDoc used to
* invalidation there degrades to "use your PIN" instead of to lockout. * claim otherwise and was describing something that has never existed. A
* fingerprint flips the session flag; it unwraps nothing. See
* docs/security/SECURITY.md for why the version that would set it is filed
* rather than built.
* - **`setUnlockedDeviceRequired`** would fail exactly in the background, * - **`setUnlockedDeviceRequired`** would fail exactly in the background,
* where the reminder worker runs with the screen off. * where the reminder worker runs with the screen off.
* - **`setIsStrongBoxBacked`** StrongBox defends against extracting the key * - **`setIsStrongBoxBacked`** StrongBox defends against extracting the key

View File

@ -74,9 +74,21 @@ Data Safety section, and never lets health data reach any of them.
KDoc on `AndroidKeyStoreMacProvider` lists every builder call omitted to keep KDoc on `AndroidKeyStoreMacProvider` lists every builder call omitted to keep
it that way. A key bound to the device credential dies when the passcode it that way. A key bound to the device credential dies when the passcode
changes or the screen lock is removed — and with no recovery path, that is changes or the screen lock is removed — and with no recovery path, that is
somebody's entire history destroyed by an unrelated action. The *biometric* somebody's entire history destroyed by an unrelated action.
key is separate and takes the opposite policy, where invalidation correctly - **There is no second, biometric-bound key.** This document and the provider's
degrades to "use your PIN" rather than to lockout. own KDoc both used to describe one, taking "the opposite policy" and
invalidating on enrolment. It does not exist: `grep -rn BiometricKey` finds
only those two sentences. A successful fingerprint flips the in-memory session
flag and nothing else — there is no `CryptoObject` and no key unwrap, so the
biometric path is a gate, not a second secret.
<br>Worth stating plainly because the absent version is not obviously wrong:
binding a key to biometric enrolment would mean that adding a fingerprint — a
partner's, say — invalidates it, which is exactly the protection somebody
would want. It is filed rather than built (tracker #63) because that only
degrades safely where a PIN sits behind it; in a fingerprint-only lock, a key
destroyed by an ordinary enrolment is the no-recovery policy turned against
its owner. A security document describing protection the code does not have is
worse than one admitting the gap, because the next reader trusts it.
- **Wrong PINs cost time and never cost data.** Four free attempts, then - **Wrong PINs cost time and never cost data.** Four free attempts, then
30s → 1m → 2m → 5m → 15m, capped at fifteen minutes forever. There is no 30s → 1m → 2m → 5m → 15m, capped at fifteen minutes forever. There is no
attempt limit and nothing is ever erased automatically: an auto-wipe would let attempt limit and nothing is ever erased automatically: an auto-wipe would let

View File

@ -54,6 +54,11 @@ The one group that is not generic. Every item proves part of
- [ ] With the app lock on, the recents card is a solid colour and `adb exec-out screencap` returns a black frame — proves `FLAG_SECURE` is actually applied, which no source check can establish - [ ] With the app lock on, the recents card is a solid colour and `adb exec-out screencap` returns a black frame — proves `FLAG_SECURE` is actually applied, which no source check can establish
- [ ] Killed with `adb shell am kill` and reopened from recents, the app lands on the lock screen — proves the unlock flag is not in saved state, which is the one bug that would make the lock look fine and never engage - [ ] Killed with `adb shell am kill` and reopened from recents, the app lands on the lock screen — proves the unlock flag is not in saved state, which is the one bug that would make the lock look fine and never engage
- [ ] A reminder action tapped while locked writes nothing until after the unlock — proves a bystander cannot record an answer in someone's history from the phone's own lock screen - [ ] A reminder action tapped while locked writes nothing until after the unlock — proves a bystander cannot record an answer in someone's history from the phone's own lock screen
- [ ] A fingerprint enrolled on API 26 or 27 opens the app through the compat dialog — proves the pre-API-28 path `USE_FINGERPRINT` exists for is real, and is the one biometric route no current emulator exercises
- [ ] An OEM biometric overlay that stops the activity does not relock the app under its own prompt — proves the `authInProgress` guard holds on the devices it was written for; without it a successful scan returns to a locked screen forever
- [ ] Rotating the phone and setting font scale to 2.0 while unlocked does not relock — proves the gate tells a configuration change apart from a real backgrounding
- [ ] TalkBack reads the lock screen as a password field and announces the wait after a wrong PIN — proves the one screen a person may meet without ever having opened the app is usable without sight
- [ ] No `FLAG_SECURE` flicker on first launch — proves the flag is set before anything can be drawn, rather than after a frame of real content has reached the recents thumbnail
- [ ] `KeystoreVerifierTest` run on `PeriodMinSdk26` as well as a current image — proves `PBKDF2WithHmacSHA256` and a non-auth-bound Keystore key exist at the oldest supported version, which is the one algorithm choice here with no margin - [ ] `KeystoreVerifierTest` run on `PeriodMinSdk26` as well as a current image — proves `PBKDF2WithHmacSHA256` and a non-auth-bound Keystore key exist at the oldest supported version, which is the one algorithm choice here with no margin
- [ ] The erase behind "Forgot your PIN?" leaves the app openable — proves the Keystore key went with the records, and the user is not erased *and* still locked out - [ ] The erase behind "Forgot your PIN?" leaves the app openable — proves the Keystore key went with the records, and the user is not erased *and* still locked out
- [ ] 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 - [ ] 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