Skip to main content

Crate vitaminc_aead

Crate vitaminc_aead 

Source
Expand description

§Vitamin C AEAD

Crates.io Workflow Status

Authenticated Encryption with Associated Data (AEAD) primitives for building secure encryption systems.

This crate is part of the Vitamin C framework to make cryptography code healthy.

§What is AEAD?

AEAD (Authenticated Encryption with Associated Data) is a form of encryption that provides both confidentiality and authenticity. It ensures that:

  • Confidentiality: The plaintext is encrypted and cannot be read without the key
  • Authenticity: The ciphertext cannot be modified without detection
  • Associated Data: Additional data can be authenticated (but not encrypted) alongside the ciphertext

This crate provides the data model and the traversals for encrypting structured values with any AEAD cipher, in a safe and ergonomic way.

§Key Features

  • One data model: a value is described once, as an Encrypt that walks itself into a Sink. Encryption, decryption, the transport frame, index terms and host bindings are all sinks or sources of the same model, so there is no intermediate value type to convert through.
  • A cipher is seal and open: the Cipher trait seals and opens one leaf; every rule about the shape of a value (how map entries are bound to their keys, how sequence elements are bound to their sequence but not to their index or count, how empty containers are authenticated, the refusal of a container with nothing sealed) lives once in the encrypting sink and the opening walk. AsyncCipher is the same for a cipher that has to be awaited, with batching hooks for a remote key service.
  • Typed decryption names a sink: a Decrypt target is any type that names the sink that builds it, so a ciphertext is decoded straight into a struct, a Vec, an Option, a Protected<T>, or a host value. Decoding is strict.
  • Self-describing leaves: every scalar seals as a tagged leaf ([tag] ++ payload), with an optional logical type (a date, a timestamp, a decimal, a UUID) over its physical kind, so a value encrypted from Rust, Go or JavaScript opens from any of them.
  • Flexible AAD handling: the IntoAad trait lets strings, byte slices, integers, tuples, and () all be used as additional authenticated data, and ContextTag moves the obligation to supply it into the type system.
  • Protected types integration: works with vitaminc-protected so sensitive plaintext stays wrapped through encrypt and decrypt.
  • Side-channel-aware errors: a cipher’s Unspecified error reveals nothing about the cause of a failure.

§Usage

The examples below use a cipher that keeps the plaintext in the clear and authenticates the AAD, so they run without a key. A real one is vitaminc_encrypt::Aes256Cipher.

use vitaminc_aead::{decrypt, encrypt};

// Encrypt a value under some associated data...
let ciphertext = encrypt(vec!["one".to_string(), "two".to_string()], &cipher, "user:42")?;

// ...and decrypt it back into the type you expect.
let plaintext: Vec<String> = decrypt(&cipher, ciphertext, "user:42")?;
assert_eq!(plaintext, ["one", "two"]);

// The wrong AAD fails at the cipher, and a ciphertext of the wrong shape
// fails at the type: a string where a sequence was stored is refused.
assert!(decrypt::<Vec<String>, _, _>(&cipher, encrypt("x", &cipher, "right")?, "wrong").is_err());
assert!(decrypt::<String, _, _>(&cipher, encrypt(vec![1u32], &cipher, ())?, ()).is_err());

§Implementing a cipher

A cipher seals and opens one leaf. The aad it receives is the context the walk derived for the leaf’s position (the caller’s AAD for a top-level scalar, Context::for_sequence_element of it for a sequence element, Context::for_map_entry for a map value, and so on); the cipher binds whatever it needs on top, such as the wire version of its leaf format.

use vitaminc_aead::{Cipher, Context, Unspecified};
use vitaminc_protected::{Controlled, Protected};

/// Not a cipher: it stores the plaintext as it is, to show the trait
/// shape only. For real data, use `vitaminc_encrypt::Aes256Cipher`.
struct ClearTextForExamplesOnly;

