Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Encryption Plugins

ENC enclaves carry opaque ciphertext end-to-end — the node never sees plaintext, and the enclave's RBAC schema authorizes against roles, not crypto primitives. Confidentiality is therefore an application-layer concern: an app picks an encryption plugin whose wire format and key schedule it uses to seal event content and tags.

This directory is a catalog of cryptographic shapes, not an app dispatcher. Each plugin is named by what it is (the cryptographic mechanism), not by the first app that adopted it — so any new app spec can pick the right plugin from this index without renaming or forking.

Each plugin spec is the single source of truth for its wire-and-key contract — two compliant implementations MUST produce byte-identical ciphertexts on the wire given identical inputs.


Table of Contents

  1. Catalog
  2. Capability matrix
  3. Reference consumers (current apps)
  4. What each plugin spec contains
  5. How an app spec references a plugin
  6. Versioning across the catalog
  7. Catalog Implementation Names

Catalog

The catalog lists exactly CATALOG_SIZE = 4 plugins, one per PluginID variant — ratchet-pair, mls-lazy, identity-aead, ecdh-envelope — each named by its cryptographic shape (shapeOf .ratchet_pair = .pairwise, shapeOf .mls_lazy = .sharedSecret, shapeOf .identity_aead = .singleOwner, shapeOf .ecdh_envelope = .oneShotDirected).

PluginCryptographic shapeOne-line use case
ratchet-pairPairwise: ECDH-derived per-pair epoch + per-sender HKDF ratchet inside the epoch; additive OR-wrap for sub-key recipients.Any 1:1 confidential thread between two identities (DMs, call setup, file drop, signed receipt).
mls-lazyShared-secret: binary ratchet tree keyed to sorted member pubs (O(log N) per commit) + per-sender HKDF ratchet + additive OR-wrap fallback.Any N-party shared confidentiality (group chat, forum, project room, channel).
identity-aeadSingle-owner: deterministic HKDF from identity_priv + enclave id; no ECDH, no rotation.Any event whose only reader is the owning identity (personal vault, per-app prefs, wallet backup).
ecdh-envelopeOne-shot directed: ECDH(sender, recipient) + AEAD; OPTIONAL inner sub-encryption for handoff.Any one-shot sealed message from an outside writer to a single recipient (cross-enclave invite, drop-box submission, anonymous tip, addressed receipt).

Capability matrix

The matrix has capabilityAxes.length = 10 rows ({confidentiality_vs_node, per_message_key_isolation, per_sender_isolation, FS_epochs, FS_removal, backward_secrecy_add, multi_device, sub_key_wallet, stateless_decryption, PCS_vs_identity_priv}).

Capabilityratchet-pairmls-lazyidentity-aeadecdh-envelope
Confidentiality vs. node
Per-message key isolationnonce-onlynonce-only
Per-sender / per-counterparty isolationn/a (single)
Forward secrecy across epochsNONENONE
Forward secrecy on removaln/an/an/a
Backward secrecy on addn/an/a
Multi-device via identity_priv
Sub-key wallet support (OR-wrap)n/a✅ (single recipient)
Stateless decryption
Post-compromise security vs. identity_privNONONONO

CT-replayability is custody-conditional. Under local custody (the identity holds identity_priv in memory), every plugin in this catalog is CT-replayable from identity_priv — the protocol's accepted trust-root tradeoff, captured by the "NO" column above. Under delegated custody (the identity is gated by an external signer oracle: NIP-07, NIP-46, WebAuthn, KMS, HSM, threshold signer), identity_priv is unreachable to the client, so offline bulk re-derivation is impossible. Recovery in that case is decrypt-on-demand: one signer round-trip per ciphertext, consent-gated by the oracle. The capability matrix above and the multi-device row (Multi-device via identity_priv) restate that local-custody guarantee. No plugin in this catalog MAY assume offline bulk re-derivation; the identity model (spec.md §Identity Custody) carries the custody descriptor.

PCS-grade variants — those that escape the trust-root tradeoff — require an off-CT side-channel under either custody.

Role × Suite

A plugin in this catalog is the pair (role, suite). The role is the trust-shape (pairwise / single-owner / N-party / one-shot-directed); the suite is the concrete ciphersuite (AEAD + nonce length + KDF + wire framing) that lives above the shared secp256k1 ECDH floor. Roles and suites are orthogonal — a role can run over multiple suites, and a suite can host multiple roles. The registry of suite ids lives in suites.md — a cross-cutting companion doc at the spec root (like smt.md / ct.md), not a fifth entry in this role catalog.

Of the four catalog entries above, three use the enc-xchacha-v1 suite (XChaCha20-Poly1305, 24-byte nonce) and one — mls-lazy — uses mls-chacha-v1 (ChaCha20-Poly1305, 12-byte nonce per RFC 9420). A future Nostr-bridged pairwise plugin would be (pairwise, nostr-nip44-v2); the role stays the existing one, the suite carries the externally-versioned wire shape.

