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:
- Generate a fresh 32-byte content key (
ck). - AEAD-encrypt the plaintext with
ckand a fresh 24-byte nonce. - HPKE-wrap
ckfor every member device's KEM public key. - Build the envelope (clip_id, channel_id, sender, device_id, timestamps, flags, TTL, key_version, payload_hash, MediaRef pointer).
- Ed25519-sign the canonical CBOR of envelope keys 1–14.
- Send
clip.publishwith{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:
- Verifies the envelope signature.
- Finds its own wrap in the
wrapslist (matches ondevice_id). - Uses its KEM private key to HPKE-decap the wrap → recovers
ck. - AEAD-decrypts the ciphertext with
ck. - 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_scratchorref_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. Sensitiveref_originclips overrideproxy_ttl_sto 10 s.SENSITIVE ∧ PINNEDis a normative reject — the grid returnsE_FORBIDDEN.ONE_SHOT(bit 1) — clip requires per-device one-shot wraps at publish. Firstclip.claimgets the ciphertext + shreds it. See the claims section.
Consent-mediated bounded access (SCSG)#
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:
- Service generates a fresh X25519 keypair for this session only, in memory.
- Service calls
session.openwith its ephemeral pubkey and requested capabilities. - Grid pushes
session.grant_requestto the user's online T1 devices. - User consents on one device; that device wraps the channel key for the ephemeral pubkey via
key.wraps.put. - Grid pushes
session.readyto the service. It's now cleared to operate within the granted scope and capability set. - 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#
- Channels, envelopes, wraps, claims — the data model this crypto is layered onto.
- Server-side integrations (SCSG) — bounded access for ScaiWave / ScaiTerm / ScaiDrive-server / ScaiEdit-server.
- Scratch storage & public links — the sharing tiers and how they preserve E2E.