impl Cipher for ClearTextForExamplesOnly {
    /// The sealed leaf: here the AAD it was sealed under and the plaintext,
    /// which a real cipher replaces with `nonce ‖ ciphertext ‖ tag`.
    type Leaf = (Vec<u8>, Vec<u8>);
    /// Opaque: a wrong key, a wrong AAD and a tampered leaf look the same.
    type Error = Unspecified;

    fn seal(&self, plaintext: Protected<Vec<u8>>, aad: &Context<'_>) -> Result<Self::Leaf, Unspecified> {
        Ok((aad.as_bytes().to_vec(), plaintext.risky_unwrap()))
    }

    fn open(&self, leaf: Self::Leaf, aad: &Context<'_>) -> Result<Protected<Vec<u8>>, Unspecified> {
        if leaf.0 != aad.as_bytes() {
            return Err(Unspecified);
        }
        Ok(Protected::new(leaf.1))
    }
}

A cipher that has to be awaited implements AsyncCipher instead, and drives encrypt_async and decrypt_async. The walk of the value finishes before the first await, so the future holds only the planned leaves and is Send even when the source is not. seal_many and open_many take every leaf of one document at once, for a provider with a batch API.

§Encrypting data

encrypt takes any Encrypt source. Built in: strings, bytes (Vec<u8>, &[u8], [u8; N]), the integers i8, i16, i32, i64, i128, u16, u32, u64 and u128, f32 and f64, bool, Option<T>, Vec<T>, HashMap<String, T> and BTreeMap<String, T>, Protected<T> (walked by reference inside the wrapper), and, behind features, chrono, rust_decimal and uuid scalars. The result is a CipherText tree mirroring the value’s shape: one sealed leaf per scalar, with the containers around them.

use std::collections::BTreeMap;
use vitaminc_aead::{encrypt, CipherText};

// A scalar seals as one leaf.
assert!(matches!(encrypt("secret", &cipher, ())?, CipherText::Single(_)));

// A sequence seals each element under the element derivation of the AAD.
assert!(matches!(encrypt(vec![1u32, 2], &cipher, ())?, CipherText::Sequence(items) if items.len() == 2));

// A map seals each value under its key, so keys cannot be swapped or renamed.
let mut map = BTreeMap::new();
map.insert("name".to_string(), "Ada".to_string());
assert!(matches!(encrypt(map, &cipher, ())?, CipherText::Map(entries) if entries[0].0 == "name"));

// An empty container is an authenticated marker; `None` is a null leaf. A
// `None` of a fixed-width type (an int, a float, a date, a timestamp, a
// decimal, a uuid) is a typed null, padded to the length of a `Some`, so the
// ciphertext's length does not show which values are absent.
assert!(matches!(encrypt(Vec::<u32>::new(), &cipher, ())?, CipherText::EmptySequence(_)));
assert!(matches!(encrypt(None::<u32>, &cipher, ())?, CipherText::Single(_)));

§Decrypting data

decrypt opens a ciphertext into any Decrypt target, named at the call site. A target names the sink that builds it, so the walk hands each opened leaf straight to the type: there is no intermediate tree, and a leaf that was wiped stays wiped. String, Vec<u8>, [u8; N], the same integer and float widths as above, bool, Option<T>, Vec<T>, HashMap<String, T>, BTreeMap<String, T> and Protected<T> are targets out of the box, and the derive below adds your own types. There is no u8 source or target on its own, because Vec<u8> is a bytes leaf and a Vec<T> of u8 scalars would overlap with it; seal a single byte as a u16 or a [u8; 1].

For a value whose shape is not known at compile time, decrypt_into takes any Sink: a FrameWriter to get the transport frame a host binding reads, or a sink of your own.

use vitaminc_aead::{decrypt, decrypt_into, encrypt, DecryptError, Frame, FrameWriter};

let ciphertext = encrypt(vec![Some(1u32), None], &cipher, "ctx")?;
let back: Vec<Option<u32>> = decrypt(&cipher, ciphertext, "ctx")?;
assert_eq!(back, [Some(1), None]);

// The same ciphertext, written as a frame instead of a Rust type.
let ciphertext = encrypt(vec![Some(1u32), None], &cipher, "ctx")?;
let mut frame = Vec::new();
decrypt_into(&cipher, ciphertext, "ctx", FrameWriter::new(&mut frame))?;
assert!(Frame::parse(&frame).is_ok());

