Skip to content

Crypto envelope

TOVIO bases protected-payload read confidentiality on cryptography rather than a storage ACL alone. A protected file is stored as a policy object whose content is encrypted under a per-object key, with that key wrapped separately for authorized recipients. Merely storing or relaying that envelope provides ciphertext, wrapped keys, and policy metadata—not a recipient private key or unwrapped data key. Public content, metadata, service availability, authorized endpoints, recovery authority, and explicit disclosure to a runner or model remain separate trust boundaries. This page describes the envelope and its fixed primitive set.

This page summarizes format version 1.

The fixed primitive set

All algorithms are pinned for format version 1. Changing any of them is a format-version break, which is what lets two implementations reproduce each other's ciphertext and addresses.

Purpose Algorithm
Content addressing / integrity BLAKE3-256
Identity signing Ed25519
Key agreement (wrap, Solo recipients) X25519
Tier 0 content AEAD ChaCha20-Poly1305 (age semantics)
Tier 1 content AEAD AES-256-GCM with a per-object DEK
DEK wrap X25519 + HKDF-BLAKE3 + AEAD
Transport TLS 1.3 (mandatory floor)
Randomness CSPRNG (OS entropy) for DEKs, nonces, change IDs

Two rules sit over all of them: every key, DEK, and nonce comes from a CSPRNG, and a DEK or file key encrypts exactly one logical object (with one exception — the chunks of a single file under one policy may share a DEK, each with its own distinct nonce).

Identity keys

An identity holds two keys, and they are never interchanged:

  • an Ed25519 signing keypair — signs commits, audit entries, capability tokens, and refs; and
  • an X25519 key-agreement keypair — receives wrapped content keys.

Private key material is encrypted at rest using a wrapping key held in the OS keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service), and is never written to objects/ and therefore never synced — a hard invariant checked by fsck and CI. Secret key material is zeroized after use and never logged.

The ABE-forward envelope

policy-blob (an encrypted small file) and policy-chunk (an encrypted chunk) share one envelope:

policy-blob | policy-chunk {
  policy_id:    string        // references a declaration in the policy manifest
  policy_expr:  string        // human-readable, for display/audit only — NOT trusted for access
  enc:          { cipher, nonce }     // the content AEAD and its 96-bit nonce
  recipients:   [ Recipient ]         // >= 1; how the content key is made available
  content_type: opt<string>           // MIME of the plaintext (policy-blob only)
  ciphertext:   bytes                 // AEAD output: enc_body || 16-byte tag
}

The shape is deliberately stable. Adding an encryption mode means adding a new recipient kind, never changing the envelope — that is the "ABE-forward" guarantee. The Enterprise CP-ABE implementation uses this extension point without changing the public format.

Recipient variants

A recipients[] entry is discriminated by its kind:

kind Tier Carries
age Tier 0 (Solo) An ephemeral public key and the wrapped file key, age-style. No recipient identity.
x25519 Tier 1 (Team) A recipient Did, an ephemeral public key, and the X25519-wrapped DEK.
abe Tier 2 (Enterprise, release-gated) An ABE scheme name (tovio-cpabe-tkn20-bls12381-v1 in the v1 private profile) and an ABE-encrypted DEK. The Enterprise implementation interprets it; the default-off public client preserves it without interpretation.

Three rules govern recipients:

  • recipients must contain at least one entry. A reader that understands none of the kinds present reports the object as unreadable-by-this-identity — distinct from corruption or obliteration — and preserves it verbatim on sync.
  • The recipient Did is a routing hint, not an authorization claim. Authorization is established solely by the AEAD unwrap succeeding under the reader's secret; an implementation must not grant or deny based on the Did field alone.
  • policy_expr is display and audit metadata only and must not be consulted when deciding whether decryption is permitted.

How the three tiers use one envelope

All tiers produce the same envelope object; they differ only in the content cipher and the recipient mechanism. The tier follows the mode chosen at tovio init and recorded in the replica-local config.

  • Tier 0 — Solo. A single developer protecting paths from anything but their own key. No Key Authority. Content is encrypted with ChaCha20-Poly1305 under a random file key; the file key is wrapped to the developer's X25519 key with age semantics. The relay learns nothing about who the recipient is.
  • Tier 1 — Team (the v1 target). Different identities read different paths, the relay is untrusted to hold plaintext, and a Key Authority decides membership. Content is encrypted with AES-256-GCM under a per-object DEK; the DEK is wrapped once per authorized recipient with X25519_wrap. The relay learns who may read (the recipient set is metadata) but can decrypt nothing — it holds no recipient secret and the KA never hands it a DEK.
  • Tier 2 — Enterprise (implemented, release-gated). Policy is enforced cryptographically at decrypt time via CP-ABE. The CIRCL TKN20 enterprise KA has passed its conformance and internal-review gate, while the public client remains default-off and preserves unsupported abe recipients. No generally available artifact enables the profile; threshold/KMS/HSM authority remains later work.

