Preview

Zero-Knowledge Architecture

A technical deep-dive into Ubimate’s end-to-end encryption, key derivation, zero-knowledge authentication, and privacy-preserving real-time collaboration.

Version 2.2  ·  June 2026  ·  Subject to change during open beta

1. Executive Summary

Ubimate is a local-first workspace. The core guarantee is that no plaintext content or private key material ever leaves the user’s device. The cloud sync and collaboration server is architecturally incapable of reading user data — not as a policy promise, but as a cryptographic fact.

This document describes precisely how that guarantee is implemented: how keys are derived, how authentication works without a password, how documents are encrypted, and how real-time collaboration operates on opaque ciphertext.

The intended audience is security researchers, enterprise customers evaluating the product, and technically curious users who want to verify the privacy claims rather than take them on faith.

What kind of privacy?

Ubimate’s privacy guarantee is best described as anti-exploitation privacy, not military-grade operational security. The specific threat it is designed to eliminate is the company as adversary: Ubimate cannot profile users, monetise their behaviour, feed documents into advertising or AI training pipelines, or sell data to third parties — not as a policy promise, but because the server only ever holds ciphertext it cannot decrypt.

Concretely, the architecture protects against:

  • Behavioural profiling and targeted advertising
  • Monetisation of document content or usage patterns
  • Surveillance capitalism (selling behaviour data to brokers)
  • Feeding user content into AI training pipelines without consent
  • Server-side data breaches exposing readable content

This is not a system designed for users facing nation-state adversaries or physical-device seizure. Local data is intentionally stored without application-layer encryption (see §3): the local SQLite database is open and queryable with any standard SQLite tool. Document content is stored inside that database as Yjs binary blobs that require the application to render; unrestricted human-readable access is provided by the HTML mirror feature, which generates a static offline-readable copy of every document independent of Ubimate software. Users with high-threat operational security requirements should layer OS full-disk encryption and their own physical security practices on top.

2. Threat Model

This section defines, precisely, what the system protects against, what capabilities each adversary class is assumed to have, what is explicitly out of scope, and what metadata is unavoidably visible to each party even when the cryptographic guarantees hold.

2.1 Adversary classes and assumed capabilities

AdversaryAssumed capabilitiesWhat they can observeWhat the system guarantees
A1 — Passive network adversary Full read access to all network traffic between client and server (e.g. ISP, VPN provider, BGP hijacker). Cannot modify traffic in transit. TLS record sizes and timing; connection endpoints; number of requests; WebSocket frame sizes and timing patterns. No plaintext content, no private keys, no password material in any observable packet. TLS 1.3 encrypts even record metadata by default.
A2 — Active network adversary (MitM) Can intercept and modify traffic; may attempt TLS stripping or certificate substitution. As A1, plus ability to replay or inject requests if TLS is successfully stripped. TLS is enforced at the edge reverse proxy in production. Where enabled, HSTS helps prevent downgrade. Challenge nonces are single-use; replayed login requests are rejected server-side.
A3 — Compromised sync server Full read/write access to the server database, file system, and process memory. Can modify response payloads. Cannot modify the client binary already installed on user devices. Encrypted document blobs (yjs_updates); encrypted metadata (properties JSON); Ed25519 and X25519 public keys; email addresses; timestamps; workspace membership lists; document graph structure (parent/child relationships, document count, update frequency). No plaintext document content. No private keys. No passphrase or passphrase hash. Decryption is computationally infeasible without the user’s private key, which the server never receives. Exception for active collaborators: see §7.2.
A4 — Malicious Ubimate employee Direct database access, ability to deploy server-side code changes, access to server logs and backups. Same as A3. Additionally: IP addresses and connection metadata in server access logs (retained for a limited period per the Privacy Policy). Same as A3. The cryptographic guarantee does not depend on employee trustworthiness — it is enforced by the architecture.
A5 — Shared-machine attacker (web) Physical access to the same machine after the user’s browser session ends. Can read localStorage, sessionStorage, browser history, cached network responses, browser memory dumps. Any data the browser has cached or persisted. Private keys and derived seed are held only in JS heap memory, in a plain atom that is never written to any storage. On tab close the garbage collector reclaims it. Browser auth uses an HttpOnly session cookie; cryptographic key material remains in-memory only. No document plaintext is cached locally by the web app.
A6 — Legal / compelled disclosure (server) Court order or equivalent compelling Ubimate to produce user data. Same as A3 — only ciphertext, public keys, and metadata. Ubimate can produce only what it holds: ciphertext that is unreadable without the user’s private key. There is no key escrow, no recovery mechanism, and no way for Ubimate to comply with a content-disclosure order beyond producing opaque blobs.

2.2 Metadata leakage analysis

Even when document content is perfectly encrypted, structural and behavioural metadata is unavoidably visible to some parties. The following is a precise inventory of what leaks and to whom.

Metadata itemVisible toMitigation / acceptance rationale
Document count per workspace A3, A4, A6 Unavoidable to support sync routing. Not mitigated; accepted as a necessary structural leak.
Document tree structure (parent/child hierarchy, nesting depth) A3, A4, A6 Parent IDs are stored as plaintext foreign keys for query routing. Partially mitigated in a future version by encrypting the parent field; accepted for now.
Document titles, icons, tags, and all custom properties Not visible These fields are stored inside the encrypted properties blob (see §6.4), sealed with the workspace content key before leaving the client. The server holds opaque ciphertext for all user-readable metadata; it cannot surface title previews or perform content-based indexing.
Update timestamp and frequency per document A3, A4, A6 Stored as updated_at. Reveals which documents are actively edited and when. Not mitigated; accepted.
Encrypted content size (ciphertext byte length) A3, A4, A6 Ciphertext length is approximately equal to plaintext length (+41 bytes: 1-byte version prefix + 24-byte nonce + 16-byte Poly1305 MAC per update). Can reveal approximate document size or edit volume. Not padded in the current protocol version; may be addressed in a future version.
Workspace membership (who collaborates with whom) A3, A4, A6 Required for routing and access-control enforcement. Not mitigated; accepted.
Email address (normalised) A3, A4, A6 Stored as the login identifier. Users who prefer pseudonymity are encouraged to register with a handle rather than their real name or email.
IP address of client connections A4, A6 (via server logs) Standard server access logs. Retained for a limited period. Use of a VPN shifts this leak to the VPN provider rather than Ubimate.
Request timing and size patterns (traffic analysis) A1, A2 A sufficiently resourced passive observer can infer editing activity patterns from WebSocket frame timing even without reading content. This adversary is out of scope; no padding or timing obfuscation is currently implemented.

2.3 Side-channel risks

