From 6039571d70b1f6de1ae384e58e11879869be356c Mon Sep 17 00:00:00 2001 From: cirakg Date: Tue, 20 Oct 2026 18:36:54 +0200 Subject: docs: record M7 (settings forms) results; bump to 0.7.0 Co-Authored-By: Claude Opus 5.5 --- docs/SECURITY.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) (limited to 'docs/SECURITY.md') diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 31723d5..f52a983 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -21,7 +21,9 @@ Everything lives in the app's private `Application.Storage`: | `x.` | bytes | Encoded 2D barcode: a SealedBox (aad `OTPW-X-1` + ID) with the entry's own key (device key, or the master key for locked entries) of width, height, quiet zone and the packed modules. Deleted when the entry is updated, removed or erased | `Vault` | | `imp`, `imp.` | dictionary, bytes | Waiting multi-part import: `imp` = `{"n": chunks, "t": saved at}`, `imp.` = chunks of one SealedBox (device key, aad `OTPW-IMP-1`) of the text | `ImportBuffer` | | `res` | dictionary | Latest settings results (`l` lines, `u` unseen) | `Results` | -| `fh` | array | Hashes (SHA-256, first 8 bytes) of the last 32 applied settings forms | `SettingsProcessor` | +| `fh` | array | Keyed hashes of the last 32 applied settings forms (below) | `Forms` | +| `q` | array | Phone changes to locked entries waiting for the PIN: one SealedBox per change (device key, aad `OTPW-Q-1`), at most 16 | `ChangeQueue` | +| `cs` | string | Keyed hash of what the About fields were last written from (IDs, pending IDs, "Encrypt names", waiting changes, version); absent while names still need converting | `AboutScan` | | `st` | array | Version and day of the last passed self-test | `AppState` | The PIN wrap and the lockout state have their own keys instead of living in `m` as README 6.2 first planned. Each wrong PIN rewrites the lockout state before the check, and a small separate value makes that write cheap and keeps it from risking the entry index. @@ -176,3 +178,17 @@ Kfp = HKDF-SHA256(device key, info = "OTPW fp v1") - **Secrets in the phone settings.** Text pasted into the Import fields sits in Garmin Connect's settings until the watch processes it. The watch then clears the fields (they hold secrets) and turns "Import now" off. Whether the phone shows the cleared fields is the open question of the M1 spike. - **Unfinished imports.** A multi-part import waiting for its next part is kept sealed with the device key under `imp`/`imp.`. It is deleted when the import completes, fails, is cancelled or is 24 hours old. A test checks that the secret's text isn't readable in storage (`importerJoinsMultiPartJson`). - **What the parsers touch.** They never decode icons or notes: long strings are skipped unread, and notes aren't kept. Parsed secrets are wiped once stored or rejected. + +## Settings forms + +- **Form hashes.** The watch remembers the last 32 applied forms (README 10.3) as keyed hashes, because a form's content includes its secret: + + ```text + hash = first 8 bytes of HMAC-SHA256(Kfp, "OTPW-FH-1" || 0x1F || form name || 0x1F || field 1 || 0x1F || ...) + ``` + + Kfp is the fingerprint key above. Without the device key a stored hash reveals nothing. With it, an attacker can only test guesses of a whole form, as with fingerprints. The fields go to the native HMAC one by one; the short ones (every secret field) are zeroed afterwards. An import form's text is too long to zero byte by byte within the watchdog limit; it stays in memory as the property's string anyway. +- **Secrets in the forms.** Add OTP's secret, Add code's contents and Manage entry's new secret are cleared first when the watch processes a form, also after a problem (the other fields stay so you can correct them). Messages never quote a secret. +- **Changes from the phone to locked entries** (README 7.9). Edit, replace, set counter, "Don't require PIN" and delete of a locked entry are not applied when they arrive. Each one is sealed with the device key under `q`, which is no weaker than the settings text it came in. They are applied only after someone enters the PIN on the watch under "Phone changes", which always asks for the PIN, even while the session is unlocked. Moving an entry is applied at once: the order isn't protected, and positions are absolute. Changes to unlocked entries apply at once and are listed on the results screen (`manageQueuesLockedEntryChanges`, `manageQueueIsSealedAndLimited`). +- **The About catalog** sends entry IDs and names to the phone's settings, where your entries came from in the first place. With "Encrypt names" on it sends only IDs and types. +- **"Encrypt names"** puts names and accounts into the encrypted part of each record. Turning it on or off re-seals every entry the watch can reach: unlocked ones at once, locked ones the next time the PIN is entered (`settingsAppliesEncryptNames`). The barcode cache is kept. -- cgit v1.2.3