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

5.5 KiB

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.