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:
[]
= "0.2.0-pre.1"
= "0.2.0-pre.1" # For key generation
= "0.2.0-pre.1" # For the Encrypt and Decrypt derives
The 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 ;
use ;
// Generate a key
let mut rng = from_entropy.expect;
let key = random.expect;
let cipher = new.expect;
// Encrypt a message under some associated data
let ciphertext = encrypt.expect;
// Decrypt it back, naming the type to decrypt into
let plaintext: String = decrypt.expect;
assert_eq!;
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 Key;
use ;
let mut rng = from_entropy.expect;
let key = random.expect;
Creating a Key from Bytes
use Key;
let key_bytes = ; // In practice, use securely generated bytes
let key = from;
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 ;
use ;
let key = random.expect;
let cipher = new.expect;
// Encrypt a string
let ciphertext = encrypt.expect;
// Encrypt a collection (element-wise; each element is sealed under its own position)
let data = vec!;
let ciphertext = encrypt.expect;
// Encrypt a fixed-size array, as one bytes leaf
let array = ;
let ciphertext = encrypt.expect;
Decrypting Data
[decrypt] opens a ciphertext into any type that implements [Decrypt], named at the call site:
use ;
let key = from;
let cipher = new.expect;
// Decrypt to String
let ciphertext = encrypt.expect;
let plaintext: String = decrypt.expect;
// Decrypt a collection
let ciphertext = encrypt.expect;
let strings: = decrypt.expect;
// Decrypt to a fixed-size array
let ciphertext = encrypt.expect;
let array: = decrypt.expect;
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 ;
let key = from;
let ciphertext = encrypt_with_key.expect;
let plaintext: String = decrypt_with_key.expect;
assert_eq!;
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 ;
let key = from;
let cipher = new.expect;
// Encrypt with context
let user_id = b"user_123".as_slice;
let ciphertext = encrypt.expect;
// Decrypt with the same context
let plaintext: String = decrypt.expect;
assert_eq!;
AAD Must Match
Decryption will fail at the cipher if the AAD doesn't match:
use ;
let key = from;
let cipher = new.expect;
// Encrypt with one context
let ciphertext = encrypt.expect;
// Try to decrypt with a different context - this will fail!
let result: = decrypt;
assert!;
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.
use ;
let aad: = empty;
let context: = aad;
assert!;
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 Protected;
use ;
let key = from;
let cipher = new.expect;
// Encrypt protected data
let sensitive = new;
let ciphertext = encrypt.expect;
// Decrypt back to protected data
let decrypted: = decrypt.expect;
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 ;
use ;
let mut rng = from_entropy.expect;
// Generate a key encryption key (KEK)
let kek = random.expect;
let cipher = new.expect;
// Generate a data encryption key (DEK)
let dek = random.expect;
// Wrap the DEK with the KEK
let wrapped_dek = dek.wrap.expect;
// Later, unwrap it straight back into a key and use it.
let dek = from_wrapped.expect;
let data_cipher = new.expect;
# let ct = encrypt.expect;
# let back: String = decrypt.expect;
# assert_eq!;
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 ;
let cipher = new.expect;
let user = User ;
let ciphertext = encrypt.expect;
let back: User = decrypt.expect;
assert_eq!;
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 ;
use ;
let key = random.expect;
match encrypt_with_key
CipherStash
Vitamin C is brought to you by the team at CipherStash.
License: MIT