Crate zeroize

source ·
Expand description

Securely zero memory with a simple trait (Zeroize) built on stable Rust primitives which guarantee the operation will not be “optimized away”.

About

Zeroing memory securely is hard - compilers optimize for performance, and in doing so they love to “optimize away” unnecessary zeroing calls. There are many documented “tricks” to attempt to avoid these optimizations and ensure that a zeroing routine is performed reliably.

This crate isn’t about tricks: it uses core::ptr::write_volatile and core::sync::atomic memory fences to provide easy-to-use, portable zeroing behavior which works on all of Rust’s core number types and slices thereof, implemented in pure Rust with no usage of FFI or assembly.

  • No insecure fallbacks!
  • No dependencies!
  • No FFI or inline assembly! WASM friendly (and tested)!
  • #![no_std] i.e. embedded-friendly!
  • No functionality besides securely zeroing memory!
  • (Optional) Custom derive support for zeroing complex structures

Minimum Supported Rust Version

Requires Rust 1.60 or newer.

In the future, we reserve the right to change MSRV (i.e. MSRV is out-of-scope for this crate’s SemVer guarantees), however when we do it will be accompanied by a minor version bump.

Usage

use zeroize::Zeroize;

fn main() {
    // Protip: don't embed secrets in your source code.
    // This is just an example.
    let mut secret = b"Air shield password: 1,2,3,4,5".to_vec();
    // [ ... ] open the air shield here

    // Now that we're done using the secret, zero it out.
    secret.zeroize();
}

The Zeroize trait is impl’d on all of Rust’s core scalar types including integers, floats, bool, and char.

Additionally, it’s implemented on slices and IterMuts of the above types.

When the alloc feature is enabled (which it is by default), it’s also impl’d for Vec<T> for the above types as well as String, where it provides [Vec::clear] / [String::clear]-like behavior (truncating to zero-length) but ensures the backing memory is securely zeroed with some caveats.

With the std feature enabled (which it is not by default), Zeroize is also implemented for [CString]. After calling zeroize() on a CString, its internal buffer will contain exactly one nul byte. The backing memory is zeroed by converting it to a Vec<u8> and back into a CString. (NOTE: see “Stack/Heap Zeroing Notes” for important Vec/String/CString details)

The DefaultIsZeroes marker trait can be impl’d on types which also impl Default, which implements Zeroize by overwriting a value with the default value.

Custom Derive Support

This crate has custom derive support for the Zeroize trait, gated under the zeroize crate’s zeroize_derive Cargo feature, which automatically calls zeroize() on all members of a struct or tuple struct.

Attributes supported for Zeroize:

On the item level:

  • #[zeroize(drop)]: deprecated use ZeroizeOnDrop instead
  • #[zeroize(bound = "T: MyTrait")]: this replaces any trait bounds inferred by zeroize

On the field level:

  • #[zeroize(skip)]: skips this field or variant when calling zeroize()

Attributes supported for ZeroizeOnDrop:

On the field level:

  • #[zeroize(skip)]: skips this field or variant when calling zeroize()

Example which derives Drop:

use zeroize::{Zeroize, ZeroizeOnDrop};

// This struct will be zeroized on drop
#[derive(Zeroize, ZeroizeOnDrop)]
struct MyStruct([u8; 32]);

Example which does not derive Drop (useful for e.g. Copy types)

#[cfg(feature = "zeroize_derive")]
use zeroize::Zeroize;

// This struct will *NOT* be zeroized on drop
#[derive(Copy, Clone, Zeroize)]
struct MyStruct([u8; 32]);

Example which only derives Drop:

use zeroize::ZeroizeOnDrop;

// This struct will be zeroized on drop
#[derive(ZeroizeOnDrop)]
struct MyStruct([u8; 32]);

Zeroizing<Z>: wrapper for zeroizing arbitrary values on drop

Zeroizing<Z: Zeroize> is a generic wrapper type that impls Deref and DerefMut, allowing access to an inner value of type Z, and also impls a Drop handler which calls zeroize() on its contents:

use zeroize::Zeroizing;

fn main() {
    let mut secret = Zeroizing::new([0u8; 5]);

    // Set the air shield password
    // Protip (again): don't embed secrets in your source code.
    secret.copy_from_slice(&[1, 2, 3, 4, 5]);
    assert_eq!(secret.as_ref(), &[1, 2, 3, 4, 5]);

    // The contents of `secret` will be automatically zeroized on drop
}

What guarantees does this crate provide?

This crate guarantees the following:

  1. The zeroing operation can’t be “optimized away” by the compiler.
  2. All subsequent reads to memory will see “zeroized” values.

LLVM’s volatile semantics ensure #1 is true.

Additionally, thanks to work by the Unsafe Code Guidelines Working Group, we can now fairly confidently say #2 is true as well. Previously there were worries that the approach used by this crate (mixing volatile and non-volatile accesses) was undefined behavior due to language contained in the documentation for write_volatile, however after some discussion these remarks have been removed and the specific usage pattern in this crate is considered to be well-defined.

Additionally this crate leverages core::sync::atomic::compiler_fence with the strictest ordering (Ordering::SeqCst) as a precaution to help ensure reads are not reordered before memory has been zeroed.

All of that said, there is still potential for microarchitectural attacks (ala Spectre/Meltdown) to leak “zeroized” secrets through covert channels. This crate makes no guarantees that zeroized values cannot be leaked through such channels, as they represent flaws in the underlying hardware.

Stack/Heap Zeroing Notes

This crate can be used to zero values from either the stack or the heap.

However, be aware several operations in Rust can unintentionally leave copies of data in memory. This includes but is not limited to:

  • Moves and Copy
  • Heap reallocation when using [Vec] and [String]
  • Borrowers of a reference making copies of the data

Pin can be leveraged in conjunction with this crate to ensure data kept on the stack isn’t moved.

The Zeroize impls for Vec, String and CString zeroize the entire capacity of their backing buffer, but cannot guarantee copies of the data were not previously made by buffer reallocation. It’s therefore important when attempting to zeroize such buffers to initialize them to the correct capacity, and take care to prevent subsequent reallocation.

The secrecy crate provides higher-level abstractions for eliminating usage patterns which can cause reallocations:

https://crates.io/crates/secrecy

What about: clearing registers, mlock, mprotect, etc?

This crate is focused on providing simple, unobtrusive support for reliably zeroing memory using the best approach possible on stable Rust.

Clearing registers is a difficult problem that can’t easily be solved by something like a crate, and requires either inline ASM or rustc support. See https://github.com/rust-lang/rust/issues/17046 for background on this particular problem.

Other memory protection mechanisms are interesting and useful, but often overkill (e.g. defending against RAM scraping or attackers with swap access). In as much as there may be merit to these approaches, there are also many other crates that already implement more sophisticated memory protections. Such protections are explicitly out-of-scope for this crate.

Zeroing memory is good cryptographic hygiene and this crate seeks to promote it in the most unobtrusive manner possible. This includes omitting complex unsafe memory protection systems and just trying to make the best memory zeroing crate available.

Structs

  • Zeroizing is a a wrapper for any Z: Zeroize type which implements a Drop handler which zeroizes dropped values.

Traits

Derive Macros