// A target refuses a shape it cannot build, before anything is trusted.
let ciphertext = encrypt("text", &cipher, "ctx")?;
assert!(matches!(decrypt::<u32, _, _>(&cipher, ciphertext, "ctx"), Err(DecryptError::Sink(_))));

§Additional Authenticated Data (AAD)

Many types can be used as AAD through the IntoAad trait:

use vitaminc_aead::encrypt;

// String AAD
encrypt("my-secret", &cipher, "user_id:123")?;

// Byte slice AAD
encrypt("my-secret", &cipher, &b"metadata"[..])?;

// Integer AAD
encrypt("my-secret", &cipher, 42u64)?;

// Tuple AAD (PAE-encoded to prevent canonicalisation attacks)
encrypt("my-secret", &cipher, ("user_id", "session_token"))?;

// No AAD
encrypt("my-secret", &cipher, ())?;

Every one of those types is a context, and a context has one encoding. The AEAD’s associated data and a PRF’s derivation context for the same value are the same bytes, because both are views of the vitaminc-context crate: a context type implements IntoContext, which names its parts as a ContextPiece tree, and IntoAad is a blanket over it. There is no into_aad to write by hand, so a type of your own cannot be given one encoding for the AEAD and another for the PRF.

use vitaminc_aead::{ContextPiece, IntoAad, IntoContext};

struct TenantId(u64);

impl<'a> IntoContext<'a> for TenantId {
    fn into_context(self) -> ContextPiece<'a> {
        ("tenant", self.0).into_context()
    }
}

let piece = TenantId(7).into_context();
assert_eq!(piece.to_string(), "(\"tenant\", 7u64)");
assert_eq!(piece.leaves().count(), 2);
assert_eq!(
    TenantId(7).into_aad().as_bytes(),
    ("tenant", 7u64).into_aad().as_bytes()
);

The parts view is for a consumer that has to name what the bytes were built from (a key service logging the field a key was issued for, an audit trail) rather than parse framing back out of them. Display renders different trees differently (in Rust literal syntax), so two contexts that authenticate different bytes never share a log line.

A parts tree is the same context as the value it came from. A context that arrives as data, for example across an FFI boundary, needs no mirror type: a ContextPiece list of one is Some(x), the empty list is None, a list of two is (a, b), nonempty!(a).with(b).with(c) is the nested ((a, b), c), and ContextPiece::Unit is (). Every leaf is typed, so 7u32 and 7i32 are different contexts and so are "ab" and b"ab". ContextPiece also implements MaybeEmpty by the same rule the static types use, so a tree can be wrapped in NonEmpty.

ContextTag pairs a value with a tag that is folded into the AAD whenever it is encrypted, so a value that must always be sealed against its context (a user id, a column name) cannot be encrypted without it; ContextTag::context(tag).decrypt(..) mirrors it on the way out.

LeafTypeAad binds the column type a schema expects, its physical kind and logical type, into the AAD at both ends, so a ciphertext written under one column type fails to open under another. The binding is a declaration, not a check of the value, and it only works when both ends supply it.

§Working with Protected Types

The crate integrates with vitaminc-protected so sensitive plaintext stays wrapped. A Protected<T> is an Encrypt whenever &T is: it is walked by reference inside the wrapper, so the secret never leaves it on the way in. On the way out, Protected<T> is a Decrypt target that builds in place: scalars are read straight into the wrapper, and a Vec or an Option builds inside it from the first element, so a failure part way wipes what was already decoded.

use vitaminc_aead::{decrypt, encrypt};
use vitaminc_protected::Controlled;

let sensitive = Protected::new("password123".to_string());
let ciphertext = encrypt(sensitive, &cipher, ())?;
let back: Protected<String> = decrypt(&cipher, ciphertext, ())?;
assert_eq!(back.risky_unwrap(), "password123");

