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

§Feature flags

  • hlist — static heterogeneous-list encryption support (enables vitaminc-aead’s hlist feature)

§Quick Start

use vitaminc_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");

// Encrypt a message
let ciphertext = vitaminc_encrypt::encrypt(&key, "secret message").expect("Failed to encrypt");

// Decrypt it back via a cipher constructed from the same key
let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
let plaintext: String = cipher.decrypt(ciphertext).expect("Failed to decrypt");
assert_eq!(plaintext, "secret message");

§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

The encrypt function can encrypt any type that implements the Encrypt trait. Built-in support includes:

  • String
  • &str
  • [u8; N] (fixed-size byte arrays)
  • Vec<T> and Option<T> where T implements Encrypt
  • Protected<T> where T implements Encrypt
use vitaminc_encrypt::{encrypt, 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");

// Encrypt a string
let ciphertext = encrypt(&key, "secret message").expect("encryption failed");

// Encrypt a collection (element-wise; each element must implement `Encrypt`)
let data = vec!["one".to_string(), "two".to_string()];
let ciphertext = encrypt(&key, data).expect("encryption failed");

// Encrypt a fixed-size array
let array = [0u8; 32];
let ciphertext = encrypt(&key, array).expect("encryption failed");

§Decrypting Data

Decryption goes through Aes256Cipher: construct a cipher from the key, then call Aes256Cipher::decrypt, annotating the expected plaintext type:

use vitaminc_encrypt::{encrypt, Aes256Cipher, Key};

let key = Key::from([0u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("cipher creation failed");

// Decrypt to String
let ciphertext = encrypt(&key, "secret message").expect("encryption failed");
let plaintext: String = cipher.decrypt(ciphertext).expect("decryption failed");

// Decrypt a collection
let ciphertext = encrypt(&key, vec!["one".to_string(), "two".to_string()]).expect("encryption failed");
let strings: Vec<String> = cipher.decrypt(ciphertext).expect("decryption failed");

// Decrypt to fixed-size array
let ciphertext = encrypt(&key, [0u8; 32]).expect("encryption failed");
let array: [u8; 32] = cipher.decrypt(ciphertext).expect("decryption failed");

§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.

AAD is supplied on the encrypt side via the Encrypt trait’s encrypt_with_aad, and on the decrypt side via Aes256Cipher::decrypt_with_aad. Any IntoAad type works — byte slices, byte arrays, or String.

use vitaminc_encrypt::{Aes256Cipher, Encrypt, 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";
let ciphertext = "secret message".encrypt_with_aad(&cipher, user_id).expect("encryption failed");

// Decrypt with the same context
let plaintext: String = cipher.decrypt_with_aad(ciphertext, user_id).expect("decryption failed");
assert_eq!(plaintext, "secret message");
§AAD Must Match

Decryption will fail if the AAD doesn’t match:

use vitaminc_encrypt::{Aes256Cipher, Encrypt, Key};

let key = Key::from([0u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("cipher creation failed");

// Encrypt with one context
let ciphertext = "secret".encrypt_with_aad(&cipher, b"context_1").expect("encryption failed");

// Try to decrypt with a different context - this will fail!
let result: Result<String, _> = cipher.decrypt_with_aad(ciphertext, b"context_2");
assert!(result.is_err());
§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:

use vitaminc_protected::Protected;
use vitaminc_encrypt::{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(&key, sensitive).expect("encryption failed");

// Decrypt back to protected data
let decrypted: Protected<String> = cipher.decrypt(ciphertext).expect("decryption failed");

§Encrypting Keys (Key Wrapping)

Keys can be encrypted with other keys, enabling key hierarchy and key wrapping:

use vitaminc_encrypt::{encrypt, Aes256Cipher, Key};
use vitaminc_protected::Protected;
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");

// 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 = encrypt(&kek, dek).expect("encryption failed");

// Later, unwrap the DEK's protected key bytes.
// (`Key` implements `Encrypt` but not yet `Decrypt`, so decrypt to the
// protected byte array and rebuild the key from it.)
let cipher = Aes256Cipher::new(&kek).expect("cipher creation failed");
let unwrapped: Protected<[u8; 32]> = cipher.decrypt(wrapped_dek).expect("decryption failed");

§Convenience Functions vs Traits

This crate provides both convenience functions and traits:

Convenience API (recommended for most use cases):

Traits (defined in vitaminc-aead):

  • Encrypt (re-exported here) - plaintext.encrypt(&cipher) / plaintext.encrypt_with_aad(&cipher, aad); implement to make your types encryptable
  • Decrypt (visitor-style, in vitaminc-aead) - implement to make your types decryptable
  • Cipher - Implement to create custom cipher algorithms

Example using the traits directly:

use vitaminc_encrypt::{Aes256Cipher, Encrypt, Key};

let key = Key::from([0u8; 32]);
let cipher = Aes256Cipher::new(&key).expect("cipher creation failed");

let ciphertext = "secret".encrypt(&cipher).expect("encryption failed");
let plaintext: String = cipher.decrypt(ciphertext).expect("decryption failed");

§Custom Encryptable Types

You can implement Encrypt (and vitaminc_aead::Decrypt) for your own types to enable selective field encryption — for example, encrypting only the sensitive field of a struct while leaving the rest in plaintext. The Decrypt trait is visitor-style; see the vitaminc-aead documentation for the trait definitions and the built-in implementations to model your own on.

§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.

§Unspecified Errors

Encryption and decryption operations return an Unspecified error type that reveals no information about failures. This prevents side-channel attacks that could leak information through error messages.

§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

All encryption operations return Result<T, Unspecified> where Unspecified is an opaque error type that reveals no details about the failure. This is intentional to prevent side-channel attacks.

use vitaminc_encrypt::{Key, encrypt, 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(&key, "message") {
    Ok(ciphertext) => println!("Encrypted successfully"),
    Err(Unspecified) => eprintln!("Encryption failed"),
}

§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.
AesDecipher
A Decipher over a single AesCipherText, produced by Aes256Cipher::decipher. Carries the cipher and ciphertext; the AAD is supplied per call by Decrypt::decrypt_with_aad.
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.
Unspecified
An error that provides no information about the failure. It is crucial when returning an error from a cipher operation that does not reveal any details about the failure as this can lead to side channel attacks.

Traits§

Cipher
A driver of an encryption operation, analogous to serde’s Serializer.
Decrypt
The counterpart to Encrypt — a type that knows how to decrypt itself using a Decipher. Analogous to serde’s Deserialize.
Encrypt
A type that knows how to encrypt itself by driving a Cipher.
IntoAad
Types that can be used as the associated data of an AEAD call.
IntoContext
Types that describe themselves as a context.

Functions§

encrypt
Encrypt the given plaintext using the provided key. Any type that implements the Encrypt trait can be used.

Type Aliases§

AadDeprecated
The associated data a context is authenticated as. Kept as a name for the transition; it is Context.
AesCipherText
The recursive ciphertext container produced by Aes256Cipher — the shared CipherText tree instantiated with LocalCipherText leaves and BoxedPassthrough passthrough values.
BoxedPassthrough
The passthrough payload type used by Aes256Cipher: type-erased boxed values, downcast back to a concrete type at decrypt time via AesDecipher::decrypt_passthrough_as.

Derive Macros§

Decrypt
Derive Decrypt for a struct. See the crate documentation for the wire shape this consumes; the attributes it accepts are reproduced below.
Encrypt
Derive Encrypt for a struct. See the crate documentation for the wire shape this produces; the attributes it accepts are reproduced below.