Skip to main content

Crate vitaminc_normalize

Crate vitaminc_normalize 

Source
Expand description

§Vitamin C Normalize

Crates.io Workflow Status

Normalized equality terms for the plaintext data model of vitaminc-aead. Part of the Vitamin C framework.

An equality index derives a key from a value. Two values that are equal must derive the same key whichever width, scale or spelling wrote them, and two values that merely look alike (a date and the integer it is stored as, a uuid and the same sixteen bytes) must not. This crate decides both: each scalar kind that can be indexed has a frozen domain label and a normalization to bytes, and everything else has no term.

There is no cryptography here and no PAE: the crate’s own dependencies are the Unicode tables. (It builds on vitaminc-aead for the data model, which brings that crate’s dependencies with it.) The encodings are its own, including the decimal one, which is ported from the frozen orderable-bytes release and checked against it byte for byte. A PRF crate such as vitaminc-prf takes a Normalized and derives under its domain.

§One scalar

use vitaminc_normalize::{domain, normalize};
use vitaminc_aead::{Int, Logical, Physical};
use vitaminc_protected::Controlled;

// Every integer width is one term.
let a = normalize(None, Physical::Int(Int::I8(34)))?;
let b = normalize(None, Physical::Int(Int::U64(34)))?;
assert_eq!(a.domain, domain::INTEGER);
assert_eq!(a.bytes.risky_ref(), b.bytes.risky_ref());

// A date is not the integer it is stored as.
let day = normalize(Some(Logical::Date), Physical::Int(Int::I32(34)))?;
assert_eq!(day.domain, domain::DATE);
assert_ne!(day.bytes.risky_ref(), a.bytes.risky_ref());

Logical types opt in: a date, a timestamp, a decimal and a uuid have domains of their own. A null, a boolean, an undefined and a logical type this crate does not know have no term, and normalize refuses them with NormalizeError::NoDomain.

§A whole value, beside encryption

Terms is a sink that collects one Term per indexable scalar, with the path to it. Teed with the encrypting sink it sees the same walk, so the terms are complete before any cipher call:

use std::collections::BTreeMap;
use vitaminc_normalize::{domain, encrypt_with_terms, Step};

let mut user = BTreeMap::new();
user.insert("email", "ada@example.com");
user.insert("name", "Ada");

let (ciphertext, terms) = encrypt_with_terms(user, &cipher, "users/v1")?;
assert_eq!(terms.len(), 2);
assert_eq!(terms[0].path.steps(), [Step::Key("email".into())]);
assert_eq!(terms[0].normalized.domain, domain::TEXT);

encrypt_with_terms_async does the same through an async cipher and has every term before its first await. plan_with_terms walks without sealing at all, and terms collects the terms with no encryption in the walk.

terms refuses every value encryption refuses, for the same reason and at the same path, so an index never holds terms for a value that cannot be stored. It keeps the same shape rules, through the same code: no duplicate key, no value nested too deep, every scalar valid for its logical type, every passthrough framable, and something sealed in every container with children, even though it seals nothing itself. Read such a refusal with TermsError::refusal; encryption reports the same one as EncryptError::Shape.

A scalar whose kind has no domain (a null, a boolean, an undefined) produces no term and does not stop the walk; passthrough subtrees get none either. A scalar whose logical type has no domain, one from a newer client, stops the walk: encrypting it with no term and no error would leave an index silently incomplete. That refusal and an unassigned code point arrive as EncryptError::Source carrying a TermError (from terms, as TermsError::term), which holds the NormalizeError and the path to the refused field. Downcast to TermError; the NormalizeError is also its source(), so a reporter that walks the error chain reaches it. The message names neither the field nor the value, because map keys can be user data; read the path with TermError::path.

§Encodings

These are protocol data. Changing any of them, or a domain label, means a new label.