Because a derived newtype is transparent, #[derive(Encrypt)] struct Key(Protected<[u8; 32]>); gets exactly that path — no hand-written impl is needed to keep key material wrapped.

§Deriving Encrypt and Decrypt

A struct walks as a map keyed by field name, each value sealed under its key, and decodes strictly: an unknown key, a duplicate key or a missing field is refused. A fieldless enum is one int leaf holding the variant’s discriminant.

use vitaminc_aead::{decrypt, encrypt, CipherText, Decrypt, Encrypt};

#[derive(Debug, PartialEq, Encrypt, Decrypt)]
enum Role {
    Admin = 1,
    Member = 2,
}

#[derive(Debug, PartialEq, Encrypt, Decrypt)]
struct User {
    #[aead(passthrough)]
    id: u64,                 // stored in the clear, readable without the key
    #[aead(rename = "mail")]
    email: String,           // sealed under the wire key "mail"
    role: Role,
    #[aead(skip)]
    cached: Option<String>,  // not in the ciphertext at all; `Default` on decrypt
}

let user = User { id: 42, email: "ada@example.com".into(), role: Role::Admin, cached: Some("x".into()) };
let ciphertext = encrypt(user, &cipher, "users")?;
let CipherText::Map(entries) = &ciphertext else { panic!("a struct seals as a map") };
assert!(entries.iter().any(|(key, node)| key == "id" && matches!(node, CipherText::Passthrough(_))));

let back: User = decrypt(&cipher, ciphertext, "users")?;
assert_eq!(back, User { id: 42, email: "ada@example.com".into(), role: Role::Admin, cached: None });

The attributes are documented on the derives: rename, passthrough, skip, take (for a type that implements Drop, such as one deriving ZeroizeOnDrop) and crate. A field’s type must be at least as visible as the struct, since the generated sink names it.

§Custom types

A hand-written source implements Encrypt by walking itself into the sink, and a hand-written target implements Decrypt by naming a sink that builds it. Most types need neither; derive both. A source that is a thin view of something else, such as a frame or a host value, is where the trait is written by hand:

use vitaminc_aead::{MapSink, Encrypt, SeqSink, Sink};

/// A record whose fields are not all one type, so no `BTreeMap` fits.
struct Reading { sensor: String, values: Vec<f64>, ok: bool }

impl Encrypt for Reading {
    fn visit<S: Sink>(self, sink: S) -> Result<S::Ok, S::Error> {
        let mut map = sink.map(Some(3))?;
        self.sensor.visit(map.entry("sensor")?)?;
        self.values.visit(map.entry("values")?)?;
        self.ok.visit(map.entry("ok")?)?;
        map.end()
    }
}

let frame = vitaminc_aead::to_frame(Reading { sensor: "a".into(), values: vec![1.5], ok: true })?;
assert!(vitaminc_aead::Frame::parse(vitaminc_protected::Controlled::risky_ref(&frame)).is_ok());

§Passing fields through in the clear

Not every column of a table needs encrypting. A record usually has a few fields that other queries select, filter, or update without holding the key — a display name, a schema version, a plain id column — beside the ones that must be sealed. Mark such a field #[aead(passthrough)] (or call Sink::passthrough from a hand-written source) and it is stored as a cleartext entry of the same ciphertext container, so the record still round-trips as one unit while that field stays an ordinary column. The payload is a PassthroughFrame: the subtree as a transport frame, in every binding and in static Rust.

Passthrough provides no security guarantees whatsoever. The value is not encrypted and not authenticated — it, and for map entries its key, can be read, edited, added, or removed in storage and every encrypted field beside it still decrypts. Treat what comes back as untrusted input: non-sensitive, non-security-deciding data only, never a field the program then trusts for authorization, tenancy, access control, or for choosing which encrypted record to trust. A container whose children are all passthrough has nothing to authenticate it and is refused.

Anything secret-bearing belongs in Protected and gets encrypted; anything that must be tamper-evident but readable belongs in the AAD, not in a passthrough.

§The frame

