Skip to main content

Crate vitaminc_encrypt

Crate vitaminc_encrypt 

Source
Expand description

§Vitamin C Encrypt

Crates.io Workflow Status

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-rs on native targets, RustCrypto’s aes-gcm on wasm32. Both produce byte-identical ciphertext, so a value sealed in one environment opens cleanly in the other.
  • wasm32-unknown-unknown support: 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-protected for 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 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 (enables vitaminc-aead’s hlist feature)

§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, and Option<T> where T: Encrypt
  • Vec<T>, HashMap<String, T> and BTreeMap<String, T> where T: Encrypt
  • Protected<T> where &T: Encrypt, walked without leaving the wrapper
  • chrono, rust_decimal and uuid scalars behind vitaminc-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.

TargetBackendNotes
cfg(not(target_arch = "wasm32"))AWS-LCFIPS-validated, AES-NI accelerated. Maintained by Amazon Web Services.
cfg(target_arch = "wasm32")RustCrypto aes-gcmPure 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

  1. Generate keys securely: Always use Key::random() with a cryptographically secure RNG
  2. Protect keys in memory: The Key type uses Protected internally to zeroize keys when dropped
  3. Use AAD for context: Include relevant context (user ID, record ID, etc.) as AAD to prevent ciphertext substitution
  4. Never reuse keys across environments: Use different keys for development, staging, and production
  5. Rotate keys regularly: Implement key rotation policies for long-lived systems
  6. 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§

Aes256Cipher
Implements AES-256-GCM. Backend is selected at compile time: aws-lc-rs on native targets, aes-gcm (RustCrypto) on wasm32.
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.
LocalCipherText
A sealed leaf: version(1) ‖ nonce ‖ ciphertext ‖ tag.
Nonce
Represents a nonce used in AEAD encryption of N bytes length.
OpensVersion1
An Aes256Cipher that also opens wire version 1 leaves, made by Aes256Cipher::opening_version_1.
Unspecified
The opaque error of a cipher: it reveals nothing about what failed.

Enums§

CipherText
The ciphertext tree encrypt produces and decrypt consumes.
DecodeError
Why a typed target refused what it was given.
DecryptError
Why decryption failed.
EncryptError
Why encryption failed.

Traits§

Cipher
The sink-based cipher trait Aes256Cipher implements, and the entry points that drive it. Seal and open one leaf, synchronously.
Decrypt
The derives for the data model: Encrypt for values going in, and Decrypt for 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: Encrypt for values going in, and Decrypt for typed values coming out. Inlined so the derives’ #[aead(...)] reference renders here too. A value that can describe itself to a Sink.
IntoAad
Types that can be used as the associated data of an AEAD call.
IntoContext
Types that describe themselves as a context.

Functions§

decrypt
Decrypt into T.
decrypt_into
Decrypt into any sink.
decrypt_with_key
Decrypt ciphertext under key with no associated data into T.
encrypt
Encrypt any source.
encrypt_with_key
Encrypt plaintext under key with no associated data.

Type Aliases§

AadDeprecated
The associated data a context is authenticated as. Kept as a name for the transition; it is Context.
AesCipherText
The ciphertext tree Aes256Cipher produces: 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: Encrypt for values going in, and Decrypt for typed values coming out. Inlined so the derives’ #[aead(...)] reference renders here too. Derive Decrypt for 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 as Encrypt. Derive Decrypt for 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: Encrypt for values going in, and Decrypt for typed values coming out. Inlined so the derives’ #[aead(...)] reference renders here too. Derive Encrypt for 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 with Decrypt to read the value back. Inlined so the #[aead(...)] reference renders here. Derive Encrypt for a struct or a fieldless enum. See the crate documentation for the wire shape this produces; the attributes it accepts are reproduced below.