Side-channelRiskMitigation
Argon2id timing on slow hardware An attacker observing login latency could distinguish Argon2id completion from network round-trip time, confirming a valid email address exists. Not a key-recovery risk. Server returns HTTP 401 for both invalid email and invalid signature after a fixed minimum delay, preventing email enumeration via timing.
Ed25519 private key in JS heap A speculative execution exploit (Spectre-class) running in a co-located browser tab could attempt to read cross-tab memory containing the private key. Modern browsers enable Site Isolation and related process isolation controls, reducing cross-origin memory exposure. The server sends Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp on all web responses, placing the app in a cross-origin isolated context and assigning it a dedicated OS process. This eliminates shared-process memory access between Ubimate and any other open tab.
XSalsa20-Poly1305 nonce bias from weak PRNG If libsodium’s randombytes_buf() produces biased output, nonce collision probability increases above the Birthday-bound estimate in §6.3. libsodium’s randombytes_buf uses the OS CSPRNG (/dev/urandom on Linux/macOS, BCryptGenRandom on Windows, SecRandomCopyBytes on iOS/macOS) and is considered cryptographically strong on all supported platforms. The risk is negligible.
Keystroke / clipboard inference A malicious browser extension or screen-capture malware could observe the user’s passphrase as it is typed, before Argon2id derivation. Out of scope per §2.4. OS-level malware on the user’s device is outside the threat model of any software-only solution.
Cold-boot attack on in-memory key An attacker with physical access to a running device could attempt to dump RAM and recover the in-memory derived seed or private key. Partially mitigated by OS memory encryption (Apple Silicon Secure Enclave, AMD SME/SEV). Full mitigation requires locking the device when unattended; software cannot prevent a physical RAM dump of a running machine.

2.4 Explicitly out of scope

ScenarioRationale
Full device compromise (OS-level malware, physical access to unlocked device)No software-only defence is effective once the OS is controlled by the adversary. Full-disk encryption (FileVault / BitLocker / LUKS) is the recommended mitigation for physical access.
Legal compulsion directed at the userA user can be compelled to disclose their own passphrase. This system cannot protect against self-disclosure.
Denial-of-service against the sync serverAvailability attacks do not affect the confidentiality or integrity of encrypted data. Infrastructure-level rate limiting and DDoS mitigations are in place but are outside this document’s scope.
Supply-chain attack on the client binary distributionA compromised build pipeline or package registry could ship a modified client. Mitigated by reproducible builds (planned), code-signing of all distributed binaries (current), and strict release-signature verification.
State-level traffic analysisA nation-state adversary with access to backbone traffic can perform large-scale timing correlation. Nemo’s anonymisation does not protect against this adversary class. Tor or equivalent is required for that threat model.

2.5 Security properties summary

PropertyHolds?Notes
Confidentiality of document content vs. serverYesXSalsa20-Poly1305 (crypto_secretbox_easy); server never receives plaintext or keys
Confidentiality of document content vs. networkYesTLS 1.3 in transit; encrypted at rest on server
Integrity of document contentYesPoly1305 MAC detects any modification
Authentication without passphrase transmissionYesEd25519 challenge-response; see §5
Forward secrecy for key envelopesYesEphemeral X25519 DH per crypto_box_seal envelope
No server-side key escrowYesServer stores only public keys and wrapped (sealed) content key
Content confidentiality vs. collaborator (non-shared documents)PartialCryptographic backstop applies to non-collaborators; for active collaborators holding the shared content key, isolation of non-shared documents relies on backend permission enforcement, not cryptography alone. See §7.2.
Collaborator key authenticity (TOFU)PartialPeer X25519 public keys are retrieved from the server on a TOFU basis. A compromised server can substitute a key, but the worst-case outcome is a detectable denial of service: User B cannot open the invitation and immediately discovers the failure. Key substitution cannot silently grant access to any third party. See §7.2.
Post-quantum confidentialityPartialAES-256 is Grover-safe; EC primitives are not Shor-safe (see §15)
Metadata confidentiality (document structure, size, timing)NoUnavoidable structural leakage; documented in §2.2
Unobservability / traffic analysis resistanceNoOut of scope; see §2.4

2.6 Design goals

  1. The server stores no private keys, no plaintext content, and no passphrase hashes.
  2. The same credentials always derive the same keypair deterministically, on every device, with no network roundtrip.
  3. Losing a device does not mean losing data; losing a passphrase means losing cloud access but not the local copy.
  4. Real-time multi-user collaboration is possible without the server ever seeing decrypted content.
  5. The web app remains safe to use on shared or untrusted machines.
  6. Metadata leakage is documented precisely rather than minimised or obscured.

3. Architecture Overview

Ubimate operates in three modes, each with a different trust perimeter:

ModeBackendData locationEncryption at rest
Local (offline) Rust / SQLite inside Tauri binary User’s device only Intentionally no application-layer encryption (see §3 note). OS full-disk encryption recommended.
Cloud sync Express + Hocuspocus + SQLite on instance node Device + encrypted cloud replica XSalsa20-Poly1305 (crypto_secretbox_easy), client-side; server stores ciphertext only
Web app Same cloud node; key in memory only Server (ciphertext); no local cache XSalsa20-Poly1305, in-session only; key zeroed on tab close

In local mode there is no network activity at all. This document focuses primarily on the cloud sync and web app modes, where the cryptographic architecture is most relevant.

Note on local data ownership: Local SQLite data is stored without application-layer encryption by deliberate design choice. Encrypting local data would require the application to hold a decryption key, which necessarily means that key can also be lost, forgotten, or held hostage by a software update. The local database is open standard SQLite and is directly queryable with any SQLite tool. Document content, however, is stored inside the database as Yjs binary blobs: these are not encrypted, but they are a structured binary format that is not human-readable without the application. Unrestricted human-readable access to all document content is provided by the HTML mirror feature, which generates a static, fully offline-readable copy of every document that does not require Ubimate software to view or archive. The threat model for local data is physical device access, which is best addressed by OS full-disk encryption (FileVault / BitLocker / LUKS) — not by an additional application-layer key that introduces its own recovery problem.

4. Identity & Key Derivation

4.1 The email address as a permanent cryptographic identity

At registration the user provides an email address that serves as both their permanent login identifier and the input from which the Argon2id salt is derived. It is stored as the immutable email column and never changes. A separate mutable contact_email column, initially set to the same value, holds the address used for all outbound communication (billing, notifications, invitation emails). Users can update contact_email at any time without any cryptographic consequence.

The critical invariant: the registration email used for login and key derivation cannot be changed. Because the Argon2id salt is derived from email, a different address would produce a different salt and therefore a different keypair, permanently locking out all previously wrapped content. The registration UI makes this explicit. Updating a contact address is always handled via contact_email, never by touching email.

FieldMutablePurpose
emailNoPermanent cryptographic identity: Argon2id salt input and login credential. Never changes after registration.
contact_emailYesContact address for billing, notifications, and invitation emails. Has no cryptographic role; can be updated freely.

4.2 Deriving the Argon2id salt

Argon2id (via libsodium) requires a fixed-length 16-byte salt. The salt is derived from the email address as the first 16 bytes of its SHA-256 digest:

salt = SHA-256( email )[0:16]   // 16 bytes, derived locally

