redoubt-buffer

Crates.ioredoubt-buffer
lib.rsredoubt-buffer
version0.1.0-rc.3
created_at2025-11-27 04:33:55.055402+00
updated_at2026-01-21 07:10:19.190431+00
descriptionPage-aligned memory buffers with mlock and mprotect support
homepage
repositoryhttps://github.com/memparanoid/redoubt
max_upload_size
id1953052
size83,819
(memparanoid)

documentation

README

Redoubt

Systematic encryption-at-rest for in-memory sensitive data in Rust.

crates.io docs.rs coverage security license


Redoubt is a Rust library for storing secrets in memory. Encrypted at rest, zeroized on drop, accessible only when you need them.

Features

  • โœจ Zero boilerplate โ€” One macro, full protection
  • ๐Ÿ” Ephemeral decryption โ€” Secrets live encrypted, exist in plaintext only for the duration of access
  • ๐Ÿ”’ No surprises โ€” Allocation-free decryption with explicit zeroization on every path
  • ๐Ÿงน Automatic zeroization โ€” Memory is wiped when secrets go out of scope
  • โšก Amazingly fast โ€” Powered by AEGIS-128L encryption, bit-level encoding, and decrypt-only-what-you-need
  • ๐Ÿ›ก๏ธ OS-level protection โ€” Memory locking and protection against dumps
  • ๐ŸŽฏ Field-level access โ€” Decrypt only the field you need, not the entire struct
  • ๐Ÿ“ฆ no_std compatible โ€” Works in embedded and WASI environments

Installation

cargo add redoubt --features full

Or in your Cargo.toml:

[dependencies]
redoubt = { version = "0.1.0-rc.3", features = ["full"] }

Quick Start

use redoubt::alloc::{RedoubtArray, RedoubtString};
use redoubt::codec::RedoubtCodec;
use redoubt::secret::RedoubtSecret;
use redoubt::vault::cipherbox;
use redoubt::zero::RedoubtZero;

#[cipherbox(Wallet)]
#[derive(Default, RedoubtCodec, RedoubtZero)]
struct WalletData {
    seed: RedoubtArray<u8, 32>,
    mnemonic: RedoubtString,
    counter: RedoubtSecret<u64>,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut wallet = Wallet::new();

    // Open the box and modify secrets
    wallet.open_mut(|w| {
        w.seed.replace_from_mut_array(&mut [0u8; 32]);

        let mut mnemonic = String::from("abandon abandon ...");
        w.mnemonic.replace_from_mut_string(&mut mnemonic);

        w.counter.replace(&mut 0u64);

        Ok(())
    })?;
    // Box is re-encrypted here

    // Read-only access
    wallet.open(|w| {
        let _ = w.counter.as_ref();
        Ok(())
    })?;

    // Field-level access (decrypts only that field)
    wallet.open_counter_mut(|counter| {
        let mut next = *counter.as_ref() + 1;
        counter.replace(&mut next);

        Ok(())
    })?;

    // Leak to use outside closure scope
    {
        let seed = wallet.leak_seed()?;
        // use seed...
    } // seed is zeroized on drop

    Ok(())
}

API

open / open_mut

Access the entire decrypted struct. Re-encrypts when the closure returns:

wallet.open(|w| {
    // read-only access to all fields
    Ok(())
})?;

wallet.open_mut(|w| {
    // read-write access to all fields
    Ok(())
})?;

open_<field> / open_<field>_mut

Access individual fields without decrypting the entire struct:

wallet.open_seed(|seed| {
    // read-only access to seed
    Ok(())
})?;

wallet.open_seed_mut(|seed| {
    // read-write access to seed
    Ok(())
})?;

leak_<field>

Get a field value outside the closure. Returns a ZeroizingGuard that wipes memory on drop:

let seed = wallet.leak_seed()?;
// use seed...
// seed is zeroized when dropped

Returning values

Closures can return values. The return value is wrapped in a ZeroizingGuard that wipes memory on drop:

let counter = wallet.open_counter_mut(|c| {
    let mut next = *c.as_ref() + 1;
    c.replace(&mut next);

    Ok(next)
})?; // Returns Result<ZeroizingGuard<u64>, CipherBoxError>
// counter is zeroized when dropped

Types

Redoubt provides secure containers for different use cases:

use redoubt::alloc::{RedoubtArray, RedoubtString, RedoubtVec};
use redoubt::secret::RedoubtSecret;

