Damage Scenarios and Quarantine
The lookup table for “what damage X is prevented by which invariant Y in which doc Z.” Each row names a concrete vector found during the audit and the single owner-doc anchor that defeats it. The table itself is the operational core of the threat model — adding a row obliges declaring the defense in exactly one owner doc.
Damage Scenario → Invariant Map
Section titled “Damage Scenario → Invariant Map”| # | Damage scenario | Defense | Owner doc |
|---|---|---|---|
| 1 | Old client writes a sidecar after stripping unknown fields | Sidecar signature covers _unknown; old client refuses to write when sidecar_schema > its max known | Metadata — Schema Versioning Rules |
| 2 | Faulty client uploads bytes that don’t match the declared content type | Server’s content_type allow-list per protocol version (no-key check) + receiving client decoder sandbox | Validation §, Clients — Sandboxed Decoder |
| 3 | Buggy client uploads chunk with wrong offset and re-tries | Idempotency tuple (upload_id, offset, chunk_hash); duplicate at offset with different hash → reject | Import — Upload Protocol |
| 4 | Hostile peer sends an old-but-validly-signed manifest to revive a deleted asset | prior_provenance_hash chain advance check on both client and server | Cryptography — Provenance, Validation § |
| 5 | Malicious client re-signs an existing manifest under a weaker crypto_suite_id | Signatures cover crypto_suite_id and protocol_version | Cryptography — Write Authorization |
| 6 | Two devices concurrently caption the same photo | Caption LWW + superseded_captions array surfaces the loser | Metadata — Surfacing Concurrent Edits |
| 7 | Client issues an OR-set remove for an element it never observed an add for | Add-id binding: removes target a specific add_id; unknown add_id is rejected | Metadata — Add-id Binding |
| 8 | Buggy client overwrites a good thumbnail with a corrupt one | Every derivative carries a signed DerivativeManifest on its own chain; overwrite is a derivative-replace lifecycle action | Cryptography — Derivative Provenance |
| 9 | A client declares timestamp = 2099-01-01 to distort the audit | timestamp is self-asserted and audit-only — never load-bearing for ordering or authorization (those ride the chain + epoch); a gross-drift sanity bound surfaces it, and the server’s own received_at is the authoritative clock | Cryptography — Write Authorization |
| 10 | Server-side TOCTOU on blob dedup creates a duplicate | Dedup-check and pending-row insert are atomic on a single Postgres transaction | Filesystem — Content-Addressing and Deduplication |
| 11 | A faulty client uploads bytes that exceed its declared size | Server bounds cumulative received at every chunk, not only at finalization | Import — Chunk Rules |
| 12 | A new client writes a manifest with a crypto_suite_id the server does not recognize | Refuse-by-default at handshake: 400 before any session is created | Validation — Protocol Negotiation |
| 13 | A federated peer floods the rejected-hash table to exhaust memory | Per-peer quota; bounded LRU memory | Federation — Soft-Fail Semantics |
| 14 | A model swap silently invalidates the AI tag namespace | Every tags_ai entry carries model_id+model_version; the vector-index insert API and query layer (capsule-core::db) reject unknown-model inserts and exclude stale entries — cross-model comparison is forbidden | Metadata — Tag Provenance and Namespacing |
| 15 | A leaked session token revokes all of a user’s other sessions to lock them out | revoke_all_sessions requires master-key proof, not session auth | Authentication — Explicit revocation |
| 16 | An attacker holding every current key tries to rewrite the asset’s history | Provenance chain references each predecessor’s hash; rewriting any past record requires forging an earlier (possibly retired) device’s hybrid signature | Cryptography — Provenance |
| 17 | A client picks a random amk_version to skip MLS | Server’s no-key check: amk_version must be monotonic per album and known to the server | Validation § |
| 18 | A v_old client tries to write into an album that has been upgraded to v_new | Album pinning + upgrade ceremony quiescence: server returns 409 for writes carrying a stale intent_id | Versioning — Album Upgrade Ceremony |
| 19 | A malformed CBOR sidecar lands on disk after a crash mid-write | Malformed sidecar → quarantined to .library/quarantine/; never silent-skipped | Filesystem — Repair |
| 20 | A federation pull returns a manifest claiming a device that’s not in the user’s directory | Server’s no-key check: created_by_device must be in the user’s published device directory | Validation § |
| 21 | A buggy client uploads a metadata blob with a hand-crafted wire format | Metadata blob wire format is byte-exact; mismatched envelope rejected at decode | Cryptography — Metadata Blob Wire Format |
| 22 | A retry of a delete manifest decrements blob refcount twice | Manifest idempotency keyed by prior_provenance_hash: a duplicate manifest is a no-op | Validation — Idempotency |
| 23 | A backup restore from 6 months ago silently overwrites current state | Restore is a chain-reconciliation, never a blind overwrite: newer local state always wins; an older or divergent restored manifest is quarantined for explicit merge, never auto-applied | Backup & Recovery — Backup Verification |
| 24 | A new device claims its key is older than the account itself | Device entry in the device directory is signed by the IK and carries added_at; a server rejects an upload from a device whose added_at postdates the manifest | Cryptography — Device Keys, Validation § |
| 25 | A peer floods notifications to make Capsule pull garbage | Notifications are advisory; pull is on Capsule’s schedule and goes through full validation | Federation — Pull-Only Federation |
| 26 | A federated server’s TLS endpoint silently changes its public key | Servers TOFU-pin each other’s keys; a rotation is accepted only after a multi-vantage perspective check corroborates it, else surfaced | Federation — Server Identity and Key Rotation |
| 27 | A buggy client writes a stack edit that updates one member’s sidecar and not the others | Stack edits are bundle-atomic: all .tmp files staged first, all renamed together; any rename failure discards the bundle | Filesystem — Atomic Writes |
| 28 | A federated peer serves a stale capability token after revocation | Capability TTL ≤ 24h + published revocation list polled ≤ 15 min | Federation — Federation Capabilities |
| 29 | A faulty client uploads embeddings derived from a model the receiver does not run | Vector index refuses inserts whose model_id is unknown | AI — Embedding Provenance |
| 30 | A client tries to forge a server-derived field (computed ciphertext hash, received_at, sync_seq, blob reference counts) | The server assigns/recomputes these itself and ignores client-supplied values; the ciphertext hash is recomputed at finalization and a mismatch is rejected | Import — Finalization and Integrity |
| 31 | A server serves a stale device directory to undo a revocation or hide a new device | Master-signed monotonic directory_version; every reader refuses a directory below its per-user high-water mark | Cryptography — Device Directory |
| 32 | A server fabricates or rewinds an album epoch, or a manifest cites an epoch whose key is mid-distribution | amk_version ceiling anchored to the admin-signed MLS commit chain, not the server’s counter; a beyond-attested epoch is rejected, an in-flight key yields verify_asset pending/retry (not a forgery) | Cryptography — Write Authorization |
| 33 | A share link is enumerated or brute-forced | ≥128-bit opaque-id + per-IP/per-link rate limits + indistinguishable 404 + home-server-only serving | Share Links — Security Contract |
| 34 | A peer mass-reports or false-flags a user to force a takedown | Federated reports are signed by the reporting server and rate-limited per (reporting_server, reported_user); per-user blocks never propagate as server-wide blocks | Moderation — Federated Reporting |
| 35 | A user pulls from many federated peers to exhaust the home server’s storage | Federated-received blobs charged to the receiving user’s quota (deduped) under a per-(receiving_user, source_peer) cap | Quota — Accounting Model |
When a scenario surfaces during implementation that does not match any of the above, the rule is: add a row here, then declare the defense in exactly one owner doc. Never restate a defense in multiple docs.
Quarantine Surfaces
Section titled “Quarantine Surfaces”Every “don’t apply, surface it” code path. The union exists so the UI surface and operator audit have a single inventory of “things that need a human to look at.”
| Surface | Where it lives on disk (client) | Source of truth doc |
|---|---|---|
verify_asset reject (any signature or chain failure) | Quarantine area surfaced via the audit log | Cryptography — Write Authorization |
| Federation soft-fail | Rejected-hash table, bounded LRU | Federation — Soft-Fail Semantics |
| Orphaned original (no sidecar) | .library/quarantine/ after a failed recovery | Filesystem — Repair |
| Malformed CBOR sidecar | .library/quarantine/ (the unparseable bytes are preserved) | Filesystem — Repair |
| Stale-revival (peer or restore sends old manifest) | Audit log + UI surface “peer sent stale state” | Cryptography — Provenance |
| Album upgrade stranded write | Local pending_until_upgrade queue | Versioning — Album Upgrade Ceremony |
| Backup restore chain conflict | Audit log + UI surface “restore conflicts” | Backup & Recovery — Backup Verification |
A quarantined item is never silently dropped and never silently applied. The user (or operator) can inspect, repair, or discard explicitly.
Provenance Immutability Rules
Section titled “Provenance Immutability Rules”The append-only hash-chained record per asset is defined in Cryptography — Provenance. This section is the policy layer.
- No path exists to overwrite or delete an existing provenance entry. Not via the API, not via the local filesystem (the client treats
.provenance.cboras append-only), not via federation. The constraint is structural, not enforced by a permission check. - Even a hard-delete preserves provenance. When an asset is purged, its
media/{YYYY}/{YYYY-MM}/{uuid}.provenance.cborremains as a tombstone-with-history. The bytes that go away are the ciphertext blob and the encrypted metadata; the audit trail does not. - Export and backup carry the chain. A backup artifact includes every asset’s full provenance chain. On restore, the chain re-enters the local index — reconciled per Backup & Recovery — Backup Verification, which never silently overwrites newer local state.
- What a key-holding attacker still cannot do. A complete current-key compromise lets the attacker append forward. It does not let them rewrite the past — every prior record is bound by a signature from a (possibly retired) device whose public half is retained in the device directory and never pruned (marked
revoked_at, not deleted), so a retired device’s signature stays verifiable forever.