aboutsummaryrefslogtreecommitdiff
path: root/docs/SECURITY.md
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-28 11:45:11 +0100
committercirakg <cirakg@cirakg.xyz>2026-10-28 11:45:11 +0100
commit09ef21af399a60ceb2e45526fe1a7dd94c53819c (patch)
tree7992abb501f75dfe393b2bfd424227d1fc06b5b6 /docs/SECURITY.md
parent3096eb7d366683caec24ed5d0a70ac5dae6f624d (diff)
chore: prepare repository for open-source release
Diffstat (limited to 'docs/SECURITY.md')
-rw-r--r--docs/SECURITY.md18
1 files changed, 9 insertions, 9 deletions
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
index 3091fb4..10d52e5 100644
--- a/docs/SECURITY.md
+++ b/docs/SECURITY.md
@@ -4,7 +4,7 @@
-->
# Security and storage format
-This document records how the watch stores your data, byte for byte. The design rationale is in [README section 7](../README.md#7-security-design) and the threat model in [section 8](../README.md#8-threat-model). The code is in `source/storage/` and `source/crypto/`. Every claim below is covered by a unit test in `test/storage/` or `test/crypto/`.
+This document records how the watch stores your data, byte for byte. The design rationale is in [PROJECT_PLAN section 7](PROJECT_PLAN.md#7-security-design) and the threat model in [section 8](PROJECT_PLAN.md#8-threat-model). The code is in `source/storage/` and `source/crypto/`. The storage and crypto behavior is tested in `test/storage/` and `test/crypto/`. For private vulnerability reports, see the root [security policy](../SECURITY.md).
## Storage keys
@@ -28,7 +28,7 @@ Everything lives in the app's private `Application.Storage`:
| `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.
+The PIN wrap and the lockout state have their own keys instead of living in `m` as PROJECT_PLAN 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.
## Keys
@@ -97,7 +97,7 @@ Measured by `recordSizesAreSmall` with fake data (M3):
| Code entry (a 24-byte payload) | 208 bytes |
| Code entry with the largest allowed payload (4096 bytes) | 4,279 bytes |
-The per-value limit is 8 KB, and records over 8,000 bytes are refused. **The entry cap stays at 150.** That many OTP entries take about 36 KB, well under the conservative 100 KB total (README 6.2). Large barcode payloads can fill the store before the cap is reached. The vault then reports "storage full" cleanly, and nothing is half-written (`vaultHandlesAFullStore`). The field limits are 64 bytes for name and account, 32 bytes for the group, 128 bytes for an OTP secret and 4,096 bytes for a payload.
+The per-value limit is 8 KB, and records over 8,000 bytes are refused. **The entry cap stays at 150.** That many OTP entries take about 36 KB, well under the conservative 100 KB total (PROJECT_PLAN 6.2). Large barcode payloads can fill the store before the cap is reached. The vault then reports "storage full" cleanly, and nothing is half-written (`vaultHandlesAFullStore`). The field limits are 64 bytes for name and account, 32 bytes for the group, 128 bytes for an OTP secret and 4,096 bytes for a payload.
## PIN wrap, version 1
@@ -116,7 +116,7 @@ PIN key = PBKDF2-HMAC-SHA256(PIN as ASCII digits, salt, n, 32 bytes)
## Wrong PINs
-`Lockout` implements README 7.6:
+`Lockout` implements PROJECT_PLAN 7.6:
- **Attempts are counted before the check.** Each attempt is written to `lk` before the PIN is checked, so killing the app mid-check still costs an attempt. A success deletes `lk`.
- **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.
@@ -145,7 +145,7 @@ A locked entry that arrives while the session is locked, or before a PIN exists,
- **`m.schema` is the data version.** `Migration.run()` creates fresh metadata on first start and migrates older schemas step by step. It refuses newer schemas (VaultError NEWER_SCHEMA) and damaged metadata (DAMAGED) without changing them (`migrationRefusesNewerAndDamagedMetadata`).
- **Version 0 never shipped.** It is a fixture layout (`m = {"order": [...]}`) that tests the migration machinery (`migrationV0ToV1`).
-- **The 0-to-1 step changes only the metadata, in one write.** Future migrations that rewrite records must write each new record under a temporary key and switch the index before deleting the old one (README 6.3).
+- **The 0-to-1 step changes only the metadata, in one write.** Future migrations that rewrite records must write each new record under a temporary key and switch the index before deleting the old one (PROJECT_PLAN 6.3).
## Memory hygiene and its limits
@@ -155,7 +155,7 @@ A locked entry that arrives while the session is locked, or before a PIN exists,
- The native HMAC object's internal copy of its key.
- Any copy the VM makes.
- This is best effort, as README 7.8 says.
+ This is best effort, as PROJECT_PLAN 7.8 says.
- **Invalid UTF-8 is checked before decoding.** `StringUtil.convertEncodedString` stops the app with an uncatchable error on invalid UTF-8, so `Bytes.toUtf8` validates first, and damaged text can't crash the app.
## Test builds and fixtures
@@ -165,7 +165,7 @@ A locked entry that arrives while the session is locked, or before a PIN exists,
## Fingerprints (duplicate detection)
-Imports skip an entry that has "the same type and the same secret" as an existing one (README 11.4). That has to work for locked entries while the watch is locked. So every record carries an 8-byte keyed fingerprint in its readable header, tag 13:
+Imports skip an entry that has "the same type and the same secret" as an existing one (PROJECT_PLAN 11.4). That has to work for locked entries while the watch is locked. So every record carries an 8-byte keyed fingerprint in its readable header, tag 13:
```text
fingerprint = first 8 bytes of HMAC-SHA256(Kfp, "OTPW-FP-1" || kind || type || secret or payload)
@@ -184,7 +184,7 @@ Kfp = HKDF-SHA256(device key, info = "OTPW fp v1")
## Settings forms
-- **Form hashes.** The watch remembers the last 32 applied forms (README 10.3) as keyed hashes, because a form's content includes its secret:
+- **Form hashes.** The watch remembers the last 32 applied forms (PROJECT_PLAN 10.3) as keyed hashes, because a form's content includes its secret:
```text
hash = first 8 bytes of HMAC-SHA256(Kfp, "OTPW-FH-1" || 0x1F || form name || 0x1F || field 1 || 0x1F || ...)
@@ -192,7 +192,7 @@ 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", 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. A replacement is validated when it arrives (Base32 decoded, or the code contents checked), and its material is sealed apart under `qm.<n>`, bound to its item number and entry ID. "Confirm all" and "Reject all" act only on the changes the menu showed (identified by their SealedBox IVs); any that arrive while the PIN is typed keep waiting (`queueStoresMaterialApart`, `queueChecksReplaceFirst`, `queueActsOnWhatWasShown`). 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** (PROJECT_PLAN 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. A replacement is validated when it arrives (Base32 decoded, or the code contents checked), and its material is sealed apart under `qm.<n>`, bound to its item number and entry ID. "Confirm all" and "Reject all" act only on the changes the menu showed (identified by their SealedBox IVs); any that arrive while the PIN is typed keep waiting (`queueStoresMaterialApart`, `queueChecksReplaceFirst`, `queueActsOnWhatWasShown`). 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.