Files
2026-05-27 23:29:53 +01:00

15 KiB
Raw Permalink Blame History

vault-sync — Domain-Specific Sync & Conflict Resolution

  • Change ID: vault-sync
  • Package: @suaveplan/vault-sync
  • Status: PROPOSED
  • Type: new-package
  • Date: 2026-04-29

Why

The Vaulted password-manager ecosystem needs an offline-first, end-to-end-encrypted sync engine that understands vault-item semantics. Generic CRDTs (@suaveplan/crdt) and generic diffs (@suaveplan/diff) provide the primitives, but they lack domain knowledge: a password field should use last-write-wins, an audit-history field should use append-only merge, and a folder move should be atomic. vault-sync layers domain-specific merge strategies, JSON Patch incremental sync, durable offline queues, and encrypted payloads on top of the generic primitives.

What changes

  • ADDED: mergeVaultItems(local, remote, strategy) — CRDT merge with "lww", "append", and "atomic" strategies
  • ADDED: diffVaultItems(before, after) / applyPatch(item, patch) — JSON Patch diff/apply for vault items
  • ADDED: SyncEngine class — pull(), push(), sync() operating on in-memory snapshots
  • ADDED: SyncQueue — durable, serializable mutation queue with vector-clock ordering and operation collapsing
  • ADDED: encryptSyncPayload(payload, key) / decryptSyncPayload(encrypted, key) — XChaCha20-Poly1305 E2EE
  • ADDED: VaultSyncError with structured error codes for merge, patch, sync, queue, and crypto failures

Gap analysis: sub-standard → enterprise

Most password-manager sync implementations fall well below enterprise standards:

Sub-standard practice Enterprise replacement
Last-write-wins blindly applied to all fields Domain-specific CRDT merge strategies: LWW for passwords, append-only for audit history, atomic for folder moves
No offline support; sync fails when disconnected Offline-first durable queue with vector-clock ordering and operation collapsing
Plaintext or TLS-only sync payloads End-to-end encrypted sync payloads (XChaCha20-Poly1305) with encrypted blobs on server
No conflict resolution; data loss on concurrent edits Client-side CRDT merge with commutativity, idempotency, and monotonicity guarantees
No ordering guarantees; edits arrive out of order Vector-clock-based causal ordering with explicit conflict detection
No sync state observability Structured OTel spans, safe metrics, and health checks without leaking vault data
Monolithic sync engine mixing transport, crypto, and domain logic Clean separation: vault-sync handles domain merge + crypto payload; transport lives in api

This package migrates the sync engine from a naive, lossy transport layer to an offline-first, enterprise-grade, end-to-end-encrypted synchronization system.

Security model

Threat model

Attacker Capability Mitigation
Local attacker (malware, keylogger) Can read process memory, intercept file system E2EE: plaintext never leaves device unencrypted; sync payloads are XChaCha20-Poly1305 ciphertext
Remote attacker (network MITM) Can intercept sync traffic, replay payloads E2EE renders intercepted data meaningless; tamper detection via Poly1305 tag verification
Compromised sync server Can read stored blobs, reorder or drop messages Client-side encryption; server is a dumb blob store; client-side CRDT merge is authoritative
Forensic recovery (stolen device) Can extract local sync queue Queue stores only encrypted payloads; keys live in crypto-vault and are zeroized on lock

Encryption boundaries

  • Plaintext lives only inside the vault memory boundary (vault package) before sync.
  • Sync payload plaintext is ephemeral: encrypted immediately by encryptSyncPayload, then zeroized.
  • Keys live only inside crypto-vault SecureBuffers; vault-sync never handles raw key material.
  • The sync server stores and transports only ciphertext + nonce + tag; it never sees plaintext, keys, or metadata.

Key hierarchy and derivation

Sync encryption uses keys derived from the standard hierarchy:

AUK ──► Personal Keyset ──► Vault Key ──► Sync Encryption Key (per vault)
  • Sync Encryption Key: A dedicated HKDF branch (info = "v1/sync-enc/<vaultIdHash>") derived from the vault key.
  • Sync MAC Key: A separate HKDF branch (info = "v1/sync-mac/<vaultIdHash>") for Poly1305 tag keys (or use XChaCha20-Poly1305 built-in AEAD).
  • Key rotation: deriving a new sync encryption key requires re-encrypting the sync payload header only; item data keys are unchanged.

Two-secret key derivation (2SKD) threat model

vault-sync does not perform 2SKD itself, but it relies on the security of the vault key, which ultimately derives from the AUK (2SKD output). Therefore:

  • Compromise of the sync server alone reveals nothing (E2EE).
  • Compromise of the sync encryption key alone reveals only sync payload contents for that vault, not the vault key or AUK.
  • Compromise of both the sync server and the sync encryption key still requires the vault key to decrypt item contents (layered encryption).

