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:
Frameis a validated, borrowed frame.Frame::parsechecks the whole buffer up front, and the frame then implementsEncryptby 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.FrameWriteris aSinkthat writes frame bytes, andto_framewalks anyEncryptinto one. The Node binding snapshots a JavaScript value this way to encrypt it off the JavaScript thread.PassthroughFrameis 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_slicereads frame bytes into anyDeserializetype, andto_vecwrites anySerializetype as frame bytes. - storage
- The storage profile: a value as one versioned blob, for bytes that are
kept.
to_vecwrites anySerializetype as a blob andfrom_slicereads one back into anyDeserializetype. - 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 invitaminc-aeadnext to the leaf codec it shares.
Structs§
- Frame
- Exactly one validated value node, borrowed.
- Frame
Error - The frame: a value as transport bytes, and back. These live in
vitaminc-aeadnext 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. - Frame
Writer - A sink that writes one value node onto the end of a buffer. Unknown
container lengths are patched in at
end. - Passthrough
Frame - An owned frame: the payload a ciphertext carries for a passthrough
subtree. Non-sensitive by contract; neither sealed nor authenticated,
and never wiped.
Debugand==matchFrame’s all the same, so a frame prints and compares one way whichever form it is in.
Enums§
- Frame
Write Error - The frame: a value as transport bytes, and back. These live in
vitaminc-aeadnext 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-aeadnext 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,u32count, then the elements. - MAX_
DEPTH - The frame: a value as transport bytes, and back. These live in
vitaminc-aeadnext 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-aeadnext 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,u32count, thenu32key length, key bytes and value for each entry. - PASSTHROUGH
- The frame: a value as transport bytes, and back. These live in
vitaminc-aeadnext 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.