Host bindings do not convert their values into a Rust value tree. Frame is a validated, borrowed transport frame that implements Encrypt by walking its own bytes in place, FrameWriter is a Sink that writes one, and to_frame walks any source into a frame. The Go binding encrypts from the frame it receives and decrypts into one; the Node binding snapshots a JavaScript value as a frame to encrypt it off the JavaScript thread.

§Index terms

Tee runs two sinks over one walk. vitaminc-normalize uses it to produce equality terms beside encryption, so a document is walked once and every term exists before any cipher is called.

§Nonce Generation

The crate provides nonce generation utilities for AEAD operations:

use vitaminc_aead::{NonceGenerator, RandomNonceGenerator};

// Create a random nonce generator for 12-byte nonces
let generator = RandomNonceGenerator::<12>::init()?;
let nonce = generator.generate()?;

§Security Considerations

  • Always use unique nonces for each encryption operation with the same key
  • Never reuse nonces with the same key, as this can compromise security
  • The Unspecified error type is used to prevent side-channel attacks by not revealing information about failures
  • When decrypting, always verify authentication before processing the plaintext: the walk opens every leaf before a typed target sees any of them
  • Map keys and passthrough subtrees travel in the clear; keys are bound into the AAD of their values, passthrough is not bound at all
  • A sequence’s order and length are not authenticated: each element is bound to its sequence, not to its index, so elements can be reordered or dropped without a cipher failure. A caller who needs them should put them in a map or in the AAD

§CipherStash

Vitamin C is brought to you by the team at CipherStash.

License: MIT

Modules§

frame
The transport frame: one value, as bytes, for crossing an FFI boundary and for carrying a passthrough subtree in the clear.
leaf
Scalars on the wire: in a sealed leaf, and in a transport frame.
plaintext
The plaintext data model: a source describes itself once (Encrypt) to a consumer (Sink), the way serde splits Serialize from Serializer.
seal
Sealing and opening leaves: the cipher traits, and the traversals that take a tree apart into leaves and put it back together.
shape
The rules a value’s shape must keep to be encrypted, for every sink that has to agree with encryption on which values it accepts.
tags
Leaf type tags: the cross-language wire commitment.
typed
Statically typed decryption as sinks.

Structs§

Bytes
A byte string borrowed from anywhere, for callers holding a slice whose element type would otherwise read as a sequence. Debug is redacted, as for every plaintext scalar.
CipherTextBuilder
Context
The canonical encoding of a context, as bytes.
ContextTag
A wrapper that pairs a plaintext value with a context tag used as additional authenticated data (AAD).
EncryptSink
The encrypting sink.
FixedType
A fixed-width scalar type, named without a value: what an absent value of that type stands in for.
Frame
Exactly one validated value node, borrowed.
FrameWriter
A sink that writes one value node onto the end of a buffer. Unknown container lengths are patched in at end.
LeafTypeAad
A context that binds an expected leaf type onto a base context.
LocalCipherText
A sealed leaf: version(1) ‖ nonce ‖ ciphertext ‖ tag.
Nonce
Represents a nonce used in AEAD encryption of N bytes length.
PassthroughFrame
An owned frame: the payload a ciphertext carries for a passthrough subtree. Non-sensitive by contract; neither sealed nor authenticated, and never wiped. Debug and == match Frame’s all the same, so a frame prints and compares one way whichever form it is in.
Pending
A leaf the walk has planned but nobody has sealed: its tagged plaintext and the AAD derived for its place in the tree.
RandomNonceGenerator
Raw
Any scalar, as given: a logical type and a physical value.
Refusal
A value refused for its shape: the rule it broke, and where.
SourceError
A failure reading the source: a host getter threw, a string had an unpaired surrogate, a big integer was out of range, a timestamp was a leap second.
Tee
A pair of sinks that forwards every call to both and pairs the results.
UnknownLogical
An id and parameter from the wire that this build has no meaning for.
UnknownLogicalId
A logical id above id::LAST_KNOWN: a logical type from a newer build, which this one has no meaning for and so cannot say what kind it is stored as. See LeafTypeAad::unknown.
Unspecified
The opaque error of a cipher: it reveals nothing about what failed.

Enums§

