aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-18 23:44:18 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-18 23:44:18 +0200
commit94181d58cf66c5b7b263fd435a344f1cd8872cff (patch)
treec4e44e4a80e76ec6e4c0b196d616faf5e5e28839
parentac60fb060414261935ac5e158b26b34e211e1e06 (diff)
docs: record M2 results and bump version to 0.2.0v0.2.0
CHANGELOG 0.2.0; PERFORMANCE (simulator benchmarks and what they mean for the watch); TESTING (vector sources, M2 results, watchdog caveat); FEASIBILITY (verified: no native HMAC-SHA1, AES-256 works, Long semantics); README status and targets; developer-guide Monkey C facts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--CHANGELOG.md30
-rw-r--r--README.md4
-rw-r--r--docs/DEVELOPER_GUIDE.md10
-rw-r--r--docs/FEASIBILITY.md7
-rw-r--r--docs/PERFORMANCE.md18
-rw-r--r--docs/TESTING.md17
-rw-r--r--source/app/AppInfo.mc2
7 files changed, 84 insertions, 4 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index acc2c9d..0cee853 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,36 @@ All notable changes to this project are documented here. The format follows
## [Unreleased]
+## [0.2.0] - 2026-10-18
+
+Milestones M1 (prepared; your phone test pending) and M2: crypto and OTP core.
+Nothing user-visible yet; the home screen still shows only name and version.
+
+### Added
+
+- Crypto (`source/crypto/`): SHA-512 in Monkey C (FIPS 180-4), generic HMAC
+ (RFC 2104) for SHA-1/SHA-256/SHA-512 plus native HMAC-SHA256, resumable
+ PBKDF2-HMAC-SHA256 with calibration, HKDF-SHA256, AES-CBC with PKCS#7
+ (AES-256 chosen), SealedBox encrypt-then-MAC records, and a start-up
+ self-test.
+- OTP (`source/otp/`): Base32 decoding with exact error positions, HOTP, TOTP
+ (64-bit time, correct after 2038) and Steam Guard codes.
+- Utilities: constant-time comparison and wiping (`Bytes`), the 2038-safe clock
+ and trusted-clock check (`Clock`), resumable jobs with a timer runner (`Jobs`).
+- `tools/gen_vectors.py` and `make vectors`: official RFC/FIPS/NIST vectors,
+ each recomputed with Python's standard library or openssl before use, plus
+ seeded cases (SHA padding boundaries, 2038, 2100, all digit counts).
+- 31 Monkey C unit tests and a benchmark, passing on all four test devices.
+- M1 settings spike: a separate test app (`spike/settings/`), `make spike`,
+ `make spike-sim`, test strings and `docs/SETTINGS_SPIKE.md`.
+
+### Changed
+
+- Screenshot checks are now strict: at most 0.01 % of pixels may differ (one
+ changed character used to pass under the old 0.5 % tolerance).
+- SHA-512 and PBKDF2 inner loops optimised (37 % and 42 % faster in the
+ simulator) without changing results.
+
## [0.1.0] - 2026-10-18
Milestone M0: scaffolding. Not useful on a watch yet; the app shows only its
diff --git a/README.md b/README.md
index e13b71e..806d3df 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
A Garmin Connect IQ watch app that shows one-time passwords (TOTP, HOTP, and optional Steam Guard codes) and stores barcodes and QR codes (loyalty cards, tickets) to show at a scanner. Everything lives encrypted on the watch. There is no cloud, no backend and no analytics, and the app requests no permissions, so it cannot use the network.
-> **Status: M0 (scaffolding) done.** The plan below was approved on 2026-10-18. The Docker toolchain, the build, test, screenshot, performance and packaging pipeline, and a placeholder app are in place and pass `make ci`. Next: M1, the settings spike (needs your phone and watch), alongside M2, crypto and OTP. The Phase 0 research, with sources, is in [docs/FEASIBILITY.md](docs/FEASIBILITY.md); the daily workflow is in [docs/DEVELOPER_GUIDE.md](docs/DEVELOPER_GUIDE.md).
+> **Status: M2 (crypto and OTP core) done; M1 (settings spike) ready for your phone test.** The plan below was approved on 2026-10-18. M0 built the Docker toolchain and pipeline; M2 added SHA-512, HMAC, PBKDF2, HKDF, AES-256 records, Base32, HOTP, TOTP and Steam codes, verified against the official test vectors on four simulated watches. The settings spike (`make spike`, [docs/SETTINGS_SPIKE.md](docs/SETTINGS_SPIKE.md)) waits for your test; M3 (storage) continues meanwhile. The Phase 0 research, with sources, is in [docs/FEASIBILITY.md](docs/FEASIBILITY.md); the daily workflow is in [docs/DEVELOPER_GUIDE.md](docs/DEVELOPER_GUIDE.md).
"OTP Wallet" is a working title.
@@ -733,6 +733,8 @@ The Garmin SDK and device files are never baked into an image (Garmin's licence)
| `make ci` | lint, test, screenshots, perf, memtest, package, docs | no |
| `make products` | Check the manifest's product list against the downloaded device files | no |
| `make lock` | Regenerate the Python and npm lock files (only releases older than the cutoff date) | yes |
+| `make vectors` | Regenerate the cross-checked crypto and OTP test vectors | no |
+| `make spike`, `make spike-sim` | Build the M1 settings spike; run it headless and screenshot it | no |
| `make shell` | Shell in the dev container | no |
| `make clean` | Delete build outputs | no |
| `make nuke` | Delete everything this project created ([15.7](#157-uninstall-and-cleanup)) | no |
diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md
index e6f53a9..eacea84 100644
--- a/docs/DEVELOPER_GUIDE.md
+++ b/docs/DEVELOPER_GUIDE.md
@@ -25,6 +25,8 @@ Everything runs in Docker through the `Makefile`; the host needs only Docker and
| Accept changed screens after reviewing build/screenshots/ | `make screenshots-approve` |
| Start-up time and memory budgets | `make perf`, `make memtest` |
| Signed package for the Store beta | `make package` |
+| Regenerate the crypto/OTP test vectors | `make vectors` |
+| Build the M1 settings spike | `make spike` (instructions: [SETTINGS_SPIKE.md](SETTINGS_SPIKE.md)) |
| Everything, in order | `make ci` |
| A shell in the container | `make shell` |
@@ -66,6 +68,14 @@ The simulator is a GTK program. The scripts start it on a virtual display (Xvfb
- `monkeydo` exits with 1 even when every unit test passed, so `tools/ciq_test_report.py` judges the console output instead.
- For a normal (non-test) run, `monkeydo` blocks while the app runs and streams the app's `System.println` output. Quotes in that output arrive escaped, which is why the diagnostics use plain `key=value` pairs.
+### Monkey C facts that matter here (verified with SDK 9.2.0)
+
+- `Long` is 64-bit and wraps on overflow; `>>` is an arithmetic shift, so logical shifts and rotations mask off the copied sign bits (see `source/crypto/Sha512.mc`).
+- Assigning a value above 255 to a ByteArray element throws; mask with `& 0xFF`.
+- Native HMAC supports only SHA-256; HMAC-SHA1 and HMAC-SHA512 go through `Hmac.generic`.
+- Function calls are expensive in the VM; hot loops inline small helpers (SHA-512 went 37 % faster that way).
+- The simulator doesn't enforce the watchdog in test runs. Anything long runs as a `Jobs.Job` with a bounded step.
+
### Screenshots
`tools/sim_screenshot.py` doesn't assume where the watch is drawn in the simulator window. It searches for the device drawing (named in the device's `simulator.json`) in the window capture, comparing only the bezel, because the app draws inside the display area. The match is near-exact (mean difference below 0.3 out of 255); the drawing sits at offset (1, 26) because of the window border and the menu bar. The display rectangle from `simulator.json` is then cut out, round screens get their corners blacked out, and the result is compared with `test/golden/<device>.png`.
diff --git a/docs/FEASIBILITY.md b/docs/FEASIBILITY.md
index cb3431a..8665139 100644
--- a/docs/FEASIBILITY.md
+++ b/docs/FEASIBILITY.md
@@ -254,6 +254,13 @@ With SDK 9.2.0 and the device files of all 72 products installed:
- **Launcher icons:** the 72 products use eight icon sizes (38 to 70 px). Each gets an exact-size icon, because the compiler warns when it scales one, and warnings fail the build.
- **SDK login (11):** the CLI's browser login can't reach the container (it waits on a random localhost port), so `make sdk` uses email and password; that worked for your account.
+## Verified during M2 (2026-10-18)
+
+- **Native HMAC-SHA1 doesn't exist (2): confirmed.** The constructor rejects HASH_SHA1 ("Invalid :algorithm option value"). HMAC-SHA1 is built on the native SHA-1 hash per RFC 2104 and passes every RFC 2202 vector.
+- **AES-256-CBC works (2).** Records use AES-256; the NIST vectors pass on all four test devices.
+- **64-bit arithmetic:** `Long` overflow wraps like unsigned arithmetic, `>>` copies the sign bit, and hex literals above 2^63 parse correctly, which is what the SHA-512 implementation relies on. ByteArrays throw on values above 255 rather than wrapping.
+- **Watchdog in the simulator:** not enforced in test runs (a 300,000-iteration loop passed on a 120,000-bytecode profile), so the job design is the protection and the real limits are checked on the watch.
+
## Decisions
Answered on 2026-10-18:
diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md
index 2e668c2..daab3c2 100644
--- a/docs/PERFORMANCE.md
+++ b/docs/PERFORMANCE.md
@@ -25,6 +25,24 @@ The targets are in [README section 18](../README.md#18-performance-budgets). Thi
## Results
+### M2, 2026-10-18 (version 0.2.0, crypto and OTP primitives)
+
+`test/BenchTest.mc`, simulator in Docker, average per call (identical within noise on all four test devices):
+
+| Operation | Time | Notes |
+| --- | --- | --- |
+| TOTP, SHA-1 | 0.35 ms | HMAC built on the native SHA-1 |
+| TOTP, SHA-256 | 0.05 ms | Native HMAC-SHA256 |
+| TOTP, SHA-512 | 17.5 ms | SHA-512 in Monkey C; was 27.9 ms before inlining the rotations |
+| PBKDF2, 1000 iterations | 52 ms | Was 89 ms before removing the per-iteration wipe; native HMAC alone is about 3 ms |
+| Seal + open a record | 0.25 ms | AES-256-CBC + HMAC-SHA256, two HKDF key derivations cached |
+| Start-up self-test | 6.7 ms | All known-answer tests |
+| PBKDF2 calibration for 1 s | about 12,000 iterations | In the simulator; the watch will choose far fewer |
+
+`make perf` and `make memtest` with the larger app: in-app start-up 1 to 7 ms; launch to first frame rose from 6.5 s to 10.5 s, because `monkeydo` now sends a larger debug program (crypto code and debug information), still under its 15 s guard. Memory after the first frame: 2.3 % (768 KB watches) to 3.4 % (Forerunner 255S). The signed package for 72 products is 1.5 MB.
+
+What this means for the watch, to be confirmed with the on-watch benchmark in M4: the simulator runs on a desktop CPU, so absolute times will be many times higher on the Forerunner 70. The ratios hold: SHA-512 entries cost about 50 times as much as SHA-1 entries, and Monkey C loop overhead, not the native HMAC, dominates PBKDF2. If the watch manages fewer than 2,000 PBKDF2 iterations per second, unlocking takes longer than the 1 s target (the lower bound is kept for security); M8 can move more of the XOR work into native calls.
+
### M0, 2026-10-18 (version 0.1.0, placeholder app)
Simulator in Docker on your PC (12 cores), debug builds:
diff --git a/docs/TESTING.md b/docs/TESTING.md
index b3b5a7b..98d984c 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -13,15 +13,28 @@ 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 |
-| Screenshots | `make screenshots` | Each test device's screen matches its approved image in `test/golden/` (at most 0.5 % of pixels may differ by more than 24 levels) |
+| 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 |
| Docs | `make docs` | No broken links or anchors, no Markdown lint issues, every Mermaid diagram renders |
Test devices (`TEST_DEVICES`): Forerunner 255S (weakest: 218 px round MIP, buttons only, 512 KB), Venu Sq 2 (rectangular AMOLED), Forerunner 70 (your watch) and Forerunner 965 (454 px round AMOLED).
+## Test vectors
+
+`make vectors` regenerates `test/data/Vectors.mc` with `tools/gen_vectors.py`. Published vectors (FIPS 180-4, RFC 2202, 4226, 4231, 4648, 5869, 6238, 7914, NIST SP 800-38A) are copied from the documents and recomputed with Python's standard library, or with openssl for AES, before they are written; the generator stops on any disagreement. Seeded extra cases cover SHA padding boundaries (55 to 256 bytes), every digit count, odd periods, times around 2038 and 2100, and Steam codes. The Monkey C code under test never generates its own expected values.
+
## Results
+### M2, 2026-10-18 (version 0.2.0)
+
+`make ci` passed in full. Monkey C unit tests: 31 of 31 on each of the four test devices, covering 57 hash vectors, 21 HMAC vectors, 5 PBKDF2, 3 HKDF, 2 AES-CBC (encrypt and decrypt), 12 Base32, 100 HOTP/TOTP and 12 Steam vectors, plus tamper detection on every byte of a sealed record, PKCS#7 edge cases, Base32 error positions, period boundaries, the 2038 widening and the job runner. Python tool tests: 44 passed.
+
+Two things the simulator can't prove, kept for the real watch:
+
+- **Watchdog.** The simulator didn't stop a 300,000-iteration loop in a test on the Forerunner 255S profile, whose budget is 120,000 bytecodes. Long work is split into jobs by design (`source/util/Jobs.mc`); the real limits are checked on your watch.
+- **Speed.** See [PERFORMANCE.md](PERFORMANCE.md).
+
### M0, 2026-10-18 (version 0.1.0)
`make ci` passed in full:
@@ -44,4 +57,4 @@ Tests never use real secrets or personal data. Fixtures are generated or hand-wr
Things the simulator can't verify, done by you on the Forerunner 70. The checklist grows with each milestone; it starts with M1.
-- [ ] M1: install the settings spike from the Store beta; record field limits, accepted characters and write-back behaviour in `docs/SETTINGS_SPIKE.md`.
+- [ ] M1: install the settings spike from the Store beta and run tests A to H in [SETTINGS_SPIKE.md](SETTINGS_SPIKE.md); record the results there.
diff --git a/source/app/AppInfo.mc b/source/app/AppInfo.mc
index 5b2a586..1dc82cf 100644
--- a/source/app/AppInfo.mc
+++ b/source/app/AppInfo.mc
@@ -12,5 +12,5 @@ import Toybox.Lang;
module AppInfo {
// Semantic version of the app: MAJOR.MINOR.PATCH.
- const VERSION = "0.1.0";
+ const VERSION = "0.2.0";
}