diff options
| -rw-r--r-- | README.md | 16 | ||||
| -rw-r--r-- | docs/DEVELOPER_GUIDE.md | 2 | ||||
| -rw-r--r-- | docs/SECURITY.md | 7 | ||||
| -rw-r--r-- | docs/USER_GUIDE.md | 11 | ||||
| -rw-r--r-- | docs/YOUR_STEPS.md | 14 |
5 files changed, 29 insertions, 21 deletions
@@ -249,7 +249,7 @@ flowchart TD ### 7.2 PIN and key derivation -- The PIN is 4 to 10 digits. The app warns below 6. +- The PIN is exactly 4 digits, entered like the watch's own lock screen: the 4th digit submits it, with no OK key (changed after the real-watch test, 0.9.0). A PIN set by 0.8.0 or earlier (4 to 10 digits) still unlocks; the watch then asks for a new 4-digit PIN. - `PIN key = PBKDF2-HMAC-SHA256(PIN digits as ASCII, 16-byte random salt, N iterations, 32 bytes)`. - N is calibrated on your watch when you create or change the PIN, so one derivation takes about 1 s. It runs as a resumable job and is stored next to the salt. The lower and upper bounds for N are set from the M2 measurements. - The PIN key unwraps the locked master key. A correct PIN is exactly "the wrap's MAC verifies". No PIN hash is stored anywhere. @@ -325,7 +325,7 @@ On the first start after an install or update, and then once a day, the app runs | 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 | -| Lost or stolen watch, attacker extracts the storage | Unlocked entries: no. Locked entries: only as strong as the PIN | 6 digits means 1,000,000 guesses, each costing one ~1 s PBKDF2 run on the watch but far less on a GPU: minutes to hours. Longer PINs help. Garmin's object-store encryption for Store apps may stop this; it's undocumented, so it isn't relied on | +| Lost or stolen watch, attacker extracts the storage | Unlocked entries: no. Locked entries: only as strong as the PIN | 4 digits means 10,000 guesses. Each costs one ~1 s PBKDF2 run on the watch but microseconds on a PC, so a copied storage falls in seconds. The PIN protects against someone using the watch (delays after wrong PINs, optional erase), not against a storage copy. Garmin's object-store encryption for Store apps may stop the copy; it's undocumented, so it isn't relied on. The debug build's files were visible over USB on the Forerunner 70 (part 4 of docs/YOUR_STEPS.md) | | Garmin's servers and the Garmin Connect app | No, for anything typed or pasted into settings | A consequence of D2 and D3: secrets travel in the clear. The watch clears the fields after reading them, but Garmin may keep copies | | Malware on your phone (clipboard, accessibility services) | No | It can read what you paste. Clear the clipboard and delete the Aegis export after importing | | Someone controlling your phone or Garmin account changes settings | Partly | They can add, or delete unlocked, entries. Changes to locked entries need your PIN on the watch ([7.9](#79-integrity-of-changes-coming-from-the-phone)) | @@ -621,7 +621,7 @@ A symbology counts as implemented only when checks 1 to 3 pass. Otherwise it's l - **OTP detail**: issuer and account, the code in large digits (grouped 3+3, 3+4 or 4+4), and a countdown ring along the bezel (a bar on rectangular screens) with the seconds left. In the last 5 seconds, the next code is shown. HOTP shows "Next" and the counter. Steam shows 5 characters. The entry ID is in small print at the bottom. - **Code detail**: the code centred on white, the name in small print if there's room, or a "Too large" message. - **PIN**: a keypad on touch screens; a digit selector on button-only watches. Digits show as dots. A wrong PIN vibrates (where available) and shows any waiting time. -- **Create or change PIN**: enter it twice. Warns below 6 digits. +- **Create or change PIN**: enter 4 digits twice. - **Menu**: Lock now, Change PIN, Reset PIN (erases locked entries), Settings results, Entry IDs, Clock status, About (version, storage use, self-test result). - **Glance**: "OTP Wallet ยท 12 entries". If you turn it on, it shows the code of the first favourite TOTP or Steam entry without the PIN, with a progress bar, instead ([docs/SECURITY.md](docs/SECURITY.md#glance-code)). @@ -664,11 +664,11 @@ stateDiagram-v2 PIN, touch PIN, buttons only Glance .-----------------. .-----------------. +----------------------+ / Enter PIN \ / Enter PIN \ | OTP Wallet | - | * * * o o o | | * * [5] o o o | | 12 entries | + | * * * o | | * * [5] o | | 12 entries | | 1 2 3 | | ^ | +----------------------+ | 4 5 6 | | UP/DOWN digit | (opt-in: favourite code | 7 8 9 | | START next | and a progress bar) - \ < 0 OK / \ BACK delete / + \ < 0 / \ BACK delete / '-----------------' '-----------------' ``` @@ -682,7 +682,7 @@ stateDiagram-v2 | Next HOTP code | Tap the code | START | | Menu | Long-press the list, or MENU | MENU (hold UP on most watches) | | Enter a PIN digit | Tap the keypad; the delete key deletes | UP/DOWN choose a digit, START adds it, BACK deletes | -| Confirm the PIN | OK on the keypad, or START | UP/DOWN to "OK", then START | +| Confirm the PIN | Automatic at the 4th digit | Automatic at the 4th digit | ## 14. Target devices and API levels @@ -941,7 +941,7 @@ Tests are written alongside the code, never after. ### 17.4 Simulator UI tests -- **Setup**: the UI-test build (`uitest.jungle`, `source/util/TestMode.mc`) starts with fake entries, the PIN 123456 and a frozen clock. `tools/ui_flow.py` drives it through xdotool: it clicks the buttons from the device files, and taps and swipes on touch screens. It waits for each screen's `DIAG shown` line instead of sleeping. +- **Setup**: the UI-test build (`uitest.jungle`, `source/util/TestMode.mc`) starts with fake entries, the PIN 1234 and a frozen clock. `tools/ui_flow.py` drives it through xdotool: it clicks the buttons from the device files, and taps and swipes on touch screens. It waits for each screen's `DIAG shown` line instead of sleeping. - **Screenshots** of every screen on the 4 test devices and the 3 spot-check devices are compared with approved goldens in `test/golden/<device>/`. At most 0.01 % of pixels may differ. - **Barcode screens** must decode with zxing-cpp. - **TOTP screens** are checked against the expected codes, from the debug build's view-model log and the golden image. @@ -1017,7 +1017,7 @@ The simulator runs on your PC's CPU, so its timings don't represent a watch. In | Large codes don't fit small screens | Some cards can't be shown | Validation and warnings at entry time | | Developer key lost | No updates possible | Backup instructions; `make nuke` confirmation | | Garmin changes the beta process | Can't install | Documented manual steps; checked in M1 | -| Short PINs can be broken offline | Locked secrets exposed if storage is extracted | Documented; longer PIN option; Garmin's object-store encryption as a bonus | +| Short PINs can be broken offline | Locked secrets exposed if storage is extracted | Documented in the user guide's known limitations; the PIN is fixed at 4 digits by the owner's choice (easier entry on the watch); Garmin's object-store encryption as a bonus | | The ~1 s PIN derivation feels slow | UX | Calibrated, with progress; one PIN per session (D1) | | Strict typing friction or compiler bugs | Slower development | SDK version pinned; issues documented | diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md index d33f383..fcecffb 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -55,7 +55,7 @@ Use other devices with `TEST_DEVICES="fr70 venu3"` on any target that takes a de `docker/scripts/build-app.sh` compiles one device in one mode: - `debug`: what `make build`, `make sim` and `make perf` use. It contains the `(:debug)` diagnostics, the debug menu items (demo entries, performance overlay, on-watch benchmark) and the fixtures. -- `uitest`: a debug build plus `uitest.jungle`, for `make screenshots`. It keeps the `(:uitest)` half of `source/util/TestMode.mc`, so the app starts with a wiped store, fake entries, the PIN 123456, a frozen clock and a 10 s auto-lock. +- `uitest`: a debug build plus `uitest.jungle`, for `make screenshots`. It keeps the `(:uitest)` half of `source/util/TestMode.mc`, so the app starts with a wiped store, fake entries, the PIN 1234, a frozen clock and a 10 s auto-lock. - `stress`: a debug build plus `stress.jungle`, for `make watchdog`. It keeps the `(:stress)` half of `source/util/StressTest.mc`: the app starts straight into the watchdog stress test instead of the list. - `release`: `monkeyc -r` removes every `(:debug)` declaration; used by `make package`. - `test`: adds `test.jungle` and `--unit-test`. diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 0deacc0..c68cdc1 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -108,7 +108,8 @@ PIN key = PBKDF2-HMAC-SHA256(PIN as ASCII digits, salt, n, 32 bytes) - **A correct PIN is exactly "the box opens".** No PIN hash is stored. Changing the salt or `n` makes the box fail to open (`pinWrapIsAuthenticated`). - **`n` must lie between 2,000 and 200,000** or the wrap counts as damaged before any derivation. A tampered huge count can't hang the watch. -- **PINs are 4 to 10 digits.** `n` is calibrated on the watch to about one second (UI in M4). +- **New PINs are exactly 4 digits** (0.9.0), stored with the marker `wl` = 4 next to the wrap. A wrap without the marker is a PIN from 0.8.0 or earlier (4 to 10 digits): it still unlocks, and the PIN screen then asks for a new 4-digit PIN (`pinLegacySixDigitWrapStillUnlocks`, `pinSetRequiresFourDigits`). `n` is calibrated on the watch to about one second; the Forerunner 70 manages 2,000 iterations per second, the minimum. +- **What 4 digits mean.** 10,000 possible PINs. On the watch, the delays after wrong PINs and the optional erase after 10 make guessing impractical. Against a copy of the app's storage they don't help: 10,000 guesses of 2,000 PBKDF2 iterations take seconds on a PC. The debug build's storage files showed up over USB on the Forerunner 70, so treat locked entries as protected from someone holding the watch, not from someone who copies its files. - **Changing the PIN re-wraps the same master key.** Entries are not re-encrypted (`pinChangeKeepsEntries`). - **Reset PIN deletes things the PIN protected:** the wrap, every locked entry and every pending item. Unlocked entries stay (`pinResetErasesOnlyLockedData`). @@ -120,7 +121,7 @@ PIN key = PBKDF2-HMAC-SHA256(PIN as ASCII digits, salt, n, 32 bytes) - **Delays:** four free attempts, then 30 s after the 5th failure, doubling up to a 1 hour cap (`lockoutDelaySchedule`). An attempt during a delay is refused and not counted. - **Delays use the trusted (GPS-synced) clock when it is available,** so restarting the app doesn't shorten them (`lockoutBlocksAndSurvivesRestart`). - **Without a synced trusted clock, a delay counts system uptime.** It restarts in full when the app restarts or the system timer goes backwards: never shorter, sometimes longer (`lockoutUsesUptimeWithoutTrustedClock`). -- **An unreadable `lk` is not a reset:** no free attempts remain (`lockoutTreatsDamagedStateConservatively`). Deleting `lk` does reset the count, but only someone who can already write app storage can do that, and app storage isn't reachable from outside the app (threat model). +- **An unreadable `lk` is not a reset:** no free attempts remain (`lockoutTreatsDamagedStateConservatively`). Deleting `lk` does reset the count, but only someone who can write app storage can do that. Other Connect IQ apps can't; someone with the watch and a USB cable may be able to (the debug build's storage files were visible over USB on the Forerunner 70), and could then also guess the PIN offline (threat model). - **With "Erase locked entries after 10 wrong PINs" on,** the 10th failure deletes the wrap, locked entries and pending items (`pinEraseAfterTenFailures`). ## Session @@ -158,7 +159,7 @@ A locked entry that arrives while the session is locked, or before a PIN exists, ## Test builds and fixtures -- **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 UI-test build** (`uitest.jungle`) wipes the store at every start and fills it with fake entries. It sets the PIN 1234 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) diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index f4fe5f8..dd15901 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -41,7 +41,7 @@ In the Garmin Connect app: your watch, then **Connect IQ apps**, then **OTP Wall Each form has a **Result** field the watch writes back, and the watch shows the same lines on its **Settings results** screen (also in its menu). After a form is applied the watch turns its switch off and clears it, the secret field first. After a problem only the secret field is cleared, so you can correct the rest and send it again. Sending exactly the same form twice does nothing the second time. -Your phone may or may not show the cleared fields and the Result text before you reopen the settings page; that depends on Garmin Connect (see [SETTINGS_SPIKE.md](SETTINGS_SPIKE.md)). +Close and reopen the settings page on the phone to see the cleared fields and the Result text. Garmin Connect on Android shows them after reopening ([SETTINGS_SPIKE.md](SETTINGS_SPIKE.md)); changes reach an open app in about a second. ## Add an authenticator code @@ -112,11 +112,11 @@ Use the **Manage entry** form with the entry's **ID**: four characters such as ` ## The PIN and locked entries -- **Set PIN** in the watch menu creates it (4 to 10 digits; the watch warns below 6). You are also asked for one the first time an entry needs it. +- **Set PIN** in the watch menu creates it (exactly 4 digits; the 4th digit enters it, there is no OK key). A PIN set with version 0.8.0 or earlier still works once; the watch then asks you for a new 4-digit PIN. You are also asked for one the first time an entry needs it. - Entering the PIN unlocks all locked entries until the app locks again: when you leave the app, after the auto-lock time (option), or with **Lock now** in the menu. - **Wrong PINs:** after 5 wrong PINs the watch makes you wait, longer with each further mistake. With the option **Erase locked entries after 10 wrong PINs**, the tenth wrong PIN erases the locked entries (unlocked entries stay). - **Change PIN** keeps your entries. **Reset PIN**, for a forgotten PIN, erases the locked entries and lets you set a new PIN; there is no other way back into them. -- Locked entries are encrypted with a key that only your PIN opens. A short PIN can still be guessed by someone who copies the watch's storage, so choose 6 or more digits for anything important. +- Locked entries are encrypted with a key that only your PIN opens. A 4-digit PIN stops someone using your watch (wrong PINs make it wait, and it can erase the locked entries after 10), but it can be guessed in seconds by someone who copies the watch's storage to a computer. Keep that in mind for very sensitive accounts. ## Daily use on the watch @@ -172,12 +172,15 @@ OTP Wallet never exports your secrets: they only flow into the watch. Your backu - **Connect IQ limits** - No unlocking with the watch's own passcode; the app has its own PIN. - App settings only work with the Store version, which is why the app is a private beta rather than sideloaded. - - The phone may not show what the watch writes back (Result fields, cleared forms, the About catalog) until you reopen the settings. + - The phone shows what the watch writes back (Result fields, cleared forms, the About catalog) only after you reopen the settings. + - Garmin Connect refuses to save some special characters in a text field. `otpauth://` links work; whether Aegis JSON (which needs `"`, `{` and `}`) can be pasted is still being tested ([SETTINGS_SPIKE.md](SETTINGS_SPIKE.md#follow-up-for-test-c)). - **Import** - Encrypted Aegis exports can't be decrypted on a watch (it would need 32 MiB of memory); export unencrypted. - Google Authenticator export links aren't supported; mOTP and Yandex entries are skipped. - Icons and notes aren't imported. - Pasted text is limited to the eight import fields per sync, and a waiting import to 32 KB. +- **PIN** + - The PIN is fixed at 4 digits. It stops someone using your watch, but someone who copies the watch's storage to a computer can try all 10,000 PINs in seconds. - **Storage** - At most 150 entries and about 100 KB in total. - At most 16 phone changes can wait for the PIN at once. diff --git a/docs/YOUR_STEPS.md b/docs/YOUR_STEPS.md index 679b139..f9fb267 100644 --- a/docs/YOUR_STEPS.md +++ b/docs/YOUR_STEPS.md @@ -37,7 +37,7 @@ Total time: about 2.5 to 3 hours, which can be split across days at any part bou | --- | --- | | Test secret A | `JBSWY3DPEHPK3PXP` | | Test secret B | `GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ` | -| Test PIN | `135790` (any 6 digits you don't use elsewhere) | +| Test PIN | `1357` (any 4 digits you don't use elsewhere) | | Test EAN-13 | `5901234123457` | Two `otpauth://` links for the import test (fake accounts, secret A and B): @@ -141,7 +141,7 @@ While a card is open: the screen should go to full brightness (the Forerunner 70 ### 4.5 Remove the debug build -1. On the watch, remove OTP Wallet: highlight it in the apps list, hold UP (MENU) and choose **Remove**, or find it under **Settings**, **Activities & Apps** (or **Apps**). The exact labels on the Forerunner 70 may differ. +1. On the watch, remove OTP Wallet: highlight it in the apps list, hold UP (MENU) and choose **Remove**. This worked on the Forerunner 70 (2026-10-21). If the apps list has no such option, look under **Settings**, **Activities & Apps** (or **Apps**). 2. Connect the watch over USB and delete its leftover storage: `fr70.DAT`, `fr70.IDX` and `fr70.IMT` in `GARMIN/APPS/DATA`, and `fr70.SET` in `GARMIN/APPS/SETTINGS`. Leave every other file alone: files with 8-character names such as `G7AA3237.*` belong to apps installed from the Store (Settings Spike and others). 3. Eject, unplug, and check that OTP Wallet is gone from the apps list. @@ -204,6 +204,8 @@ Keep OTP Wallet open on the watch while you save forms, unless a step says other The entry IDs are in the About group on the phone, or in **Entry IDs** on the watch menu. +For every Manage entry change: fill in the entry ID and the action, then turn on **Apply** and save. Without Apply the watch ignores the form. + 1. Rename `TestTwo` to `Renamed`. 2. Move `Renamed` to position 1. 3. Delete `Renamed`. @@ -211,9 +213,11 @@ The entry IDs are in the About group on the phone, or in **Entry IDs** on the wa ### 6.6 Locked entry changes need the PIN -1. With Manage entry, set **Require PIN** on `TestOne`. Save. -2. Then send **Don't require PIN** for `TestOne`. The watch must **not** apply it by itself. -3. On the watch: menu, **Phone changes**, **Confirm all**, enter the PIN. Now it applies. +1. Close OTP Wallet on the watch and start it again, so the PIN isn't entered yet. With Manage entry, action **Require PIN** on `TestOne`, **Apply** on, save. The Result says "Locks when you next enter your PIN on the watch". +2. On the watch, open `TestLocked` and enter the PIN. `TestOne` now requires the PIN too (it shows the lock mark; reopen the app to check that it asks for the PIN). +3. While the PIN is still entered, a Require PIN applies at once: try it on another entry if you like. +4. Then send **Don't require PIN** for `TestOne`. The watch must **not** apply it by itself, even while the PIN is entered. +5. On the watch: menu, **Phone changes**, **Confirm all**, enter the PIN. Now it applies. ### 6.7 Options |
