Envelope encryption — @withnen/seal

@withnen/client and @withnen/server protect a live session — a handshake, then encrypted request/response traffic. But a lot of sensitive data has no session: a webhook fired between two services, a form field on its way to your database, an image uploaded to object storage. @withnen/seal covers that case.

You seal a payload to a recipient's ML-KEM-768 public key; only the holder of the secret key can open it. There's no shared session and nothing secret on the wire — the same post-quantum guarantee as the rest of Nen, applied to data at rest and in flight. As always: this keeps data ciphertext between the endpoints; the endpoints hold plaintext by design. It is not DRM.

See it run

Pick an image (or use the sample). It's sealed to a freshly generated public key in your browser with real ML-KEM-768 + ChaCha20-Poly1305, then opened back with the secret key. What sits in the middle — what your CDN and storage would hold — is ciphertext.

loading crypto…
Original
Pick an image to start
Sealed (what the CDN sees)
ciphertext appears here
Decrypted (secret key)
opens back to the original

Real ML-KEM-768 + ChaCha20-Poly1305 in your browser. The image is ciphertext the moment it leaves the page — your storage, CDN, and logs only ever see the sealed bytes. It is not DRM: whoever is allowed to view it can still capture it.

Seal & open

import { generateSealKeypair, sealJSON, openJSON } from "@withnen/seal";

const recipient = generateSealKeypair();             // { publicKey, secretKey }

const env = sealJSON(recipient.publicKey, { amount: 4200 });
// env is base64-only: { v, kem, n, ct } — store it or send it in the clear.

const data = openJSON(recipient.secretKey, env);     // { amount: 4200 }

Bind an envelope to a purpose with context — it can't be opened under a different one:

seal(pk, bytes, { context: "invoice:2026-06" });

Field-level encryption

Encrypt the sensitive fields of an object before it reaches your logs, queue, or database. The rest of the record stays plain and queryable.

import { sealFields, openFields } from "@withnen/seal";

const safe = sealFields(user, ["ssn", "profile.phone"], recipient.publicKey);
// safe.id / safe.email stay readable; ssn & phone are sealed envelopes.

const full = openFields(safe, recipient.secretKey);  // original object back

Encrypted webhooks

Seal and sign a webhook body. The receiver verifies the sender (ML-DSA) and the event before it ever touches a handler.

import { sealWebhook, openWebhook } from "@withnen/seal";

// Sender
const { headers, body } = sealWebhook(receiverPk, "payment.succeeded", payload, signer);
await fetch(url, { method: "POST", headers, body });

// Receiver — pins the sender, checks the event
const { event, payload } = openWebhook(receiverSk, rawBody, {
  signerPublicKey: senderPk,
  expectedEvent: "payment.succeeded",
});

Sealed forms & uploads

Encrypt a file or image in the browser before it's uploaded — it's ciphertext the moment it leaves the page, and stays that way in storage and on the CDN.

import { sealBlob, openBlob, openObjectURL } from "@withnen/seal";

// Browser — seal before upload
const sealed = await sealBlob(serverPublicKey, file);
await fetch("/upload", { method: "POST", body: JSON.stringify(sealed) });

// Anywhere with the secret key — rebuild it
const blob = openBlob(serverSecretKey, sealed);
const url  = openObjectURL(serverSecretKey, sealed);   // for <img>/<video src>

Large files are split into independent AEAD frames (sealFramed / openFramed), so a single key-encapsulation covers the whole object.

What it protects — and what it doesn't

Adversary / locationProtected?
CDN, edge cache, object storage, logs, proxies, third-party hops✅ ciphertext only
Quantum harvest-now-decrypt-later✅ ML-KEM-768
Casual theft: right-click-save, hotlink, leaked URL, scraping✅ no plaintext at rest
An authorized viewer screen-recording what they're shown❌ no system stops this
A compromised endpoint❌ endpoints hold plaintext by design

Pair it with DRM if you need playback/license control; reach for @withnen/seal when you need the content itself to stay confidential everywhere it travels and rests. See the Threat Model for the full boundary.

Install

npm install @withnen/seal

It's also re-exported from @withnen/client and @withnen/server, so anything already importing those gets seal, sealFields, sealWebhook, and sealBlob for free.