Expand description
§Vitamin C Encrypt
Secure, flexible and fast encryption for Rust types.
This crate is part of the Vitamin C framework to make cryptography code healthy.
§Features
- Type-safe encryption: Encrypt and decrypt Rust types with compile-time safety
- AES-256-GCM: Hardware-accelerated authenticated encryption —
aws-lc-rson native targets, RustCrypto’saes-gcmonwasm32. Both produce byte-identical ciphertext, so a value sealed in one environment opens cleanly in the other. wasm32-unknown-unknownsupport: Builds and runs in browsers, Node.js, and edge runtimes (e.g. Supabase Edge Functions, Cloudflare Workers) with no C toolchain or feature flags- 256-bit keys only: Enforces quantum-resistant key sizes for future security
- Protected types integration: Works seamlessly with
vitaminc-protectedfor sensitive data handling - Additional Authenticated Data (AAD): Support for authenticated but unencrypted data
- Automatic nonce generation: Secure random nonce generation for each encryption operation
§Installation
Add this to your Cargo.toml:
[dependencies]
vitaminc-encrypt = "0.2.0-pre.1"
vitaminc-random = "0.2.0-pre.1" # For key generation
vitaminc-aead = "0.2.0-pre.1" # For the Encrypt and Decrypt derivesThe Encrypt and Decrypt derives are re-exported here, but the code they
generate names the vitaminc_aead crate, so a crate that derives them for its
own types must depend on vitaminc-aead too.
§Feature flags
hlist— static heterogeneous-list encryption support (enablesvitaminc-aead’shlistfeature)
§Quick Start
use vitaminc_encrypt::{decrypt, encrypt, Aes256Cipher, Key};
use vitaminc_random::{SafeRand, SeedableRng, Generatable};
// Generate a key
let mut rng = SafeRand::from_entropy().expect("Failed to seed RNG");
let key = Key::random(&mut rng).expect("Failed to generate key");
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
// Encrypt a message under some associated data
let ciphertext = encrypt("secret message", &cipher, "user:42").expect("Failed to encrypt");
// Decrypt it back, naming the type to decrypt into
let plaintext: String = decrypt(&cipher, ciphertext, "user:42").expect("Failed to decrypt");
assert_eq!(plaintext, "secret message");§How it fits together
Aes256Cipher is the sink-based Cipher of ADR 0001: it seals and
opens one tagged leaf under the AAD derived for the leaf’s place in the
value, and nothing else. Everything about the shape of a value — how a
sequence’s elements and a map’s entries are bound to their positions, how
an empty container is authenticated, the refusal of a container with
nothing sealed — lives once in vitaminc-aead’s encrypting sink and
opening walk, and is driven by encrypt and decrypt (re-exported
here). A value encrypted from static Rust therefore seals the same tagged
leaves a value from a host binding does, and every ciphertext is
self-describing.
The value going in is anything that implements Encrypt, and the
value coming out is anything that implements the sink-based Decrypt.
Both are derivable.
§Usage
§Key Management
The Key type represents a 256-bit encryption key. Vitamin C only supports 256-bit keys to ensure quantum security and consistent behaviour across both backends.
§Generating a Key
use vitaminc_encrypt::Key;
use vitaminc_random::{SafeRand, SeedableRng, Generatable};
let mut rng = SafeRand::from_entropy().expect("Failed to seed RNG");
let key = Key::random(&mut rng).expect("Failed to generate key");§Creating a Key from Bytes
use vitaminc_encrypt::Key;
let key_bytes = [0u8; 32]; // In practice, use securely generated bytes
let key = Key::from(key_bytes);§Encrypting Data
encrypt takes any Encrypt source. Built-in sources include:
String,&str,Vec<u8>,&[u8]and[u8; N]- every integer and float width,
bool, andOption<T>whereT: Encrypt Vec<T>,HashMap<String, T>andBTreeMap<String, T>whereT: EncryptProtected<T>where&T: Encrypt, walked without leaving the wrapperchrono,rust_decimalanduuidscalars behindvitaminc-aead’s features
use vitaminc_encrypt::{encrypt, Aes256Cipher, Key};
use vitaminc_random::{SafeRand, SeedableRng, Generatable};
let key = Key::random(&mut SafeRand::from_entropy().expect("Failed to seed RNG")).expect("Failed to generate key");
let cipher = Aes256Cipher::new(&key).expect("cipher creation failed");
// Encrypt a string
let ciphertext = encrypt("secret message", &cipher, ()).expect("encryption failed");
// Encrypt a collection (element-wise; each element is sealed under its own position)
let data = vec!["one".to_string(), "two".to_string()];
let ciphertext = encrypt(data, &cipher, ()).expect("encryption failed");
// Encrypt a fixed-size array, as one bytes leaf
let array = [0u8; 32];
let ciphertext = encrypt(array, &cipher, ()).expect("encryption failed");§Decrypting Data
decrypt opens a ciphertext into any type that implements Decrypt, named at the call site:
use vitaminc_encrypt::{decrypt, encrypt, Aes256Cipher, Key};
let key = Key::from([0u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("cipher creation failed");
// Decrypt to String
let ciphertext = encrypt("secret message", &cipher, ()).expect("encryption failed");
let plaintext: String = decrypt(&cipher, ciphertext, ()).expect("decryption failed");
// Decrypt a collection
let ciphertext = encrypt(vec!["one".to_string(), "two".to_string()], &cipher, ()).expect("encryption failed");
let strings: Vec<String> = decrypt(&cipher, ciphertext, ()).expect("decryption failed");
// Decrypt to a fixed-size array
let ciphertext = encrypt([0u8; 32], &cipher, ()).expect("encryption failed");
let array: [u8; 32] = decrypt(&cipher, ciphertext, ()).expect("decryption failed");A ciphertext that does not have the shape the type expects — a string where a
sequence was stored, a map missing a field — is refused as a
DecryptError rather than decoded wrongly.
§Upgrading from 0.5
The plaintext in every leaf now starts with a type tag, and the leaf is sealed
under wire version 2.
The Encrypt path of 0.5 sealed raw, untagged bytes under wire version 1,
so decrypt refuses every version 1 leaf with
DecryptError::Cipher rather than read the first byte of the value as a
tag. To keep data written by 0.5, decrypt it with 0.5 and encrypt it again
with this version.
Ciphertext from the 0.5 Value path (vitaminc-aead-value and the Go
binding) was already tagged, and opens through
Aes256Cipher::opening_version_1. Do not use that view for data from the
Encrypt path: it can read such a leaf as a different value.
§One-shot helpers
encrypt_with_key and decrypt_with_key build the cipher from a key
for a single call with no associated data:
use vitaminc_encrypt::{decrypt_with_key, encrypt_with_key, Key};
let key = Key::from([0u8; 32]);
let ciphertext = encrypt_with_key(&key, "secret message").expect("encryption failed");
let plaintext: String = decrypt_with_key(&key, ciphertext).expect("decryption failed");
assert_eq!(plaintext, "secret message");§Additional Authenticated Data (AAD)
AAD allows you to authenticate additional context alongside the ciphertext without encrypting it. This is useful for binding metadata to encrypted data.
The third argument of encrypt and decrypt is the AAD. Any IntoAad type works — byte slices, byte arrays, String, or () for none.
use vitaminc_encrypt::{decrypt, encrypt, Aes256Cipher, Key};
let key = Key::from([0u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("cipher creation failed");
// Encrypt with context
let user_id = b"user_123".as_slice();
let ciphertext = encrypt("secret message", &cipher, user_id).expect("encryption failed");
// Decrypt with the same context
let plaintext: String = decrypt(&cipher, ciphertext, user_id).expect("decryption failed");
assert_eq!(plaintext, "secret message");§AAD Must Match
Decryption will fail at the cipher if the AAD doesn’t match:
use vitaminc_encrypt::{decrypt, encrypt, Aes256Cipher, DecryptError, Key};
let key = Key::from([0u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("cipher creation failed");
// Encrypt with one context
let ciphertext = encrypt("secret", &cipher, b"context_1".as_slice()).expect("encryption failed");
// Try to decrypt with a different context - this will fail!
let result: Result<String, _> = decrypt(&cipher, ciphertext, b"context_2".as_slice());
assert!(matches!(result, Err(DecryptError::Cipher(_))));§Deprecated Aad alias
Aad remains available as a deprecated alias of Context during the
transition. Existing imports continue to compile; use Context in new code.
#![allow(deprecated)]
use vitaminc_encrypt::{Aad, Context};
let aad: Aad<'static> = Aad::empty();
let context: Context<'static> = aad;
assert!(context.is_empty());§Working with Protected Types
Vitamin C Encrypt integrates with vitaminc-protected to ensure sensitive data is handled securely. A Protected<T> is walked by reference inside the wrapper, so the secret never leaves it on the way in, and comes back wrapped on the way out:
use vitaminc_protected::Protected;
use vitaminc_encrypt::{decrypt, encrypt, Aes256Cipher, Key};
let key = Key::from([0u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("cipher creation failed");
// Encrypt protected data
let sensitive = Protected::new("password123".to_string());
let ciphertext = encrypt(sensitive, &cipher, ()).expect("encryption failed");
// Decrypt back to protected data
let decrypted: Protected<String> = decrypt(&cipher, ciphertext, ()).expect("decryption failed");§Encrypting Keys (Key Wrapping)
Keys can be encrypted with other keys, enabling key hierarchy and key wrapping. Key::wrap seals a key’s protected 32 bytes under a key encryption key, and Key::from_wrapped reads them straight back into a Key, so the bytes never leave the wrapper in either direction. A Key is not an Encrypt source, so encrypt does not take one: a walk over a value hands its bytes to a sink, and any crate can write a sink.
use vitaminc_encrypt::{decrypt, encrypt, Aes256Cipher, Key};
use vitaminc_random::{SafeRand, SeedableRng, Generatable};
let mut rng = SafeRand::from_entropy().expect("Failed to seed RNG");
// Generate a key encryption key (KEK)
let kek = Key::random(&mut rng).expect("key generation failed");
let cipher = Aes256Cipher::new(&kek).expect("cipher creation failed");
// Generate a data encryption key (DEK)
let dek = Key::random(&mut rng).expect("key generation failed");
// Wrap the DEK with the KEK
let wrapped_dek = dek.wrap(&cipher, "dek:1").expect("encryption failed");
// Later, unwrap it straight back into a key and use it.
let dek = Key::from_wrapped(&cipher, wrapped_dek, "dek:1").expect("decryption failed");
let data_cipher = Aes256Cipher::new(&dek).expect("cipher creation failed");§Your Own Types
Derive Encrypt for a type to encrypt it, and the sink-based Decrypt to decrypt into it. Fields become map entries, each sealed under its own key; #[aead(passthrough)] stores a field in the clear, #[aead(skip)] leaves one out, and #[aead(rename = "...")] changes its wire key. See the vitaminc-aead documentation for the attributes.
use vitaminc_encrypt::{decrypt, encrypt, Aes256Cipher, Decrypt, Key, Encrypt};
#[derive(Debug, PartialEq, Encrypt, Decrypt)]
struct User {
#[aead(passthrough)]
id: u64,
email: String,
}
let cipher = Aes256Cipher::new(&Key::from([0u8; 32])).expect("cipher creation failed");
let user = User { id: 42, email: "ada@example.com".into() };
let ciphertext = encrypt(User { id: 42, email: "ada@example.com".into() }, &cipher, "users").expect("encryption failed");
let back: User = decrypt(&cipher, ciphertext, "users").expect("decryption failed");
assert_eq!(back, user);§Security Features
§AES-256-GCM
This crate uses AES-256-GCM (Galois/Counter Mode) which provides:
- Confidentiality: Data is encrypted with AES-256
- Authenticity: Built-in authentication prevents tampering
- Performance: Hardware-accelerated on most modern CPUs
§Cryptographic Backends
The AES-256-GCM implementation is selected at compile time based on the target architecture — there is no feature flag to choose between them, and downstream code uses the same Aes256Cipher API regardless.
| Target | Backend | Notes |
|---|---|---|
cfg(not(target_arch = "wasm32")) | AWS-LC | FIPS-validated, AES-NI accelerated. Maintained by Amazon Web Services. |
cfg(target_arch = "wasm32") | RustCrypto aes-gcm | Pure Rust, no C toolchain. Required because aws-lc-rs’s C deps don’t cross-compile to wasm. |
Both backends conform to RFC 5116 AES-256-GCM and produce byte-identical ciphertext for the same inputs. CI gates this with a Known-Answer Test that runs against all three configurations on every PR — native aws-lc-rs, native RustCrypto, and RustCrypto compiled to wasm32-unknown-unknown and executed in Node via wasm-pack test --node.
This means ciphertext written from a native server can be opened in a browser or edge runtime (and vice versa) without any compatibility shim.
§256-bit Keys Only
Vitamin C enforces 256-bit keys to ensure:
- Resistance to quantum computer attacks (via AES-256)
- Consistent parameters across both the AWS-LC and RustCrypto backends
- No risk of accidentally using weaker key sizes
§Automatic Nonce Generation
Each encryption operation generates a unique random nonce, preventing nonce reuse which could compromise security. The nonce generator is a RefCell, so Aes256Cipher is !Sync: it is the synchronous cipher, used from one thread at a time.
§Unspecified Errors
The cipher reports a failure as Unspecified, which reveals no information: a wrong key, a wrong AAD and a tampered leaf are indistinguishable. The walk around it reports what it can say safely, such as a ciphertext whose shape does not match the type, through EncryptError and DecryptError.
§Best Practices
- Generate keys securely: Always use
Key::random()with a cryptographically secure RNG - Protect keys in memory: The
Keytype usesProtectedinternally to zeroize keys when dropped - Use AAD for context: Include relevant context (user ID, record ID, etc.) as AAD to prevent ciphertext substitution
- Never reuse keys across environments: Use different keys for development, staging, and production
- Rotate keys regularly: Implement key rotation policies for long-lived systems
- Store keys securely: Use key management systems (KMS) for production deployments
§Performance
Vitamin C Encrypt is designed for both security and performance:
- Zero-copy operations where possible
- Hardware acceleration via AWS-LC’s AES-NI support on native targets
- Minimal allocations during encryption/decryption
On wasm32-unknown-unknown the RustCrypto backend takes over and runs as pure Rust — slower than AES-NI but still constant-time and substantially faster than a JavaScript polyfill.
§Error Handling
The cipher’s own failures are the opaque Unspecified; the walk’s are EncryptError and DecryptError, which carry no plaintext.
use vitaminc_encrypt::{encrypt_with_key, EncryptError, Key, Unspecified};
use vitaminc_random::{SafeRand, SeedableRng, Generatable};
let key = Key::random(&mut SafeRand::from_entropy().expect("Failed to seed RNG")).expect("Failed to generate key");
match encrypt_with_key(&key, "message") {
Ok(ciphertext) => println!("Encrypted successfully"),
Err(EncryptError::Cipher(Unspecified)) => eprintln!("The cipher failed"),
Err(other) => eprintln!("The value could not be encrypted: {other}"),
}§CipherStash
Vitamin C is brought to you by the team at CipherStash.
License: MIT
Structs§
- Aes256
Cipher - Implements AES-256-GCM. Backend is selected at compile time:
aws-lc-rson native targets,aes-gcm(RustCrypto) onwasm32. - Context
- The canonical encoding of a context, as bytes.
- Key
- 256-bit key type for use with symmetric encryption algorithms like AES-256-GCM. Vitaminc does not support smaller key sizes to ensure quantum security and compatibility with AWS-LC.
- Local
Cipher Text - A sealed leaf:
version(1) ‖ nonce ‖ ciphertext ‖ tag. - Nonce
- Represents a nonce used in AEAD encryption of
Nbytes length. - Opens
Version1 - An
Aes256Cipherthat also opens wire version 1 leaves, made byAes256Cipher::opening_version_1. - Unspecified
- The opaque error of a cipher: it reveals nothing about what failed.
Enums§
- Cipher
Text - The ciphertext tree
encryptproduces anddecryptconsumes. - Decode
Error - Why a typed target refused what it was given.
- Decrypt
Error - Why decryption failed.
- Encrypt
Error - Why encryption failed.
Traits§
- Cipher
- The sink-based cipher trait
Aes256Cipherimplements, and the entry points that drive it. Seal and open one leaf, synchronously. - Decrypt
- The derives for the data model:
Encryptfor values going in, andDecryptfor typed values coming out. Inlined so the derives’#[aead(...)]reference renders here too. A type that can be decrypted: it names the sink that builds it. - Encrypt
- The derives for the data model:
Encryptfor values going in, andDecryptfor typed values coming out. Inlined so the derives’#[aead(...)]reference renders here too. A value that can describe itself to aSink. - IntoAad
- Types that can be used as the associated data of an AEAD call.
- Into
Context - Types that describe themselves as a context.
Functions§
- decrypt
- Decrypt into
T. - decrypt_
into - Decrypt into any sink.
- decrypt_
with_ key - Decrypt
ciphertextunderkeywith no associated data intoT. - encrypt
- Encrypt any source.
- encrypt_
with_ key - Encrypt
plaintextunderkeywith no associated data.
Type Aliases§
- Aad
Deprecated - The associated data a context is authenticated as. Kept as a name for
the transition; it is
Context. - AesCipher
Text - The ciphertext tree
Aes256Cipherproduces: one sealed leaf per scalar, with the shape of the value around them. - Node
- A ciphertext tree whose passthrough subtrees are frames.
Derive Macros§
- Decrypt
- The derives for the data model:
Encryptfor values going in, andDecryptfor typed values coming out. Inlined so the derives’#[aead(...)]reference renders here too. DeriveDecryptfor 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 asEncrypt. DeriveDecryptfor 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
- The derives for the data model:
Encryptfor values going in, andDecryptfor typed values coming out. Inlined so the derives’#[aead(...)]reference renders here too. DeriveEncryptfor 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 withDecryptto read the value back. Inlined so the#[aead(...)]reference renders here. DeriveEncryptfor a struct or a fieldless enum. See the crate documentation for the wire shape this produces; the attributes it accepts are reproduced below.