Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
41f8492
perf(aead-frame): refuse a repeated key as the storage writer writes,…
coderdan Oct 27, 2026
0ad15fa
perf(protected): wipe sixteen bytes a store, so a growing frame or bl…
coderdan Oct 27, 2026
6c6880a
perf(protected)!: return frames and blobs in a Buffer, so the plainte…
coderdan Oct 27, 2026
13c2892
refactor(protected)!: make Buffer a Controlled type, so its bytes are…
coderdan Oct 27, 2026
1cabc0a
perf(aead-frame): build an entry's error path only when an error is p…
coderdan Oct 27, 2026
b9499f7
perf(aead-frame): tell a node's kind from its tag, so a text or bytes…
coderdan Oct 27, 2026
7d4e650
perf(aead): compare an object's keys one by one while there are few, …
coderdan Oct 27, 2026
615a035
fix(protected): unwind the wipe_raw proof 65 times, so Kani can finis…
coderdan Oct 28, 2026
e9f9fc8
test(protected): check that a Buffer's drop wipes it and that risky_i…
coderdan Oct 28, 2026
094bbe7
test(aead-frame): check a null, a boolean and a float read as a strin…
coderdan Oct 28, 2026
40dd628
docs(aead-frame): say the storage blob comes back in a Buffer, since …
coderdan Oct 28, 2026
04b3d12
docs(aead-frame): rewrap the Buffer return docs and name Buffer in th…
coderdan Oct 28, 2026
c8b9a9e
docs(aead-frame): state that the performance tables predate the Buffe…
coderdan Oct 28, 2026
e1bc397
refactor(protected): keep wipe_vec inside the crate, since only Buffe…
coderdan Oct 28, 2026
55745a3
refactor(protected): drop Buffer::clear and Buffer::capacity, since n…
coderdan Oct 28, 2026
fccc2fe
refactor(aead): seal the scalar writer's output trait and name it Byt…
coderdan Oct 28, 2026
eec32dc
docs(protected): limit Buffer's no-copy promise to its own methods, s…
coderdan Oct 28, 2026
e26d907
test(protected): check that Buffer wipes the old block when it grows,…
coderdan Oct 28, 2026
0fb1740
fix(aead): check in debug builds that a Vec leaf sink was sized with …
coderdan Oct 28, 2026
0ce4346
docs(aead-frame): say that the writer's wipes run on drop, so a panic…
coderdan Oct 28, 2026
6f983f9
perf(protected): tell the compiler reserve made room, so Vec's own re…
coderdan Oct 28, 2026
5e67c17
test(aead): pin the debug checks on a Vec leaf sink and document thei…
coderdan Nov 2, 2026
0b1a5fc
test(protected): check that Buffer::reserve panics when it cannot all…
coderdan Nov 2, 2026
efac5e4
refactor(aead): hide SeenKeys from the docs, since only vitaminc-aead…
coderdan Nov 2, 2026
bd45c10
refactor(aead): move SeenKeys into frame::__private, so its path says…
coderdan Nov 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ properties that are fine in flight and wrong at rest:
deserializer, and has its own validator, writer and error type. `to_vec`
returns the blob `Protected`, since it is the value's plaintext.

