Phase 4: Encryption and Sealed Tesseras
2026-02-14
Some memories are not meant for everyone. A private journal, a letter to be opened in 2050, a family secret sealed until the grandchildren are old enough. Until now, every tessera on the network was open. Phase 4 changes that: Tesseras now encrypts private and sealed content with a hybrid cryptographic scheme designed to resist both classical and quantum attacks.
The principle remains the same — encrypt as little as possible. Public memories need availability, not secrecy. But when someone creates a private or sealed tessera, the content is now locked behind AES-256-GCM encryption with keys protected by a hybrid key encapsulation mechanism combining X25519 and ML-KEM-768. Both algorithms must be broken to access the content.
What was built
AES-256-GCM encryptor (tesseras-crypto/src/encryption.rs) — Symmetric
content encryption with random 12-byte nonces and authenticated associated data
(AAD). The AAD binds ciphertext to its context: for private tesseras, the
content hash is included; for sealed tesseras, both the content hash and the
open_after timestamp are bound into the AAD. This means moving ciphertext
between tesseras with different open dates causes decryption failure — you
cannot trick the system into opening a sealed memory early by swapping its
ciphertext into a tessera with an earlier seal date.
Hybrid Key Encapsulation Mechanism (tesseras-crypto/src/kem.rs) — Key
exchange using X25519 (classical elliptic curve Diffie-Hellman) combined with
ML-KEM-768 (the NIST-standardized post-quantum lattice-based KEM, formerly
Kyber). Both shared secrets are combined via blake3::derive_key with a fixed
context string ("tesseras hybrid kem v1") to produce a single 256-bit content
encryption key. This follows the same "dual from day one" philosophy as the
project's dual signing (Ed25519 + ML-DSA): if either algorithm is broken in the
future, the other still protects the content.
Sealed Key Envelope (tesseras-crypto/src/sealed.rs) — Wraps a content
encryption key using the hybrid KEM, so only the tessera owner can recover it.
The KEM produces a transport key, which is XORed with the content key to produce
a wrapped key stored alongside the KEM ciphertext. On unsealing, the owner
decapsulates the KEM ciphertext to recover the transport key, then XORs again to
recover the content key.
Key Publication (tesseras-crypto/src/sealed.rs) — A standalone signed
artifact for publishing a sealed tessera's content key after its open_after
date has passed. The owner signs the content key, tessera hash, and publication
timestamp with their dual keys (Ed25519, with ML-DSA placeholder). The manifest
stays immutable — the key publication is a separate document. Other nodes verify
the signature against the owner's public key before using the published key to
decrypt the content.
EncryptionContext (tesseras-core/src/enums.rs) — A domain type that
represents the AAD context for encryption. It lives in tesseras-core rather than
tesseras-crypto because it's a domain concept (not a crypto implementation
detail). The to_aad_bytes() method produces deterministic serialization: a tag
byte (0x00 for Private, 0x01 for Sealed), followed by the content hash, and for
Sealed, the open_after timestamp as little-endian i64.
Domain validation (tesseras-core/src/service.rs) —
TesseraService::create() now rejects Sealed and Private tesseras that don't
provide encryption keys. This is a domain-level validation: the service layer
enforces that you cannot create a sealed memory without the cryptographic
machinery to protect it. The error message is clear: "missing encryption keys
for visibility sealed until 2050-01-01."
Core type updates — TesseraIdentity now includes an optional
encryption_public: Option<HybridEncryptionPublic> field containing both the
X25519 and ML-KEM-768 public keys. KeyAlgorithm gained X25519 and MlKem768
variants. The identity filesystem layout now supports node.x25519.key/.pub
and node.mlkem768.key/.pub.
Testing — 8 unit tests for AES-256-GCM (roundtrip, wrong key, tampered ciphertext, wrong AAD, cross-context decryption failure, unique nonces, plus 2 property-based tests for arbitrary payloads and nonce uniqueness). 5 unit tests for HybridKem (roundtrip, wrong keypair, tampered X25519, KDF determinism, plus 1 property-based test). 4 unit tests for SealedKeyEnvelope and KeyPublication. 2 integration tests covering the complete sealed and private tessera lifecycle: generate keys, create content key, encrypt, seal, unseal, decrypt, publish key, and verify — the full cycle.
Architecture decisions
- Hybrid KEM from day one: X25519 + ML-KEM-768 follows the same philosophy as dual signing. We don't know which cryptographic assumptions will hold over millennia, so we combine classical and post-quantum algorithms. The cost is ~1.2 KB of additional key material per identity — trivial compared to the photos and videos in a tessera.
- BLAKE3 for KDF: rather than adding
hkdf+sha2as new dependencies, we useblake3::derive_keywith a fixed context string. BLAKE3's key derivation mode is specifically designed for this use case, and the project already depends on BLAKE3 for content hashing. - Immutable manifests: when a sealed tessera's
open_afterdate passes, the content key is published as a separate signed artifact (KeyPublication), not by modifying the manifest. This preserves the append-only, content-addressed nature of tesseras. The manifest was signed at creation time and never changes. - AAD binding prevents ciphertext swapping: the
EncryptionContextbinds both the content hash and (for sealed tesseras) theopen_aftertimestamp into the AES-GCM authenticated data. An attacker who copies encrypted content from a "sealed until 2050" tessera into a "sealed until 2025" tessera will find that decryption fails — the AAD no longer matches. - XOR key wrapping: the sealed key envelope uses a simple XOR of the content key with the KEM-derived transport key, rather than an additional layer of AES-GCM. Since the transport key is a fresh random value from the KEM and is used exactly once, XOR is information-theoretically secure for this specific use case and avoids unnecessary complexity.
- Domain validation, not storage validation: the "missing encryption keys"
check lives in
TesseraService::create(), not in the storage layer. This follows the hexagonal architecture pattern: domain rules are enforced at the service boundary, not scattered across adapters.
What comes next
- Phase 4 continued: Resilience and Scale — Shamir's Secret Sharing for heir key distribution, advanced NAT traversal (STUN/TURN), performance tuning, security audits, OS packaging
- Phase 5: Exploration and Culture — Public tessera browser by era/location/theme/language, institutional curation, genealogy integration, physical media export (M-DISC, microfilm, acid-free paper with QR)
Sealed tesseras make Tesseras a true time capsule. A father can now record a message for his unborn grandchild, seal it until 2060, and know that the cryptographic envelope will hold — even if the quantum computers of the future try to break it open early.