The DEK wrap

X25519_wrap seals a content key K for a recipient public key. It generates a fresh ephemeral X25519 keypair per recipient per object, derives a single-use key-encryption key via HKDF (with BLAKE3 as the PRF) that binds in the recipient's public key, and seals K under AES-256-GCM, with the recipient's public key bound into the AAD as well. A fixed all-zero wrap nonce is safe only because each wrap derives a fresh single-use KEK from a fresh ephemeral scalar — so the (KEK, nonce) pair is used exactly once. Reusing an ephemeral across wraps is forbidden.

The content AEAD additionally authenticates a domain-separated AAD binding the ciphertext to its envelope context (the object type tag, policy_id, and cipher), so a ciphertext cannot be cut and pasted into a different policy. Readers recompute the AAD and fail closed on a tag mismatch.

Grant and revoke

Membership changes never require re-keying the whole repository:

  • Grant is additive. To give an identity read access, a holder who already has the DEK unwraps it and adds an x25519 recipient wrap, producing a new envelope object. No content is re-encrypted. The Key Authority's only role is to authorize the recipient and supply its public key — it never wraps a DEK itself, because it never holds one.
  • Revoke is rotate-and-re-wrap. Removing a reader means generating a fresh DEK, re-encrypting the affected content (a new object with a new address), and wrapping the new DEK for the remaining recipients only. You cannot revoke by merely deleting a wrap, since the revoked party may retain the prior object.

Reads are offline: holding the X25519 secret and a cloned envelope, a reader unwraps the DEK and decrypts with no Key Authority round-trip. The KA is consulted only at grant and enrollment time, never at read time.

Large files: chunk, then encrypt

For a policy-protected large file the order is chunk-then-encrypt per chunk: FastCDC splits the plaintext (so dedup boundaries stay stable), each chunk is encrypted into a policy-chunk, and a chunk-manifest lists the ordered ciphertext addresses. All chunks of one file under one policy may share a DEK, but each chunk uses a distinct nonce.

Deduplication of encrypted chunks holds only within one policy/DEK domain — identical plaintext under different DEKs produces different ciphertext and does not dedupe. Cross-policy dedup would require convergent encryption, which leaks plaintext equality across access domains, and is out of scope for v1.

Snapshot-time encryption

Because TOVIO has no staging area and continuously snapshots the working copy, a protected path is matched and encrypted at snapshot time, before its content is hashed into a tracked tree. The clear bytes of a protected file therefore never land in a tracked tree or as a clear blob/chunk in objects/ — they exist only in the working-copy file on disk and transiently in memory. This is what stops an AI tool or a careless commit from capturing a .env as plaintext.

What's signed, and what isn't

A signature covers a deterministic signing payload: the object's canonical CBOR with the signature field omitted, prefixed by a versioned, per-type domain tag — so a signature for one object type can never be replayed as a signature for another. Audit entries, capability tokens, and agent session grants are always signed; commits and lane refs carry an additive signature envelope that a repository's policy manifest can require for new commits and protected-lane updates; an obliteration carries the authorization proof its governing write policy demands. Policy objects are not signed: their integrity comes from the content AEAD tag and their content address, and authorship comes from the commit that references them.

Honest limitations (v1)

The envelope is honest about what it does not do. Documented v1 residual risks include: read-revocation is forward-only (re-encrypt-on-rotate, never retroactive); the Tier-1 Key Authority is trusted not to mis-wrap a DEK for an unauthorized identity (only Tier 2 closes that with the math); write control is relay-enforced, not cryptographic; and the recipient set, policy_id, sizes, and graph shape are metadata in the clear. These must be presented to users as known limitations, not as cryptographically closed.

Related pages

The policy language that drives recipient selection is summarized in policy & tokens.

Last reviewed September 9, 2026

Suggest an improvement to this page Not for security reports — see disclosure