CipherText
The ciphertext tree encrypt produces and decrypt consumes.
ContextPiece
One part of a context, or a list of parts.
DecodeError
Why a typed target refused what it was given.
DecryptError
Why decryption failed.
EncryptError
Why encryption failed.
Float
A float, with the width it came from. Compared and carried by bit pattern, so NaN payloads and -0.0 survive, and -0.0 != 0.0.
Int
An integer, with the width it came from.
Kind
A physical kind, named without a value: what a schema expects a column to hold. Closed, like Physical, and with a wire byte of its own for the places a kind is bound rather than carried, such as LeafTypeAad.
KnownLogical
A logical type this build knows, named without its parameter: what a schema says a column holds. A Logical says “a decimal of scale 2” about one value; a KnownLogical says “decimals” about a column, whose values may each have a scale of their own.
Logical
What a physical value means. Carried per value, so a parameter such as a decimal’s scale can differ from one value to the next.
Physical
One scalar value, as stored. Borrowed, so a source can hand over text and bytes without copying.
TimeUnit
The unit of a Logical::Timestamp.
Unencryptable
Why a value cannot be encrypted as given.
Width
The physical type of a fixed-width scalar whose leaf has a payload: an int or a float, with its width, or sixteen bytes. What a FixedType rides on.

Constants§

WIRE_VERSION
First byte of every LocalCipherText — the wire-format version.

Traits§

AsyncCipher
Seal and open leaves through something that has to be awaited.
Cipher
Seal and open one leaf, synchronously.
Decrypt
A type that can be decrypted: it names the sink that builds it.
DecryptProtected
A type that can be decrypted straight into Protected<Self>: it names the sink that builds the wrapped value.
Encrypt
A value that can describe itself to a Sink.
FromScalar
A type that decodes from one scalar.
IntoAad
Types that can be used as the associated data of an AEAD call.
IntoContext
Types that describe themselves as a context.
MapSink
The sink for a map’s entries. Keys are strings and travel in the clear.
NonceGenerator
SeqSink
The sink for a sequence’s elements.
Sink
A consumer of one value’s description.

Functions§

decrypt
Decrypt into T.
decrypt_async
Decrypt into T through an async cipher. Gives the same result as decrypt for the same ciphertext.
decrypt_into
Decrypt into any sink.
decrypt_into_async
Decrypt through an async cipher. Every leaf is opened before the sink sees anything, so the sink itself is never awaited.
decrypt_to_frame
Decrypt into a frame: the Go guest’s whole decrypt.
encrypt
Encrypt any source.
encrypt_async
Encrypt any source through an async cipher. A cipher with a batch API seals the document in one round trip.
plan
Walk a source into a tree of pending leaves. No cipher is involved: this is the whole of the data-model work, and it is synchronous.
to_frame
Write any source as a frame. The buffer is Protected, since a frame of plaintext is plaintext, and it grows without leaving copies behind.
walk
Feed an opened tree to a sink.

Type Aliases§

AadDeprecated
The associated data a context is authenticated as. Kept as a name for the transition; it is Context.
AadPieceDeprecated
The parts view of a context. Kept as a name for the transition; it is ContextPiece.
Node
A ciphertext tree whose passthrough subtrees are frames.
Opened
A leaf after opening: its tagged plaintext.

Derive Macros§

Decrypt
Derive Decrypt for a struct or a fieldless enum: the sink that builds it. A struct reads a map keyed by field name; a fieldless enum reads an int leaf holding the variant’s discriminant. Takes the same #[aead(...)] attributes as Encrypt. Derive Decrypt for a struct or a fieldless enum: the sink that builds it. See the crate documentation for the wire shape this consumes; the attributes it accepts are reproduced below.
Encrypt
Derive Encrypt for a struct or a fieldless enum. A struct walks as a map keyed by field name; a fieldless enum as an int leaf holding the variant’s discriminant. Pair it with Decrypt to read the value back. Inlined so the #[aead(...)] reference renders here. Derive Encrypt for a struct or a fieldless enum. See the crate documentation for the wire shape this produces; the attributes it accepts are reproduced below.