Files
Marcos Paulo a71e294566
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
docs: specs, designs, research and agent rules for mobile, sync and collaboration
- specs: mobile-and-sync, collaboration, vault-launcher, e2ee, attachments-and-history, capture-and-publish, agents (brief and orchestrator notes for MiniMax and GLM agents)
- research: sync landscape, security and server, mobile platform (browsed, cited)
- design/mobile: 24 PaperCraft exports (dark and light) of the phone, desktop, sharing and launcher screens
- .claude: project skills and path rules so delegated agents follow the repo rules; CLAUDE.md imports AGENTS.md
- ARCHITECTURE and ROADMAP updated; AGENTS.md lists the approved sync dependencies

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 07:46:30 -03:00

46 lines
5.5 KiB
Markdown

# End-to-end encryption for synced vaults
Source: `specs/mobile-and-sync/research/security-and-server.md` (findings on Obsidian Sync and Syncthing, recommendations 1 to 3). **Needs owner approval for new crates** before any code (see Open Questions).
## Problem
The sync server (a homelab, possibly shared with other people through invites) stores every note and every path in plaintext (SQLite `files.path`, blob files hold note text). Anyone with the database or disk can read all notes. Obsidian Sync encrypts end to end by default; NotesCraft does not.
## Goal
A vault can be created or converted to "end-to-end encrypted": the server only ever holds ciphertext and opaque ids, and devices still converge.
## Non-Goals
- Server-side search, transcription, OCR or publish of encrypted vaults (impossible by design; the UI says so).
- Live co-editing in an encrypted vault (the server must read operations to order them): file sync only, as stated in `specs/collaboration/spec.md`.
- Protecting against a hostile client or malware on a paired device; hiding file sizes and timing (documented leak).
## Design
1. **Key hierarchy.** A random 256-bit **vault key** (VK) per vault. VK is wrapped (a) by a key derived from the owner's passphrase with **Argon2id** (memory 64 MiB, 3 passes, 1 lane as a floor, parameters stored with the wrapped key), and (b) by a printed/saved **recovery key** (random 256 bits shown as 24 words or base32). Each device receives VK wrapped for its own **device key** when paired (QR carries the pairing code; the passphrase is typed once on the first device and never leaves it). Subkeys by HKDF-SHA256 with labels `content`, `path`, `manifest`.
2. **Content.** Each file is encrypted client-side with **XChaCha20-Poly1305** (random 192-bit nonce per blob, associated data = vault id + blob id + format version), in chunks of 1 MiB for large files so a bit flip or truncation is detected (chunk index and "last chunk" flag in the associated data).
3. **Paths.** The server never sees real paths: it stores an **opaque blob id** (HMAC-SHA256 of the path with the `path` subkey, hex) per file and an encrypted **manifest record** `{path, size, mtime}` sealed with the `manifest` subkey and bound to the opaque id (so replaying one record under another id fails). The `/changes` records carry `{opaque id, sealed manifest, seq, deleted}`; clients decrypt to learn the path. Ledger rules (revisions, conflicts, tombstones) work unchanged on opaque ids.
4. **Hashes.** The plaintext SHA-256 is part of the sealed manifest, not a server field, so equal notes do not leak equality (documented: no cross-file dedup in encrypted vaults).
5. **Pairing a new device.** The owner shows a QR (server, vault id, one-time code); the new device pairs, receives the account token; VK is delivered either by typing the passphrase on the new device or by scanning a second QR from a trusted device (wrapped VK + nonce, valid 5 minutes). Recovery: passphrase or recovery key re-derives VK and unwraps the wrapped copy the server stores.
6. **Conversion.** Plain to encrypted: a client re-uploads every file as ciphertext, then the owner confirms and the server deletes old plaintext blobs and rewrites the path column to opaque ids. Encrypted to plain is not offered in v1 (export instead).
7. **Zeroize and memory.** Keys live in `Zeroizing` buffers, never in logs or settings; the passphrase is not stored; the keychain holds the device key.
## Acceptance Criteria
- [ ] Given an encrypted vault synced from two devices, when the server's database and blob folder are captured, then no note text, path or file name appears in any byte of them (string search of 1,000 random words from the notes finds nothing).
- [ ] Given a blob whose ciphertext byte is flipped, a chunk dropped, or a sealed manifest swapped between ids, then the client refuses it with a clear error and writes nothing.
- [ ] Given a lost device, when the owner revokes it and rotates VK, then the revoked token gets 401 and a copy of the old wrapped VK no longer opens new data.
- [ ] Given the passphrase is forgotten and the recovery key is present, then a clean device restores the vault; with neither, the data is unrecoverable and the UI said so at creation.
- [ ] Given 10,000 notes, then initial encrypted sync is within 3x of the plaintext time on the same LAN.
## Constraints
- Rust, no JavaScript. Crates need written approval: `chacha20poly1305` (audited, 0.11.0 at research time), `argon2` (0.6.0), `hkdf`, `hmac` (RustCrypto), `zeroize`, `rand` (OS RNG via `getrandom`). `sha2` is already approved.
- Wire format versioned; a crypto test vector file lives in the repo and is checked in CI.
## Open Questions
1. Approve the crates above? (Recommended: yes, all RustCrypto.)
2. Is a hybrid of server-readable "shared vault" and E2EE "private vault" acceptable (live editing and publish only in the readable one)? Recommended: yes, chosen per vault at creation.
3. Recovery key as words (needs a 2048-word list file, MIT/CC0) or base32? Recommended: base32 first.
## Tasks
1. New crate `crates/sync-crypto` (L1, pure): key hierarchy, chunked AEAD, opaque ids, sealed manifest, test vectors, property tests (flip, truncate, reorder).
2. Server: opaque-id mode per vault (`encrypted` flag), no plaintext path column, size caps by ciphertext.
3. Engine: encrypting `Remote` wrapper around `HttpRemote` (path mapping, chunking), key storage in the OS keychain, conversion command.
4. UI: create encrypted vault, passphrase and recovery key screens, device pairing QR, lock indicators.