---
title: Encryption & consent
order: '11'
summary: Every byte of clipboard content is client-encrypted. Server holds ciphertext
  and opaque key wraps only. Here's exactly which primitives, why, and what that guarantees.
path: concepts/encryption
status: published
---

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](./channels.md#claims).

## 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](./service-consumers.md)) 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

- [Channels, envelopes, wraps, claims](./channels.md) — the data model this crypto is layered onto.
- [Server-side integrations (SCSG)](./service-consumers.md) — bounded access for ScaiWave / ScaiTerm / ScaiDrive-server / ScaiEdit-server.
- [Scratch storage & public links](./scratch-and-public-sharing.md) — the sharing tiers and how they preserve E2E.