summaryrefslogtreecommitdiff
path: root/docs/SECURITY.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/SECURITY.md')
-rw-r--r--docs/SECURITY.md26
1 files changed, 25 insertions, 1 deletions
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
index ee2f94d..45faba6 100644
--- a/docs/SECURITY.md
+++ b/docs/SECURITY.md
@@ -18,8 +18,12 @@ Everything lives in the app's private `Application.Storage`:
| `lk` | dictionary | Wrong-PIN state: `f` failures, `d` delay in seconds, `t` trusted time of the last attempt; absent after a success | `Lockout` |
| `e.<id>` | bytes | One entry record (format below) | `Vault` |
| `p.<id>` | bytes | Pending locked entry: a record sealed with the device key | `Vault` |
+| `imp`, `imp.<n>` | dictionary, bytes | Waiting multi-part import: `imp` = `{"n": chunks, "t": saved at}`, `imp.<n>` = 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` |
+| `st` | array | Version and day of the last passed self-test | `AppState` |
-`x.<id>` (barcode cache, M6) and `imp` (import buffer, M5) are added by later milestones.
+`x.<id>` (barcode cache) arrives with M6.
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.
@@ -66,6 +70,7 @@ TLV field: `tag (1 byte) | type (1 byte) | length (2 bytes, big-endian) | value`
| 10 | HOTP counter | int | header |
| 11 | created (unix seconds) | int | header |
| 12 | names encrypted | bool | header |
+| 13 | fingerprint (8 bytes, see below) | bytes | header |
| 20, 21, 22 | algorithm (0 SHA-1, 1 SHA-256, 2 SHA-512), digits, period | int | box (OTP) |
| 23 | secret (raw bytes) | bytes | box (OTP) |
| 24 | payload (UTF-8 bytes) | bytes | box (code) |
@@ -153,3 +158,22 @@ A locked entry that arrives while the session is locked, or before a PIN exists,
- **The UI-test build** (`uitest.jungle`) wipes the store at every start and fills it with fake entries. It sets the PIN 123456 by storing a precomputed wrap of a fixed fake master key (`tools/gen_fixture_wrap.py`), and freezes the clock. All of this is `(:uitest)` or `(:debug)` code; release builds, and therefore the Store package, contain none of it.
- **The PIN screen hands the derivation a copy of the typed digits** and wipes both the copy and its own buffer once the derivation is done. The native HMAC object may keep its own copy (see above).
+
+## Fingerprints (duplicate detection)
+
+Imports skip an entry that has "the same type and the same secret" as an existing one (README 11.4). That has to work for locked entries while the watch is locked. So every record carries an 8-byte keyed fingerprint in its readable header, tag 13:
+
+```text
+fingerprint = first 8 bytes of HMAC-SHA256(Kfp, "OTPW-FP-1" || kind || type || secret or payload)
+Kfp = HKDF-SHA256(device key, info = "OTPW fp v1")
+```
+
+- **Without the device key it reveals nothing.** Equal fingerprints only show that two entries share a type and secret.
+- **With the device key, it allows checking guesses of a locked entry's secret.** An attacker who could read app storage could compare a guessed secret against the fingerprint. Real TOTP secrets are random and at least 80 bits long, so guessing them is infeasible; a user-chosen, guessable secret would be weakened.
+- **The fingerprint is part of the header, so the record's MAC covers it.** It is recomputed on every add and update (`importerFingerprintsIdentifySecrets`).
+
+## Import text
+
+- **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.<n>`. 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.