aboutsummaryrefslogtreecommitdiff
path: root/docs/SECURITY.md
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-25 13:13:39 +0100
committercirakg <cirakg@cirakg.xyz>2026-10-25 13:13:39 +0100
commit67e7ce3d496c34a30ff498eaff2074543350318a (patch)
treef346175ab300afe733d2dcbcabab720d04c80188 /docs/SECURITY.md
parent404b505a41362f340e79374ee3ad4dbd468da3e4 (diff)
docs: 4-digit PIN, the offline-guessing limit, Manage needs Apply
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'docs/SECURITY.md')
-rw-r--r--docs/SECURITY.md7
1 files changed, 4 insertions, 3 deletions
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)