Secure memory

  • All plaintext diff and patch structures are standard in-memory objects (not sensitive key material), but intermediate JSON Patch strings are zeroized after encryption.
  • Sync queue entries store only ciphertext + metadata (vector clock, strategy, timestamp).
  • No key material is ever serialized into the queue or persisted to disk unencrypted.

Side-channel resistance

  • Merge strategies operate on plaintext vault items (already decrypted) and do not involve secret keys.
  • XChaCha20-Poly1305 tag verification is constant-time via the underlying @suaveplan/crypto-cipher implementation.
  • Vector clock comparison uses integer arithmetic only; no secret-dependent branches.

Compliance

  • NIST SP 800-63B: Sync authentication relies on SRP-6a (in api layer); encryption relies on NIST-approved AEAD primitives (ChaCha20-Poly1305 is acceptable for TLS and data-at-rest in many NIST profiles).
  • OWASP Cryptographic Storage Cheat Sheet: E2EE with AEAD, unique nonces, and key separation satisfies recommended controls.
  • GDPR Article 32: E2EE with client-side key management provides a strong technical safeguard for personal data processing.

Performance SLOs

Metric Target Measurement
Sync latency (pull+push) P99 ≤ 500 ms for ≤ 100 changed items End-to-end from sync initiation to merged state
Merge latency (single item) P99 ≤ 5 ms CRDT merge of two item versions
Patch apply latency P99 ≤ 2 ms JSON Patch application to in-memory item
Encrypt/decrypt sync payload P99 ≤ 10 ms per 100 KiB payload XChaCha20-Poly1305 AEAD
Memory bound (10K items) ≤ 64 MiB resident Sync queue + in-memory snapshot
Offline queue depth Max 10,000 pending operations Enforced with backpressure and structured rejection
Max sync payload size 10 MiB Rejects larger payloads to prevent memory abuse
Scalability: max vault size 100,000 items Vector clocks scale linearly; merge is per-item
Scalability: max attachment size 100 MiB Attachments stream through drive; sync layer handles metadata only

Observability plan

All telemetry is safe-by-construction: no key material, no item content, no plaintext fields, no vault-specific identifiers, and no user data in spans, metrics, or logs.

OTel spans and metrics

  • vault-sync.sync.duration — histogram of full sync latency (pull + merge + push)
  • vault-sync.merge.duration — histogram of merge latency per item (no item type, no count)
  • vault-sync.patch.apply.duration — histogram of patch application latency
  • vault-sync.payload.encrypt.duration — histogram of payload encryption time (no size, no vault ID)
  • vault-sync.queue.depth — gauge of pending offline operations (count only)
  • vault-sync.queue.flush.duration — histogram of queue flush latency
  • Span names: sync.pull, sync.merge, sync.push, queue.flush, payload.encrypt

Structured logging

Safe-to-log fields only:

  • event: "sync_completed" — durationMs, success, conflictCount (no vault ID, no item IDs)
  • event: "merge_conflict_detected" — strategy (e.g., "lww"), resolved: boolean (no field values)
  • event: "queue_flush_started" — pendingCount
  • event: "queue_flush_completed" — processedCount, failedCount
  • event: "payload_encrypt_completed" — durationMs (no size, no key info)

Error telemetry

  • Aggregate error counts by VaultSyncError code (e.g., MERGE_STRATEGY_UNKNOWN, PATCH_INVALID, SYNC_PAYLOAD_TAMPERED).
  • No vault ID, item ID, operation content, or vector clock values attached.

Health checks

  • health.vault-sync.merge — periodic merge of synthetic test items; fails if latency > 2× SLO.
  • health.vault-sync.encryption — periodic encrypt/decrypt of a test payload; fails on tamper or latency spike.
  • health.vault-sync.queue — verifies queue serialization and deserialization succeed.

Audit logging

  • audit.vault-sync.sync_start — timestamp, deviceIdHash, direction (pull/push/bidirectional).
  • audit.vault-sync.sync_complete — timestamp, deviceIdHash, success, conflictCount.
  • audit.vault-sync.queue_flush — timestamp, deviceIdHash, processedCount.
  • Retention: 90 days hot, 1 year cold.

Dependency audit

Dependency Justification Supply-chain risk Tree-shakeable License
@suaveplan/crypto-cipher XChaCha20-Poly1305 encrypt/decrypt Internal monorepo; audited in CI Yes MIT
@suaveplan/crypto-vault Sync encryption key derivation Internal monorepo; audited in CI Yes MIT
@suaveplan/crdt Generic CRDT primitives Internal monorepo Yes MIT
@suaveplan/diff JSON Patch diff/apply Internal monorepo Yes MIT
@suaveplan/encoding Base64 for ciphertext transport Internal monorepo Yes MIT
@suaveplan/error Base error class Internal monorepo Yes MIT