// Fixed-size arrays (automatically zeroized on drop)
let mut api_key = RedoubtArray::<u8, 32>::new();
let mut signing_key = RedoubtArray::<u8, 64>::new();

// Dynamic collections (zeroized on realloc and drop)
let mut tokens = RedoubtVec::<u8>::new();
let mut password = RedoubtString::new();

// Primitives wrapped in Secret
let mut counter = RedoubtSecret::from(&mut 0u64);
let mut timestamp = RedoubtSecret::from(&mut 0i64);

When to use each type

  • RedoubtArray<T, N>: Fixed-size sensitive data (keys, hashes, seeds). Size known at compile time.
  • RedoubtVec<T>: Variable-length byte arrays that may grow (encrypted tokens, variable-size keys).
  • RedoubtString: Variable-length UTF-8 strings (passwords, mnemonics, API keys).
  • RedoubtSecret<T>: Primitive types (u64, i32, bool) that need protection. Prevents accidental copies via controlled access.

โš ๏ธ Critical: CipherBox fields MUST come from these types

All sensitive data in #[cipherbox] structs MUST ultimately come from: RedoubtArray, RedoubtVec, RedoubtString, or RedoubtSecret.

These types were forensically validated (see forensics/README.md) to leave no traces during the encryption-at-rest workflow. You can compose them into nested structures, but the leaf values containing sensitive data must be these types. Using standard types (Vec<u8>, String, [u8; 32], u64) would leave unzeroized copies during encoding/decoding, defeating the security guarantees.

How they prevent traces

RedoubtVec / RedoubtString: Pre-zeroize old allocation before reallocation

  • When capacity is exceeded, performs a safe 3-step reallocation:
    1. Copy data to temporary buffer
    2. Zeroize old allocation completely
    3. Allocate new buffer with 2x capacity and copy from temp (zeroizing temp)
  • Safe methods: extend_from_mut_slice, extend_from_mut_string (zeroize source)
  • ~40% performance penalty for guaranteed security (double allocation during growth)
  • Without this, standard Vec/String leave copies in abandoned allocations

RedoubtArray: Prevents copies during assignment

  • Simply redeclaring arrays (let arr2 = arr1;) can leave copies on stack
  • replace_from_mut_array uses ptr::swap_nonoverlapping to exchange contents without intermediate copies
  • Zeroizes the source after swap, ensuring no plaintext remains

RedoubtSecret: Prevents accidental dereferencing of Copy types

  • Forces explicit as_ref()/as_mut() calls to access the inner value
  • Critical for primitives like u64 which implement Copy and could silently duplicate if accessed directly

Protections

  • All types implement Debug with REDACTED output (no accidental leaks in logs)
  • No Copy or Clone traits (prevents unintended copies of sensitive data)
  • Automatic zeroization on drop

Security

  • Encryption at rest: Sensitive data uses AEAD encryption (AEGIS-128L)
  • Guaranteed zeroization: Memory is wiped using compiler barriers that prevent optimization
  • OS-level protections: On Linux, the master key lives in a memory page protected by prctl and mlock, inaccessible to non-root memory dumps
  • Field-level encryption: Decrypt only what you need, minimizing exposure time

Testing

CipherBox generates failure injection methods for testing error handling:

#[cipherbox(WalletBox, testing_feature = "test-utils")]
struct Wallet { /* ... */ }

// In tests:
let mut wallet = WalletBox::new();
wallet.set_failure_mode(WalletBoxFailureMode::FailOnNthOperation(2));

assert!(wallet.open(|_| Ok(())).is_ok());  // 1st succeeds
assert!(wallet.open(|_| Ok(())).is_err()); // 2nd fails
  • In the same crate, test utilities are always available under #[cfg(test)]
  • For external crates, use testing_feature to export them conditionally

See examples/wallet/tests for a complete example.

Platform support

Platform Protection level
Linux Full (prctl, rlimit, mlock, mprotect)
macOS Partial (mlock, mprotect)
Windows Encryption only
WASI Encryption only
no_std Encryption only

Project Insights

For detailed information about testing methodology and other interesting technical details, see INSIGHTS.md.

Benchmarks

To run benchmarks:

cargo bench -p benchmarks --bench aegis128l
cargo bench -p benchmarks --bench alloc
cargo bench -p benchmarks --bench cipherbox
cargo bench -p benchmarks --bench codec

License

This project is licensed under the GNU General Public License v3.0-only.

Commit count: 744

cargo fmt