Skip to content

Security model

The full threat model with every invariant cross-referenced to source is in SECURITY.md. This page is the operator-facing companion: the shape of the system, the parts you can rely on, and what an attacker with X capability can or can’t do.

┌────────────────────────┐ ┌──────────────────────────┐
│ Browser SPA + WW │ │ Storage backend │
│ (sees plaintext) │ │ (sees V7 ciphertext) │
│ │ │ │
│ - vault key │ │ - encrypted file blobs │
│ - file content keys │ │ - encrypted filenames │
│ - filenames, content │ │ - encrypted manifest │
└──────────┬─────────────┘ └──────────────────────────┘
│ V7 ciphertext over HTTPS / SSH-via-relay
┌──────────┴────────────────────────────────────────────────────┐
│ byo-relay (sees ciphertext + per-device JWT) │
│ - device enrolment + cookies │
│ - SFTP transport (proxies SSH for the SPA) │
│ - share blob storage (V7 only, sweeper-purged) │
│ - never plaintext, never key material, never filenames │
└────────────────────────────────────────────────────────────────┘

The plaintext trust boundary is your browser. Nothing outside that boundary holds plaintext or key material — this is enforced by code, not by policy.

Every API call in the system carries either encrypted blobs, one-way auth tokens, or public keys. The seven invariants enumerated as ZK-1 … ZK-7 in SECURITY.md:

IDThe relay never seesEnforced by
ZK-1Plaintext passwordsArgon2id runs in the Web Worker; only the derived auth_hash is sent.
ZK-2The client_kek_halfDerived client-side; never serialised or transmitted.
ZK-3The full KEKTwo-factor split across client_kek_half + a server shard; the relay sees only its half.
ZK-4Plaintext private keysKEK-wrapped before transmission.
ZK-5Plaintext file contentAES-256-GCM encrypted before upload.
ZK-6Plaintext filenamesAES-GCM-SIV (deterministic nonce) encrypted before upload.
ZK-7Plaintext recovery keyDisplayed once at vault creation; never stored, never sent.

Reviewers can grep for each ZK-N tag in source. New API endpoints land with a “verified ZK-1…ZK-7” check before merge.

Conceptually, every key in the system descends from one of three roots: the user’s passphrase, the recovery key, or a passkey-derived secret.

passphrase ──Argon2id──▶ kek_half_client ──┐
├──▶ KEK ──unwrap──▶ vault_key ──▶ content_keys
kek_half_server ──┘ │
recovery_key ──HKDF──▶ rec_wrapping_key ──unwrap──▶ vault_key ──────┘
passkey (PRF mode) ──HKDF──▶ wrapping_key_vk ──unwrap──▶ vault_key (opt-in)
  • vault_key is the per-vault root. Any of the three paths (passphrase, recovery key, opt-in passkey) unwrap it independently. Compromise of one wrap does not compromise the others.
  • content_keys are per-file random keys derived inside WASM, wrapped under the vault key, and stored in the encrypted manifest. File-level keys never cross the WASM ↔ JS boundary in plaintext.
  • device_key is a per-device-per-vault wrapping key for IndexedDB state. When the WebAuthn/PRF gate is on, the device key is itself wrapped by an HKDF output from the authenticator’s PRF result.

The HKDF info strings ("SecureCloud BYO …", "Wattcloud device key v1", "Wattcloud vault_key wrap v1", etc.) are frozen protocol constants — renaming any of them would invalidate every existing wrap. They live in sdk-core and are append-only.

FunctionAlgorithmNotes
Passphrase KDFArgon2id64 MiB / 3 iter / 4-way parallel — heavy enough to make GPU/ASIC search expensive.
Key agreementHybrid X25519 + ML-KEM-1024Both must fail for a break. No classical-only fallback path.
File encryptionAES-256-GCM, chunkedEach chunk independently authenticated; chunk HMAC keyed by HKDF(content_key, "chunk-hmac-v1").
Key commitmentBLAKE2b-256(content_key ‖ file_iv)Prevents partitioning-oracle attacks.
Filename encryptionAES-GCM-SIV (deterministic nonce)Same filename + key → same ciphertext, so dedup and rename detection work without leaking plaintext.
Key derivationHKDF-SHA256Frozen info strings ("SecureCloud v6", "Wattcloud device key v1", …).

