Skip to main content

The key hierarchy

K_blob (per blob, long-lived)
  ↓ wrapped at play time by
epoch_key (rotating every 5 min, per app server)
  ↓ delivered to client over cdn/keys/v1
  ↓ client unwraps → K_blob → decrypts ciphertext from CDN
plaintext bytes
Why envelope? Encrypting per-viewer would defeat caching — the whole network would store N ciphertext copies for N viewers. Envelope encryption encrypts the key per-session, not the bytes. One ciphertext, one hash, one cached copy, N envelopes.

Ingest-time key generation

At the moment your ingest pipeline receives a blob:
  1. Generate K_blob — a random 32-byte XChaCha20-Poly1305 key.
  2. Encrypt the blob with K_blob — use a fresh 24-byte nonce.
  3. Compute the BLAKE3 hash of the ciphertext — this is the content-addressed identifier the CDN sees.
  4. Upload the ciphertext to your origin backend.
  5. Store K_blob in your app server’s key store, keyed by the ciphertext hash.
  6. Insert (hash, bucket, key, size, content_type) into the catalog.
Your CDN nodes serve the ciphertext. They have no idea what’s inside.

Epoch keys

The app server rotates an epoch key every 5 minutes. At any given time:
  • The current epoch key is used to wrap K_blob for new play requests.
  • The previous epoch key is still accepted for a grace period (covers requests in flight during rotation).
  • Older epoch keys are retired.
Close an active client’s cdn/keys/v1 stream to revoke — within one epoch, the client can no longer unwrap new envelopes for new hash requests.

Client decryption flow

Offline leases

The app server can issue a sealed envelope that the client stores locally for offline playback:
lease = client_device_key.seal(K_blob, expires_at, policy)
The client persists the lease to its device keystore. On offline playback, it unseals with its device key. Lease policies can bind to:
  • A specific device ID
  • An expiration timestamp
  • A per-device play count
Lease design is your product decision; the CDN protocol is agnostic.

What CDN nodes see vs. don’t

PartySees ciphertext?Sees K_blob?Sees epoch keys?
CDN node
App server✅ (if also hosting ingest)
Client✅ (only for authorized hashes)
A CDN node that serves a blob gets the ciphertext hash and the ciphertext bytes. Nothing else. Even if a node operator colluded with a client, they’d still need a valid envelope from the app server to decrypt.

What compromise looks like

  • Single CDN node compromised — attacker gets ciphertext. Useless without K_blob.
  • Client device compromised — attacker gets plaintext for content the client has already played, plus whatever offline leases are valid. Limited to that one user’s catalog.
  • App server compromised — attacker gets K_blob for every blob in your catalog. Catastrophic. This is the single trust anchor and should be treated accordingly.
Mitigations:
  • Separate key-store infrastructure (Vault, KMS) from the auth/billing layer; compromise of the web tier should not grant key-store access.
  • Rotate the epoch key root on any suspicion of compromise.
  • Forward secrecy for stored K_blob material is an open item — epoch keys provide session-level forward secrecy, but a long-lived K_blob compromise remains catastrophic.

Accepted trade-offs

  • No forward secrecy for stored K_blob. Epoch key rotation limits access-control blast radius (5 minutes), not forward secrecy of historical content. Content previously delivered with a compromised K_blob remains decryptable.
  • App server is a trust root. There is no protocol-level mitigation for app server compromise — operational security is the mitigation.