aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-20 01:51:05 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-20 01:51:05 +0200
commit0edb4bb6d76939aa456cbbc8deafeed50fd6182d (patch)
tree3f22349f6d64ff0fa33b6ff9bbcc10584f8b152c /docs
parent42f9331e693f5058fcfcd7aef5a92cabc04c20ee (diff)
docs: record M5 results; bump to 0.5.0v0.5.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'docs')
-rw-r--r--docs/DEVELOPER_GUIDE.md2
-rw-r--r--docs/PERFORMANCE.md7
-rw-r--r--docs/SECURITY.md26
-rw-r--r--docs/TESTING.md34
4 files changed, 68 insertions, 1 deletions
diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md
index 7b56f3b..d03a8fe 100644
--- a/docs/DEVELOPER_GUIDE.md
+++ b/docs/DEVELOPER_GUIDE.md
@@ -124,5 +124,7 @@ Debug builds print `DIAG first_frame startup_ms=... mem_used=... mem_total=...`
- **A device**: add `<iq:product id="..."/>` to `manifest.xml`, then `make sdk` (downloads its files), `make icons` and `make products` (checks it meets the target rule: a watch, API 5.0 or newer, at least 512 KB).
- **A test device**: add it to `TEST_DEVICES` (unit tests, perf and screenshots) or `SPOT_DEVICES` (screenshots only) in the `Makefile`. Then run `make screenshots`, review `build/screenshots/<device>/`, and run `make screenshots-approve`.
- **A screen in the UI flow**: print `Diag.shown("<name>")` after its first complete draw, add the steps to `run_flow` in `tools/ui_flow.py`, and approve the new goldens after reviewing them.
+- **Import inputs**: new parser cases go into `test/import/` (fixtures are built in `ImportSupport.mc`, all fake). To widen the fuzz corpus, change the seeds or mutations in `tools/gen_fuzz_corpus.py` and run `make corpus`. The Python tests check that the committed corpus matches the generator.
+- **A settings form** (M7 adds the rest): declare its properties in `resources/settings/properties.xml` (reading an undeclared one crashes), add the form to `resources/settings/settings.xml` with its strings, and handle it in `source/settings/SettingsProcessor.mc`. The form is applied only when its switch is on. The processor clears the fields afterwards and remembers the form's hash.
- **On-watch numbers**: sideload a debug build (`make build DEVICE=fr70`, copy `build/debug/fr70.prg` to the watch's `GARMIN/APPS` folder over USB). Then open the menu and choose "Debug: benchmark".
- **A barcode symbology** (from M6): one encoder module under `source/codes/`, its validator, unit tests with reference patterns, and the checks in [README 12.4](../README.md#124-how-each-symbology-is-proven-scannable).
diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md
index 8c5adb4..9a5bcbf 100644
--- a/docs/PERFORMANCE.md
+++ b/docs/PERFORMANCE.md
@@ -25,6 +25,13 @@ The targets are in [README section 18](../README.md#18-performance-budgets). Thi
## Results
+### M5, 2026-10-20 (version 0.5.0, import)
+
+- **Compute.** An Aegis export with 100 entries (19 KB, compact) takes 332 ms of compute in the unit-test benchmark, spread over 159 job steps.
+- **On the simulated watches.** Through the real path (settings form, timer-driven job, 50 ms pause between steps), the UI-test build imports 100 entries in about 7.6 s on the Forerunner 255S. Most of that is the pauses that keep each event below the watchdog limit. Peak memory during the import is 41.5 % of the 255S's 512 KB: the pasted text is held as a string and as bytes, along with the parsed items. The flow fails above 70 %.
+- **Pasted text size.** Aegis exports take about 190 bytes per entry compact, and 400 to 500 pretty-printed. The multi-part buffer is capped at 32 KB, so very large pretty-printed exports must be imported in parts.
+- **Watchdog, again.** Even the UI-test fixture code tripped the watchdog on the 255S, by Base32-encoding 96 secrets in one event. The importer itself works in steps of 40 JSON tokens, one URI, four headers or one stored entry.
+
### M4, 2026-10-19 (version 0.4.0, screens)
- **Start-up.** In-app start-up (object creation to the first complete frame) measured 1 to 18 ms in the simulator; the `startup_ms` budget is now 100 ms. Launch to first frame rose to 16-18 s, because the simulator first loads the glance and the scripts then open the app from it. Its regression guard is now 36 s.
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.
diff --git a/docs/TESTING.md b/docs/TESTING.md
index 54a331e..593b120 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -26,6 +26,39 @@ Test devices (`TEST_DEVICES`): Forerunner 255S (weakest: 218 px round MIP, butto
## Results
+### M5, 2026-10-20 (version 0.5.0)
+
+Monkey C unit tests: 101 per device (29 new in `test/import/`):
+
+- **JSON tokenizer:** every token kind; escapes including surrogate pairs; integers versus other numbers; 16 kinds of invalid document; incomplete input (8 cases); the depth limit; long strings (icons, including escaped quotes) skipped unread; long keys refused.
+- **`otpauth://`:** URIs pasted run together; percent-encoded labels; a missing issuer; an issuer mismatch; `;` separators; `+` as a space; unknown parameters; HOTP with and without a counter; both Steam forms; nine error cases, each with its message.
+- **Aegis:**
+ - Database versions 1, 2 and 3, with groups listed after the entries.
+ - totp, hotp and steam imported; motp and yandex skipped.
+ - SHA-1, SHA-256 and SHA-512; 6, 7 and 8 digits; periods 20, 30, 50 and 60.
+ - Favourites, icons, notes and Unicode names.
+ - Encrypted exports refused, in both forms.
+ - Bad entries (9 digits, a duplicated `secret` field, invalid Base32, a non-integer digit count), and non-Aegis, newer-version and too-deeply-nested documents.
+ - An incomplete export.
+- **Importer:**
+ - The report text.
+ - Duplicates of an earlier import, of a locked entry while the watch is locked, and within one import.
+ - Possible replacements; "Require PIN" entries going pending.
+ - Replace mode keeping manual entries.
+ - A two-part JSON import, sealed while it waits, and the expiry of a waiting part.
+ - Unusable text: empty, unknown, Google Authenticator links, encrypted, invalid.
+ - A store that fills up mid-import, keeping only whole entries.
+ - Fingerprints.
+ - 100 entries.
+- **Fuzz corpus** (`tools/gen_fuzz_corpus.py`, seed 4242, `make corpus`): 50 mutated Aegis documents and 50 mutated URIs.
+ - The tokenizer always finishes.
+ - The Aegis reader always ends in a definite state, with every item either valid or explained.
+ - The full importer never stores an invalid entry: 21 entries were stored from the inputs that survived mutation.
+
+Python tool tests: 55. The new ones cover the corpus generator, including that the committed corpus matches it.
+
+Scripted UI flow: the UI-test build now puts an Aegis export of 100 entries into the Import form, as the phone would, and the flow starts with the results screen that opens after the import. 11 screens per watch (10 on the touch-only ones). Every watch reports the import's duration and peak memory, and the flow fails above 70 % of the watch-app memory.
+
### M4, 2026-10-19 (version 0.4.0)
`make ci` passed in full.
@@ -102,3 +135,4 @@ Things the simulator can't verify, done by you on the Forerunner 70. The checkli
- [ ] M4: sideload a debug build (`make build DEVICE=fr70`, then copy `build/debug/fr70.prg` to `GARMIN/APPS` over USB). Open the menu, choose "Debug: benchmark", and send me the numbers. They set the real PBKDF2 iteration count and the SHA-512 cost on your watch.
- [ ] M4: with "Debug: demo entries", check the list, a code, the countdown ring, HOTP "next", and the PIN keypad with your finger. Also check a wrong PIN (vibration) and the 30 s wait after five wrong PINs.
- [ ] M4: check that leaving the app and coming back asks for the PIN again for the locked demo entry. Check that the glance in the glance loop shows the number of entries.
+- [ ] M5: in Garmin Connect, paste a few `otpauth://` links of test accounts (not your real ones) into the Import fields, turn "Import now" on and save. Check the result on the watch and in the Result field. Also check that the text fields are emptied afterwards: this answers part of the M1 question.