SHA-256 here is a length-normalisation step, not a security primitive — the salt is not secret. It maps an arbitrary-length email string to the fixed 16-byte input required by libsodium’s crypto_pwhash. Email addresses should be normalised (lowercased and trimmed) before this step to ensure that the same address always produces the same salt regardless of capitalisation.

The immutability of email is a deliberate design choice, not a limitation. Because the salt is derived from the registration email entirely on the client, key derivation requires no server round-trip — the user types their original email address and passphrase, and the client computes the salt and derives the keypair locally before any network contact. Allowing email to change would break this self-contained property: the client would need to fetch a salt anchor from the server before it could derive the correct keypair. The two-field model described in §4.1 resolves the practical concern: contact_email can be updated freely whenever a user changes their email provider, while email remains the permanent cryptographic identity used for login and key derivation.

4.3 Argon2id key derivation

The user’s passphrase is combined with a salt derived from the registration email address and run through Argon2id on the client. Argon2id is the Password Hashing Competition winner and the current OWASP recommendation for password-based key derivation; its memory-hard parameters make brute-force attacks economically infeasible even with a full database dump.

seed = Argon2id(
  password    : user_supplied_passphrase,
  salt        : SHA-256( email )[0:16],  // 16 bytes, derived locally from the registration email
  memory      : 64 MiB,
  iterations  : 3,
  parallelism : 1,
  output_len  : 32 bytes
)

The 32-byte seed is never stored anywhere. It exists only in memory, for the duration of the derivation step, and is immediately used to generate the Ed25519 keypair described in §4.4.

4.4 Ed25519 keypair (authentication)

An Ed25519 signing keypair is derived deterministically from the seed. The public key is registered with the server at sign-up. The private key never leaves the device.

The ECDSA nonce-reuse catastrophe — and why Ed25519 is immune

The most widely deployed EC signing standard is ECDSA. ECDSA requires a random nonce k per signature. If k is ever reused across two different messages signed with the same private key, the private key is immediately and trivially recoverable via linear algebra — no brute force required. This is not a theoretical concern:

  • Sony PlayStation 3 (2010) — a constant k was inadvertently used for all firmware signatures. Two publicly signed messages were sufficient to extract the entire platform root key, enabling arbitrary code execution on every PS3 ever manufactured.
  • Bitcoin Android wallet (2013) — a defective PRNG produced repeated nonces across signing operations, allowing attackers to recover private keys and drain thousands of wallets from public blockchain data alone.

Ed25519 eliminates this attack class entirely. RFC 8032 §5.1.6 specifies that the per-signature nonce is derived deterministically from a hash of the private key and the message. There is no per-signature randomness. Nonce reuse is impossible by construction — even if the system PRNG is completely broken.

Why not RSA

RSA-2048 achieves 112 bits of classical security; Ed25519 achieves 128 bits with a 32-byte key and 64-byte signature (vs. 256-byte RSA-2048 key, 256-byte signature — 8× larger on the wire). RSA private-key operations require large-exponent modular exponentiation, which is significantly slower. Naive RSA-OAEP and RSA-PKCS1v1.5 implementations are vulnerable to Bleichenbacher-class padding oracle attacks; Ed25519 has no padding surface at all.

Additional Ed25519 properties: operations are constant-time by design (no timing side-channel); the twisted Edwards addition formula is complete (no exceptional points to mis-handle); cofactor 8 of Curve25519 is handled correctly and automatically by all standard libraries (@noble/curves, libsodium, and the Rust ed25519-dalek crate used in the Tauri backend).

4.5 X25519 keypair (key agreement)

An X25519 key agreement keypair is derived from the Ed25519 keypair using libsodium’s conversion functions: crypto_sign_ed25519_pk_to_curve25519 for the public key and crypto_sign_ed25519_sk_to_curve25519 for the private key. There is no separate Argon2id derivation pass — both X25519 keys are deterministically produced from the Ed25519 seed via these conversions. The X25519 public key is stored on the server. The X25519 private key never leaves the device.

X25519 is used for content key wrapping and sharing: the user’s symmetric content key is sealed with their own X25519 public key at sign-up (producing the wrapped_content_key stored on the server), and sealed with a collaborator’s X25519 public key when sharing access. Only the holder of the corresponding X25519 private key can unseal it.

Why X25519, not ECDH over NIST P-256/P-384

ECDH over NIST curves requires that the implementation validates the peer’s public key is an actual point on the curve before computing the shared secret. If this check is omitted or incorrect, an attacker can supply a crafted point on a small-order subgroup and recover the private key after a small number of exchanges. This invalid-curve attack has been demonstrated against real-world TLS implementations (e.g., Java’s JSSE prior to 2017). The check is easy to forget and easy to get wrong.

X25519 uses a Montgomery-form parameterisation where every possible 255-bit value is a valid public key. The scalar multiplication function is defined to handle all inputs safely — there are no invalid points, no small-subgroup points to reject, and no validation step that can be forgotten. Key clamping (clearing bits 0–2 and 255, setting bit 254 of the scalar) is applied automatically by the algorithm, ensuring correct cofactor handling without any caller action.

X25519 and Ed25519 share the same underlying Curve25519, so the security of both primitives rests on the discrete-logarithm hardness of a single, extensively studied elliptic curve.

4.6 Full derivation diagram

email (immutable by design — permanent KDF anchor; see §4.1)
      │
      ▼  SHA-256( email )[0:16]  →  16-byte salt  (derived locally)
      │
passphrase ──┤
      │
      ▼  Argon2id (client-side, memory-hard)
  32-byte seed
      │
      ▼  crypto_sign_seed_keypair( seed )
  Ed25519 keypair   (signing)                pubkey → server registry
      │
      ├──▶  crypto_sign_ed25519_pk_to_curve25519  →  X25519 pubkey  → server registry
      └──▶  crypto_sign_ed25519_sk_to_curve25519  →  X25519 privkey (never leaves device)

Because the derivation is fully deterministic and client-side, the same passphrase on any device always produces the same keypair. There is nothing to sync. There is no key backup on the server. If the device is lost, providing the email and passphrase on a new device re-derives the same keypair, which can then be used to unwrap the wrapped_content_key from the server and restore full cloud access.

5. Authentication Protocol

5.1 Challenge-response login

Login is performed via a signed challenge-response protocol. No passphrase or passphrase derivative is ever transmitted.

// Step 1: derive all credentials locally (no network)
salt    = SHA-256(email)[0:16]
seed    = Argon2id(passphrase, salt: salt)
ed25519 = crypto_sign_seed_keypair(seed)
x25519  = { pk: pk_to_curve25519(ed25519.pk), sk: sk_to_curve25519(ed25519.sk) }

// Step 2: fetch a one-time nonce
nonce ← GET /api/auth/challenge?email=<email>
        ← { nonce: "<32 cryptographically random bytes, single-use, TTL: 30s>" }

// Step 3: sign the nonce and authenticate
token ← POST /api/auth/login {
  email,
  signature: Ed25519.sign(nonce, ed25519.privateKey)
}
// Server:
//   1. Verifies the Ed25519 signature against the stored public key
//   2. Marks the nonce as consumed (rejects any replay within the TTL window)
//   3. Issues an HttpOnly session cookie

