Skip to main content

Crate vitaminc_aead_frame

Crate vitaminc_aead_frame 

Source
Expand description

§vitaminc-aead-frame

The transport for carrying Vitamin-C AEAD values and ciphertexts across an FFI boundary in one linear-memory copy: the frame for values, and the ciphertext codec for the trees encrypt produces.

A host binding does not convert its values into a Rust value tree. It implements Encrypt for values coming in and Sink for values going out (ADR 0001). The frame is the one binding-neutral shape of a value:

  • Frame is a validated, borrowed frame. Frame::parse checks the whole buffer up front, and the frame then implements Encrypt by walking its own bytes in place, so no value tree is built and no scalar leaves the buffer except to go straight to a sink.
  • FrameWriter is a Sink that writes frame bytes, and to_frame walks any Encrypt into one. The Node binding snapshots a JavaScript value this way to encrypt it off the JavaScript thread.
  • PassthroughFrame is an owned frame: the payload a ciphertext carries for a passthrough subtree, in every binding and in static Rust.

These live in vitaminc-aead, next to the leaf codec they share with the sealed leaf format, and are re-exported here.

The ciphertext codec, encode_ciphertext and decode_ciphertext, carries a CipherText tree whose passthrough payloads are frames. The Go guest uses it in both directions.

use vitaminc_aead::{CipherText, Node};
use vitaminc_aead_frame::{decode_ciphertext, encode_ciphertext};

let tree: Node<Vec<u8>> = CipherText::Single(vec![0xAB; 16]);
let mut bytes = Vec::new();
encode_ciphertext(&tree, &mut bytes).expect("encodes");

let back: Node<Vec<u8>> = decode_ciphertext(&bytes).expect("decodes");
let mut again = Vec::new();
encode_ciphertext(&back, &mut again).expect("encodes");
assert_eq!(again, bytes);

§Transport, and one storage profile

The frame, the ciphertext codec and the serde format in serde are transport: nothing about them is a compatibility commitment, since hosts and guests ship together. The one exception is the storage profile, storage::to_vec and storage::from_slice (feature serde), which write and read a value as a versioned blob for bytes that are kept. Its format is specified in STORAGE.md, frozen at version 1, and pinned by the golden corpus in testdata/storage-v1.json. Use it, and only it, for bytes that outlive the process that wrote them. The other frozen format in the stack is the sealed leaf payload ([tag] ++ payload, the tag table in tags), which lives inside the AEAD envelope and never touches this codec.

A value frame is one node: a scalar in its leaf encoding, with text and bytes carrying a u32 length, or a container. The framing tags 0xF0 (array), 0xF1 (object) and 0xF2 (passthrough) sit above the scalar tags, and 0xF3 is the envelope for a logical type with no shorthand tag. A ciphertext node is one byte of kind (CT_SINGLE, CT_SEQ, CT_MAP, CT_PASSTHROUGH, CT_EMPTY_SEQ, CT_EMPTY_MAP), then a sealed leaf as length and bytes, a container as count and children, or a passthrough as the frame of its subtree. All lengths and counts are u32 little-endian; map keys are UTF-8. The kind 0x02 was the CT_NONE absence marker, which ADR 0001 retires: a None is a null leaf now, so the marker is never written and refused when read.

§serde (feature serde)

vitaminc_aead_frame::serde is a serde data format over the frame: from_slice reads any Deserialize type from frame bytes and to_vec writes any Serialize type as frame bytes. It is for data a host exchanges with Rust that is not plaintext to seal, such as a call’s options or a result. Plaintext still crosses as a frame walked into a sink.

The mapping keeps what a frame carries: an integer reads at its exact width through deserialize_any, text and bytes are borrowed from the input, and the format is not human readable, so a type with a binary form (a UUID) uses it. Enums are externally tagged: a unit variant is its name, any other variant an object of one entry. A scalar with a logical type and a passthrough are refused, except JavaScript’s undefined, which reads as null. An error names the kind of value it found and the path to it, never the value, and drops a type’s own message (from a try_from check, say), which may print it. A type’s expecting text is kept, so a hand-written one must not include a value. The path names only the fields and variants a type declares; an entry whose key is data, such as a map’s, is placed by position.

§Storage profile (feature serde)

storage::to_vec writes any Serialize type as a blob: the header "VCS" and a version byte, then the value as a node. It differs from the transport format in four ways. A Some is marked (0xF5, then its value), so None, Some(()) and Some(None) are three blobs, and an option inside an untagged, internally tagged or flattened type survives serde’s buffering. A serde map is a node of its own (0xF6) whose keys are nodes, so an integer, text or bytes key keeps its kind. Only the plain scalars are admitted: no logical types, no undefined, no passthrough. And every refusal has a stable ErrorKind, so a caller can tell a reader that needs upgrading from a corrupt blob. The blob is plaintext, so it comes back Protected, and is wiped when dropped.