Catalog pluginRoleSuite
ratchet-pairpairwiseenc-xchacha-v1
mls-lazysharedSecret (N-party)mls-chacha-v1
identity-aeadsingleOwnerenc-xchacha-v1
ecdh-envelopeoneShotDirectedenc-xchacha-v1

Suite authentication, no tag-sniffing, sender-side suite selection, and fail-closed behavior are defined in suites.md §Downgrade resistance and suites.md §Peer capability advertisement.

Reference consumers (current apps)

referenceConsumers.length = 3enclaves/dm.mdratchet-pair, enclaves/group.mdmls-lazy, enclaves/personal.mdidentity-aead + ecdh-envelope.

AppPlugin(s)Covers
enclaves/dm.mdratchet-pairinvite, message, sent, rotate, Move(O→F) epoch payload
enclaves/group.mdmls-lazymessage, reaction, notice, rotate, admin membership Move epoch payload
enclaves/personal.mdidentity-aead + ecdh-envelopeprivate (owner-only) + notice (cross-enclave inbound)

Reference consumers exercise the wire-and-key contract, but the plugins themselves are app-agnostic — naming them after the first consumer was a historical accident, corrected by this catalog.


What each plugin spec contains

Every plugin spec follows the same template so reviewers and reimplementers know exactly what to expect — requiredSections.length = 10:

  1. Role & status — what cryptographic shape the plugin provides, its reference consumer(s), and its production-readiness.
  2. Primitives (§1) — fixed concrete choices: AEAD, KDF, ECDH, CSPRNG, encoding (primitiveCategories.length = 5). No negotiation.
  3. Domain separators (§2) — the exhaustive ASCII list. Anything not on this list is forbidden.
  4. Key schedule — the full HKDF/ECDH chain producing each key used, with exact info strings and lengths.
  5. Wire format — JSON shape on the wire for every event content the plugin touches, with byte-level encoding (hex/base64).
  6. State-machine concerns — monotonicity, rotation, recovery, multi-key / OR-wrap fallback where applicable (stateMachineConcerns.length = 4).
  7. Error & rejection rules — what makes a wire value invalid; normative MUST/MAY/SHOULD.
  8. Security properties — MUST include an explicit YES/NO/OPTIONAL table for FS, PCS, multi-device, etc.
  9. Compliance vectors — required KAT (known-answer test) categories that an implementation MUST publish.
  10. Versioning — what triggers a -v2 and what migration looks like.

How an app spec references a plugin

App specs (app/*.md) MUST satisfy three normative requirements (appSpecRequirements.length = 3):

  1. Name the plugin(s) they use for each event type that carries encrypted content or tags. The catalog above is the picker — choose by cryptographic shape, not by which app first used the plugin.
  2. Declare the app-side payload contract — which event-content / tag fields the plugin's encryption applies to (e.g. "message.content is a ratchet-pair message envelope; Move(O→F).content.epoch is a ratchet-pair epoch wrap").
  3. State the security guarantees the app relies on — minimum properties (multi-device, FS-on-removal, sub-key support, etc.) that the chosen plugin MUST provide. If the app's threat model needs a property the plugin lists as NO, the app must say so explicitly.

App specs MUST NOT duplicate the key schedule or wire format — those live here. A drift between an app spec and a plugin spec is a spec bug; the plugin spec wins.


Versioning across the catalog

All plugin specs in this directory are version 1 (CATALOG_VERSION = 1). The set of v2 triggers has v2Triggers.length = 7: anything that changes a domain separator, the AEAD primitive / nonce length, the KDF, member-sort order, tree topology, or a wire field's encoding requires a new plugin file (<name>-v2.md) and a new manifest negotiation hook. Versions are negotiated by manifest, never by tag-sniffing (versionNegotiation = .byManifest).

A v1 plugin's wire-level domain-separator prefixes are fixed (v1DomainPrefixes.length = 5: e.g. enc:dm:*, enc:group:*, enc:mls:*, enc:personal:*, enc-personal-private:). A v2 of any plugin MAY adopt a generic prefix (e.g. enc:ratchet-pair:*, given by v2GenericPrefixFor).


Catalog Implementation Names

referenceImpls.length = 4 — one row per catalog plugin: ratchet-pair, mls-lazy, identity-aead, ecdh-envelope.

The reference impls still use their historical slot / package names (historicalImplNames.length = 5: DMCryptoFn, GroupCryptoFn, plugin-group-mls-lazy, PersonalPrivateCryptoFn, PersonalNoticeCryptoFn). The plugin spec name is canonical going forward; implementations MAY add aliases for backward compatibility but new code SHOULD prefer the catalog name.