The server stores no passphrase and no passphrase hash for newly registered users. It stores Ed25519 and X25519 public keys plus a wrapped_content_key blob; none of these values expose private key material or plaintext content.

Attack scenarios and protocol responses

AttackWhat the attacker hasWhy it fails
Network eavesdropping Captured login request (email + signature over nonce) The nonce is single-use and server-invalidated on first use. The captured signature cannot be applied to any future nonce. No passphrase is in the request to harvest.
Replay attack Captured login request replayed verbatim, within the TTL Server tracks consumed nonces and immediately rejects any second use of a nonce that has already been verified. HTTP 401 is returned.
Server database dump email, ed25519_public_key, x25519_public_key, wrapped_content_key, encrypted blobs Public keys are not secret — by definition. Signing future challenges still requires the Ed25519 private key, which was never transmitted to or stored on the server. Encrypted blobs are unreadable without the content key, which the server can only access in wrapped form sealed under the user’s X25519 private key — also never on the server.
Offline brute-force against stolen hash Attacker wants to guess the user’s passphrase There is no passphrase hash to steal — only a public key. An attacker wanting to brute-force the passphrase must run a full Argon2id derivation (64 MiB, ∼1 s) per guess. There is no online oracle that accepts raw passphrases.
Phishing for credentials Email and passphrase captured from a fake login page Possessing credentials alone is insufficient. The attacker must also fetch a live challenge nonce from the real server (requiring an active session window) and sign it within 30 seconds — an active, time-critical, server-cooperating operation, not a passive replay.

Protocol security level

The protocol’s security reduces to the unforgeability of Ed25519 signatures. Ed25519 provides 128 bits of classical security (equivalent to an exhaustive AES-128 key search) and approximately 64 bits of security against a quantum adversary running Shor’s algorithm. The 64-bit post-quantum margin is a known limitation addressed in the protocol versioning plan.

5.2 Session token

On successful authentication the server issues a signed JWT delivered as an HttpOnly; SameSite=Strict; Secure cookie. The current session token lifetime is 7 days (or session-scoped when the client disables remember-me). JavaScript cannot read this cookie, eliminating the primary XSS token-theft vector. Cross-site requests are refused by the browser due to SameSite=Strict.

For WebSocket connections (Hocuspocus real-time sync), a separate 60-second token is fetched immediately before each connection attempt via GET /api/auth/ws-token. This short-lived token is passed as the Hocuspocus token parameter and is validated server-side in the onAuthenticate hook. The long-lived session JWT never travels over the WebSocket wire.

5.3 What the server stores per user

FieldValue
emailPermanent login identifier and Argon2id salt input. Immutable after registration.
contact_emailMutable contact address for billing and notifications. No cryptographic role.
ed25519_public_keyFor challenge-response signature verification
x25519_public_keyFor content key sealing (collaboration sharing flow)
workspace_keys table rowsOne row per (workspace, user): the workspace’s symmetric content key sealed with that user’s X25519 public key; opaque to the server

Not stored: passphrase, passphrase hash, private keys, derived seed, plaintext content key, plaintext document content, plaintext metadata.

6. Document Encryption

6.1 Per-workspace symmetric content key

Each workspace has its own independent random 256-bit symmetric content key generated on the client when the workspace is created. This key encrypts all content belonging to that workspace — Yjs document updates, document properties, and file attachments. Keys for different workspaces are independent: a compromise of one workspace key does not affect other workspaces. The content key itself is never stored or transmitted in plaintext.

workspace_key = libsodium.randombytes_buf(32)  // 256-bit key, generated once per workspace

Decoupling the content key from the keypair means the content key can be shared with collaborators independently of authentication credentials, and future key rotation does not require re-running Argon2id.

6.2 Wrapping and unwrapping workspace content keys

When a workspace is created, the new content key is sealed with the creator’s own X25519 public key using libsodium’s crypto_box_seal (anonymous-sender ECIES: ephemeral X25519 DH + XSalsa20-Poly1305). The resulting sealed box is stored on the server in the workspace_keys table, keyed by (workspace_id, user_id).

// At workspace creation — seal key for own use
wrapped_workspace_key = crypto_box_seal(workspace_key, user.x25519_public_key)
// → base64-encoded sealed box stored in workspace_keys(workspace_id, user_id, wrapped_key)

// At session start — server returns all wrapped keys for this user
// Client unseals each one:
for (workspace_id, wrapped_key) in workspace_keys:
  workspace_key = crypto_box_seal_open(
    wrapped_key,
    user.x25519_public_key,
    user.x25519_private_key   // derived locally, never left the device
  )
  // → 32-byte key held in memory, keyed by workspace_id; never written to any storage

The server holds only the sealed boxes. It cannot unseal any of them without the user’s X25519 private key, which it never receives.

6.3 Encrypting document content (Yjs updates)

Ubimate uses Yjs CRDTs as the document format. Edits are represented as binary update deltas. Before any update is sent to the server or stored in the cloud, it is encrypted:

encrypted_update = crypto_secretbox_easy(
  plaintext  : yjs_update_bytes,
  nonce      : libsodium.randombytes_buf(24),  // 192-bit, fresh per update
  key        : workspace_key                   // 256-bit, per-workspace
)
// wire format: version (1 byte) ‖ nonce (24 bytes) ‖ ciphertext+MAC
// version byte is currently 0x01; clients MUST reject blobs with an unknown version

The server’s yjs_updates table stores only these opaque blobs and their document ID. The Hocuspocus relay receives and forwards them without inspecting their contents. The server never sees the Yjs state.

XSalsa20-Poly1305 is an AEAD scheme — not just encryption

XSalsa20-Poly1305 (libsodium’s crypto_secretbox_easy) is an Authenticated Encryption with Associated Data (AEAD) construction from the NaCl family. It simultaneously provides:

  • Confidentiality via XSalsa20 stream cipher (Salsa20 extended to a 192-bit nonce): the key stream is computationally indistinguishable from random to any party without the key.
  • Integrity and authenticity via a 128-bit Poly1305 MAC computed over the entire ciphertext. Any single-bit modification causes MAC verification to fail deterministically. The probability of an adversary constructing a valid forgery is 2−128 per attempt.

The application must reject all updates whose MAC verification fails unconditionally and must not apply them to the Yjs document state. This prevents both corruption and chosen-ciphertext attacks. The integrated construction avoids the encrypt-then-MAC ordering pitfalls that affect AES-CBC + HMAC schemes.

Why XSalsa20-Poly1305 (libsodium crypto_secretbox_easy)

All symmetric and asymmetric operations in the Ubimate client use a single library: libsodium. Consolidating on one audited, dependency-free cryptographic library reduces attack surface compared to mixing multiple libraries with different APIs, trust histories, and versioning cadences. crypto_secretbox_easy’s API surface is minimal — key, nonce, message — with no mode selection, no padding, and no IV management beyond nonce generation, making misuse much harder than raw AES-GCM primitives.

