redoubt 0.1.0-rc.2

Systematic encryption-at-rest for in-memory sensitive data in Rust
Documentation
<picture>
    <p align="center">
    <source media="(prefers-color-scheme: dark)" width="320" srcset="/logo_light.png">
    <source media="(prefers-color-scheme: light)" width="320" srcset="/logo_light.png">
    <img alt="Redoubt" width="320" src="/logo_light.png">
    </p>
</picture>

<p align="center"><em>Systematic encryption-at-rest for in-memory sensitive data in Rust.</em></p>

<p align="center">
    <a href="https://crates.io/crates/redoubt"><img src="https://img.shields.io/crates/v/redoubt.svg" alt="crates.io"></a>
    <a href="https://docs.rs/redoubt"><img src="https://docs.rs/redoubt/badge.svg" alt="docs.rs"></a>
    <a href="#"><img src="https://img.shields.io/badge/coverage-98.40%25-49e500" alt="coverage"></a>
    <a href="#"><img src="https://img.shields.io/badge/vulnerabilities-0-brightgreen" alt="security"></a>
    <a href="#license"><img src="https://img.shields.io/badge/license-GPL--3.0--only-blue" alt="license"></a>
</p>

---

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
```bash
cargo add redoubt --features full
```

Or in your `Cargo.toml`:
```toml
[dependencies]
redoubt = { version = "0.1.0-rc.2", features = ["full"] }
```

## Quick Start

```rust
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:

```rust
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:

```rust
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:

```rust
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:

```rust
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:

```rust
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](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:

```rust
#[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](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](INSIGHTS.md).

## Benchmarks

To run benchmarks:

```bash
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](LICENSE).