aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/DEVELOPER_GUIDE.md9
-rw-r--r--docs/FEASIBILITY.md5
-rw-r--r--docs/PERFORMANCE.md13
-rw-r--r--docs/SECURITY.md18
-rw-r--r--docs/TESTING.md29
5 files changed, 71 insertions, 3 deletions
diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md
index 2646e1b..395cf00 100644
--- a/docs/DEVELOPER_GUIDE.md
+++ b/docs/DEVELOPER_GUIDE.md
@@ -95,6 +95,9 @@ The simulator is a GTK program. The scripts start it on a virtual display (Xvfb
- `Attention.vibrate` makes the simulator shake the device drawing, which spoils screenshots; the UI-test build doesn't vibrate.
- A `ConfirmationDelegate` closes its dialog after `onResponse`, so a view switch from `onResponse` must wait for a short timer.
- `Attention has :vibrate` inside a class that has a private `vibrate` method warns (the symbol lookup finds the method); the helper is called `buzz`.
+- `has` and `hidden` are reserved words; they can't name a function or a variable.
+- Under strict typing, a property value (`Application.Properties.ValueType`) can't be compared with `==` to a Boolean or Number: narrow it with `instanceof` first (`Forms.flag`, `Forms.number`).
+- Zeroing a ByteArray is a byte loop in the VM: zeroing a 10 KB import text in one event tripped the watchdog on the Forerunner 255S. The native HMAC takes data in pieces (`update`), so large content is hashed without copying it into one buffer.
### Screenshots and the UI flow
@@ -127,7 +130,11 @@ Debug builds print `DIAG first_frame startup_ms=... mem_used=... mem_total=...`
- **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.
+- **A settings form or field**:
+ 1. Declare each property in `resources/settings/properties.xml`. Reading an undeclared property crashes the app in a way that can't be caught.
+ 2. Add the setting to `resources/settings/settings.xml` and its strings to `resources/strings/strings.xml`. `make lint` validates both against the SDK schema and checks that they agree (`tools/check_resources.py`).
+ 3. Read the fields through a `Forms.Store` in a reader like `source/settings/OtpForm.mc`, so tests can use `Forms.MemoryStore`. Put every field in the form's hash content and its default in the form's `defaults()`; the test `settingsDefaultsMatchProperties` checks those against `properties.xml`.
+ 4. Add a phase to `SettingsJob` in README 10.3 order. Heavy work (encoding, many records) goes into its own steps: one step is one timer event under the watchdog.
- **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**: add an encoder module under `source/codes/` and dispatch to it in `Codes.encode`. Then:
- Add reference rows in `tools/gen_code_vectors.py` (zxing-cpp's writer, or segno for QR) and run `make codes`.
diff --git a/docs/FEASIBILITY.md b/docs/FEASIBILITY.md
index f586f10..08e9461 100644
--- a/docs/FEASIBILITY.md
+++ b/docs/FEASIBILITY.md
@@ -274,6 +274,11 @@ With SDK 9.2.0 and the device files of all 72 products installed:
- **Properties:** reading a property that isn't declared in `properties.xml` stops the app with an error `try` can't catch. The option properties are declared ahead of M7's form.
- **Touch input:** on watches with touch and buttons, a tap arrives first as the select behaviour. The Venu X1 has no MENU button, and the Venu Sq 2 has no UP/DOWN.
+## Verified during M7 (2026-10-20)
+
+- **Settings schema.** The SDK's `bin/resources.xsd` allows property types `string`, `number`, `long`, `float`, `double`, `boolean` and `array`, and setting types `list`, `boolean`, `numeric`, `alphaNumeric`, `phone`, `email`, `url`, `date`, `password` and `array`, with `readonly`, `min`, `max`, `maxLength` and `errorMessage`. The form uses `long` for HOTP counters and `readonly` for the Result and About fields. Every resource file validates against it (`make lint`).
+- **Still open (3d, 3e):** how the phone shows values the watch writes back (Result fields, cleared forms, the About catalog), and how long a text field may be. The catalog is capped at 800 characters, the Garmin Express limit reported in 3d. Your M1 spike and the M7 beta check answer these.
+
## Decisions
Answered on 2026-10-18:
diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md
index 0fbb2f0..9e89e40 100644
--- a/docs/PERFORMANCE.md
+++ b/docs/PERFORMANCE.md
@@ -25,6 +25,19 @@ The targets are in [README section 18](../README.md#18-performance-budgets). Thi
## Results
+### M7, 2026-10-20 (settings forms, version 0.7.0)
+
+- **Memory is rising with the code.** The app's code is loaded into the same memory as its data, so every milestone adds to it:
+
+ | Forerunner 255S (512 KB) | M5 | M6 | M7 | Budget |
+ | --- | --- | --- | --- | --- |
+ | First frame (`make memtest`) | | 26 % | 31.4 % | 70 % |
+ | Peak during the 100-entry import (UI flow) | 41.5 % | | 56.2 % | 70 % |
+
+ The 768 KB watches use 21 % at the first frame and about 38 % at the import's peak. The 255S still has room, but M8 should claw some back, for example by keeping rarely used encoders and the settings code out of memory until needed, before adding more.
+- **Settings run.** The forms are applied in timer steps after the first frame, so start-up is unchanged (`startup_ms` 1 to 17 ms). Each form takes one step (a 2D code more, while it is encoded); the About scan reads four headers per step, so 110 entries take 28 steps, at least 1.4 s at 50 ms per step. It runs only when the entry list changed.
+- **Package:** 12 MB for all 72 products (9.5 MB in M6).
+
### M6, 2026-10-20 (barcodes, in 0.6.0)
- **Linear codes** encode in milliseconds while the screen opens; nothing is cached.
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.<id>` | 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.<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` |
+| `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.<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.
+
+## 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.
diff --git a/docs/TESTING.md b/docs/TESTING.md
index d8302af..b77ab5d 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -12,7 +12,7 @@ The full test plan is in [README section 17](../README.md#17-test-plan). This do
| --- | --- | --- |
| Python tool tests | `make test` (first part) | The tools that judge everything else are correct: test-log parser, screenshot checker, budget checker, product rule, link checker, linter, icon generator. Includes Hypothesis fuzzing of the log parser and integration checks against the real docs and sources |
| Monkey C unit tests | `make test` (second part) | Garmin's Run No Evil tests, run in the headless simulator on each test device; judged from the console output, because `monkeydo` always exits with 1 |
-| Static checks | `make lint` | Project policy (headers, no logging, no network, no secret-like literals), ruff and strict mypy on the tools, the product rule for all 72 products, and strict debug and release builds with zero compiler warnings |
+| Static checks | `make lint` | Project policy (headers, no logging, no network, no secret-like literals), ruff and strict mypy on the tools, the product rule for all 72 products, the resource XML against the SDK's `resources.xsd` with the settings cross-check (`tools/check_resources.py`), and strict debug and release builds with zero compiler warnings |
| Screenshots | `make screenshots` | Each test device's screen matches its approved image in `test/golden/` (at most 0.01 % of pixels may differ by more than 24 levels, less than one changed character) |
| Performance and memory | `make perf`, `make memtest` | First-frame time and memory use within `tools/budgets.json` ([PERFORMANCE.md](PERFORMANCE.md)) |
| Packaging | `make package` | The signed `.iq` builds for all 72 products with zero warnings |
@@ -26,6 +26,25 @@ Test devices (`TEST_DEVICES`): Forerunner 255S (weakest: 218 px round MIP, butto
## Results
+### M7, 2026-10-20 (phone settings forms, version 0.7.0)
+
+Monkey C unit tests: 154 per device (34 new, in `test/settings/`), all passing on the four test devices.
+
+- **Add OTP:** every type (TOTP, HOTP with its counter, Steam ignoring algorithm, digits and period), and each validation message of README 10.4: Base32 character and position, `=` padding, missing secret, digits, period, algorithm, type, counter, control characters, length in bytes, a missing name, the warning below 80 bits. After a problem only the secret is cleared; after success the whole form, with the switch off.
+- **Add code:** a valid payload for each of the 12 symbologies (2D codes come out cached and decodable) and an invalid one for each, with the encoder's message (check digit, length, character and position, capacity). Options and "Fix if wrong" are stored as sent, and codes too large for the screen are added with the README 12.2 warning.
+- **Manage entry:** every action on unlocked entries; locked entries' changes queued even while unlocked, then applied by confirmation; "Require PIN" applied at once or queued; unknown, malformed, pending and damaged IDs; a queued change whose entry vanished; changes arriving while the queue is applied; tampered queue items dropped; the 16-item limit.
+- **Idempotency:** the same Add OTP, Manage and Import form twice is applied once; any change makes a new request.
+- **The whole run:** all forms at once in README order; the About catalog, its 800-character cut and the storage line; "Encrypt names" on and off, with locked entries converted after the PIN; work detected only when needed.
+- **The contract with `properties.xml`:** every key the forms read exists and every form default equals its property's default (`settingsDefaultsMatchProperties`). The phone's side, all 13 resource files against the SDK's `resources.xsd` plus the settings, property and string cross-check, runs in `make lint` (`tools/check_resources.py`, 4 Python tests of its own).
+
+Scripted UI flow (end-to-end): the UI-test build fills in the Add OTP, Add code and Manage entry forms next to the import, as the phone would, so they take the real path: properties, `onSettingsChanged`-style processing after the first frame, the timer-driven job and the results screen. 20 screens per watch (17 on the touch-only ones), on seven watches:
+
+- `otp_form`: the TOTP added through the form shows 309 848 and next 489 290, the RFC 6238 values for its fake key at the frozen time, computed in Python.
+- `code_form`: the Code 128 card added through the form decodes from the screenshot on every watch.
+- `queue`, `queue_results` (watches with buttons): the Manage form's edit of the locked Bank entry waits under "Phone changes"; "Confirm all" asks for the PIN and then applies it.
+
+The flow caught one watchdog trip on the Forerunner 255S that the unit tests can't: hashing the 100-entry import form by copying it into one buffer and zeroing it. The fields now go to the native HMAC one by one.
+
### M6d, 2026-10-20 (Aztec; M6 complete, version 0.6.0)
Monkey C unit tests: 120 per device.
@@ -197,3 +216,11 @@ Things the simulator can't verify, done by you on the Forerunner 70. The checkli
- [ ] M6b: open "Member QR" (a demo entry) and scan it with your phone's camera. The first opening encodes it; note roughly how long that takes. The second opening should be instant (cache).
- [ ] M6c: scan the "Parcel" (Data Matrix) and "Ticket" (PDF417) demo cards with a phone app that reads them, for example the zxing-based "Barcode Scanner".
- [ ] M6d: scan the "Rail pass" (Aztec) demo card with a phone app that reads Aztec.
+- [ ] M7, with the Store beta, using a test account (never a real one):
+ - **Add OTP:** fill in the form with the account's Base32 secret, turn on "Add this entry" and save. The watch shows the results; the code must match the one in your phone's authenticator app. Check the form's Result field and whether the form is emptied on the phone.
+ - **Add code:** add a test EAN-13 (for example `5901234123457`) and a short QR text; scan both from the watch.
+ - **Manage entry:** with the IDs from the About group on the phone (or "Entry IDs" on the watch), rename one entry, move it to position 1 and delete it.
+ - **Locked entries:** set "Require PIN" on an entry, then send "Don't require PIN" for it. The watch must not apply it until you open the menu, choose "Phone changes", "Confirm all" and enter the PIN.
+ - **Repeats:** send the same Add OTP form twice (turn the switch on again without changing anything). The second time the result is "Already added".
+ - **Options:** change Theme and Code size; the watch changes as soon as the phone saves.
+ - **Limits:** does the About catalog (up to 800 characters) show completely on the phone?