The relevant attacker for post-quantum is harvest-now-decrypt-later: record encrypted traffic today, store it, decrypt when quantum hardware matures. Defending against this requires the post-quantum primitives in the current handshake, not “we’ll add them later”.

Wattcloud’s hybrid construction:

  • X25519 — classical elliptic-curve Diffie–Hellman. Today’s strongest classical defence; broken by a sufficiently large quantum computer.
  • ML-KEM-1024 — NIST-standardised post-quantum KEM (formerly Kyber). Believed secure against quantum adversaries; new enough that classical cryptanalysis is still maturing.

Session keys are derived from a hybrid of both. An attacker has to break both to break the session — not either-or. There is no “downgrade to classical-only” path on the wire; the protocol does not negotiate it. Full discussion in SECURITY.md §5.

A capability-based way to read the threat model.

Attacker capabilityWhat they getWhat they don’t
Reads relay diskEncrypted enrolment DB rows, share-blob V7 ciphertext + share index columns (share_id, expires_at, kind, total_bytes).Plaintext file content, filenames, vault keys, recovery key, device keys, share fragment keys, client IPs.
Reads relay memory at runtimePer-device JWT signing key, in-memory rate-limiter counters (per IP), live SFTP session credentials (only while a session is active).Vault keys, content keys, recovery key — none of these ever reach the relay.
MITM on the WebSocketCiphertext traffic, public keys, auth tokens.Anything plaintext. The handshake binds session keys to the (X25519, ML-KEM) hybrid.
Steals an admin device cookieCan mint invites, revoke devices, fill disk with shares (subject to the per-IP byte budget).Cannot decrypt another member’s files. The zero-knowledge guarantees are math, not admin policy.
Same-origin DOM XSS in the SPALive access to the unlocked vault session for that device.If the WebAuthn gate is in PRF mode, no access to the at-rest device-key wrapping — re-locking forces a new authenticator touch to re-derive.
Steals the storage backendEncrypted vault directory tree (V7 ciphertext + AES-GCM-SIV filenames).Vault keys, recovery key — same as relay-reads-disk.
Captures a session cookie post-sign-outNothing. Sign-out is server-side: the cookie row is revoked_at, every subsequent request 401s.

The full row-by-row capability matrix lives in SECURITY.md. The above is the operator’s pocket version.

The relay is small and runs under a locked-down systemd unit. The relevant sandbox directives:

DirectiveEffect
DynamicUser=yesPer-service UID minted on each start. No persistent home.
ProtectSystem=strict/usr, /boot, /etc read-only.
ProtectHome=yes/root, /home invisible to the service.
MemoryDenyWriteExecute=yesNo W+X memory pages. Blocks most shellcode + JIT-spray.
CapabilityBoundingSet= emptyNo Linux capabilities.
SystemCallFilter=@system-service ~@privileged @resourcesStrict seccomp allowlist.
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIXNo raw sockets; no routing-table sockets.
RestrictNamespaces=yesNo namespace creation.
PrivateTmp=yesPer-service /tmp.
PrivateDevices=yesNo /dev/mem, /dev/kmem, raw block devices.

For a zero-state relay, this is roughly equivalent to a minimal container — without the runtime trust boundary or container-daemon attack surface. Full table in SECURITY.md §13.1.

Every tarball, install.sh, and CHECKSUMS.txt is keyless cosign-signed via the GitHub Actions workflow’s OIDC identity. There is no long-lived signing key. Verification:

  • Pinned identity regex: the release.yml workflow path on the upstream repo, with issuer https://token.actions.githubusercontent.com.
  • install.sh and wattcloud-update run cosign verify-blob against this regex before extraction. A swapped release asset fails verification and the script aborts.
  • Forks override TRUSTED_SIGNER_IDENTITY in /etc/wattcloud/wattcloud.env. No script patching.
  • Every signature is logged in the Sigstore Rekor transparency log; you can verify any release independently.

There is no “skip verification” flag and there will not be one.

  • Sharing — how the share relay extends the trust boundary without breaking ZK.
  • Multi-device — pairing, SAS verification, owner / member roles.
  • Identity & passkeys — passphrase, recovery key, presence vs PRF passkey.