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:
recipientsmust 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
recipientDidis 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 theDidfield alone. policy_expris 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
agesemantics. 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
aberecipients. 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
x25519recipient 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