1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
|
<!--
Author: cirakg & claude ai
Summary: Notable changes per version, in Keep a Changelog format with semantic versioning.
-->
# Changelog
All notable changes to this project are documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
[Semantic Versioning](https://semver.org/spec/v2.0.0.html). Versions stayed
at 0.x until milestone M8; each milestone raised the minor version.
## [Unreleased]
## [1.0.0] - 2026-10-28
First public release, after the second real-watch round on the Forerunner
70 (2026-10-27): one-time codes, the 4-digit PIN, touch scrolling,
`otpauth://` import and Aegis JSON import work on the watch.
### Added
- Connect IQ Store listing: text in `docs/STORE_LISTING.md`, and the hero,
cover and screen images in `docs/store/`, built by
`tools/store_assets.py` from the Forerunner 70 goldens.
### Fixed
- On touch screens, START no longer submits a PIN set by 0.8.0 or earlier:
a tap that also arrives as START could submit the digits typed so far as
a wrong PIN. Tap the OK key instead.
- "Change PIN" with a PIN from 0.8.0 or earlier asked for the new PIN
twice: the new 4-digit PIN after the unlock is now the change.
- Entering the PIN could stop the app (watchdog) when changes from the
phone were waiting: in 0.9.0, 16 waiting "Require PIN" changes were
enough on the Forerunner 70. After the PIN, pending locked entries are
promoted and "Require PIN" changes applied in small steps while the PIN
screen shows "Checking…". What was applied, or why not, appears under
"Results ready" in the menu, and the glance also refreshes after a PIN
is set.
- Waiting phone changes are counted without decrypting them, so a large
waiting replacement can no longer stop the app at start. "Phone changes"
reads its list behind a short "Reading…" screen.
- A replacement secret or code contents is checked when it arrives: an
invalid one is refused at once instead of failing after the PIN. Valid
contents are stored apart from the queue and deleted with their change;
replacing a code with 4 KB of contents runs in steps throughout. Changes
queued by 0.9.0 are still read.
- "Confirm all" could apply changes that arrived from the phone after
the list was shown. "Confirm all" and "Reject all" now act only on the
changes shown; later ones keep waiting.
- With storage full, every correct PIN stopped the app while promoting
pending entries; it now reports "Storage full" and continues.
- Pasting about 100 `otpauth://` links tripped the watchdog: splitting the
text looped over every character in one event.
### Tests
- `make watchdog` also runs the work after a PIN (150 entries pending, 16
"Require PIN" waiting), reading the change queue and a 4 KB code
replacement (also one queued by 0.9.0), at twice the work per step.
### Documentation
- Settings spike test C, split (`docs/SETTINGS_SPIKE.md`): Garmin Connect
refuses only the backslash `\`. Aegis JSON without backslashes saves, and
a real Aegis export was imported on the Forerunner 70. The user guide
explains the `\/` workaround for Aegis exports with icons.
## [0.9.0] - 2026-10-25
Fixes from the first real-watch test on the Forerunner 70 (2026-10-21) and
the settings spike results (M1).
### Changed
- The PIN is exactly 4 digits and is entered on the 4th digit, like the
watch's own lock screen; the keypad has no OK key. A PIN set by 0.8.0 or
earlier still unlocks, and the watch then asks for a new 4-digit PIN.
New PINs are marked with `wl` in app storage.
- "Require PIN" sent from the phone while the watch is locked now applies
at the next PIN entry, without "Confirm all". Its Result says "Locks when
you next enter your PIN on the watch".
### Fixed
- Swiping up and down scrolls the entry list and the text screens (results,
About, benchmark); on the Forerunner 70 only the buttons worked.
- One tap could open an entry twice (a tap also arriving as START), and on
the PIN screen START submitted the digits typed so far: the likely cause
of "Wrong PIN" followed by the code opening.
- The first-time encoding progress of 2D codes jumped to 90 % and stayed
there; it now follows the work.
- "Entry IDs" with many entries tripped the watchdog and closed the app; it
now reads the entries in steps.
- Auto-lock now closes a locked barcode, as it does a locked code.
- A locked barcode shows the padlock in the list; a queued Require PIN no
longer shows "Confirm in menu".
- Clipped text: the Reset PIN question, the last line of text screens on
rectangular watches, the glance title on round watches; the warning colour
is darker in the light theme.
### Tests
- The UI flow has a second scenario (`uitest-extra.jungle`): legacy PIN and
the forced new PIN, Change and Reset PIN, the queued Require PIN, UPC-A,
Code 39, the code error screen, the encoding progress, auto-lock on a
barcode, the light theme and the glance without a code. The main flow
adds the Steam and Library details and the menu's text screens, scrolled
by swipes on touch screens. 42 to 44 screens per watch.
### Documentation
- The settings spike results (`docs/SETTINGS_SPIKE.md`): 8000 characters per
field and 64,000 in total arrive; `otpauth://` URIs save; a string with
`< > \ " { } %`, accented letters and an emoji was refused, with split test
strings to find out which.
- The first Forerunner 70 benchmark (`docs/PERFORMANCE.md`) and what a
4-digit PIN means against a copy of the watch's storage (`docs/SECURITY.md`).
## [0.8.0] - 2026-10-21
Milestone M8: polish, performance and the release candidate. 1.0.0 follows
once the on-watch checklist is done (docs/TESTING.md).
### Added
- The favourite code in the glance (opt-in, "Show the favourite code in the
glance"): the first favourite TOTP or Steam entry without the PIN, with a
countdown bar. The app seals that entry for the glance under `g` after
every change (`source/app/GlanceCode.mc`); the glance checks the option
itself, so turning it off hides the code at once.
- Manage entry: "Mark as favourite (glance)" and "Remove favourite".
- Brightness: full brightness on AMOLED watches and the backlight on MIP
watches while a barcode is shown, renewed for up to a minute; a tap starts
the minute again.
- `make watchdog` (in `make ci`): a stress build (`stress.jungle`,
`source/util/StressTest.mc`) encodes the largest code of every 2D
symbology, packs it, finds its runs and draws it, with two steps per
event, on the Forerunner 255S and 70. The last code stays on the screen
and must decode.
- `make memtest` checks the glance too (at most 70 % of its 64 KB).
- `make products` checks each product's watchdog limit against the job
runner's assumption.
- `docs/USER_GUIDE.md`, with the list of known limitations.
### Changed
- Imports use far less memory: parsed items are kept packed (about 110
bytes each instead of 760 as objects), and the pasted text is held once.
The peak of a 100-entry import on the Forerunner 255S fell from 56 % to
45 % of its memory.
- QR mask scoring works on rows and columns packed 16 modules per word
(the same penalties as before, checked against the per-module rules on
every mask): a version 10 QR takes 40 % less compute.
- Watches with 768 KB (every target except the Forerunner 255 and 255S) run
two job steps per timer tick, which halves import and encoding times.
- Large 2D codes are drawn as runs (one rectangle per run), from a bitmap
painted in steps when there are too many runs for one frame.
- The barcode cache has a new format with 16-bit sizes; caches from 0.7 are
re-encoded once.
### Fixed
- Large 2D codes could stop the app with "Watchdog Tripped" on every watch:
the QR segmentation, Reed-Solomon blocks, function patterns and the final
mask; Data Matrix encodation, placement and drawing; Aztec bit encoding,
stuffing, the GF(4096) tables and generator; PDF417 compaction; the cache
packing; and drawing a code module by module. Every step is now bounded
and measured (`make watchdog`). An entry like that added from the phone
would have crashed the app during settings processing, at every start.
- A PDF417 code wider than 255 modules (big screens) was cached with a
wrong width.
- The Data Matrix placement grid used up to 90 KB; it now uses a byte
array (about 35 KB for the largest symbol).
## [0.7.0] - 2026-10-20
Milestone M7: the phone settings forms. Entries can now be added and changed
from Garmin Connect on the phone, one form at a time.
### Added
- The full settings form (`resources/settings/`): Add OTP, Add code, Manage
entry, Import, Options and a read-only About group, with every property of
README 10.2 and its strings.
- Processing (`source/settings/SettingsJob.mc`) in README 10.3 order: Import,
Add OTP, Add code, Manage, then the About fields. Each form is applied only
when its switch is on. On success the form is cleared (secret first) and
its keyed hash remembered, so the same form sent again does nothing. After a
problem only the secret is cleared, so the rest can be corrected. Every form
writes a short result into its Result field and the results screen.
- Add OTP (`OtpForm.mc`): TOTP, HOTP and Steam, validated as README 10.4 says
(Base32 errors name the character and position, a warning below 80 bits).
- Add code (`CodeForm.mc`): all 12 symbologies, validated by the real encoder
(`CodeCheck`, a job for 2D codes), with a warning when the code doesn't fit
this screen. 2D codes are cached encoded on the way in, so the first
opening is instant.
- Manage entry (`ManageForm.mc`): edit details, move, require or drop the
PIN, replace the secret or contents, set a HOTP counter, delete. Entry IDs
are case-insensitive.
- Changes to locked entries wait for the PIN (README 7.9,
`ChangeQueue.mc`): they are sealed with the device key under `q`, listed
under "Phone changes" in the menu and applied only after the PIN is
entered on the watch, or rejected.
- About fields (`AboutScan.mc`): the entry catalog (IDs and names, at most
800 characters; IDs and types only with "Encrypt names"), the version and
the storage use, rewritten only when something changed.
- "Encrypt names" now works: new entries follow it, and existing ones are
re-sealed (locked ones after the next PIN entry).
- Options are applied as soon as the phone saves them.
- `tools/check_resources.py` (in `make lint`): every resource file validates
against the SDK's `resources.xsd`, and settings, properties and strings
agree. The dev image gains `xmlschema` for it.
- Tests: 34 new Monkey C tests (154 per device), including every symbology
valid and invalid, every Manage action, the queue, idempotency and a
check that the form defaults match `properties.xml`. The UI flow sends the
forms the way the phone does and checks the results: a TOTP with its known
code, a Code 128 card that must decode, and a queued change confirmed with
the PIN.
### Changed
- Form hashes are keyed (HMAC-SHA256 with a key derived from the device
key) instead of plain SHA-256 of the content, because the content includes
secrets. Fields are fed to the native HMAC one by one: copying a large
import form into one buffer and zeroing it tripped the watchdog on the
Forerunner 255S during M7.
## [0.6.0] - 2026-10-20
Milestone M6: barcodes. Code entries now show their barcode: all 12
symbologies of README 12.1, encoded on the watch.
### Added
- M6a, linear barcodes (`source/codes/`): EAN-13, EAN-8, UPC-A and UPC-E,
with check digits added, verified, fixed or off, and canonical UPC-E zero
suppression; Code 128 (sets B and C); Code 39 with an optional mod-43 check
character; ITF with an optional check digit; Codabar with start and stop
characters.
- The code screen: black on white, the largest whole module size, human-
readable text, and a full-backlight request.
- Reference rows from zxing-cpp (`tools/gen_code_vectors.py`, `make codes`).
Barcodes from the unit tests are decoded with zxing-cpp in every test run
(`tools/check_codes.py`), and the UI flow decodes a screenshot of the code
screen.
- M6b, QR Code (`source/codes/Qr.mc`, `ReedSolomon.mc`, `QrTables.mc`):
- Versions 1-40 and levels L-H, with optimal numeric, alphanumeric and byte
segmentation and all eight masks scored as segno does.
- Encoded as a watchdog-safe job with progress, and cached sealed with the
entry's key (`x.<id>`).
- Drawn in the largest centred square at 2 px per module or more.
- Checked against segno (`tools/gen_qr_tables.py`, `make codes`), decoded
with zxing-cpp, and decoded from a screenshot of a version 6 card.
- M6c, Data Matrix (`DataMatrix.mc`):
- ECC 200 square sizes 10x10 to 144x144, ASCII with digit pairs, and
Base256 or Upper Shift for UTF-8.
- Encoded as a job; identical to ppf.datamatrix in ASCII mode.
- M6c, PDF417 (`Pdf417.mc`):
- Text, byte and numeric compaction as pdf417gen does it, with automatic
(ISO) or fixed error-correction levels.
- Columns and module size chosen for the screen.
- The pattern tables are generated JSON resources, loaded only while
encoding (`tools/gen_pdf417_tables.py`).
- Identical to pdf417gen.
- 2D codes now share a matrix type with a row height, a fit rule for round
screens, deferred cache saving and 8-at-a-time packing.
- Dev image: pdf417gen, ppf-datamatrix and aztec-code-generator as reference
encoders (pure Python, MIT).
- M6d, Aztec (`Aztec.mc`):
- Compact 1-4 and full range 1-32 layers, error correction of 10, 23, 36
or 50 %, Upper, Lower and Digit modes with Binary Shift.
- The geometry is ported from aztec_code_generator, and symbols match it
for single-class text.
- Decoded with zxing-cpp, also from a screenshot.
## [0.5.0] - 2026-10-20
Milestone M5: import. You can now add entries by pasting `otpauth://` links or
an unencrypted Aegis JSON export into the new Import group of the app settings.
### Added
- `source/import/`:
- A streaming JSON tokenizer: strict, with depth and length limits; skips
icons natively; reports incomplete input.
- The Aegis reader: database versions 1-3, groups, encrypted exports
refused, duplicate fields rejected.
- The `otpauth://` parser: run-together URIs, `;` separators, Steam forms,
issuer mismatch notes.
- The import job: Replace mode, the duplicate and replacement checks, a
multi-part buffer sealed with the device key (32 KB, 24 h), and a report.
- `source/settings/`:
- The settings processor: the Import form, applied after the first frame
and on every settings change. It clears the fields, writes the status,
remembers form hashes and opens the results screen.
- The results store; the list shows "Results ready" until they're read.
- The Import group in the phone settings (8 text fields, mode, "Require PIN",
"Import now", result).
- An 8-byte keyed secret fingerprint in every record header, to find
duplicates of locked entries while locked (docs/SECURITY.md).
- Seeded fuzz corpus (`tools/gen_fuzz_corpus.py`, `make corpus`); 29 Monkey C
and 5 Python tests. The UI flow imports 100 entries through the settings
form and checks the peak memory.
### Changed
- Info screens space out their items; the results screen is titled "Results".
### Deferred
- Google Authenticator `otpauth-migration://` exports (README 11.1): the
importer explains that they aren't supported yet.
## [0.4.0] - 2026-10-19
Milestone M4: the screens. The app is usable in the simulator with demo
entries; adding real entries comes with M5 (import) and M7 (settings).
### Added
- List of entries with live codes, countdowns, lock and barcode icons, and
groups. Rows adapt to narrow screens, and headers are read only for the
visible rows.
- OTP detail: large grouped code, countdown ring (bar on rectangular screens),
next code in the last 5 seconds, HOTP "next" with the counter, entry ID.
- PIN screen: touch keypad or button digit selector. It covers create (twice,
with a warning below 6 digits), unlock and change. Wrong PINs vibrate, and
the lockout wait is shown. Pending locked entries are promoted after
unlocking.
- Menu: lock now, set/change/reset PIN, settings results, entry IDs, clock
status, about. Debug builds add demo entries, a performance overlay and an
on-watch benchmark.
- Glance with the number of entries; the crypto self-test on first start and
daily; auto-lock while a locked entry is open.
- Option properties with defaults (`resources/settings/properties.xml`).
- UI-test build with fake entries, PIN and a frozen clock. A scripted UI flow
(`tools/ui_flow.py`) checks 10 screens on each of seven simulated watches.
Also new: `tools/gen_fixture_wrap.py`, and 7 Monkey C and 6 Python tests.
### Changed
- PBKDF2 XORs 32-bit words: 2.3× faster, and 100 iterations per job step,
because 200 tripped the watchdog on the Forerunner 255S.
- Codes are computed in their own timer events, at most one SHA-512 code per
event.
- `make screenshots` runs the scripted flow; goldens are now
`test/golden/<device>/<screen>.png`. Start-up budgets are set from the M4
measurements.
## [0.3.0] - 2026-10-19
Milestone M3: storage, keys and PIN logic. Nothing user-visible yet; the
screens come with M4.
### Added
- Storage (`source/storage/`): the vault (ordered entries, add, update, move,
remove, HOTP counter), the deterministic TLV codec, the record format (readable
header plus an encrypt-then-MAC box bound to the entry ID), the device key and
the PIN-wrapped master key, resumable PIN derivation, the wrong-PIN lockout
(trusted clock with an uptime fallback, optional erase after 10 failures), the
session with auto-lock and key wiping, the pending queue for locked entries,
schema migrations, the 150-entry cap and clean store-full errors.
- `Bytes.toUtf8` with strict UTF-8 validation.
- `docs/SECURITY.md`: the stored byte formats and what each protection is
tested by.
- 34 Monkey C tests (65 in total per device) and storage benchmarks.
### Changed
- The PIN wrap and the lockout state are stored under their own keys (`w`,
`lk`) instead of inside the metadata (README 6.2 updated).
## [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
name and version.
### Added
- Project plan (`README.md`), Phase 0 feasibility findings with what M0
verified (`docs/FEASIBILITY.md`), developer guide, testing and performance
documents.
- Docker images with pinned versions: `garmin-otp/dev` (Ubuntu 22.04 snapshot,
Java 17, headless simulator stack, hash-locked Python tools,
checksum-verified SDK manager CLI) and `garmin-otp/docs` (Markdown lint and
Mermaid rendering).
- `Makefile` running every step in hardened containers: no capabilities,
read-only root filesystem, only the project folder mounted, no network
outside image builds and `make sdk`, serial even under `make -j`.
- `make sdk` (agreements, login, SDK 9.2.0, device files for 72 watches,
token deleted afterwards) and `make key` (RSA-4096 developer key).
- Build pipeline with strict type checking and warnings as errors, per-size
launcher icons for all 72 products, unit tests in the headless simulator on
four devices, golden-image screenshot tests, start-up and memory budgets, and
the signed `.iq` package.
- Python tools with 44 tests: test-log judge, screenshot checker, budget
checker, product rule checker, Markdown link checker, project linter (code
rules and the Author/Summary header rule) and icon generator.
- A minimal watch app (home screen with name and version) and a smoke test.
|