*Amended 2026-10-28 (#559):* `to_vec` now returns the blob in a
`vitaminc_protected::Buffer` rather than a `Protected<Vec<u8>>`. It is
still the value's plaintext and still read through `risky_ref` and
wiped when dropped; a `Buffer` also grows without leaving copies of its
bytes behind. The blob's bytes are unchanged.

2. **A header.** `"VCS"`, then a version byte, `0x01`. No header byte is a
node tag, so a transport frame never reads as a blob, nor a blob as a
frame. A version the reader does not read gets its own `ErrorKind`,
Expand Down
6 changes: 3 additions & 3 deletions packages/aead-frame/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ 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.
comes back in a `Buffer`: read through `risky_ref`, wiped when dropped.

```rust
# #[cfg(feature = "serde")]
Expand All @@ -111,7 +111,7 @@ 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]);
assert_eq!(blob.risky_ref().as_slice(), &[0x56, 0x43, 0x53, 0x01, 0xF5, 0x00]);
let back: Option<Option<u8>> = storage::from_slice(blob.risky_ref()).expect("reads");
assert_eq!(back, Some(None));
# }
Expand Down Expand Up @@ -176,7 +176,7 @@ What the numbers show:
- Reading text or bytes costs little more than copying them out: 64 KiB of bytes reads at about 32 GiB/s, as fast as MessagePack.
- On values with many small parts, the storage profile is the slowest of the five. It writes 1,000 records 6.7 times as slowly as MessagePack and 31 times as slowly as Borsh, and reads them 3.2 and 6.4 times as slowly. MessagePack carries the same field names, so the names are not the cost; each element or field costs the storage writer about 40 to 90 ns more.
- The storage profile is also the largest: 1,000 records take 57% more bytes than in MessagePack, and the map 89% more. Every length and count is a fixed four bytes and every value has a kind byte, where MessagePack packs small lengths and integers into one byte. Borsh is the smallest for records, but writes an integer at its full width, so the map of `u64` values is nearly as large as in the storage profile.
- A storage write costs more than a transport write on values with many parts, 26% more for 1,000 records and 43% more for the map, because it reads the blob back to find a repeated key before it returns it. Storage and transport reads are within about 12% of each other.
- The tables predate a change to the storage writer. It used to read the whole blob back before returning it, to find a repeated key, and that pass was 55% of a 1,000-record write. It now refuses a repeated field name as it writes, comparing the names an object has written on the stack, and checks a map's keys where they lie in the blob when the map ends. Measured against the tables' code on the same machine, that made a 1,000-record write 47% faster, a single record 41% faster and the map 28% faster, and left reads unchanged. The tables also predate later changes to the writer and the reader: the blob is now written into a `Buffer` that is wiped sixteen bytes at a time, and the reader tells a value's kind from its tag. They have not been measured again since.

## Cargo features

Expand Down
22 changes: 13 additions & 9 deletions packages/aead-frame/STORAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ The words MUST, MUST NOT and MAY are used as in RFC 2119.
A blob is the plaintext of a ciphertext that is kept. That asks more of a
format than a compact encoding, and the profile is built for it:

- **The plaintext is handled as a secret.** `to_vec` returns the blob
`Protected`. It grows its buffer into a fresh allocation and wipes the
old one, writes a `Display` value straight into the blob, keeps no copy
of a key, and wipes what it wrote if writing fails. The reader borrows
- **The plaintext is handled as a secret.** `to_vec` returns the blob in
a `Buffer`, which is read through `risky_ref` and wiped when it is
dropped. The writer grows its buffer into a fresh allocation and wipes
the old one, writes a `Display` value straight into the blob, keeps no
copy of a key, and wipes what it wrote if writing fails. The reader borrows
text and bytes from the blob rather than copying them. See
[Memory and timing](#memory-and-timing).
- **A refusal never repeats the plaintext.** Error messages are fixed text.
Expand Down Expand Up @@ -371,7 +372,7 @@ that, and never changes because the type did.

## The blob inside an outer envelope

A blob is plaintext, and `to_vec` returns it `Protected`. The caller seals
A blob is plaintext, and `to_vec` returns it in a `Buffer`. The caller seals
it whole, header included, under an envelope of its own (Stack Encrypt's
ciphertext). The header is therefore encrypted with the value: nothing
about the blob, its version included, can be seen before decryption, and
Expand All @@ -392,10 +393,13 @@ choice, and needs nothing from this profile.

The writer grows its buffer into a fresh allocation and wipes the old one,
formats a `Display` value straight into the blob, keeps no copy of a key,
and wipes everything if writing fails. The blob it returns is `Protected`,
and wiped when dropped. The reader borrows text and bytes from the blob
rather than copying them, and finds a repeated map key by comparing keys
where they lie in the blob. The writer frees no heap block that still
and wipes everything if writing fails. The wipes run when a `Buffer` is
dropped, so under `panic = "unwind"` a panic still wipes a partly written
blob, and under `panic = "abort"` nothing is wiped when the process aborts.
The blob it returns is a `Buffer`: its bytes are read through `risky_ref`,
and it is wiped when dropped. The reader borrows text and bytes from the
blob rather than copying them, and finds a repeated map key by comparing
keys where they lie in the blob. The writer frees no heap block that still
holds the value, and a zeroize audit checked in LLVM IR and assembly that
every wipe survives optimization.

Expand Down
7 changes: 3 additions & 4 deletions packages/aead-frame/benches/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@
//! of the bytes each format writes, so the rates compare work per byte and
//! the times compare work per value.
//!
//! A storage write reads the blob back before it returns it, to find a
//! repeated key as a reader would. A write and both `to_vec`s wipe their
//! buffers as they grow and when they are dropped. Both costs are part of
//! what is measured. MessagePack, Borsh and JSON wipe nothing.
//! A write and both `to_vec`s wipe their buffers as they grow and when
//! they are dropped. That cost is part of what is measured. MessagePack,
//! Borsh and JSON wipe nothing.
//!
//! Run with `cargo bench -p vitaminc-aead-frame --features serde`.

Expand Down
Loading
Loading