vitaminc-encrypt 0.5.0

Secure, flexible and fast encryption for Rust types. Part of the Vitamin-C cryptographic suite.
Documentation

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):

  • [encrypt] - One-shot encrypt with no AAD
  • [Aes256Cipher::decrypt] - Decrypt with no AAD
  • [Aes256Cipher::decrypt_with_aad] - Decrypt with AAD

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.

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.

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