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

Plugin: identity-aead

Role: Single-owner deterministic confidentiality. The content key is a deterministic HKDF derivation from the owner's identity_priv keyed by the enclave id; any device with identity_priv re-derives the same key. No ECDH, no ratchet, no rotation. Maximally simple — appropriate when only the owner ever reads.

Reference consumer: enclaves/personal.md — confidentiality for the private content event (owner-only encrypted documents). Any future event whose only reader is the owning identity (per-app preferences, secret notes, wallet backups, anything OWNER:R-exclusive) is a candidate consumer.

Slot type (suggested): IdentityAeadCryptoFn (client-side). Implementations MAY alias to their app-specific slot name (the reference impl uses PersonalPrivateCryptoFn).

This document is the complete wire-and-key contract.


Table of Contents

  1. 1. Primitives (fixed; no negotiation)
  2. 2. Domain separator (single, enclave-scoped)
  3. 3. Key derivation
  4. 4. Encrypt / decrypt
  5. 5. Event wire shape
  6. 6. Updates (U on private)
  7. 7. Error & rejection rules (normative)
  8. 8. Security properties
  9. 9. Compliance vectors (required in plugin tests)
  10. 10. Versioning

1. Primitives (fixed; no negotiation)

The primitives below are normative; implementations MUST use the concrete choice specified for every row. The content key itself is deterministic — it is derived per §3 and MUST NOT be CSPRNG-sampled.

PrimitiveConcrete choice
AEADXChaCha20-Poly1305 — 24-byte nonce, 16-byte Poly1305 tag
KDFHKDF-SHA-256 — salt = undefined (i.e. SHA-256 zero block), info = enclave-scoped ASCII separator (see §2), L = 32
CSPRNGglobalThis.crypto.getRandomValues (for the AEAD nonce only — the key itself is deterministic)
Encoding{ ciphertext: <hex>, nonce: <hex> } — two separate hex fields per event

There is no ECDH and no curve operation in this plugin: the IKM is identity_priv directly. (The 32-byte secp256k1 private scalar has full entropy and is itself the secret root for the per-identity content domain.)


2. Domain separator (single, enclave-scoped)

info = "enc-personal-private:" || enclave_id_hex_lowercase

The literal prefix MUST be enc-personal-private:.

enclave_id_hex_lowercase MUST be the consumer enclave's id encoded as a lowercase 64-hex string. The enclave id is baked into the info so that two distinct enclaves owned by the same identity produce independent content keys.


3. Key derivation

The content_key MUST be derived deterministically from (identity_priv, enclave_id) by the code block below.

info        = utf8("enc-personal-private:" || lowercase_hex(enclave_id))
content_key = HKDF(IKM=identity_priv, salt=∅, info, L=32)

The content_key is fixed for the (identity, enclave) pair and MUST NOT rotate. Implementations MUST NOT cache content_key in any persistent store — it MUST be re-derived from identity_priv on each encrypt/decrypt. Live-only caching during a session is permitted.


4. Encrypt / decrypt

4.1 Encrypt

Encryption MUST follow the code block below; the nonce length MUST be exactly 24 bytes.

content_key = HKDF(identity_priv, salt=∅, info="enc-personal-private:" || enclave_id_hex, 32)
nonce       = CSPRNG(24)
ct          = XChaCha20-Poly1305(content_key, nonce).encrypt(utf8(plaintext))
event.content = { ciphertext: hex(ct), nonce: hex(nonce) }

4.2 Decrypt

Decryption MUST follow the code block below.

content_key = HKDF(identity_priv, salt=∅, info="enc-personal-private:" || enclave_id_hex, 32)
plaintext   = utf8_decode(
                XChaCha20-Poly1305(content_key, fromHex(nonce)).decrypt(fromHex(ciphertext))
              )

5. Event wire shape

private events use a JSON content whose fields are this plugin's encrypted envelope. The application MAY embed the envelope under a stable key (e.g. { "doc": { ciphertext, nonce } }); implementations MUST agree on the embed shape per application convention. When no application convention dictates otherwise, the envelope MUST be the top-level object below.

{
  "ciphertext": "<hex>",
  "nonce":      "<hex>"
}

The private event's RBAC is OWNER: CRUD — only the owner reads or writes. The node MUST NOT decrypt.


6. Updates (U on private)

A private event MAY be Updated by OWNER:U. The Update event MUST carry a fresh (ciphertext, nonce) pair under the same content_key (the key is identity-derived and never rotates). The update mechanism is the protocol's standard Update/Delete ([r, <target_id>] tag); this plugin does not alter it.


7. Error & rejection rules (normative)

An implementation MUST:

  1. Reject when AEAD verification fails (content_key is wrong → the loader was passed a different identity).
  2. Reject when ciphertext or nonce are not lowercase hex; when nonce does not decode to exactly 24 bytes (the XChaCha20-Poly1305 nonce length pinned in §1); or when ciphertext decodes to fewer than 16 bytes (the Poly1305 tag minimum — any shorter ciphertext cannot pass AEAD verification).
  3. NOT silently re-derive content_key under a different info. The enclave_id used in info MUST be the current enclave the event is stored in — never a referenced enclave from another field.

8. Security properties

Every row below is normative for compliant implementations.

PropertyStatus
Confidentiality against the nodeYES — node stores opaque ciphertext
Confidentiality against non-owner readersYES — OWNER:R is the only R operator on private in enclaves/personal.md
Multi-device supportYES — every device with identity_priv re-derives the same content_key
Forward secrecy against identity-key compromiseNO — identity_priv is the IKM; compromising it reveals every past, present, and future private document. By design.
Post-compromise securityNO — same reason; no key rotation. The threat model assumes the identity key is the trust root for owner-only data.
RotationNONE — keys never change. Use the protocol's Delete event to retire content if the threat model requires it.
Per-event key isolationYES — each event has a unique random nonce; AEAD provides per-nonce key reuse safety.

This is the simplest of the four encryption plugins on purpose: single-owner, single-key, deterministic. Any future "owner-key-rotation" capability would necessitate a new plugin (identity-aead-v2) and a new consuming-event content schema.


9. Compliance vectors (required in plugin tests)

Implementations MUST publish KAT vectors for:

  1. Deterministic key derivation: fixed (identity_priv, enclave_id) → expected content_key bytes.
  2. Round-trip encrypt/decrypt under one (identity, enclave) for two different plaintexts and verify both nonces differ.
  3. Cross-enclave isolation: same identity_priv, different enclave_idcontent_key differs and ciphertext from one cannot be decrypted by a key derived for the other.

10. Versioning

This plugin is version 1. Any change to the domain separator (including switching to the colon-canonical form enc:identity-aead:), the AEAD, the nonce length, or the KDF MUST require a new plugin (identity-aead-v2) and an application-layer migration path (re-encrypt existing events under the new key, store both in a transition window).