clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/141-effect-library
lofi / docs / threat-model.md
threat-model.md 8.2K rendered
1# What the sync server can see23A lofi app is local-first: without sync, everything lives on the device and this page is moot. The4moment an account syncs — to a managed deployment or to a self-hosted node — a server holds a copy5of the data. This page states plainly what that server can and cannot do with it, what the user6holds that the server never does, and which of those lines move as the encryption surface grows.78## The server's view910The pinned data layer (Jazz 2) uses a trusted-server model: the sync server materializes table11state, evaluates permission policies, and serves queries. Three consequences follow.1213- **Ordinary column values are readable by the store operator.** Permissions bound which14 _identities_ may read or write rows; they do not hide data from the server that enforces them.15- **Structure and metadata are always visible**: which tables exist, row identities, who wrote,16 when, and how much. This holds even for encrypted columns.17- **Choosing the operator is a real security decision.** Self-hosting (a18 [lofi-node](https://lofi.host/node)) does not make the server blind; it makes the reader _you_.19 "Your server reads your data" and "someone else's server reads your data" are different threat20 models, and lofi treats the choice of sync location as user data, not developer configuration.2122## What the user holds2324- **The account identity secret.** Accounts are device-local and cryptographic; there is no25 server-side account to reset or impersonate. The server never holds recoverable account material.26- **Recovery material**: the recovery phrase, and optionally a recoverable passkey. Losing both27 loses the account; no operator can restore it, which is the honest half of holding the keys.28- **The sync location**, enrolled by ticket and revocable at the node.29- **Sealed local state.** Nothing bearer-shaped sits in browser storage in cleartext: the sync30 declaration persists as an envelope under a device-bound key, and store-administration capability31 is either sealed behind the user's passkey or never persisted at all.3233## Encrypted columns: content the server cannot read3435Fields declared with `s.encryptedText`, `s.encryptedJson`, `s.encryptedNumber`, or `s.encryptedDate`36are sealed on the client before they enter Jazz. The key derives from the account secret, so every37device holding the account decrypts and nobody else can, including the store operator. Plaintext is38padded to bucketed sizes before sealing, so for those fields the server's view degrades to "rows of39a certain size class and cadence exist" — all short values share one class, and larger values reveal40only a coarse bucket.4142The constraints are mechanical, not policy, and enforced: a `where` on an encrypted column is a43compile error and a permission policy referencing one fails configuration, because the server cannot44evaluate what it cannot read; filtering on decrypted values runs client-side instead. An encrypted45column is account-private — a row shared with another account is undecryptable for them by design,46and reads refuse loudly rather than return garbage. Details:47[encrypted columns](data-and-ui.md#encrypted-columns).4849## Shared fields: content the server relays but cannot read5051Shared encrypted columns extend sealing to group collaboration: a per-group field key seals the52content, and the key itself travels as ordinary synced data — wrapped to each member's public key,53which every account self-publishes in a key directory. The wrap is authenticated with the sender's54own key, so a server that knows every public key still cannot mint one; forging a wrap requires a55member's secret.5657For shared fields the server is therefore an **active attacker, not a passive reader**: its one move58is substituting public keys during membership changes, and that move is detected. Each device pins59peer keys on first sight, sharing identities carry the account's key fingerprint so members added60person-to-person are pinned with no server first-sight window, and a key that later disagrees is61refused — the field stays sealed and the mismatch surfaces as an alert, until the user re-trusts62after out-of-band verification.6364The residual exposures, stated plainly: a peer first seen only through the directory is trusted on65first sight by that device; a removed member keeps the key generations they already held (removal66seals future content, not history — re-encrypting merged history is unsound); and structure, timing,67and size-class metadata remain visible exactly as for account-private fields. Details:68[shared encrypted columns](data-and-ui.md#shared-encrypted-columns).6970## Tickets: what possession grants7172An app-connect ticket is a bearer credential with 256-bit entropy.7374- A **sync-scoped** ticket is transport capability. Holding one allows connecting and syncing as the75 store's transport layer permits; reading data still requires an identity the permissions admit.76 Revocation is one command at the node and closes live connections within seconds.77- A **provision-scoped** ticket is store administration by possession. The app therefore refuses to78 hold one loosely: on enrollment it is exchanged for a derived sync ticket (which becomes the79 everyday transport credential and dies with its parent on revocation), and the provision original80 is sealed behind the user's passkey or kept only in memory, with the user's password manager as81 the durable copy.82- The **derived sync ticket is possession-bound in use** against a node that supports the exchange:83 enrollment offers a non-extractable device key, and connecting thereafter means answering a fresh84 challenge with a signature only that device can produce. The everyday credential then stops being85 a pure bearer string — a copy lifted from a backup, a log line, or a pasted URL no longer connects86 from anywhere else. Losing the device key (a wiped browser store) is deliberately unrecoverable;87 re-enrolling a fresh ticket is the recovery.8889**Same-origin script (XSS) is the attacker this custody is shaped around.** Script running on the90app's origin can use whatever the page can use: the page's own device key answers the challenge for91whatever runs inside it, so a compromised origin holds transport capability while its page is live.92Possession binding narrows what that compromise is worth — the credential material script can read93no longer connects from anywhere else, because the signing key cannot leave the device. Exfiltration94loses its value; in-page abuse remains, and the Content-Security-Policy below is the control aimed95at that residue. What script cannot do silently is obtain admin capability — unlocking sealed96provision material is a user-verifying passkey prompt, so theft degrades from silent to prompted,97and a prompt the user did not initiate is the signal to decline. At-rest theft (disk images,98backups, storage exfiltration) gets the stronger guarantee: sealed records are ciphertext without99the device key or the passkey, and the sync credential inside them is bound to a key that was never100in the record at all.101102Generated apps also narrow the surface that creates same-origin script in the first place: every103built page carries a Content-Security-Policy that admits only the app's own hashed scripts — no104remote script origins, no inline execution beyond the hashed island bootstraps. The build reports105the policy and warns on weakenings; `LOFI_CSP=off` and the extension points are documented in the106deployment guide. A compromised dependency or injected markup then fails to execute rather than107inheriting the page's capability.108109## Where the lines move110111- **Engine-authenticated member keys** would upgrade the shared-field trust story: today's key112 directory is self-published and pinned; identity material the sync engine itself authenticates113 would remove the first-sight window entirely. Tracked against the pinned engine and reviewed on114 every version bump.115- **Metadata blindness is out of scope** under the pinned engine: a server that evaluates policies116 and queries must see structure. A fully blind relay is a different architecture and is not117 claimed.118119Read this page as the definition behind the phrase "the user holds the keys": identity, recovery,120location, and at-rest custody, with content blindness per field where the schema declares it —121account-private or shared across a group.