Skip to content

feat: Application-level E2EE for vault sync #18

@DarrenZal

Description

@DarrenZal

Summary

Vault sync currently encrypts at the transport layer (WireGuard) but transmits file contents as plaintext in KOI-net event payloads. The relay operator routing WireGuard packets can read all synced file contents, paths, sizes, and timestamps.

This issue tracks adding application-level end-to-end encryption so only the intended peer can decrypt vault sync events.

Current Security Model

Aspect Status
Transport encryption ✅ WireGuard (IP layer)
Peer authentication ✅ ECDSA signatures on events
File integrity ✅ Content hashing
App-level encryption ❌ File contents plaintext in event payloads
Metadata privacy ❌ Paths, sizes, timestamps visible to relay

Root Cause

KOI-net events carry file content as plaintext JSON. Any node or relay operator with access to the network traffic can read the full event payload — including file content.

Proposed Solution

Phase 1: Symmetric E2EE (content only)

  1. During peer edge establishment (handshake), perform ECDH to derive a shared secret per peer pair
  2. Add encrypted_vault_sync: true flag to vault sync peer config
  3. Before queuing a vault sync event, encrypt file content with AES-256-GCM using key derived from shared secret
  4. On apply, detect encrypted payload and decrypt before writing to disk
  5. Backward-compatible: support plaintext events during migration

Key derivation:

from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes

def derive_vault_sync_key(shared_secret: bytes, peer_rid: str) -> bytes:
    hkdf = HKDF(
        algorithm=hashes.SHA256(),
        length=32,
        salt=None,
        info=b"koi-vault-sync:" + peer_rid.encode()
    )
    return hkdf.derive(shared_secret)

Encryption:

from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os

def encrypt_payload(payload: dict, key: bytes) -> bytes:
    nonce = os.urandom(12)
    cipher = AESGCM(key)
    ciphertext = cipher.encrypt(nonce, json.dumps(payload).encode(), None)
    return nonce + ciphertext  # prepend nonce

def decrypt_payload(data: bytes, key: bytes) -> dict:
    nonce, ciphertext = data[:12], data[12:]
    return json.loads(AESGCM(key).decrypt(nonce, ciphertext, None))

Phase 2: Metadata encryption (optional)

Extend encryption to wrap file paths, sizes, and timestamps alongside content — so relay sees only an opaque encrypted blob per event.

Files to Modify

  • api/vault_sync.py — encrypt on queue, decrypt on apply
  • api/vault_sync_models.py (or equivalent) — add encrypted_payload field to event schema
  • api/koi_net_handler.py / handshake — store derived shared secret per peer
  • scripts/federation/personal-env.template — add VAULT_SYNC_E2EE=true option

Acceptance Criteria

  • Vault sync events with encrypted_payload are decryptable only by the intended peer
  • Relay operator cannot read file contents from intercepted events
  • Existing unencrypted peers continue to work (backward compat flag)
  • VAULT_SYNC_E2EE=true env var enables encryption for all vault sync events
  • Smoke test: create file → check event in DB → payload is not plaintext → peer decrypts and writes file correctly

References

  • VAULT-SYNC-ARCHITECTURE.md — full architecture notes and implementation sketch shared between peers
  • WireGuard handles transport; this issue is purely application layer
  • cryptography package (already a dep via koi-net) provides ECDH + AES-256-GCM

Priority

Medium — current setup is acceptable for non-sensitive collaboration, but should be addressed before using vault sync for private notes or sensitive organizational data.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions