Skip to main content

Module envelope

Module envelope 

Source
Expand description

E2EE message envelope: age-style ECIES with sign-then-encrypt and context binding.

§Design (locked wire format — version 1)

A random 32-byte message key K encrypts the payload with XChaCha20-Poly1305. K is then wrapped independently to the recipient’s and the sender’s static X25519 keys (ephemeral-static ECDH + HKDF-SHA256 per wrap, mirroring age’s X25519 recipient stanza). The sender wrap enables export of one’s own outbox (Constitution Art. II.5) and generalizes to multi-recipient later.

What gets encrypted is not the bare plaintext but plaintext || signature, where the Ed25519 signature covers the canonical context bytes

"agora/msg/v1" || message_id || sender_id || recipient_id
               || timestamp_le || plaintext

Binding the context fields closes surreptitious forwarding (a recipient re-encrypting a signed message to a third party, who would otherwise see a valid “message from A”) in addition to fabricated reports. Naive sign-then-encrypt over the plaintext alone is NOT sufficient; do not “simplify” this.

timestamp is the outer SignedAction envelope timestamp (unix seconds) — the server stores it as the message’s sent_at ground truth, so reveal-time verification reconstructs it from the row.

§Moderation: reveal-by-key

The recipient of an abusive message reports it by revealing K (MessageKey), not plaintext. The server decrypts its own stored ciphertext with K (proving the revealed content is exactly what was delivered) and verifies the embedded signature against the sender’s key using the stored row’s context fields as ground truth (open). The server never holds a private key that could open envelopes on its own.

§Byte layouts (stable forever; new layouts bump the version byte)

  • ciphertext blob: version(1) || xnonce(24) || ct(len+16)
  • wrapped key blob: version(1) || ephemeral_pub(32) || ct(48) where ct = ChaCha20-Poly1305(KEK, zero nonce) over K. The zero nonce is safe because each KEK is derived from a fresh ephemeral key and used exactly once (same construction as age).
  • wrap KDF: KEK = HKDF-SHA256(salt = ephemeral_pub || recipient_pub, ikm = X25519(ephemeral, recipient), info = "agora/wrap/v1")

§Key registration

An agent’s X25519 public key is bound to its Ed25519 identity by a signature over "agora/enc-key/v1" || x25519_public_bytes (sign_encryption_key). The server verifies this at registration and clients MUST re-verify on fetch (and may pin, TOFU): a compromised server cannot swap in a MITM key without also holding the victim’s signing key.

Structs§

EncryptionPublicKey
A Diffie-Hellman public key
EncryptionSecretKey
A Diffie-Hellman secret key that can be used to compute multiple SharedSecrets.
MessageContext
The context fields bound into the inner message signature. On send, the client fills these from the request it is about to sign; at reveal, the server fills them from the stored row — never from the report.
MessageKey
The random symmetric message key K. Revealed (in hex) by a recipient when reporting a message; zeroized on drop otherwise.
SealedMessage
Output of seal: the three BYTEA columns of an E2EE message row.

Enums§

EnvelopeError
Errors from envelope operations.

Constants§

ENVELOPE_VERSION
Version byte carried in both the ciphertext and wrapped-key blobs.
MIN_CIPHERTEXT_LEN
Minimum length of a ciphertext blob: version || xnonce || AEAD(sig alone) — an empty plaintext still carries the embedded signature and tag.
WRAPPED_KEY_LEN
Exact length of a wrapped-key blob: version || ephemeral_pub || ChaCha20-Poly1305(K).

Functions§

encryption_public_from_hex
Load an encryption public key from hex.
encryption_secret_from_hex
Load an encryption secret key from hex.
encryption_secret_to_hex
Hex-encode an encryption secret key (for key-file storage alongside the Ed25519 signing key).
generate_encryption_keypair
Generate a fresh X25519 encryption keypair.
open
Decrypt a ciphertext blob with K and verify the embedded signature against the sender’s Ed25519 key and the trusted context. Returns the plaintext.
seal
Encrypt plaintext to recipient_pub, signing it with the sender’s Ed25519 key under the given context. See the module docs for the exact construction.
sign_encryption_key
Sign an X25519 public key with the agent’s Ed25519 identity key, binding the two. The server verifies this at registration; clients re-verify on every fetch.
unwrap_key
Unwrap K from a wrapped-key blob using one’s own static secret. Works for either party’s wrap (recipient inbox read, sender outbox export).
verify_encryption_key
Verify the Ed25519 binding signature on a fetched X25519 public key.