A small password-based encrypted vault for named secrets, with authenticated encryption, bounded parsing, and explicit memory-hygiene trade-offs.
Status:
memsealis an experimental0.xcrate and has not been independently audited.
Note: This crate is not a wrapper around Linux
mseal(2). "memseal" refers to sealing secrets in an encrypted in-memory vault.
memseal stores named secrets in an encrypted vault protected by a password.
It is designed for applications that need a small, self-contained encrypted vault that can be kept in memory, exported to bytes, or saved to disk.
It is not a replacement for OS keyrings, HSMs, cloud secret managers, or mature password managers.
use memseal::Vault;
let mut vault = Vault::create(b"my-password-here").unwrap();
// Store secrets
vault.store("api_key", b"sk-secret-12345").unwrap();
vault.store("db_url", b"postgres://user:pass@host/db").unwrap();
// Export to bytes
let bytes = vault.export().unwrap();
// Reopen with the same password
let vault = Vault::open(b"my-password-here", &bytes).unwrap();
let api_key = vault.retrieve("api_key").unwrap();
assert_eq!(api_key, Some(b"sk-secret-12345".to_vec()));use memseal::{Vault, VaultError};
use std::path::Path;
fn main() -> Result<(), VaultError> {
let mut vault = Vault::create(b"my-password-here")?;
vault.store("api_key", b"sk-secret-12345")?;
// Save to disk
vault.save(Path::new("secrets.seal"))?;
// Later: load and retrieve
let vault = Vault::load(Path::new("secrets.seal"), b"my-password-here")?;
let api_key = vault.retrieve("api_key")?;
assert_eq!(api_key, Some(b"sk-secret-12345".to_vec()));
Ok(())
}use memseal::{Vault, VaultError};
use std::path::Path;
fn main() -> Result<(), VaultError> {
let password = b"my-password-here";
// Create
let mut vault = Vault::create(password)?;
// Store, retrieve, remove
vault.store("name", b"secret")?;
let data = vault.retrieve("name")?; // Option<Vec<u8>>
assert_eq!(data, Some(b"secret".to_vec()));
let existed = vault.remove("name")?; // bool
assert!(existed);
// Change password
vault.change_password(password, b"new-password-here")?;
// Export to bytes, then reopen with the new password
let bytes = vault.export()?;
let reopened = Vault::open(b"new-password-here", &bytes)?;
assert_eq!(reopened.retrieve("name")?, None);
// File I/O
vault.save(Path::new("vault.seal"))?;
let loaded = Vault::load(Path::new("vault.seal"), b"new-password-here")?;
let _ = loaded;
Ok(())
}| Item | Limit |
|---|---|
| Password length | Minimum 8 bytes |
| Entry name | Maximum 255 bytes |
| Entry data | Maximum 64 MiB |
| Vault file | Maximum 256 MiB |
| Index entries | Maximum 1024 |
retrieve() returns decrypted data as Option<Vec<u8>>.
This is convenient, but it means the caller owns the returned plaintext and is responsible for handling it carefully.
In particular, caller code should avoid:
- logging returned secrets;
- cloning or converting them unnecessarily;
- keeping plaintext alive longer than needed;
- assuming returned plaintext is protected by
mlock.
Internal temporary plaintext and key material are zeroized where possible, but returned plaintext belongs to the caller.
If the caller wants drop-time zeroization, the returned Vec<u8> can be wrapped by the caller using zeroize::Zeroizing:
use memseal::{Vault, VaultError};
use zeroize::Zeroizing;
fn main() -> Result<(), VaultError> {
let mut vault = Vault::create(b"my-password-here")?;
vault.store("api_key", b"sk-secret-12345")?;
if let Some(secret) = vault.retrieve("api_key")? {
let secret = Zeroizing::new(secret);
// Use secret here.
assert_eq!(secret.as_slice(), b"sk-secret-12345");
// This allocation will be zeroized when `secret` is dropped.
}
Ok(())
}This only zeroizes that returned allocation on drop. It does not prevent accidental copies made by caller code or by the allocator/runtime.
- A small embedded vault for named secrets.
- Password-based: vault keys are derived from a caller-provided password.
- Self-contained: vaults can be exported to bytes or saved to disk.
- Authenticated: encrypted data is protected against tampering.
- Explicit about its limitations and memory-hygiene trade-offs.
- Not independently audited.
- Not an OS keyring wrapper.
- Not a cloud secret manager.
- Not an HSM.
- Not a password manager.
- Not a general secure-memory allocator.
- Not related to Linux
mseal(2).
- Embedded encrypted vaults - Store named secrets in an application-managed encrypted vault.
- Portable secret bundles - Export/load a password-protected vault without relying on an OS credential store.
- Credential caches - Keep secrets encrypted at rest in memory and on disk, while accepting explicit caller-owned plaintext boundaries.
- Application-managed secret storage - Store small sets of API keys, tokens, or credentials where a lightweight Rust-native vault is appropriate.
For the exact byte format, key derivation chain, nonce derivation, and AAD bindings that back these claims, see DESIGN.md.
| Threat | Mitigation |
|---|---|
| Tampered vault data | The vault header is authenticated as AAD for index decryption; the index JSON and every per-entry payload are encrypted and authenticated with XChaCha20-Poly1305. Bit flips in authenticated header fields, nonces, ciphertext, or Poly1305 tags are detected during open() or entry retrieval. |
| Entry swap attacks | Each entry's data and name ciphertexts share an AAD made of the entry's HMAC-derived key and its data_counter. Swapping either the encrypted data or the encrypted name across entries causes AEAD verification to fail. |
| Entry name leakage in serialized vaults | Entry names are not stored in plaintext. Index keys are derived with HMAC-SHA256. |
| KDF parameter downgrade | The vault header is authenticated as AAD, so tampering with persisted KDF parameters is detected. Header KDF fields are also bounded before they reach Argon2i: out-of-range values (for example, below-minimum memory cost or zero iterations) are rejected by validate_header() during open(), blocking forged headers that would make password guessing artificially cheap. |
| Nonce reuse | Nonces are derived deterministically via HKDF-SHA256 from monotonic counters. The index stream uses its own counter; entry data and entry name nonces use the entry data_counter with disjoint HKDF info prefixes for domain separation. Counter overflow at u64::MAX is a hard error. The index nonce is rotated on every export(). |
| Key reuse across roles | The Argon2i-derived master key is never used directly. HKDF-SHA256 derives two 32-byte subkeys with disjoint info strings: one for XChaCha20-Poly1305, one for HMAC-SHA256 entry-name hashing. The master key is zeroized as soon as both subkeys exist. |
| Plaintext lifetime inside the library | Internal temporary plaintext and key material are zeroized where possible, including error paths. |
| Resource exhaustion from crafted files | Vault file size, header length, KDF parameters, entry name length, entry data size, and the decoded index entry count are bounded before processing. open() rejects an index whose entry count exceeds the 1024 cap. |
| Swap exposure of ciphertext buffers | Internal ciphertext buffers in SecureMemoryVault are locked with mlock via memsec where supported. |
| Threat | Reason |
|---|---|
| Kernel-level or root attacker | A privileged attacker can read process memory regardless of user-space protections. |
| Debugger-based extraction | A debugger attached to the process can read decrypted data while it is being processed or after it has been returned by retrieve(). |
| Caller-owned plaintext leaks | retrieve() returns Vec<u8>. The caller is responsible for avoiding logs, copies, long-lived plaintext, and unsafe conversions. |
| Side-channel attacks | memseal does not attempt to mitigate Spectre, cache timing, power analysis, or other side channels. |
| Compromised dependencies | The crate trusts its dependency chain, including orion, memsec, and zeroize. |
| Denial of service | memseal detects corruption and refuses to open tampered files, but it cannot recover from them. An attacker with write or delete access to the vault file can deny access until a clean copy is restored. Backups and replication are the integrator's responsibility. |
| Full swap protection | Only internal ciphertext buffers are locked. Internal keys, nonces, allocator metadata, returned plaintext, and caller-owned copies are outside that guarantee. |
| Rollback protection | memseal does not provide rollback protection. An attacker who can replace a vault file with an older valid copy can cause the application to load older data unless the application stores freshness/version information externally. |
| Formal cryptographic assurance | The crate has not been independently audited. The integration layer should be reviewed before high-risk use. |
Password (>= 8 bytes)
|
Argon2i
128 MiB, 4 iterations
random 16-byte salt
|
Master Key (32B)
|
HKDF-SHA256
salt = KDF salt
/ \
enc_subkey hmac_subkey
(32B) (32B)
| |
| +--> HMAC-SHA256 entry-name hashing
|
+--> XChaCha20-Poly1305 encryption
Per-entry encryption:
nonce = HKDF(enc_subkey, counter, domain)
aad = hex(HMAC-SHA256(hmac_subkey, plaintext_name)) || data_counter (u64 LE)
ct = XChaCha20-Poly1305(enc_subkey, nonce, plaintext, aad)
Index encryption:
index nonce rotates on every export
vault header is authenticated as AAD
Note: mlock is applied only to internal ciphertext buffers inside SecureMemoryVault. It does not lock every secret-related allocation.
| Primitive | Implementation | Purpose |
|---|---|---|
| Argon2i | orion |
Password-based key derivation |
| HKDF-SHA256 | orion |
Subkey derivation and nonce derivation |
| XChaCha20-Poly1305 | orion |
Authenticated encryption |
| HMAC-SHA256 | orion |
Entry-name hashing |
| OsRng | rand_core |
Random salt generation |
mlock / munlock |
memsec |
Best-effort locking of internal ciphertext buffers |
| Zeroization | zeroize |
Clearing internal temporary secrets where possible |
- Small public API. The crate exposes
VaultandVaultError; internal modules are private. - Unsafe code is isolated. The crate uses
#![deny(unsafe_code)]at the crate root. The memory-locking module explicitly allows unsafe code formlock/munlockandunsafe impl Send/Sync. - Domain separation. Key derivation and nonce derivation use distinct domain labels.
- Authenticated encryption. Vault index data and entries are encrypted with AEAD.
- Bounded parsing. Untrusted vault data is checked against size and parameter bounds before processing.
- Atomic file writes.
save()writes to a temporary file, fsyncs it, renames it, and uses0600permissions on Unix. - Best-effort memory hygiene. Internal temporary key material and plaintext are zeroized where possible.
- Partial swap protection. Internal ciphertext buffers are locked with
mlockwhere supported, but this does not cover every allocation.
memseal is not a replacement for lower-level memory-hygiene crates such as zeroize, secrecy, or memsec.
It is also not an OS credential-store wrapper like keyring.
memseal is useful when an application wants a small, portable, self-contained encrypted vault for named secrets that it can export, save, and load directly.
cargo build
cargo test
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warningsRun benchmarks with:
cargo bench --bench full_benchGitHub Actions runs on every push and PR to main:
cargo check --all-targetscargo fmt --all -- --checkcargo clippy --all-targets -- -D warningscargo testrustsec/audit-checkaction
memseal currently targets the Rust stable toolchain and uses the Rust 2024 edition.
This crate has not been independently audited.
Please report security issues privately. See SECURITY.md.
MIT - see LICENSE.