aboutsummaryrefslogtreecommitdiff
path: root/docs/SECURITY.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/SECURITY.md')
-rw-r--r--docs/SECURITY.md26
1 files changed, 22 insertions, 4 deletions
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
index f52a983..0deacc0 100644
--- a/docs/SECURITY.md
+++ b/docs/SECURITY.md
@@ -1,6 +1,6 @@
<!--
Author: cirakg & claude ai
- Summary: How stored data is protected: storage keys, the record and PIN-wrap byte formats, lockout, session, migration and limits.
+ Summary: How stored data is protected: storage keys, the record and PIN-wrap byte formats, lockout, session, migration, the glance code and limits.
-->
# Security and storage format
@@ -18,12 +18,13 @@ Everything lives in the app's private `Application.Storage`:
| `lk` | dictionary | Wrong-PIN state: `f` failures, `d` delay in seconds, `t` trusted time of the last attempt; absent after a success | `Lockout` |
| `e.<id>` | bytes | One entry record (format below) | `Vault` |
| `p.<id>` | bytes | Pending locked entry: a record sealed with the device key | `Vault` |
-| `x.<id>` | bytes | Encoded 2D barcode: a SealedBox (aad `OTPW-X-1` + ID) with the entry's own key (device key, or the master key for locked entries) of width, height, quiet zone and the packed modules. Deleted when the entry is updated, removed or erased | `Vault` |
+| `x.<id>` | bytes | Encoded 2D barcode: a SealedBox (aad `OTPW-X-1` + ID) with the entry's own key (device key, or the master key for locked entries) of the packed matrix, format 2: `2`, width and height (2 bytes each), quiet zone, row height, then the modules 8 per byte. Format 1 (0.6 and 0.7, one byte of width) is no longer read: such a cache is re-encoded once. Deleted when the entry is updated, removed or erased | `Vault`, `Codes.Packed` |
+| `g` | bytes | The glance code (below): a SealedBox (device key, aad `OTPW-GL-1`) of one favourite entry's label, type, algorithm, digits, period and secret. Only while "Show the favourite code in the glance" is on | `GlanceStore` |
| `imp`, `imp.<n>` | dictionary, bytes | Waiting multi-part import: `imp` = `{"n": chunks, "t": saved at}`, `imp.<n>` = chunks of one SealedBox (device key, aad `OTPW-IMP-1`) of the text | `ImportBuffer` |
| `res` | dictionary | Latest settings results (`l` lines, `u` unseen) | `Results` |
| `fh` | array | Keyed hashes of the last 32 applied settings forms (below) | `Forms` |
| `q` | array | Phone changes to locked entries waiting for the PIN: one SealedBox per change (device key, aad `OTPW-Q-1`), at most 16 | `ChangeQueue` |
-| `cs` | string | Keyed hash of what the About fields were last written from (IDs, pending IDs, "Encrypt names", waiting changes, version); absent while names still need converting | `AboutScan` |
+| `cs` | string | Keyed hash of what the About fields and the glance code were last written from (IDs, pending IDs, "Encrypt names", the glance option, waiting changes, version); absent while names still need converting | `AboutScan` |
| `st` | array | Version and day of the last passed self-test | `AppState` |
The PIN wrap and the lockout state have their own keys instead of living in `m` as README 6.2 first planned. Each wrong PIN rewrites the lockout state before the check, and a small separate value makes that write cheap and keeps it from risking the entry index.
@@ -189,6 +190,23 @@ Kfp = HKDF-SHA256(device key, info = "OTPW fp v1")
Kfp is the fingerprint key above. Without the device key a stored hash reveals nothing. With it, an attacker can only test guesses of a whole form, as with fingerprints. The fields go to the native HMAC one by one; the short ones (every secret field) are zeroed afterwards. An import form's text is too long to zero byte by byte within the watchdog limit; it stays in memory as the property's string anyway.
- **Secrets in the forms.** Add OTP's secret, Add code's contents and Manage entry's new secret are cleared first when the watch processes a form, also after a problem (the other fields stay so you can correct them). Messages never quote a secret.
-- **Changes from the phone to locked entries** (README 7.9). Edit, replace, set counter, "Don't require PIN" and delete of a locked entry are not applied when they arrive. Each one is sealed with the device key under `q`, which is no weaker than the settings text it came in. They are applied only after someone enters the PIN on the watch under "Phone changes", which always asks for the PIN, even while the session is unlocked. Moving an entry is applied at once: the order isn't protected, and positions are absolute. Changes to unlocked entries apply at once and are listed on the results screen (`manageQueuesLockedEntryChanges`, `manageQueueIsSealedAndLimited`).
+- **Changes from the phone to locked entries** (README 7.9). Edit, replace, set counter, "Don't require PIN", the favourite mark and delete of a locked entry are not applied when they arrive. Each one is sealed with the device key under `q`, which is no weaker than the settings text it came in. They are applied only after someone enters the PIN on the watch under "Phone changes", which always asks for the PIN, even while the session is unlocked. Moving an entry is applied at once: the order isn't protected, and positions are absolute. Changes to unlocked entries apply at once and are listed on the results screen (`manageQueuesLockedEntryChanges`, `manageQueueIsSealedAndLimited`).
- **The About catalog** sends entry IDs and names to the phone's settings, where your entries came from in the first place. With "Encrypt names" on it sends only IDs and types.
- **"Encrypt names"** puts names and accounts into the encrypted part of each record. Turning it on or off re-seals every entry the watch can reach: unlocked ones at once, locked ones the next time the PIN is entered (`settingsAppliesEncryptNames`). The barcode cache is kept.
+
+## Glance code
+
+With "Show the favourite code in the glance" on (off by default), the glance in the watch's glance loop shows the current code of one entry without opening the app:
+
+- **Which entry.** The first favourite (from Aegis, or "Mark as favourite" in Manage entry) that is a TOTP or Steam entry and doesn't require the PIN. Locked entries never appear, whatever their mark; HOTP entries don't either (showing a code would need the counter to advance).
+- **Why a copy.** The glance gets 64 KB and can't open the vault, so after every change to the entries or the option the app's About scan seals that one entry for it under `g`:
+
+ ```text
+ g = SealedBox(device keys, aad "OTPW-GL-1",
+ 1 | type | algorithm | digits | period (2 bytes) | label length | label (UTF-8) | secret)
+ ```
+
+ This is the protection the entry itself has (unlocked entries are sealed with the same device key), so the copy adds no new way in. The glance opens it with the same SealedBox code and checks its MAC first (`glanceItemRefusesTamperingAndOtherKeys`).
+- **Turning it off.** The glance reads the option itself, so the code disappears as soon as the phone saves the change; the app deletes `g` at its next start or settings change. Removing the favourite mark, requiring the PIN for the entry, or deleting it also removes or replaces `g` (`glanceScanChoosesTheFirstQualifyingFavourite`).
+- **What it means for you.** Anyone who can see your watch's glance loop sees that one code, exactly as if they opened the app to that entry. Choose an entry for which that's acceptable, or leave the option off.
+- **Memory.** The glance computes the code once per period and redraws the countdown every second; the secret is wiped when the glance is hidden.