Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

Encryption & consent

ScaiClip is end-to-end encrypted. The server holds ciphertext, HPKE-wrapped keys, and metadata. It never sees plaintext. It has no path to plaintext.

This page walks through the stack — what encrypts what, where the keys live, and how a service like ScaiWave gets bounded access without breaking the model.

The primitives#

Three cryptographic building blocks:

Primitive RFC Where
XChaCha20-Poly1305 AEAD (Sodium) Clip ciphertext
HPKE (DHKEM-X25519-HKDF-SHA256 + ChaCha20-Poly1305) RFC 9180 Key wraps
Ed25519 RFC 8032 Envelope signatures

Every device generates its own X25519 KEM keypair and Ed25519 signing keypair at enrolment. Private keys never leave the device. Public keys go to ScaiKey's directory.

How a clip gets encrypted#

The publishing device does all of this locally:

  1. Generate a fresh 32-byte content key (ck).
  2. AEAD-encrypt the plaintext with ck and a fresh 24-byte nonce.
  3. HPKE-wrap ck for every member device's KEM public key.
  4. Build the envelope (clip_id, channel_id, sender, device_id, timestamps, flags, TTL, key_version, payload_hash, MediaRef pointer).
  5. Ed25519-sign the canonical CBOR of envelope keys 1–14.
  6. Send clip.publish with {envelope, ciphertext, wraps}.

The grid verifies the signature (against the sender's sig_pub from ScaiKey), assigns seq and grid_ts, persists the row, fans out clip.new to other subscribers. The wraps and ciphertext travel with the fan-out.

How a receiver decrypts#

The receiving device:

  1. Verifies the envelope signature.
  2. Finds its own wrap in the wraps list (matches on device_id).
  3. Uses its KEM private key to HPKE-decap the wrap → recovers ck.
  4. AEAD-decrypts the ciphertext with ck.
  5. Hits the OS clipboard.

Total round-trip on a warm connection: under 250 ms.

What the server sees#

For every clip, the server sees:

  • Envelope fields (metadata: sender, timestamps, TTL, cflags, hash, signature).
  • Ciphertext bytes — opaque.
  • Wrap bytes — opaque, one per device.
  • MediaRef pointer if the clip is ref_scratch or ref_origin.

For every scratch object, the server sees:

  • Object metadata (owner, size, sha256, mime hint).
  • Opaque ciphertext in Garage.

For every public share link:

  • Random 128-bit token.
  • Which scratch object it points at.
  • Whether it's password-bound.
  • Download counter, expiry.
  • Hash of the mint-time revoke_token.

The server never sees:

  • Any plaintext.
  • Any content key.
  • Any channel key.
  • Any device private key.
  • Any URL fragment on a public link.

Signatures and impersonation#

Every clip carries an Ed25519 signature made with the publishing device's key. Verification catches:

  • Envelope tampering. Any change to a signed field flips the sig invalid.
  • Cross-device forgery. Alice's laptop can't publish clips that claim to come from her phone.
  • Grid-side edits. The server can't rewrite an envelope without invalidating the sig; receivers reject it.

Signature verification uses sig_pub fetched from ScaiKey's directory (with local caching). Device key rotation bumps key_version and invalidates every cached sig_pub for that device — grid catches this on the webhook consumer.

Sensitive and one-shot flags#

Two flags in cflags change the crypto or lifecycle posture:

  • SENSITIVE (bit 0) — clip auto-expires in seconds (default 90 s). Never cached beyond the sensitive TTL. Sensitive ref_origin clips override proxy_ttl_s to 10 s. SENSITIVE ∧ PINNED is a normative reject — the grid returns E_FORBIDDEN.
  • ONE_SHOT (bit 1) — clip requires per-device one-shot wraps at publish. First clip.claim gets the ciphertext + shreds it. See the claims section.

Server-side services (ScaiWave, ScaiTerm, ScaiDrive-server, ScaiEdit-server) need clipboard access without holding channel keys forever.

The SCSG protocol (see Server-side integrations) reconciles this:

  1. Service generates a fresh X25519 keypair for this session only, in memory.
  2. Service calls session.open with its ephemeral pubkey and requested capabilities.
  3. Grid pushes session.grant_request to the user's online T1 devices.
  4. User consents on one device; that device wraps the channel key for the ephemeral pubkey via key.wraps.put.
  5. Grid pushes session.ready to the service. It's now cleared to operate within the granted scope and capability set.
  6. On session close, the wrap is shredded, the ephemeral device revoked, the in-memory keypair destroyed.

The user's own device stays the only long-lived holder of the private key that unlocks the channel. The service gets a short-lived, capability-scoped, per-session copy of the content key, only for clips published during its window.

The user can revoke mid-session from any of their devices.

Public sharing without breaking E2E#

Public share links are a special case. Anyone with the URL decrypts — that's the point.

The trick: the AEAD key rides in the URL fragment (#<key>), which browsers never send to servers. The server sees:

  • The token in the request path.
  • The ciphertext.
  • Nothing else.

Client-side JS in the download landing page reads the fragment, decrypts, saves. Server-side we never handle the key.

For password-bound links, the fragment carries a KDF salt instead of the raw key. Client-side Argon2id(password, salt) derives the key; a wrong password fails the AEAD decrypt cleanly. Each decrypt failure POSTs an empty pixel to /p/{token}/pw for rate-limiting.

What ScaiClip does not protect against#

Two threat models are explicitly out of scope:

  • Endpoint compromise. If an attacker roots the user's device, they have the private keys. That's outside our perimeter.
  • URL leaks with the fragment intact. A public URL shared over a compromised channel (a customer's malware-infected browser, a shared screen) grants access. Recipients need to treat share URLs like credentials — the design ensures the server doesn't leak them, not that recipients don't.

What's next#

Updated 2026-07-12 00:26:08 View source (.md) rev 5