Expand description
§Vitamin C Protected
This crate is part of the Vitamin C framework to make cryptography code healthy.
§Safe wrappers for sensitive data
Protected is a set of types that remove some of the sharp edges of working with sensitive data in Rust.
Its interface is conceptually similar to Option or Result.
§Sensitive data footguns
Rust is a safe language, but it’s still possible to make mistakes when working with sensitive data. These can include (but are not limited to):
- Not zeroizing sensitive data when it’s no longer needed
- Accidentally leaking sensitive data in logs or error messages
- Performing comparison operations on sensitive data in a way that leaks timing information
- Serializing sensitive data in a way that leaks information
Protected and the other types in this crate aim to make it easier to avoid these mistakes.
§Usage
The Protected type is the most basic building block in this crate.
You can use it to wrap any type that you want to protect so long as it implements the Zeroize trait.
use vitaminc_protected::Protected;
let x = Protected::new([0u8; 32]);Protected will call zeroize on the inner value when it goes out of scope.
Because that wipe is a real Drop, Protected is never Copy, even when the
inner type is: unlike a move, a Copy value has no single owner whose drop can
wipe it. Duplicating the wrapper is an explicit clone(), and each clone is
wiped when it drops. See the Protected type docs for the full rationale.
risky_unwrap is the escape hatch: it hands back the plain inner value along
with the obligation to wipe it. Combinators such as map show the plain value
only to their closure and return the result re-wrapped.
It also provides an “opaque” implementation of the Debug trait so you can debug protected values
without accidentally leaking their innards.
use vitaminc_protected::{Controlled, Protected};
let x = Protected::new([0u8; 32]);
assert!(format!("{x:?}").contains("Protected<[u8; 32]>"));The inner value is not accessible directly, but you can use the risky_unwrap method as an escape hatch to get it back.
risky_unwrap is defined in the Controlled trait so you’ll need to bring that in scope.
use vitaminc_protected::{Controlled, Protected};
let x = Protected::new([0u8; 32]);
assert_eq!(x.risky_unwrap(), [0; 32]);Protected does not implement Deref so you cannot access the data directly.
This is to prevent accidental leakage of the inner value.
It also means comparisons (like PartialEq) are not implemented for Protected.
If you want to safely compare values, you can use Equatable.
§Equatable
The Equatable type is a wrapper around Protected that implements constant-time comparison.
It implements PartialEq for any inner type that implements ConstantTimeEq.
use vitaminc_protected::{Equatable, Protected};
let x: Equatable<Protected<u32>> = Equatable::new(100);
let y: Equatable<Protected<u32>> = Equatable::new(100);
assert_eq!(x, y);§Exportable
The Exportable type is a wrapper around Protected that supports safe serialization via the SafeSerialize and SafeDeserialize traits.
§Usage
The Usage type is a wrapper around Protected that allows you to specify a scope for the data.
This adapter is WIP.
§Working with wrapped values
None of the adapters implement Deref so you can’t access the inner value directly.
This is to prevent accidental leakage of the inner value by being explicit about when and how you want to work with the inner value.
You can map over the inner value to transform it, so long as the adapter is the same type.
For example, you can map a Protected<T> to a Protected<U>.
use vitaminc_protected::{Controlled, Protected};
// Calculate the sum of values in the array with the result as a `Protected`
let x: Protected<[u8; 4]> = Protected::new([1, 2, 3, 4]);
let result: Protected<u8> = x.map(|arr| arr.as_slice().iter().sum());
assert_eq!(result.risky_unwrap(), 10);If you have a pair of Protected values, you can zip them together with a function that combines them.
use vitaminc_protected::{Controlled, Protected};
let x: Protected<u8> = Protected::new(1);
let y: Protected<u8> = Protected::new(2);
let z: Protected<u8> = x.zip(y, |a, b| a + b);If the inner type is an Option you can call transpose to swap the Protected and the Option.
use vitaminc_protected::{Controlled, Protected};
let x = Protected::new(Some([0u8; 32]));
let y = x.transpose();
assert!(y.is_some());A Protected of Protected can be “flattened” into a single Protected.
let x = Protected::new(Protected::new([0u8; 32]));
let y = x.flatten();
assert_eq!(y.risky_unwrap(), [0u8; 32]);Use flatten_array to convert a [Protected<T>; N] into a Protected<[T; N]>.
§Protected digests
ProtectedDigest requires a fixed-output implementation that zeroizes its
internal state on drop. Enable the digest crate’s zeroization feature, such as
sha2 = { version = "0.11", features = ["zeroize"] }.
Unkeyed digests use new; keyed fixed-output functions implementing KeyInit
use new_with_key so the key remains in a Controlled container at the API
boundary.
Secret inputs use update and protected outputs use finalize_into. Public
protocol framing and intentionally exposed outputs cross separate, explicitly
named channels:
use sha2::Sha256;
use vitaminc_protected::{Controlled, Protected, ProtectedDigest};
let secret = Protected::new(*b"secret");
let mut digest = ProtectedDigest::<Sha256>::new();
digest.update_public(b"example/domain/v1");
digest.update(&secret);
let mut output = Protected::new([0u8; 32]);
digest.finalize_into(&mut output);
assert_ne!(output.risky_ref(), &[0u8; 32]);§Also in this crate
Beyond the adapters above, the crate exports TimingSafeEq and Choice (timing-safe comparison), OpaqueDebug and Redacted (leak-resistant Debug), ProtectedDigest, Zeroed, and AsProtectedRef — see the docs.rs API reference for details.
§Non-empty contexts
An AEAD associated-data value or PRF context can legitimately be empty, but a caller that uses one value to domain-separate fields needs it not to be. NonEmpty<T> carries that invariant in the type, checked once at construction: nonempty!("users/email") is checked at compile time (an empty literal does not compile), and NonEmpty::new(value) checks a dynamic value structurally — "", None, Some("") and ("", "") are all rejected, without parsing any encoding. An API that requires the invariant takes NonEmpty<T> directly; a bare &str argument cannot be value-checked at compile time, so there is deliberately no implicit conversion from a string or byte slice. Integers are never empty and convert with From, and a proven value extends with .with(tail) without a second check.
use vitaminc_protected::{nonempty, EmptyError, NonEmpty};
// Compile-time checked: nonempty!("") does not compile.
assert_eq!(nonempty!("users/email").get(), &"users/email");
// Runtime checked, once, for dynamic values.
assert!(NonEmpty::new(String::from("users/email")).is_ok());
assert_eq!(NonEmpty::new(("", None::<&str>)).unwrap_err(), EmptyError);§Locked storage for long-lived secrets
Protected wipes a value when it is dropped, which is the right guarantee for a value that lives for one call. Nothing drops a static, a leaked Arc, or anything at all when the process dies on SIGTERM, SIGKILL or process::exit, so a key that lives for the process needs its protection applied when it is allocated, not when it is dropped. On Unix, Locked<T> stores the value in memory obtained from the operating system that is locked against swapping (mlock), excluded from core dumps on Linux (MADV_DONTDUMP), fenced by guard pages, and never moved. It is wiped on drop like Protected, and while locked() is true the kernel will not page the bytes out to swap, nor on Linux include them in a core dump, even if the drop never happens (a hibernation image is written outside that mechanism and is not covered). On other targets the value lives on the ordinary heap, wiped on drop but neither locked nor fenced, and lock_error() reports Unavailable.
// Built in place: the key bytes are never on the stack.
let key: Locked<[u8; 32]> = Locked::generate(fill_from_csprng)?;
key.with(|k| assert_eq!(k[0], 7));
if !key.locked() {
// Best-effort by default: the value exists, and says why it is not locked.
eprintln!("key memory is not locked: {}", key.lock_error().unwrap());
}The operating system can refuse to lock memory, most often because RLIMIT_MEMLOCK (64 KiB by default on many Linux hosts) is too small; every value costs at least one page of it, whatever its size. A seccomp filter can refuse the core-dump exclusion. By default the value is created anyway and the refusal is readable from lock_error; call require_locked() on a value that must be locked, or set LockPolicy::Strict once at startup to make every constructor fail instead. Locked protects the bytes of T itself, so use it for inline types such as [u8; N]; a Vec<u8> inside it has only its header in the locked region.
§Generators
Protected supports generating new values from functions that return the inner value.
fn array_gen<const N: usize>() -> [u8; N] {
core::array::from_fn(|i| (i + 1) as u8)
}
let input: Protected<[u8; 8]> = Protected::generate(array_gen);You can also generate values from functions that return a Result with the inner value.
use std::string::FromUtf8Error;
let input: Result<Protected<String>, FromUtf8Error> = Protected::generate_ok(|| {
String::from_utf8(vec![1, 2, 3, 4, 5, 6, 7, 8])
});§CipherStash
Vitamin C is brought to you by the team at CipherStash.
License: MIT
Modules§
Macros§
- nonempty
- A
NonEmpty<&'static str>checked at compile time. - nonempty_
bytes - A
NonEmpty<&'static [u8]>checked at compile time.
Structs§
- Choice
- The
Choicestruct represents a choice for use in conditional assignment. - Default
Scope - Empty
Error - The error returned when a value that must carry caller-supplied data turned out to be empty.
- Equatable
- A controlled wrapper type that allows for constant time equality checks of a Controlled type. The immediate inner type must also be Controlled (typically Protected).
- Exportable
- Exportable is a wrapper type that allows for controlled types to be serialized and deserialized.
Serialization has a bias towards efficient byte representation and uses
serde_bytesfor byte arrays. - Invalid
Length - The error type returned when key and/or IV used in the
KeyInit,KeyIvInit, andInnerIvInitslice-based methods had an invalid length. - Lock
Policy Error - The process policy was already set to a different value.
- Locked
- A secret stored in memory that is locked against swapping, excluded from core dumps where the platform allows, fenced by guard pages, and wiped when dropped.
- NonEmpty
- A context value proven to carry caller-supplied bytes.
- Protected
- The most basic controlled type.
It ensures inner types are
Zeroizeand implementsDebugandDisplaysafely (i.e. inner sensitive values are redacted). - Protected
Digest - A fixed-output cryptographic state whose implementation wipes itself on drop.
- Protected
Ref - A wrapper around a reference to prevent inner access.
Conceptually similar to
&Tbut prevents direct access to the inner value outside of this crate. - Redacted
- Wrapper type for redacting debug output which implements
OpaqueDebugfor all types. - Usage
Enums§
- Lock
Error - Why a
Lockedvalue could not be created, or why its memory is not locked. - Lock
Policy - What a
Lockedconstructor does when the operating system refuses to lock the memory or, on Linux, to exclude it from core dumps.
Traits§
- Acceptable
- Marker trait for types that are acceptable in a certain scope.
- AsProtected
Ref - Trait for types that can be converted to a
ProtectedRef. Conceptually similar to theAsReftrait instdbut forProtectedtypes. This prevents the inner value from being accessed directly.ProtectedRefcannot be constructed outside this crate, so downstream implementations must delegate to an existing protected source rather than wrapping an arbitrary reference. - Constant
Time Eq - Controlled
- IsEmpty
- The name
MaybeEmptyhad before it was renamed: it read as a marker (“values of this type are empty”) when it is a capability (“this value can be asked whether it is empty”). Bounds and impls written againstIsEmptykeep compiling; switch toMaybeEmptyat your convenience. Structural emptiness of a value, decided before any encoding is applied. - Maybe
Empty - Structural emptiness of a value, decided before any encoding is applied.
- Opaque
Debug - Opaque
Debugfor secret-bearing types. - ReplaceT
- ReplaceT is a sealed trait that is used to replace the inner value of a type. It is only implemented for types that are Controlled.
- Safe
Deserialize - Safe
Serialize - Scope
- Marker trait for a type that defines a usage scope
- Timing
Safe Eq - This module defines the
TimingSafeEqtrait for performing equality checks in constant-time with respect to secret data, and theTimingSafeEqderive macro to automatically implement it for structs and enums. - Zeroed
- Similar to
Default, but doesn’t rely on the standard library, is only implemented for Paranoid types, and covers array sizes up to 1024.
Functions§
- flatten_
array - Convenience function to flatten an array of Protected into a Protected array.