Nonce strategy and collision analysis

The 192-bit (24-byte) nonce is always generated fresh by libsodium’s randombytes_buf() for each encryption operation. XSalsa20-Poly1305 requires that the same (key, nonce) pair is never reused. Because a single content key covers all documents in one workspace, the nonce space is bounded per workspace — a substantially smaller corpus than a single per-user key spanning all workspaces. The 192-bit nonce makes random collisions astronomically unlikely even at very high message volumes within a workspace. The Birthday-bound collision probability for n random 192-bit nonces under one key is:

P(collision) ≈ n² / 2¹⁹³

At n = 2³² (around 4 billion updates, a very large corpus):
P(collision) ≈ 2⁶⁴ / 2¹⁹³ = 2⁻¹²⁹  (negligible)

At n = 10⁹ (a billion updates, unrealistic in practice):
P(collision) ≈ 10¹⁸ / 2¹⁹³ ≈ 10⁻³⁹  (astronomically small)

The 192-bit nonce is the primary reason XSalsa20 was chosen over Salsa20 (which uses a 64-bit nonce) in the NaCl secretbox design: the extended nonce makes random-nonce strategies safe even when one key is reused across a very large number of messages. Workspace content key rotation on collaborator revocation resets the nonce space entirely.

6.4 Metadata encryption

The documents table schema distinguishes between structural fields that must remain in plaintext for the server to perform routing and access-control queries, and user-readable content fields that are encrypted before leaving the client:

FieldStorageRationale
id (document UUID)PlaintextPrimary key; required for foreign-key references and routing
parent_idPlaintext UUID (nullable)Plaintext foreign key so the server can enforce tree structure and route queries without decrypting anything
type (document type enum)PlaintextRequired for server-side permission and routing logic
positionPlaintextRequired for server-side ordering queries
titleEncrypted (inside properties blob)User-readable content; inaccessible to the server
iconEncrypted (inside properties blob)User-readable content; inaccessible to the server
All tags and user-defined custom propertiesEncrypted (inside properties blob)User-readable content; inaccessible to the server

The properties blob is encrypted as a whole with crypto_secretbox_easy using the workspace content key before being sent to the server. The server cannot perform server-side title previews, search, or content indexing. Full-text search runs locally against a decrypted client-side index.

6.5 File attachment encryption

Binary file attachments — images, documents, audio, video, and all other uploaded file types — are encrypted with the same content key before any bytes are transmitted to the server.

// Upload path — encrypt before transmission
nonce      = libsodium.randombytes_buf(24)  // 192-bit, fresh per file
ciphertext = crypto_secretbox_easy(
  plaintext : file_bytes,                   // raw file content
  nonce     : nonce,
  key       : content_key
)
// wire format: version (1 byte = 0x01) ‖ nonce (24 bytes) ‖ ciphertext+MAC
// filename : original_name.ext → original_name.ext.enc
// POST /api/uploads carries the opaque binary blob

The .enc suffix is appended to the filename before upload so the server can recognise ciphertext uploads without inspecting their contents. The server stores and serves the blob as-is; it cannot read file content.

// Download / preview path — decrypt after fetch
raw_bytes  = GET <upload URL>               // opaque blob from server
version    = raw_bytes[0]                   // must be 0x01; legacy blobs (no prefix) are handled transparently
nonce      = raw_bytes[1:25]                // bytes 1–24
ciphertext = raw_bytes[25:]
file_bytes = crypto_secretbox_open_easy(ciphertext, nonce, content_key)
blob_url   = URL.createObjectURL(new Blob([file_bytes]))
// → temporary blob: URL used for display; caller revokes it when done

Files uploaded before encryption was enabled (plaintext uploads, identifiable by the absence of the .enc suffix) are returned unchanged for backward compatibility. New uploads are always encrypted. The same AEAD construction guarantees documented in §6.3 apply: the Poly1305 MAC detects any server-side modification of the stored ciphertext before the file is rendered.

6.6 Yjs update compaction

As a document is edited, individual update deltas accumulate in yjs_updates. When row count reaches a threshold, the client merges all deltas into a single snapshot via Y.encodeStateAsUpdate(). This compacted snapshot is itself encrypted before being stored, replacing the individual delta rows.

Compaction is safe at any time — including during active multi-device sessions — because of how Yjs CRDTs work. When an offline device reconnects, the Hocuspocus state vector exchange transmits only the diff between the offline device’s state and the current server state, regardless of how many times the server-side history has been compacted.

7. Real-time Collaboration

7.1 Yjs CRDTs and the zero-knowledge relay

Real-time collaboration is powered by Yjs, a Conflict-free Replicated Data Type (CRDT) library. CRDTs allow multiple clients to independently produce and merge edits without a central coordinator that understands the content. This property is essential to zero-knowledge collaboration: the server can relay and merge encrypted update blobs without ever needing to parse them.

The Hocuspocus WebSocket server handles:

  • Maintaining the current encrypted document state
  • Broadcasting encrypted updates to all connected clients
  • Performing state vector exchange for reconnecting offline clients
  • Persisting encrypted updates to SQLite

At no point during any of these operations does Hocuspocus decrypt, inspect, or transform the content of any update.

7.2 Collaboration key exchange

When a workspace owner invites a collaborator, the workspace content key must be shared in a way that the server cannot intercept in plaintext. The invitation flow is an out-of-band delivery model, not a server-persisted key escrow:

  1. User A looks up User B’s X25519 public key from the server’s public key registry.
  2. User A seals the workspace content key with User B’s X25519 public key client-side using crypto_box_seal.
  3. User A constructs an invitation payload containing the sealed workspace key, workspace ID, permission level, and expiry — entirely on the client.
  4. User A’s client generates a mailto: link pre-populated with User B’s email address and the invitation deep-link as the message body. Clicking the link opens User A’s own email client, which sends the email directly to User B. The server plays no role in delivering the invitation payload. It never holds the invitation payload or sealed key material. It may dispatch a lightweight out-of-band notification to User B (e.g. “Someone wants to invite you to a workspace”) and a confirmation to User A when User B accepts. Note: the invitation token travels over normal email infrastructure rather than Ubimate’s TLS channel, but this does not weaken security — the sealed content key inside the token is cryptographically bound to User B’s X25519 private key and cannot be extracted by any party who intercepts the email.
  5. User B opens the deep-link, opens the sealed workspace key with their own X25519 private key (client-side via crypto_box_seal_open), validates invitation metadata, and holds the workspace key in memory for the session.

The server stores the X25519 public keys of all users (these are by definition public) but plays no role in invitation delivery and never holds invitation payloads or sealed key material. At no step does it have access to any private key or plaintext content key.