No external runtime dependencies. All sync primitives are internal monorepo packages. This eliminates hidden transitive dependencies and minimizes supply-chain attack surface.

Operational readiness

Graceful degradation

  • If sync server is unreachable, the sync engine enters offline mode: mutations accumulate in the durable queue; reads continue from local snapshot.
  • If sync payload exceeds max size, the engine falls back to chunked sync: splits vault items into multiple encrypted payloads.
  • If merge strategy is unknown for a field, the engine falls back to LWW and emits a telemetry event for operator review.

Resource limits

  • Max sync queue depth: 10,000 pending operations. Beyond this, new mutations are rejected with VaultSyncError and the user is notified.
  • Max sync payload size: 10 MiB. Prevents memory exhaustion from oversized payloads.
  • Max concurrent sync operations: 1 per vault. Prevents race conditions and vector-clock corruption.

Error recovery

  • Sync failures are retried with exponential backoff (base 2, max delay 60s, max 10 retries).
  • Queue is serialized to disk (encrypted) after every mutation; crash recovery replays the queue on restart.
  • Tampered sync payloads (bad Poly1305 tag) are rejected and trigger a full re-pull from server.
  • Merge conflicts are resolved client-side and do not block subsequent operations.

Backup/restore compatibility

  • Sync queue serialization uses standard JSON with base64-encoded ciphertext fields — portable across platforms.
  • Vector clocks are plain { nodeId: counter } maps — no proprietary formats.
  • Restore replays the queue in causal order; merging is deterministic.

Upgrade/downgrade safety

  • Sync payload format includes a version field ("v1" in the AEAD associated data). New versions can add fields; old versions ignore unknown fields.
  • Vector clock schema is stable (node ID → integer counter). No migrations required.
  • Merge strategies are versioned by field name convention ("password:v1"). Strategy evolution is additive.

Key rotation without full re-encryption

  • Sync encryption key rotation: Derive new sync key from vault key; re-encrypt only pending queue entries and the latest sync payload header. Historical server blobs remain readable with old key until purged.
  • Vault key rotation: Requires re-deriving the sync encryption key and re-encrypting all pending queue entries. Server blobs are unaffected because they are per-vault-key encrypted separately.

Out of scope

  • Network transport implementation — SyncEngine accepts snapshots and returns payloads; HTTP/WebSocket/peer-to-peer transport lives in @suaveplan/api and @suaveplan/native-messaging
  • Server-side conflict resolution — server is a dumb encrypted-blob store; all merging happens client-side
  • Real-time collaborative editing with live cursors — deferred post-MVP
  • Sync payload compression — deferred; payloads are small (individual vault items)

Verification approach

  • src/merge/merge.test.ts — strategy correctness, commutativity, idempotency, unknown-strategy rejection
  • src/patch/patch.test.ts — diff minimality, patch application, invalid-patch rejection, round-trip
  • src/engine/engine.test.ts — pull, push, bidirectional sync, tombstone propagation, conflict detection
  • src/queue/queue.test.ts — offline accumulation, flush ordering, redundant-op collapsing, serialization
  • src/crypto-sync/crypto-sync.test.ts — encrypt/decrypt round-trip, wrong-key failure, tamper detection
  • src/error/error-codes.test.ts — every error code is thrown by at least one production path
  • tests/dep-graph.test.ts — dependency invariants

Coverage and quality targets

  • 100% coverage target: All source files must achieve 100% statement and branch coverage.
  • Property-based tests: fast-check properties for merge commutativity, merge idempotency, patch round-trip, vector-clock monotonicity, queue serialization equivalence, and encrypted payload tamper rejection.
  • Load/stress tests: 10,000-item vault sync with 1,000 concurrent modifications to verify merge latency and memory bounds; 10 MiB payload encryption/decryption to verify throughput.
  • Security audit checklist:
    • All sync payloads encrypted with XChaCha20-Poly1305 before leaving device
    • Server stores and transports only ciphertext + nonce + tag
    • No plaintext item content in logs, errors, or telemetry
    • Vector clocks are monotonic and causality-preserving
    • Merge strategies are deterministic and commutative
    • Offline queue persists only encrypted payloads
    • Tampered payloads rejected with SYNC_PAYLOAD_TAMPERED error
    • Max payload size enforced at serialization boundary
  • Penetration test scenarios:
    • Intercept sync traffic — verify only ciphertext visible
    • Modify sync payload ciphertext — verify tamper detection rejects with non-retryable error
    • Crash during sync queue flush — verify no plaintext in crash dump or queue file
    • Replay old sync payload — verify vector clock rejects stale data

Migration impact

None — additive only. @suaveplan/vault-sync is a new package with no existing consumers in the monorepo.