Encryption & Confidentiality
Confidentiality in Calimero is not a single switch. It is three independent layers, and a byte is only as private as the weakest layer that carries it:
- Transport — the libp2p connection between two nodes (Noise/TLS on TCP, TLS 1.3 on QUIC). Protects everything in flight against a network eavesdropper, but terminates at each peer.
- Payload — application-layer encryption applied to specific fields before they are gossiped, so that even a peer subscribed to the topic cannot read them without the right key.
- At-rest — encryption of values written to local storage, gated on an optional KMS-backed wrapper.
The headline question this page answers is: for any given piece of data, which of these layers actually covers it. The short version is that payload-layer encryption is selective — it protects the state-change artifact and group governance mutations, but a large amount of routing metadata (and, today, execution events) rides in cleartext on the gossip topic.
This page cross-links Identities & Keys (which key is which), Key Rotation (how group keys turn over), Networking (the transport and gossip layers), and the operator-facing Security guide.
The two primitives
Section titled “The two primitives”All payload-layer encryption goes through a single type, calimero_crypto::SharedKey (crates/crypto/src/lib.rs). It is always AES-256-GCM (via ring), always a 12-byte nonce, and — importantly — always empty AAD. There are two ways to construct it, and conflating them is the most common source of confusion:
| Constructor | Key material | Used for |
|---|---|---|
SharedKey::from_sk(sk) | The 32 bytes are the symmetric key, used directly. | Bulk data encrypted under a group key: state-delta artifacts and group governance ops. |
SharedKey::new(sk, pk) | ECDH on the Ed25519 identity keys (sk × pk), producing a shared secret. | Key delivery only: wrapping a group key so a specific recipient can unwrap it. |
So the symmetric variant moves data under a key the whole group already shares, and the ECDH variant moves the key itself to one recipient. Both call the identical encrypt/decrypt methods underneath.
What is encrypted, and with what
Section titled “What is encrypted, and with what”This is the master map. “Encrypted?” refers to payload-layer encryption; everything additionally gets transport encryption on the wire (and may get at-rest encryption on disk, see below).
| Data | Encrypted? | Key | Algorithm / notes |
|---|---|---|---|
State-delta artifact (the CRDT actions) | Yes | Group/scope key (from_sk) | AES-256-GCM. Encrypted at crates/node/primitives/src/client.rs:557; decrypted at crates/node/src/handlers/state_delta/crypto.rs:113. |
State-delta events (execution events) | No | — | Plaintext serde_json shipped alongside the artifact (client.rs:570). See the caution below. |
Delta envelope: root_hash, parent_ids, hlc, author_id, key_id, governance_position, delta_signature | No | — | Plaintext routing/causality metadata. |
key_id | n/a (it is a tag) | — | SHA-256(group_key); cleartext so receivers can resolve the key. |
Governance Root ops (GroupCreated, AdminChanged, MemberJoined, KeyDelivery, …) | No | — | Cleartext, visible to all namespace members by design. |
Governance Group ops (EncryptedGroupOp.ciphertext) | Yes | Group key (from_sk) | AES-256-GCM over borsh(GroupOp). group_id / key_id / signer stay cleartext as routing tags; non-members store an OpaqueSkeleton (crates/governance-types/src/lib.rs). |
Key delivery (KeyEnvelope.ciphertext) | Yes | ECDH new(sender_sk, recipient_pk) | AES-256-GCM over the 32-byte group key. On member removal the new key is re-wrapped once per remaining member; the removed member gets no envelope and is cryptographically locked out (KeyRotation). |
| Store values at rest | Only if EncryptedDatabase is enabled | DEK from HKDF-SHA256(KMS master, "calimero-dek-v{n}") | AES-256-GCM, 12-byte nonce, versioned DEKs for rotation (crates/store/encryption/). Off by default. |
| Store keys at rest | No | — | Plaintext even with the wrapper on, so range scans / prefix seeks still work. |
| Blobs | No | — | Not encrypted at rest or at the app layer. BlobId = SHA-256(plaintext); only the transport protects them in flight (crates/node/.../blobs). |
| libp2p transport | Yes | Noise / TLS session keys | Noise or TLS on TCP, TLS 1.3 on QUIC (crates/network/src/behaviour.rs). |
| Gossipsub messages | Signed, not encrypted | Ed25519 node key | MessageAuthenticity::Signed (behaviour.rs:113) — authenticity, not confidentiality. |
The pattern: bulk payloads that should be group-private are encrypted under the group key; the metadata that makes routing, causal ordering, and authorization work is cleartext. Confidentiality protects what changed, not that something changed, by whom, and in what shape.
What an observer on the gossip topic can see
Section titled “What an observer on the gossip topic can see”The gossip topic for a context is TopicHash::from_raw(context_id), and gossipsub messages are signed but not encrypted. So any node subscribed to the topic — including one that holds no group key and can decrypt nothing — can still read the full delta envelope and reconstruct a surprising amount:
- The context itself — the topic hash is the
context_id. - Who is active —
author_idon every delta,signeron every governance op. - The causal graph —
parent_ids,hlc, androot_hashreveal the DAG shape, write cadence, and convergence state, even though the payloads are opaque. - Group structure and membership churn —
group_idandkey_idtags on governance ops, plusKeyDelivery/MemberJoined/MemberRemovedRoot ops in cleartext, expose who joined or left which group and when. - Execution events — today, the full event stream (kinds, data, handler names) is in cleartext. See below.
In other words, payload encryption gives you content confidentiality, but not metadata privacy or membership privacy. An observer who cannot read a single byte of your application state can still map your group’s membership, activity, and topology.
One real message: what each reader gets
Section titled “One real message: what each reader gets”Take a chat app where a member runs send_message("hi"). The node encrypts the artifact under the group key (SharedKey::from_sk) and publishes one BroadcastMessage::StateDelta on TopicHash(context_id) (crates/node/primitives/src/client.rs). Identical bytes reach every subscriber; only the key differs. (Hashes below are abbreviated and illustrative.)
BroadcastMessage::StateDelta { context_id: 0x7a1c…e0 // = the gossip topic itself author_id: 0x9f4b…21 // the sending member's public key delta_id: 0x3c08…9d // hash(parents ‖ actions) parent_ids: [ 0x11aa…02 ] // previous head(s) hlc: 1750.000123:#0 // (seconds.fraction, logical counter) root_hash: 0x6e72…ff // author's expected scope_root after apply key_id: 0x4d90…7c // = SHA-256(group_key) — which key decrypts nonce: 0x1b…(12 bytes) // AES-GCM nonce artifact: 0xa3f1…(opaque) // CIPHERTEXT — borsh(actions) under group key events: [ { kind: "MessageSent", data: { len: 2 } } ] // PLAINTEXT delta_signature: Some(0x82c5…) // author's envelope signature}| Field group | Non-member observer (no key) | Member peer (holds group_key where SHA-256 == key_id) |
|---|---|---|
artifact | Opaque ciphertext — no key for key_id, stays unreadable | SharedKey::from_sk(group_key).decrypt(artifact, nonce) → the CRDT action(s): a Put to the messages collection entry with value borsh("hi") |
Envelope (author_id, delta_id, parent_ids, hlc, root_hash, key_id) | Fully readable — reconstructs who wrote, when, on top of what, and the convergence state | Same cleartext, plus it now means something: the peer folds the decrypted action and confirms its recomputed root matches root_hash |
events | Readable: sees a MessageSent event fired (kind + data) — even though it never learns the text was "hi" | Same — events are plaintext for everyone |
So the observer learns that member 0x9f4b…21 sent a chat message of some length at a known time, building on a known parent — it just never learns the content "hi". The member learns everything.
How at-rest encryption works
Section titled “How at-rest encryption works”When the EncryptedDatabase wrapper is enabled, its mechanics are worth
understanding because they make key rotation a non-event (crates/store/encryption/src/lib.rs,
key_manager.rs):
- A key hierarchy. The KMS hands the wrapper a single master key. That master
never encrypts values directly — it feeds
HKDF-SHA256(saltcalimero-dek-v{n}) to derive a versioned data-encryption key (DEK). Values are encrypted under the current DEK with AES-256-GCM and a fresh 12-byte nonce. - A 1-byte version prefix. Each ciphertext is laid out as
version ‖ nonce ‖ ciphertext+tag. On read, the wrapper reads byte zero and auto-selects the matching DEK, deriving it from the master if it isn’t cached. Rotating the key just bumps the current version for new writes; old values keep decrypting under their own prefix, so rotation needs no migration pass over existing data. - Transparent reads. Iteration returns a
DecryptingIterthat decrypts each value as it is yielded, so callers above the store never see ciphertext — the wrapper is a drop-in around anyDatabase. - Keys stay plaintext. Only values are encrypted; record keys are written in the clear so range scans and prefix seeks still work (the column-family layout in Anatomy of state depends on this).
Gotchas & boundaries
Section titled “Gotchas & boundaries”These are the sharp edges. Read them before reasoning about what is and isn’t private.
Summary
Section titled “Summary”- Two crypto modes, one API:
from_sk(symmetric, group key, bulk data) andnew(ECDH, key delivery). Both AES-256-GCM, 12-byte nonce, empty AAD. - Encrypted: state-delta artifacts, group governance ops, key envelopes; optionally store values (KMS); always the transport.
- Not encrypted: execution events, the entire delta/governance envelope metadata, store keys, blobs. Gossip is signed, not encrypted.
- Confidentiality covers content, not metadata or membership. Plan your threat model around the cleartext envelope, the plaintext events, and the off-by-default at-rest layer.
For how group keys are created and turned over, see Key Rotation; for the keys themselves, Identities & Keys.
Next: Key Rotation — how a scope’s symmetric key turns over when a member is removed.