Trust model: User B’s public key is retrieved from the server on a Trust-on-First-Use (TOFU) basis. This is a documented vulnerability: a compromised or malicious server (adversary A3 or A4) can substitute its own X25519 public key in place of User B’s when responding to User A’s key lookup. User A then seals the workspace content key to the attacker’s key rather than to User B’s. Because the invitation is delivered directly from User A’s email client (via a mailto: link) and the server never handles or stores the payload, the sealed box never passes back through the server — the attacker therefore cannot unseal it and does not obtain the content key. The practical effect of the attack is a denial of service: User B cannot open the sealed box and will immediately discover they cannot access the workspace, revealing the tampering to both parties. This attack requires no client modification; it happens at the API layer during the key-lookup response.

The cryptographic outcome already provides implicit key verification: if A sealed the workspace key to the correct public key, B can open the invitation and access the workspace; if the server substituted a different key, B cannot open it and the failure is immediately apparent to both parties. There is no scenario in which a substituted key silently grants access to anyone — including the attacker — because the invitation payload is sent directly from A’s email client and never passes back through the server. The question of whether A is sending to the right person is answered by the ordinary email UX: A can see the To: address in their own email client before hitting send, the same check they apply to any email.

Content-access scope: User B holds the workspace content key, scoped to the invited workspace only — B does not obtain keys to any of A’s other workspaces. B’s actual access to individual documents within the workspace is bounded by the backend permission layer: REST endpoints and Hocuspocus rooms enforce which document ciphertext B can fetch. For users who are not collaborators and for adversaries who do not hold the workspace key (including A3 — compromised server), no document content is accessible regardless. For active collaborators, confidentiality of non-shared documents in other workspaces is fully cryptographically guaranteed, as B holds no key for those workspaces. Confidentiality of individual documents within the shared workspace depends on the correctness of permission enforcement rather than cryptography alone. A permission bug (e.g. an IDOR in a document route) would expose ciphertext that B could decrypt, since B holds that workspace’s key. This is a known and accepted consequence of the per-workspace key design; the mitigation is rigorous permission test coverage and defence-in-depth at the API layer.

Invitation authenticity: Invitation payloads are Ed25519-signed by the sender. The canonical signed string is 'ubimate_invite:{token}:{email}:{expires_at}'; the Ed25519 signature and the sender’s public key are included in the invitation record stored on the server. When User B registers using the invitation token, the server returns sender_public_key and sender_signature in the 201 response; B’s client verifies the signature against the sender’s registered public key before proceeding. An attacker constructing a fake invitation cannot produce a valid Ed25519 signature over the canonical payload without A’s private key, which never left A’s device — eliminating the phishing vector. The sealed content key remains independently protected by the X25519 cryptographic envelope regardless of signature validity.

7.3 Collaborator revocation

When a collaborator’s access is revoked, the workspace content key must be rotated to prevent the former collaborator from decrypting future updates. Because the content key is per-workspace, revoking access requires rotating only that workspace’s key — all other workspaces and their content are unaffected:

  1. The owner generates a new workspace_key for that workspace.
  2. The owner re-encrypts all existing documents and properties in that workspace with the new key.
  3. The owner re-seals the new workspace_key for their own use and re-issues invitations to any remaining collaborators with the new key.
  4. The server replaces the old ciphertext and workspace_keys rows with the new versions.

This operation is client-side and scoped to a single workspace; the UI surfaces a progress indicator during key rotation. Access control (who may read or write to a workspace) is also enforced server-side and can be revoked immediately at the server layer regardless of key rotation status.

8. Platform-specific Security Considerations

8.1 Tauri desktop app (trusted device)

The Tauri app runs a full Rust backend locally. The email and passphrase (or the derived keypair) are stored in the OS keychain via Tauri Stronghold (macOS Keychain / Windows Credential Manager), unlocked by the user’s system login or biometrics. The user does not need to re-enter their Ubimate passphrase on subsequent launches.

Local data is intentionally stored unencrypted at the application layer. The SQLite database file on disk is standard, unencrypted SQLite — not encrypted by Ubimate. Document content is stored inside this database as Yjs binary blobs, which the application renders but which are not human-readable with a generic SQLite viewer. Unrestricted human-readable access to all document content is provided by the HTML mirror feature, which exports a static offline-readable copy of every document that does not depend on Ubimate software to view or archive. The appropriate protection for local data at rest is OS full-disk encryption (FileVault on macOS, BitLocker on Windows, LUKS on Linux), which is recommended for all users. Application-layer encryption of local data would introduce a key-management problem (what if the key is lost?) that full-disk encryption solves at the OS level without any additional recovery risk.

8.2 Web app (untrusted machine)

The web app is designed to be safely usable on shared or untrusted machines. It applies stricter key-storage rules than the desktop app:

DataTauri (trusted device)Web app (untrusted machine)
Private key / derived seed OS keychain (persistent) Memory only — never written to any storage
Session token Secure persistent storage sessionStorage — cleared when the tab closes
Decrypted document content In-memory while open In-memory while open
Node URLs, UI preferences localStorage via atomWithStorage localStorage via atomWithStorage (no secrets)
Encrypted document blobs SQLite on device Never cached locally

Re-authentication on page reload is intentional. The keypair exists only for the duration of the browser tab. When the tab closes, the private key is gone. The next visit requires the user to re-enter their passphrase to re-derive it. This mirrors the security model of Proton Mail’s web app.

Implementation invariant: the Jotai atoms holding the Ed25519 keypair and the plaintext content key must never use atomWithStorage. They must be plain in-memory atoms (e.g. atom<Uint8Array | null>(null)), populated on login and zeroed on logout. Accidental persistence to localStorage would silently compromise the privacy guarantee on shared machines.

8.3 Mobile app

The mobile app (iOS / Android) follows the same model as the Tauri desktop app — private keys are stored in the platform secure enclave (iOS Keychain / Android Keystore), and local data is encrypted at the file-system level by the OS. Network sync follows the same protocol as the desktop client.

9. Private AI & the E2EE Compatibility Problem

Forward-looking content. This chapter describes the intended design for Ubimate’s private AI features (Robo and Nemo). Neither feature has been implemented yet, and the architecture described here has not been finalised. Details are subject to change before release.

AI assistance and end-to-end encryption appear to be in fundamental tension: if a model runs on a remote server, it must be able to read the content it processes — breaking the zero-knowledge guarantee. Ubimate resolves this tension with two architecturally distinct layers that never require the server to see plaintext.

9.1 The core incompatibility

To process text, a model needs plaintext. This means:

  • An AI feature that sends your document content to a cloud API (OpenAI, Anthropic, Google, etc.) is a privacy break, regardless of the provider’s own privacy policy.
  • An AI feature that runs on the sync server is equally a privacy break: the server, which should only ever hold ciphertext, would be decrypting content to feed a model.
  • An AI feature that silently transmits selected context to any external service — even “anonymously” — without explicit per-request user consent is a privacy break.

This is why most notes apps that have added AI features have quietly abandoned their previous privacy claims, or have no meaningful privacy claims to begin with. Ubimate’s architecture does not permit either path: the sync server cannot decrypt content even if instructed to, because it does not hold the keys.

9.2 Robo — fully on-device inference