use vitaminc_aead_frame::storage;
use vitaminc_protected::Controlled;

let blob = storage::to_vec(&Some(Option::<u8>::None)).expect("writes");
assert_eq!(blob.risky_ref(), &[0x56, 0x43, 0x53, 0x01, 0xF5, 0x00]);
let back: Option<Option<u8>> = storage::from_slice(blob.risky_ref()).expect("reads");
assert_eq!(back, Some(None));

§Hardening

Both directions are attacker-reachable input. Nesting is bounded at MAX_DEPTH (128), counted across a ciphertext tree and the frame payload behind a passthrough as one structure. A count or length is checked against the bytes remaining before anything is reserved, and the eager part of a reservation is clamped so a megabyte of hostile bytes cannot pin gigabytes. Duplicate map keys are refused, as the frame refuses them. Errors are opaque (CodecError, FrameError): they carry no detail about the bytes.

§Tests

tests/conformance.rs runs the shared binding corpus, testdata/value-conformance.json, which Rust, Node and Go all read: every vector parses as one frame and writes back byte for byte, from its own walk and from a scalar built independently with std, chrono and rust_decimal, and every malformed vector is refused. tests/storage.rs reads and writes every row of the storage corpus, testdata/storage-v1.json, which describes each value in a model any language can build, refuses every malformed row with the error of its class, and reads every typed row as its type says. A second reader, written from STORAGE.md alone in tests/support/reference.rs, must agree with the crate on every row and on random and corrupted blobs. CI checks that no row the corpus had is changed or removed, with scripts/storage_corpus_append_only.py (mise run storage:corpus:check). tests/legacy_fixture.rs opens a ciphertext sealed before ADR 0001 on the new path and gets the frame it was sealed from, so the leaf format, the AAD derivations and the container rules stay what shipped.

Re-exports§

pub use transport::decode_ciphertext;
pub use transport::encode_ciphertext;
pub use transport::CodecError;

Modules§

serde
serde over the transport frame: from_slice reads frame bytes into any Deserialize type, and to_vec writes any Serialize type as frame bytes.
storage
The storage profile: a value as one versioned blob, for bytes that are kept. to_vec writes any Serialize type as a blob and from_slice reads one back into any Deserialize type.
tags
The leaf tag table, which lives with the leaf codec in vitaminc-aead. Leaf type tags: the cross-language wire commitment.
transport
Transport-only binary encoding for ciphertext trees crossing an FFI boundary (e.g. the Go/WASI binding’s wasm linear-memory copy). The value side of the transport is the frame (Frame, FrameWriter), which lives in vitaminc-aead next to the leaf codec it shares.

Structs§

Frame
Exactly one validated value node, borrowed.
FrameError
The frame: a value as transport bytes, and back. These live in vitaminc-aead next to the leaf codec they share, and are re-exported here so a binding that depends on this crate has the whole transport. The bytes are not a well-formed frame.
FrameWriter
A sink that writes one value node onto the end of a buffer. Unknown container lengths are patched in at end.
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.

Enums§

FrameWriteError
The frame: a value as transport bytes, and back. These live in vitaminc-aead next to the leaf codec they share, and are re-exported here so a binding that depends on this crate has the whole transport. Why a value could not be written as a frame.

Constants§

ARRAY
The frame: a value as transport bytes, and back. These live in vitaminc-aead next to the leaf codec they share, and are re-exported here so a binding that depends on this crate has the whole transport. Framing tag for a sequence: ARRAY, u32 count, then the elements.
MAX_DEPTH
The frame: a value as transport bytes, and back. These live in vitaminc-aead next to the leaf codec they share, and are re-exported here so a binding that depends on this crate has the whole transport. Maximum nesting depth of a frame. Bounds recursion so a deeply nested input cannot overflow the stack. The same limit, counted the same way, as the host bindings’, so any frame they accept parses here.
OBJECT
The frame: a value as transport bytes, and back. These live in vitaminc-aead next to the leaf codec they share, and are re-exported here so a binding that depends on this crate has the whole transport. Framing tag for a map: OBJECT, u32 count, then u32 key length, key bytes and value for each entry.
PASSTHROUGH
The frame: a value as transport bytes, and back. These live in vitaminc-aead next to the leaf codec they share, and are re-exported here so a binding that depends on this crate has the whole transport. Framing tag for a passthrough subtree: PASSTHROUGH, then one node that travels in the clear.

Functions§

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.