KindDomainBytes
integer, any widthvitaminc/prf/value/integer-orderable/v1sign byte (0x00 negative, 0x01 otherwise), then the big-endian 128-bit two’s-complement word
float, either widthvitaminc/prf/value/float-orderable/v1the f64 value’s orderable bytes; f32 widened exactly, zero signs and NaN payloads folded
datevitaminc/prf/value/date-orderable/v1orderable days from 0001-01-01
timestamp, any unitvitaminc/prf/value/timestamp-micros-orderable/v1orderable seconds, then nanoseconds truncated to microseconds
decimalvitaminc/prf/value/decimal-orderable/v1the 14-byte orderable encoding of the value (see decimal), scale and signed zero normalized
textvitaminc/prf/value/text-nfc/unicode-16/v1Unicode 16 NFC; a code point unassigned in Unicode 16 is refused
bytesvitaminc/prf/value/bytes/v1as they are
uuidvitaminc/prf/value/uuid/v1the sixteen raw bytes

Text normalization is variable-time. The integer and float paths use no floating-point arithmetic.

Modules§

domain
Frozen domain labels for equality terms, one per kind that has one.

Structs§

Equality
The equality normalizer: normalize.
Normalized
The normalized bytes for one equality term and the domain they belong to.
Path
The steps from the root of a value to one node in it, root first. A node at the root has no steps.
Term
One term: where the scalar sits in the value, and what the normalizer made of it. An equality term, a Normalized, unless Terms::by was given another normalizer.
TermError
A scalar Terms refused: why, and where in the value.
Terms
A sink that collects one Term per scalar with an equality domain, in walk order.
TermsError
What terms fails with: what Terms refuses, or the source’s own failure.
TermsMap
The map side of Terms: each entry’s path is the map’s plus its key.
TermsSeq
The sequence side of Terms: each element’s path is the sequence’s plus its index.

Enums§

NormalizeError
Why a scalar has no term. Never includes plaintext.
Step
One step from a container to one of its children.

Traits§

Normalize
How Terms turns one scalar into a term.

Functions§

bytes
Bytes, as they are.
date
A date’s day count since 1970-01-01, as the orderable bytes of its count of days from 0001-01-01 (the same bytes chrono’s NaiveDate orders by). None outside the date range.
decimal
A decimal’s mantissa and scale, as the 14-byte orderable encoding of its value, ported from the frozen orderable-bytes release and checked against it, which normalizes the scale and a signed zero. None if the mantissa does not fit 96 bits or the scale is above 28, the range rust_decimal represents.
domain_of
The opt-in table: the domain for a scalar, or None for one that has no term. A logical type decides on its own; a value without one goes by its kind. Neither looks at the value: normalize checks that.
encrypt_with_terms
Encrypt value and produce its equality terms in the same walk.
encrypt_with_terms_async
Encrypt value through an async cipher and produce its equality terms in the same walk. The walk, and so every term, is complete before the future is returned, so the future holds only the planned leaves and is Send even when the source is not.
float
A float of either width, as the orderable bytes of its f64 value with zero signs and NaN payloads folded.
integer
An integer of any width: a sign byte, then the big-endian 128-bit word.
normalize
Encode one scalar for equality, with the domain its term belongs to.
plan_with_terms
Walk value into the encrypting sink and the terms sink at once, without sealing: the planned tree of pending leaves, and every term.
terms
Every equality term of value, in walk order, without encrypting it.
terms_by
Every term of value under the normalizer by, in walk order, without encrypting it: terms for an index of another kind.
text
NFC in the pinned Unicode version, without case or accent folding.
timestamp
A count of units since the Unix epoch, as orderable seconds then big-endian nanoseconds truncated to microseconds (the same bytes chrono’s DateTime<Utc> orders by, truncated). None outside the timestamp range.
uuid
A uuid’s sixteen raw bytes. None for any other length.

Type Aliases§

WithTerms
A ciphertext tree and the terms of the value it was sealed from, in walk order.