Robo is a fine-tuned small-parameter language model (SLM) that runs entirely on the user’s device via Ollama. It will ship as a free optional companion app separate from the main Ubimate installation.

PropertyDetail
Execution locationUser’s device only — the model binary and weights are downloaded locally by Ollama
Data flowUbimate decrypts the document locally (as it does for display), then passes plaintext to the local Ollama process over localhost. No data leaves the device.
Network trafficZero: local IPC / loopback only. The Ollama API endpoint at http://localhost:11434 is not exposed to any external network.
Model training dataRobo is trained on Ubimate’s public schema and documentation only — no user data is used for training, because no user data ever reaches Ubimate’s infrastructure
Privacy guaranteeIdentical to using the editor without AI: plaintext exists on-device in memory while the document is open, and nowhere else
Encrypted document requirementRobo only operates on already-decrypted in-memory state. It never reads raw ciphertext from disk or the sync server.

Because inference runs locally, Robo is available offline, has no per-query cost, and adds no latency beyond the model’s own inference time. The trade-off is hardware: local inference requires a capable CPU or GPU. Robo’s SLM is specifically selected for the parameter range that runs acceptably on mid-range consumer hardware.

9.3 Nemo — anonymous relay for cloud LLMs

Nemo is a privacy-preserving proxy for cases where on-device capability is insufficient — general knowledge lookups, research, creative tasks that benefit from a frontier model. The design constraint is that no workspace content and no user identity ever reaches a third-party model provider.

PropertyDetail
What is sent to the cloud modelOnly the user’s explicitly typed prompt. No document content, no workspace metadata, no user ID, no account linkage is included unless the user deliberately pastes it into the prompt.
AnonymisationRequests are routed through a Ubimate relay node that strips all HTTP headers that could identify the originating device or account (IP, User-Agent, authentication cookies). The cloud provider sees the relay’s IP only.
No persistent contextNemo sessions are stateless. The relay does not log prompts, does not maintain conversation history server-side, and does not associate requests with a user account.
Explicit consent per queryNemo is not an ambient background feature. The user must explicitly open the Nemo panel and type a prompt. There is no automatic document summarisation, no silent context injection, and no background telemetry.
What Ubimate’s server handlesToken authentication for billing (optional subscription) and outbound relaying. The server never interprets, logs, or stores prompt content.
What the cloud provider receivesAn anonymised API call from the relay’s IP, containing only the user-typed prompt. Indistinguishable from any other API consumer using the same relay pool.

Robo-assisted prompt screening

Because Robo runs locally, it can screen a Nemo-bound prompt before it leaves the device. When the user submits a Nemo prompt, Robo analyses it for patterns that suggest sensitive workspace content — document titles, database values, named entities that appear in the open workspace, or fragments resembling encrypted field values.

If a potential data-leakage risk is detected, the UI surface an inline warning:

⚠ This prompt may contain content from your workspace.
   Review it before sending to an external model.
   [Edit prompt]  [Send anyway]

The check is advisory, not a gate. The user can dismiss the warning and proceed if they intentionally want to include context. The goal is to catch accidental paste-and-forget scenarios, not to restrict deliberate choices. When Robo is not installed, the warning step is skipped and Nemo operates without screening.

Full transparency requires stating these limitations explicitly:

  • User-pasted content (partially mitigated): If the user manually copies document text into a Nemo prompt, that content is sent to the cloud model. Robo-assisted prompt screening (see above) attempts to catch this and warn the user before submission, but the warning is advisory — the user can proceed if the inclusion is intentional.
  • Cloud provider logging: Nemo routes through a Ubimate relay, but the destination cloud provider (e.g. the API endpoint operator) may log the prompt on their infrastructure. Users should treat Nemo prompts as inputs to a third-party service, not as end-to-end encrypted communications.
  • Traffic analysis: A powerful network adversary observing the relay’s outbound traffic could correlate request timing if they also monitor the user’s connection to the relay. This adversary model is out of scope; Nemo’s anonymisation target is the cloud provider and Ubimate’s own server, not a state-level traffic analyser.

9.5 Why this architecture resolves the tension

AI scenarioUbimate’s approachE2EE preserved?
AI operates on document contentRobo: local model on decrypted in-memory state. No data leaves device.Yes — by architecture
User queries a cloud LLMNemo: user types a freestanding prompt. No document content or identity transmitted. Robo screens the prompt locally before dispatch and warns if workspace content is detected.Yes — by explicit design constraint
Server-side AI summarisation / searchNot offered. The server cannot decrypt content; this feature class is architecturally impossible.N/A — feature does not exist
Automatic context injectionNot implemented. No background AI agent reads or summarises documents without user action.N/A — feature does not exist

The E2EE guarantee is not relaxed to accommodate AI features. Instead, AI features are designed around the guarantee: local where the model is small enough, explicitly opt-in and context-free where a cloud model is involved.

10. Backend Constraints by Design

Because the server only ever holds ciphertext, several features that are standard in cloud notes applications are intentionally unavailable server-side:

FeatureStatusReason
Server-side full-text searchNot availableServer cannot read document content; search runs against a local decrypted index
Server-side title previewsNot availableTitles are encrypted as part of the properties blob
Admin content accessNot availableNo Ubimate employee can read user documents
Account-based passphrase resetNot availablePassphrase loss means keypair loss; no server-side recovery path exists
Server-side content moderationNot availableStructurally impossible without decryption

11. Known Constraints of the Zero-Knowledge Model

ScenarioConsequence
User changes passphrase Not currently supported. Changing the passphrase would produce a new keypair and a new Argon2id salt, rendering the existing wrapped_content_key inaccessible. A future migration path will re-wrap the content key under the new public key before committing the passphrase change.
Passphrase lost, no local copy Cloud access is permanently lost — the keypair cannot be re-derived without the passphrase, and the server holds no recovery copy. The local device copy remains fully accessible.
Email address change requested The contact address (contact_email) can be updated freely at any time with no cryptographic consequences. The login / identity email (email) is the Argon2id salt input and is immutable after registration; changing it would produce a different keypair and permanently lock out the existing wrapped_content_key. The registration UI makes this distinction explicit.
Collaborator revocation Because all documents share one content key, revoking a collaborator requires generating a new content key and re-encrypting all documents in the workspace. Access is also revocable immediately at the server layer (permission enforcement) independently of key rotation.
Device loss / theft Providing the email and passphrase on a new device re-derives the same keypair, which unseals the wrapped_content_key from the server, restoring full cloud access. Enable OS full-disk encryption to protect the local copy on the lost device.

12. Infrastructure & Operational Security

11.1 Cloud node model

The paid sync service runs on a sharded set of instance nodes, each running Express + Hocuspocus + SQLite. Workspaces are routed to nodes by workspace ID; all members of a workspace are always assigned to the same node, enabling Hocuspocus collaboration without cross-node state sharing.

11.2 Transport security

  • All HTTP traffic uses TLS 1.2+ (typically TLS 1.3) at the edge reverse proxy. HSTS can be enabled at the proxy layer.
  • WebSocket connections are wss:// only in production.
  • Cookies carry Secure, HttpOnly, and SameSite=Strict attributes.

