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

241 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` `SecureBuffer`s; `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:
```text
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.