shrouded
It is my eternal curse, that each thing I learn sloughs off me.
— Adrian Tchaikovsky, Shroud
shrouded provides secure memory management in Rust for the paranoid. Built
with mlock, guard pages, automatic zeroization, and a healthy dose of
humility.
Overview
shrouded provides types for storing secrets in protected memory that is:
- Locked to RAM (
mlock/VirtualLock) to prevent swapping to disk.- Why? See the KeePass password dump (2023) and cold-boot attacks.
- Guard-paged to catch buffer overflows/underflows.
- Why? See Heartbleed (2014) and Cloudbleed (2017)
- Excluded from core dumps to avoid writes to disk.
- Why? See Storm-0558 (2023) and Bitcoin Core (2019).
- Automatically zeroized on drop using volatile writes and never logged.
- Why? See Facebook (2019) and Github (2018).
Design goals
secrecy-style ergonomics: Simple.expose()API to access protected datamemsec-level protection: Platform-specific memory protection with graceful degradation- Defense in depth: Multiple layers of protection (mlock + guard pages + zeroization)
- Explicit operations: No automatic
Clone,Display, orSerialize - Configurable policy: Choose strict, best-effort, or disabled memory protection per allocation
Types
| Type | Description |
|---|---|
ShroudedBytes |
Dynamic-size protected byte buffer |
ShroudedString |
UTF-8 string with protected storage |
ShroudedArray<N> |
Fixed-size protected array |
Shroud<T> |
Generic protected box for any Zeroize type |
ShroudedHasher<D> |
Hasher with protected internal state (requires digest feature) |
Usage
use ;
// Strings - original is consumed and zeroized
let password = Stringfrom;
let secret = new.unwrap;
assert_eq!;
// Bytes - source slice is zeroized
let mut key_data = vec!;
let key = from_slice.unwrap;
assert!; // Source zeroized
// Fixed arrays - initialized in protected memory
let nonce: = new_with.unwrap;
Threat model
What shrouded aims to protect against
- Swap attacks: Secrets locked to RAM cannot be swapped to disk
- Core dump leaks: Secrets excluded from core dumps on Linux
- Buffer overflows: Guard pages cause immediate crash on out-of-bounds access
- Memory remnants: Volatile zeroization prevents optimizer from eliding cleanup
- Accidental logging: Debug output shows
[REDACTED] - Accidental serialization: No
Serializeimpl (onlyDeserialize)
What shrouded does NOT protect against
- Root access: A privileged attacker can read process memory directly
- Memory snapshots: VM snapshots or hibernation may capture secrets
- Side channels: Timing attacks, speculative execution, etc.
- Heap remnants: For heap types like
Vec, consider usingShroudedBytesdirectly
Performance
shrouded prioritizes security over performance. Each allocation uses mmap
(not malloc) to obtain page-aligned memory for guard pages, making allocation
significantly slower than a normal heap allocation. With guard pages enabled, a
single-byte secret occupies at least 3 memory pages (~12KB on most systems).
This also affects mlock budgets. The kernel limits locked memory per process
(RLIMIT_MEMLOCK,
often 64–256KB by default), and guard pages inflate each allocation's footprint.
expose_guarded() adds two mprotect syscalls per access; expose() avoids
this at the cost of keeping memory readable between accesses.
We believe these costs are worthwhile in the typical case, handling a handful of API keys or passwords. If you're handling many secrets concurrently, though, or create them in a hot loop, the performance cost is real.
Features
| Feature | Default | Description |
|---|---|---|
mlock |
✓ | Enable memory locking |
guard-pages |
✓ | Enable guard pages |
serde |
✗ | Enable deserialize support |
digest |
✗ | Enable ShroudedHasher<D> for custom digest algorithms |
sha1 |
✗ | Enable ShroudedSha1 (includes digest) |
sha2 |
✗ | Enable ShroudedSha256, ShroudedSha384, ShroudedSha512 (includes digest) |
Platform support
| Platform | mlock | Guard Pages | Core Dump Exclusion |
|---|---|---|---|
| Linux | ✓ | ✓ | ✓ (MADV_DONTDUMP) |
| macOS | ✓ | ✓ | ✗ |
| Windows | ✓ | ✓ | ✗ |
| WASM/Other | ✗ | ✗ | ✗ |
On unsupported platforms, shrouded falls back to standard allocation with
zeroization on drop.
Comparison with similar crates
| Feature | shrouded | secrecy | memsec | secstr |
|---|---|---|---|---|
| Zeroize on drop | yes | yes | yes | yes |
mlock |
yes | no | yes | yes |
| Guard pages | yes | no | yes | no |
mprotect (re-lock after access) |
yes | no | no | no |
| Core dump exclusion | yes | no | no | yes |
| Expose-style API | .expose() |
.expose_secret() |
— | .unsecure() |
| Policy control | yes | no | no | no |
| Debug redaction | yes | yes | no | no |
No implicit Serialize |
yes | yes | N/A | N/A |
| Optional serde | deser only | ser + deser | no | ser + deser |
| Fixed-size array type | yes | no | yes | no |
| Windows support | yes | yes | no | yes |
Migrating from another crate? See the migration guide.
Usage notes
Some behaviors may be surprising if you're used to standard Rust types:
-
No
Clonetrait: Usetry_clone()explicitly to copy protected values. This returnsResultbecause each clone allocates new protected memory (with mlock). -
No
Serializetrait: OnlyDeserializeis implemented. To serialize, explicitly call.expose()and serialize the inner value. This prevents accidental serialization of secrets. -
expose()vsexpose_guarded()- why one is fallible:-
expose()is infallible because it returns a direct reference without changing memory permissions. Memory is allocated with read/write access by default. -
expose_guarded()returnsResultbecause it must callmprotect()to change permissions fromPROT_NONEto readable, then back toPROT_NONEwhen the guard is dropped. System calls can fail.
// Quick access (memory stays accessible) let value = password.expose; // Guarded access (memory locked except during access) let guard = password.expose_guarded?; do_something; // Memory automatically re-locked when guard is droppedUse
expose()for convenience; useexpose_guarded()for maximum security when you want memory inaccessible except during brief access windows. -
-
Constant-time comparison:
PartialEquses constant-time comparison to prevent timing attacks. Comparing twoShroudedStringvalues is safe. -
try_clone()allocates new protected memory: Each clone gets its own mlock'd region with guard pages. This is intentional for security but has performance implications. -
Source data is zeroized: When creating a
ShroudedStringfrom aString, the originalStringis zeroized. The data now lives only in protected memory.