11.3 Database backup

Each instance node streams continuous SQLite WAL backups to object storage using Litestream. Backups are encrypted at rest. Near-real-time replication means recovery point objective (RPO) is measured in seconds, not hours.

11.4 Infrastructure access

  • Servers are accessible via SSH with key-based authentication only; password authentication is disabled.
  • No production server has publicly exposed management ports beyond SSH and HTTPS.
  • Dependencies are pinned and reviewed with pnpm audit and automated Dependabot alerts.

13. Transparency and Verification Roadmap

Ubimate’s privacy model is designed so the sync service stores and relays ciphertext, while encryption keys remain on user devices. This section outlines how that model is validated in practice.

13.1 Independent review

Independent security audits of the core cryptographic module are planned on a recurring basis, with findings and remediation summaries published after each review cycle.

13.2 Behavioural verification

Users can verify the zero-knowledge behavior directly by inspecting application traffic:

  • Network requests: outbound traffic contains encrypted payloads and protocol metadata, not readable document content.
  • WebSocket frames: live collaboration frames are opaque binary updates rather than plaintext data structures.
  • Endpoint scope: normal operation communicates only with configured Ubimate service endpoints.

13.3 Security contact and disclosure

Security researchers and enterprise teams can request additional technical clarification and coordinated disclosure support at security@ubimate.com.

14. Cryptographic Primitive Summary

PurposeAlgorithmParametersClassical securityPost-quantum securityWhy chosen
Passphrase-based key derivation Argon2id 64 MiB / 3 iter / p=1 / 32-byte output; salt = SHA-256(email)[0:16] Determined by passphrase entropy Determined by passphrase entropy PHC winner; memory-hard defeats GPU/ASIC brute-force; hybrid id variant resists both side-channel and time–memory tradeoff attacks
Authentication signing Ed25519 Curve25519; 32-byte key; 64-byte signature 128-bit ∼64-bit (Shor) Deterministic per-signature nonce eliminates ECDSA nonce-reuse key recovery; constant-time; complete addition formula; no padding oracle
Asymmetric key agreement X25519 (ECDH) Curve25519; 32-byte public key; converted from Ed25519 via libsodium 128-bit ∼64-bit (Shor) All 255-bit strings are valid public keys — invalid-curve attacks impossible by construction; automatic key clamping; same underlying curve as Ed25519; no separate derivation pass
Symmetric encryption (content) XSalsa20-Poly1305 (crypto_secretbox_easy) 256-bit key; 192-bit random nonce; 128-bit Poly1305 MAC 256-bit (stream) / 128-bit (MAC) ∼128-bit (Grover on key) AEAD: XSalsa20 confidentiality + Poly1305 integrity; 192-bit nonces eliminate nonce-collision risk at any realistic message volume; 1-byte version prefix in wire format enables algorithm migration without flag-day re-encryption; consistent with libsodium stack; proven NaCl design
Content key sealing (wrapping / sharing) crypto_box_seal (X25519 + XSalsa20-Poly1305) Ephemeral X25519 keypair per sealed box 128-bit ∼64-bit (Shor) Anonymous-sender ECIES from libsodium; ephemeral DH provides forward secrecy: static X25519 key compromise does not expose previously sealed boxes; consistent with rest of libsodium stack
Argon2id salt derivation SHA-256 256-bit digest; first 16 bytes used as Argon2id salt 128-bit collision / 256-bit preimage ∼128-bit preimage (Grover) Maps arbitrary-length email to the fixed 16-byte salt required by libsodium’s crypto_pwhash; not used as a standalone security primitive

The web and cloud-sync client implementations use libsodium-wrappers-sumo (the WebAssembly build of libsodium) for all cryptographic operations: Argon2id key derivation, Ed25519 signing, X25519 key agreement, XSalsa20-Poly1305 content encryption, and crypto_box_seal key wrapping. SHA-256 (for salt derivation) is performed via the browser SubtleCrypto API. The Tauri / Rust backend uses libsodium bindings (sodiumoxide or equivalent) for the same operations.

15. Post-Quantum Readiness & Protocol Versioning

The elliptic-curve primitives in this protocol (Ed25519, X25519, crypto_box_seal) provide no meaningful security against a cryptographically relevant quantum computer running Shor’s algorithm. We state this explicitly rather than obscuring it.

Current quantum exposure

PrimitiveClassical securityPost-quantum securityThreat timeline
Ed25519 (signing)128-bit∼0-bit (Shor)A cryptographically relevant quantum computer: NSA / NIST consensus is 10–20+ years; no timeline is considered firm
X25519 / ECIES (key exchange)128-bit∼0-bit (Shor)
XSalsa20-Poly1305 (symmetric)256-bit stream key∼128-bit (Grover)128-bit post-quantum margin is considered safe for the foreseeable future per NIST SP 800-57
Argon2id (KDF output)∼128-bit∼64-bit (Grover on seed)Mitigated by requiring passphrase entropy > 64 bits; a 5-word passphrase achieves ∼65 bits

Protocol versioning scheme

All symmetric ciphertext blobs (Yjs updates, document properties, file attachments) carry a 1-byte version prefix that identifies the encryption algorithm and wire layout. This prefix is present in the current production format and allows algorithm migration without a flag-day re-encryption of all user data.

// Version 1 wire format (current)
version (1 byte = 0x01) ‖ nonce (24 bytes) ‖ ciphertext+MAC (variable)   // XSalsa20-Poly1305 via crypto_secretbox_easy
// The 16-byte Poly1305 MAC is appended by libsodium inside the ciphertext+MAC region.
// Total overhead: +41 bytes per blob (1 version + 24 nonce + 16 MAC).

// Clients receiving an unknown version byte MUST refuse to decrypt
// and MUST surface a "please update your client" prompt.

Planned Version 2: hybrid classical + post-quantum suite

When NIST-standardised post-quantum algorithms are sufficiently mature and available in audited libraries, Ubimate will define a Version 2 protocol:

  • Key encapsulation: X25519 + ML-KEM-1024 (FIPS 203 / CRYSTALS-Kyber) — both shared secrets are combined via HKDF, so security holds as long as either primitive is unbroken.
  • Signatures: Ed25519 + ML-DSA-65 (FIPS 204 / CRYSTALS-Dilithium) — dual-signed; both signatures must verify independently.
  • Symmetric: XSalsa20-Poly1305 unchanged — the 256-bit stream key already provides ∼128-bit post-quantum security against Grover’s algorithm.

The hybrid approach means the transition introduces no regression: if either the classical or the post-quantum component has an undiscovered flaw, the other still provides full protection. Clients supporting both version tags handle the migration transparently as documents are re-saved.

16. Questions & Responsible Disclosure

If you have questions about this document, want to request a deeper technical briefing, or have identified a vulnerability, please reach out:

We welcome scrutiny. A privacy-first product should be able to defend its claims in public.