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. Primitives (fixed; no negotiation)
- 2. Domain separator (single, enclave-scoped)
- 3. Key derivation
- 4. Encrypt / decrypt
- 5. Event wire shape
- 6. Updates (U on
private) - 7. Error & rejection rules (normative)
- 8. Security properties
- 9. Compliance vectors (required in plugin tests)
- 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.
| Primitive | Concrete choice |
|---|---|
| AEAD | XChaCha20-Poly1305 — 24-byte nonce, 16-byte Poly1305 tag |
| KDF | HKDF-SHA-256 — salt = undefined (i.e. SHA-256 zero block), info = enclave-scoped ASCII separator (see §2), L = 32 |
| CSPRNG | globalThis.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_lowercaseThe 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:
- Reject when AEAD verification fails (
content_keyis wrong → the loader was passed a different identity). - Reject when
ciphertextornonceare not lowercase hex; whennoncedoes not decode to exactly 24 bytes (the XChaCha20-Poly1305 nonce length pinned in §1); or whenciphertextdecodes to fewer than 16 bytes (the Poly1305 tag minimum — any shorter ciphertext cannot pass AEAD verification). - NOT silently re-derive
content_keyunder a differentinfo. Theenclave_idused ininfoMUST 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.
| Property | Status |
|---|---|
| Confidentiality against the node | YES — node stores opaque ciphertext |
| Confidentiality against non-owner readers | YES — OWNER:R is the only R operator on private in enclaves/personal.md |
| Multi-device support | YES — every device with identity_priv re-derives the same content_key |
| Forward secrecy against identity-key compromise | NO — identity_priv is the IKM; compromising it reveals every past, present, and future private document. By design. |
| Post-compromise security | NO — same reason; no key rotation. The threat model assumes the identity key is the trust root for owner-only data. |
| Rotation | NONE — keys never change. Use the protocol's Delete event to retire content if the threat model requires it. |
| Per-event key isolation | YES — 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:
- Deterministic key derivation: fixed
(identity_priv, enclave_id)→ expectedcontent_keybytes. - Round-trip encrypt/decrypt under one
(identity, enclave)for two different plaintexts and verify bothnonces differ. - Cross-enclave isolation: same
identity_priv, differentenclave_id→content_keydiffers 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).