summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-18 20:28:09 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-18 20:28:09 +0200
commit8b0e632526d9504c5c0e09bab72ab83db1c46f9b (patch)
tree164954f84d24316f41f7aa62774291157806a17a
parent5396ace6a695ecf530c2ecc9793dfe01f523e70d (diff)
docs: record final decisions, test watch and SDK login details
- README: Forerunner 70 is the test watch, the phone is Android, Docker access is settled; how `make sdk` logs in and why the browser login can't be used; the new `make products` and `make lock` targets. - FEASIBILITY: final decisions D1-D5. - Space out table separator rows and turn bold-only lines into headings so markdownlint passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--README.md88
-rw-r--r--docs/FEASIBILITY.md17
2 files changed, 58 insertions, 47 deletions
diff --git a/README.md b/README.md
index aa1af67..bdfe0ea 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,7 @@ A Garmin Connect IQ watch app that shows one-time passwords (TOTP, HOTP, and opt
### What it won't do
| Not supported | Why |
-|---|---|
+| --- | --- |
| Unlocking with the watch's own PIN or passcode | Connect IQ has no API for it |
| Encrypted Aegis exports | Decrypting one needs 32 MiB of RAM; watch apps get under 0.8 MB. You chose plain JSON import over an off-watch converter (decision D2) |
| Aegis icons and notes, mOTP and Yandex entries | The watch doesn't decode images, notes may hold private text, and mOTP/Yandex weren't requested. They're skipped with a message |
@@ -62,27 +62,32 @@ A Garmin Connect IQ watch app that shows one-time passwords (TOTP, HOTP, and opt
Decided on 2026-10-18:
| # | Decision | Choice |
-|---|---|---|
+| --- | --- | --- |
| D1 | PIN policy | Session unlock: enter the PIN once per session. Everything re-locks when you leave the app or after an inactivity timeout |
| D2 | Import | Paste `otpauth://` URIs or an unencrypted Aegis JSON export into the settings; the watch parses them. No converter tool, no encrypted Aegis import |
| D3 | Manual-entry layout | Inbox forms ("Add OTP", "Add code") plus a "Manage entry" form |
| D4 | Targets | Watches with API 5.0 or newer and at least 512 KB for apps |
-Still open. None of these block this plan:
+Also settled on 2026-10-18, after this plan was approved:
+
+| # | Question | Answer |
+| --- | --- | --- |
+| D5 | Docker access | You joined the `docker` group |
+| Q1 | Test watch | Forerunner 70 (2026): API 6.0, 390×390 AMOLED, touch plus 5 buttons, 768 KB. It's one of the simulator test devices |
+| Q2 | Phone | Android, so the iPhone `&` and `\` problem doesn't affect you |
+
+Still open:
| # | Question | Needed by |
-|---|---|---|
-| D5 | Docker access: join the `docker` group, or set up rootless Docker? | Phase 2 |
-| D6 | Your Garmin account: accept the SDK licence and log in inside the container; create a developer account for the beta | Phase 2 and M1 |
-| Q1 | Which watch will you test on? | M1 |
-| Q2 | Is your phone Android or iPhone? The iPhone Garmin app reportedly rejects `&` and `\`, which affects both paste formats | M1 |
+| --- | --- | --- |
+| D6 | Your Garmin account: accept the SDK licence and log in inside the container (`make sdk`); create a developer account for the beta | Phase 2 and M1 |
## 3. Feasibility findings
A summary. The details, evidence and sources are in [docs/FEASIBILITY.md](docs/FEASIBILITY.md).
| # | Question | Verdict | Consequence |
-|---|---|---|---|
+| --- | --- | --- | --- |
| 1 | Unlock entries with the Garmin device PIN | Not possible | App PIN with session unlock (D1). Garmin's system passcode, where a watch has one, shuts apps down when it locks and complements the app lock |
| 2 | SHA-1/256/512, HMAC, PBKDF2, AES on the watch | Partially | Native: SHA-1, SHA-256, AES-ECB/CBC, HMAC-SHA256, random bytes. Built from their standards in Monkey C: HMAC-SHA1, SHA-512, PBKDF2, HKDF, encrypt-then-MAC |
| 3 | Import and manual entry through app settings | Partially | Text fields only, no file upload. Settings only work for Store installs, so delivery is a private beta. Array settings are broken (CIQQA-4751), hence the inbox design (D3) |
@@ -142,7 +147,7 @@ flowchart TB
```
| Module | Responsibility |
-|---|---|
+| --- | --- |
| `app/` | `AppBase`: lifecycle, initial view, glance view, `onSettingsChanged`, locking in `onStop` |
| `crypto/` | Wrappers around the native hashes, HMAC (RFC 2104), SHA-512 (FIPS 180-4), PBKDF2 (RFC 8018), HKDF (RFC 5869), AES-CBC with PKCS#7 padding, encrypt-then-MAC records, constant-time comparison, zeroing, random bytes, start-up self-test |
| `otp/` | Base32, HOTP, TOTP, Steam, parameter validation |
@@ -169,7 +174,7 @@ Design rules:
### 6.1 Entry fields
| Field | Applies to | Where it's stored | Notes |
-|---|---|---|---|
+| --- | --- | --- | --- |
| `id` | all | header | 4 characters from `23456789ABCDEFGHJKMNPQRSTVWXYZ`, random and unique. Shown on the watch and used by the Manage form |
| `kind` | all | header | OTP or code |
| `type` | OTP | header | TOTP, HOTP or Steam |
@@ -191,7 +196,7 @@ Design rules:
### 6.2 Storage layout (`Application.Storage`)
| Key | Content | Protected by |
-|---|---|---|
+| --- | --- | --- |
| `m` | Metadata: schema version, entry index (the order of IDs), KDF salt and iteration count, wrapped locked master key, lockout state, hashes of applied forms | The wrapped key is encrypted. The rest is not secret |
| `dk` | Device key, 32 random bytes | Stored as is ([7.4](#74-what-stays-readable)) |
| `e.<id>` | One entry record | Device key, or the locked master key for locked entries |
@@ -299,7 +304,7 @@ On the first start after an install or update, and then once a day, the app runs
**Assets**: OTP secrets, barcode contents, the PIN, entry names.
| Scenario | Protected? | Notes |
-|---|---|---|
+| --- | --- | --- |
| Someone picks up your watch while the app is closed | Locked entries: yes. Unlocked entries: no, by design | Lock what matters. Turn on Garmin's system passcode if your watch has one |
| Someone grabs the watch during an unlocked session | Until auto-lock (default: 60 s without input) | Choose a shorter timeout, or "Immediately" |
| Lost or stolen watch, attacker uses the screen | Locked entries: yes | Lockout delays, optional erase |
@@ -331,10 +336,10 @@ Garmin's "array settings" (growable lists) are broken today. A failed save reset
### 10.2 Groups and fields
-**Add OTP**
+#### Add OTP
| Property | Type | Field | Notes |
-|---|---|---|---|
+| --- | --- | --- | --- |
| `otpType` | number | list | TOTP, HOTP, Steam |
| `otpIssuer` | string | alphaNumeric (max 40) | |
| `otpAccount` | string | alphaNumeric (max 64) | |
@@ -348,10 +353,10 @@ Garmin's "array settings" (growable lists) are broken today. A failed save reset
| `otpAdd` | boolean | switch | **Add this entry**: the watch only acts when this is on |
| `otpStatus` | string | read-only | Result, written by the watch |
-**Add code**
+#### Add code
| Property | Type | Field | Notes |
-|---|---|---|---|
+| --- | --- | --- | --- |
| `codeName` | string | alphaNumeric (max 40) | |
| `codeSymbology` | number | list | The 12 symbologies |
| `codePayload` | string | alphaNumeric | The code's text or digits |
@@ -363,10 +368,10 @@ Garmin's "array settings" (growable lists) are broken today. A failed save reset
| `codeAdd` | boolean | switch | **Add this entry** |
| `codeStatus` | string | read-only | |
-**Manage entry**
+#### Manage entry
| Property | Type | Field | Notes |
-|---|---|---|---|
+| --- | --- | --- | --- |
| `mgId` | string | alphaNumeric | The entry ID shown on the watch, for example `K7F2` (case-insensitive) |
| `mgAction` | number | list | Edit details, Move to position, Require PIN, Don't require PIN, Replace secret or contents, Set HOTP counter, Delete |
| `mgName`, `mgAccount`, `mgGroup` | string | alphaNumeric | Blank means unchanged. `-` clears the group |
@@ -376,20 +381,20 @@ Garmin's "array settings" (growable lists) are broken today. A failed save reset
| `mgApply` | boolean | switch | **Apply** |
| `mgStatus` | string | read-only | |
-**Import**
+#### Import
| Property | Type | Field | Notes |
-|---|---|---|---|
+| --- | --- | --- | --- |
| `impText1` to `impText8` | string | alphaNumeric | Joined in order. The number of fields is set after the settings spike (M1) |
| `impMode` | number | list | Append (default), Replace imported entries, Cancel pending import |
| `impLocked` | boolean | switch | Require PIN for every entry in this import |
| `impApply` | boolean | switch | **Import now** |
| `impStatus` | string | read-only | |
-**Options**
+#### Options
| Property | Field | Default |
-|---|---|---|
+| --- | --- | --- |
| `optAutoLock` | list: Immediately, 15 s, 30 s, 1 min, 2 min, 5 min | 1 min |
| `optWipe` | switch: Erase locked entries after 10 wrong PINs | off |
| `optTimeOffset` | numeric, −120 to 120 s | 0 |
@@ -501,7 +506,7 @@ The full files are written in M7.
### 11.3 Parsing rules
-**`otpauth://` URIs**
+#### `otpauth://` URIs
- The label is percent-decoded and split on the first `:` (or `%3A`) into issuer and account, with surrounding spaces trimmed.
- An `issuer` parameter wins over the label's issuer. A mismatch is noted in the report.
@@ -511,7 +516,7 @@ The full files are written in M7.
- Unknown parameters (`image`, `color` and so on) are ignored.
- Errors name the URI's position, for example "URI 3: digits=9 is not supported (6–8)".
-**Aegis JSON**
+#### Aegis JSON
- **Encrypted exports are refused** with a message explaining how to export unencrypted. They're recognised by `header.slots` not being null, or `db` being a string.
- **Types**: `totp`, `hotp` and `steam` are imported. `motp`, `yandex` and unknown types are skipped and reported.
@@ -538,7 +543,7 @@ Imported entries behave exactly like manual ones. You rename, regroup, move, loc
### 12.1 Symbologies
| Symbology | Accepted content | Options | Validation | Scope |
-|---|---|---|---|---|
+| --- | --- | --- | --- | --- |
| QR | Any text (UTF-8) | Error correction L/M/Q/H (default M) | Capacity for the chosen level; fits this screen | Versions 1 to 40, numeric/alphanumeric/byte modes with automatic segmentation, all 8 masks with standard penalty scoring. No Kanji, ECI or Micro QR |
| Code 128 | ASCII 32 to 126 | Human-readable text | Characters; fits | Automatic switching between code sets A, B and C for the shortest result. GS1-128 (FNC1) deferred |
| Code 39 | `0–9`, `A–Z`, space, `- . $ / + %` | Mod-43 check digit on or off; text | Characters; fits | Full ASCII mode deferred |
@@ -565,7 +570,7 @@ Imported entries behave exactly like manual ones. You rename, regroup, move, loc
### 12.3 What fits (QR, byte mode, error correction M)
| Screen | Largest centred square | At 3 px per module | At 2 px per module |
-|---|---|---|---|
+| --- | --- | --- | --- |
| 218 px round (Forerunner 255S) | 154 px | version 6, 106 bytes | version 13, 331 bytes |
| 260 px round (fēnix 7, Forerunner 255) | 184 px | version 9, 180 bytes | version 16, 450 bytes |
| 390 px round (vívoactive 5) | 276 px | version 16, 450 bytes | version 28 |
@@ -648,7 +653,7 @@ stateDiagram-v2
### 13.4 Input
| Action | Touch | Buttons |
-|---|---|---|
+| --- | --- | --- |
| Scroll the list | Swipe up and down | UP and DOWN |
| Open an entry | Tap it | START |
| Back | Swipe right, or BACK | BACK |
@@ -661,9 +666,10 @@ stateDiagram-v2
- **Manifest**: type `watch-app`, `minApiLevel` 5.0.0, no permissions, English.
- **Products**: generated in M0 from the downloaded device definitions: every watch with API 5.0 or newer and at least 512 KB for watch apps. That includes fēnix 7, 8 and E; epix 2; Forerunner 165, 255, 265, 570, 955, 965 and 970; Venu 2, 3, 4, Sq 2 and X1; vívoactive 5 and 6; Instinct 3 AMOLED; Enduro 3; and related MARQ, quatix, tactix, D2, Descent and Approach models.
-- **Simulator test devices**:
+- **Simulator test devices** (`TEST_DEVICES` in the Makefile):
- Forerunner 255S: the weakest target (218 px round MIP, buttons only, 512 KB).
- Venu Sq 2: rectangular AMOLED with touch.
+ - Forerunner 70: your watch (390 px round AMOLED, touch and buttons).
- Forerunner 965: 454 px round AMOLED with touch.
- Spot checks: fēnix 7 (MIP with touch and buttons), Instinct 3 AMOLED (buttons only), Venu X1 (the largest rectangle).
- **Memory**: watch app 512 to 768 KB; glance 64 KB. Budgets are in [section 18](#18-performance-budgets).
@@ -673,7 +679,7 @@ stateDiagram-v2
### 15.1 Images
| Image | Base | Contents | Network while building |
-|---|---|---|---|
+| --- | --- | --- | --- |
| `garmin-otp/dev` | `ubuntu:22.04` pinned by digest, apt from a dated `snapshot.ubuntu.com` mirror | OpenJDK 17; simulator libraries (`libwebkit2gtk-4.0-37`, `libusb-1.0-0`, `libsm6`, GTK); Xvfb, openbox, x11vnc, noVNC, xdotool, ImageMagick; a Python 3.10 virtual environment (pytest, hypothesis, zxing-cpp, segno, Pillow, all exact versions with hashes); `connect-iq-sdk-manager-cli` v0.8.4 (SHA-256 checked); make, openssl | yes |
| `garmin-otp/docs` | Node LTS pinned by digest | markdownlint-cli2, mermaid-cli | yes |
@@ -693,17 +699,19 @@ The Garmin SDK and device files are never baked into an image (Garmin's licence)
- `sudo usermod -aG docker cirakg`, then log out and back in. Membership in that group is equivalent to root on this machine.
- Or rootless Docker, which needs the AUR package `docker-rootless-extras`.
2. **`make image`** builds the images.
-3. **`make sdk`**:
- - Shows Garmin's agreements (`agreement view`). You accept them (`agreement accept`) and log in, both interactively.
- - Downloads SDK 9.2.0 and the device files for the manifest's products into `.cache/home/.Garmin/ConnectIQ`.
- - Fallback if the third-party CLI's login fails: run Garmin's own SDK Manager inside the container and use it through noVNC.
+3. **`make sdk`** (interactive, in a terminal):
+ - Shows the link to Garmin's agreements and their version hash. You read them in your browser and type `accept`; the hash makes sure you accept exactly the text you read.
+ - Asks for your Garmin email and password. They're passed only to the SDK manager CLI, which sends them to Garmin's sign-in page; neither is written to disk. The CLI's browser-based login can't be used, because it waits for a redirect to a random port inside the container, which your browser can't reach without host networking.
+ - Downloads SDK 9.2.0 and the device files and simulator fonts for the manifest's products into `.cache/home/.Garmin/ConnectIQ`.
+ - Deletes the login token at the end (`KEEP_LOGIN=1 make sdk` keeps it), so no Garmin credential stays in the project.
+ - **Two-step verification**: password login doesn't support it. Fallback (not built yet): sign in through a browser running inside the container, shown at 127.0.0.1:6080, so the redirect stays inside the container.
4. **`make key`** generates your developer key into `.cache/keys/` and prints backup instructions.
5. **`make test`** and **`make sim`** confirm everything works.
### 15.4 Make targets
| Target | What it does | Network |
-|---|---|---|
+| --- | --- | --- |
| `make image` | Build the Docker images | yes |
| `make sdk` | Licence, login, download the SDK and device files | yes (Garmin only) |
| `make key` | Generate the developer key (never overwrites an existing one) | no |
@@ -717,6 +725,8 @@ The Garmin SDK and device files are never baked into an image (Garmin's licence)
| `make package` | Signed `.iq` for the Store beta, plus per-device `.prg` files | no |
| `make docs` | Markdown lint, link check, Mermaid render check | no (yes when building the docs image) |
| `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 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 |
@@ -864,7 +874,7 @@ Tests are written alongside the code, never after.
### 17.1 Layers
| Layer | Tool | Runs in |
-|---|---|---|
+| --- | --- | --- |
| Unit tests (Monkey C) | Garmin's Run No Evil framework, `(:test)` functions | Headless simulator, 3 devices |
| Test-vector generation, reference encoders, decoding, screenshot analysis | Python: pytest, hypothesis, zxing-cpp, segno, Pillow | Dev container |
| UI and screenshot tests | Simulator, Xvfb, ImageMagick, xdotool | Dev container |
@@ -947,7 +957,7 @@ The full checklist will be in `docs/TESTING.md`. It covers what the simulator ca
## 18. Performance budgets
| Metric | Target on a watch | Simulator gate | How it's measured |
-|---|---|---|---|
+| --- | --- | --- | --- |
| Cold start to the first unlocked TOTP code drawn | ≤ 1.0 s on a mid-range watch (Forerunner 255 or 265) | ≤ 2× the M4 baseline | `System.getTimer()` from `AppBase.initialize` to the end of the first list draw. On the watch, the debug overlay |
| TOTP with SHA-1 or SHA-256 | ≤ 30 ms | ≤ 2× baseline | Timer around the computation, averaged over 100 runs |
| TOTP with SHA-512 (pure Monkey C) | ≤ 400 ms | ≤ 2× baseline | Same |
@@ -964,7 +974,7 @@ The simulator runs on your PC's CPU, so its timings don't represent a watch. In
## 19. Risks and mitigations
| Risk | Impact | Mitigation |
-|---|---|---|
+| --- | --- | --- |
| Real settings limits are smaller than assumed, or write-back doesn't work | Pastes don't fit; fields don't clear | Spike in M1, early; multi-part import; idempotent forms; results on the watch |
| The iPhone Garmin app rejects `&` and `\` | Paste import fails on iPhone | Confirm in the spike; `;` accepted in URIs; Android workaround documented |
| Plain-text secrets in settings (D2, D3) | Garmin and the phone see secrets | Documented in the threat model; fields cleared; advice to delete export files |
@@ -985,7 +995,7 @@ The simulator runs on your PC's CPU, so its timings don't represent a watch. In
## 20. Documentation
| Document | Content |
-|---|---|
+| --- | --- |
| `README.md` | This plan, kept up to date as the project's overview |
| [`docs/FEASIBILITY.md`](docs/FEASIBILITY.md) | Phase 0 findings with sources |
| `docs/USER_GUIDE.md` | Install the beta, import from Aegis, add codes, PIN and locking, daily use, moving to a new watch, troubleshooting |
diff --git a/docs/FEASIBILITY.md b/docs/FEASIBILITY.md
index e150a48..255f195 100644
--- a/docs/FEASIBILITY.md
+++ b/docs/FEASIBILITY.md
@@ -9,7 +9,7 @@ Verdicts: **Supported**, **Partially** (works with a workaround or a limit), **N
## Summary
| # | Question | Verdict | Consequence |
-|---|---|---|---|
+| --- | --- | --- | --- |
| 1 | Unlock entries with the Garmin device PIN | Not possible | App-level PIN instead (decision D1) |
| 2 | SHA-1/256/512, HMAC, PBKDF2, AES on the watch | Partially | Native: SHA-1, SHA-256, AES-ECB/CBC, HMAC-SHA256, ECDH, random bytes. SHA-512 and PBKDF2 are built from their standards in Monkey C |
| 3 | Import and manual entry through app settings | Partially | Text fields only, no file upload. Settings only work for apps installed from the Connect IQ Store (a private beta counts). Dynamic lists are broken today |
@@ -32,7 +32,7 @@ Verdicts: **Supported**, **Partially** (works with a workaround or a limit), **N
Alternatives (decision **D1**):
| Option | How it works | Trade-off |
-|---|---|---|
+| --- | --- | --- |
| **A. App PIN, session unlock** (recommended) | 4 to 10 digit PIN (6 or more advised), entered with an on-screen keypad (touch) or up/down/start (buttons). PBKDF2 turns it into a key that unwraps the key for locked entries. Everything stays unlocked until you leave the app, or until an inactivity timeout (default 60 s). | One PIN entry per session. |
| **B. PIN every time** | Same derivation, but the key is discarded as soon as you leave a locked entry. | Safer if the watch is grabbed mid-session. You wait for the key derivation (about 1 s, to measure) every time. |
| **C. Per-entry choice of A or B** | Each entry's lock setting picks "session" or "every time". | Most flexible. More UI states to build and test. |
@@ -50,7 +50,7 @@ Wrong attempts, for every option:
`Toybox.Cryptography` exists since API 3.0.0 and needs no manifest permission. Every candidate watch in section 5 appears in the Hash, Cipher and HMAC device lists (the original vívoactive 3 is missing from Cipher and HMAC).
| Primitive | Native? | Notes |
-|---|---|---|
+| --- | --- | --- |
| SHA-1, SHA-256, MD5 | Yes | `Hash` class, streaming |
| SHA-512 | **No** | |
| HMAC | SHA-256 only | Docs: "HASH_SHA256 is the only supported algorithm in Connect IQ 3.x" |
@@ -94,7 +94,7 @@ Garmin documents growable lists that the app reads as an array of dictionaries.
Verdict: not usable. Alternatives (decision **D3**):
| Option | How it works | Trade-off |
-|---|---|---|
+| --- | --- | --- |
| **S1. Fixed slots** (recommended) | N slot groups per kind (for example 10 OTP and 10 code slots, a build-time constant). Each slot has every field from requirement 11, plus "enabled" and "order". Edit in place, reorder with "order", delete by switching off or clearing. Secret fields are write-only: blank means "keep the stored secret". | Hard cap on hand-entered entries. The phone shows groups named "OTP slot 1" and so on, because group titles can't be dynamic. Imported entries aren't shown in slots; you delete, reorder and lock them on the watch. Works even if write-back (3e) doesn't. |
| **S2. Inbox plus manage form** | One "new OTP" and one "new code" form that the watch reads and then clears. A "manage" form takes an entry ID (shown on the watch) and an action: edit fields, delete, move to position, lock. | No cap on entries and a small settings payload. You need the entry ID from the watch. The watch must ignore forms it has already applied. |
| **S3. One entry per line** | One text field with one line per entry (`otpauth://...` lines and a similar `code://...` format), parsed and validated on the watch with an error per line. | Compact, for power users. Limited by field length (about 800 characters, see 3d) and the `&` problem on iOS. Secrets stay in the field unless write-back works. |
@@ -126,7 +126,7 @@ The watch can change its own settings values with `Properties.setValue` (saved w
Memory in KB from Garmin's device reference pages, API level from the compatible-devices table:
| Device | API | Screen | Touch | Watch app | Glance |
-|---|---|---|---|---|---|
+| --- | --- | --- | --- | --- | --- |
| Forerunner 255S | 5.2 | 218×218 round, MIP 64 colours | no | 512 | 64 |
| Forerunner 255 | 5.2 | 260×260 round, MIP | no | 512 | 64 |
| fēnix 7 / 7S / 7X | 5.2 | 240 to 280 round, MIP | yes | 768 | 64 |
@@ -225,7 +225,7 @@ Docker 29.9.1 with buildx is installed and running. Your user isn't in the `dock
## 13. Import pipeline candidates (decision D2)
| Candidate | Pros | Cons |
-|---|---|---|
+| --- | --- | --- |
| **A. Paste `otpauth://` lines into settings.** The watch parses them and clears the field. | No tools needed. | Secrets pass through the phone and Garmin in the clear. iOS may reject the `&` in every URI. Only a handful of entries fit per field. Needs the Store beta. |
| **B. Offline converter to an encrypted blob** (recommended). The converter runs in Docker with no network and turns an Aegis export (plain or encrypted), `otpauth://` lists or Google migration exports into an encrypted blob. You paste the blob into settings and type a 12-digit transfer code on the watch once. | Only ciphertext reaches Garmin. Validation and warnings (for example, a code too big for your screen) happen before anything reaches the watch. Compact, about 70 characters per entry. The blob is encrypted, so any channel can carry it to your phone; the converter can also show it as QR codes for your phone's camera. | Needs a computer. Needs the Store beta. The transfer code is worth about 40 bits times the key-derivation cost: fine against bulk exposure, not against a determined attacker who also gets hold of the blob. |
| **B′. Like B, with ECDH pairing instead of a transfer code** | No code to type, and full 128-bit security. | A one-time pairing step: you copy the watch's public key from the phone settings into the converter. Connect IQ's little-endian key format needs interoperability testing on a real watch. |
@@ -251,9 +251,10 @@ Answered on 2026-10-18:
- **D3 Manual-entry layout: S2, inbox plus manage form.**
- **D4 Targets: API 5.0+ watches** with at least 512 KB for apps. No low-memory build.
-Still open:
+Also answered: your test watch is a Forerunner 70 (2026; API 6.0, 390×390 AMOLED, touch and buttons, 768 KB), and your phone is Android, so the iPhone problem with `&` and `\` in 3d doesn't affect you. You chose option 1 for D5 below.
+
+The options as they were presented:
-- Which watch you'll test on, and whether your phone is Android or iPhone (the `&` and `\` problem in 3d affects iPhone only).
- **D5 Docker access** (needs sudo, which you run):
- Option 1: `sudo usermod -aG docker cirakg`, then log out and back in. Membership in the `docker` group is equivalent to root on this machine.
- Option 2: rootless Docker, which needs the AUR package `docker-rootless-extras`.