secure-gate
Note: This is the LTS (Long-Term Support) branch for secure-gate 0.8.x (
release/0.8). It targets Rust Edition 2021 and MSRV 1.70, making it the right choice for projects that cannot yet move to Rust 1.85+. For the latest features see themainbranch (v0.9.x).
| Aspect | 0.8.x | 0.9.x |
|---|---|---|
| Edition | 2021 | 2024 |
| MSRV | 1.70 | 1.85 |
| Status | LTS / stable patches | Active development |
| Branch | release/0.8 |
main |
Current crates.io version: 0.8.0-rc.5 (see Cargo.toml for exact version).
no_std-compatible secret wrappers with explicit, auditable access and mandatory zeroization on drop.
Security Notice: This crate has not undergone independent audit. Review the code and SECURITY.md before production use. No unsafe code — enforced with
#![forbid(unsafe_code)].
What changed in 0.8.0
- Zeroize is now mandatory — memory wiping on drop is always enabled with no feature gate.
Fixed<T>requiresT: Zeroize;Dynamic<T>requiresT: ?Sized + Zeroize.- Removed the old optional
zeroizefeature and related toggles (insecure,secure, andstd). - Real
impl Dropnow callszeroize()on the inner value — the documented zeroization guarantee is fully enforced. - All previous versions (0.1.0–0.7.0-rc.15) were yanked from crates.io.
- Greatly expanded zeroization test suite with multi-size coverage, spare-capacity checks for both
VecandString, runtime heap verification viaProxyAllocator, and AddressSanitizer integration. ExposeSecret→RevealSecrettrait rename —ExposeSecret/ExposeSecretMutare nowRevealSecret/RevealSecretMut. Method names (expose_secret,with_secret, etc.) are unchanged; only code that names the trait explicitly needs updating.ct-eq-hashfeature removed —ConstantTimeEqExt,ct_eq_hash, andct_eq_autoare gone. Use thect-eqfeature and.ct_eq()instead.- Bech32 / Bech32m constructor API changed — Primary decode is now
try_from_bech32(s, hrp)(HRP-validated); unchecked single-arg form istry_from_bech32_unchecked(s)._expect_hrpvariants renamed to_with_hrp. ToHex::to_hex_leftremoved — The partial-reveal logging helper was removed; construct redacted output manually if needed.
What You Get
- Explicit access only —
.with_secret()(preferred) or.expose_secret()required; no silentDeref/AsRefleaks - Mandatory zeroize on drop — always active, no feature gate (inner type must implement
Zeroize) - Timing-safe equality —
ct-eqfeature for deterministic constant-time byte comparison (subtle) - Secure random generation —
from_random()(systemOsRng) andfrom_rng()for any caller-suppliedTryCryptoRng+TryRngCore(randfeature) - Orthogonal encoding — symmetric per-format traits + direct
try_from_*constructors onFixedandDynamic<Vec<u8>>(hex, base64url, bech32/BIP-173, bech32m/BIP-350); each format is opt-in and zero-overhead when unused - Serde — direct deserialization to inner types (binary-safe); opt-in serialization requires
SerializableSecretmarker - Ergonomic aliases —
dynamic_alias!,fixed_alias!,fixed_generic_alias!,dynamic_generic_alias!for typed newtypes - Auditable — every secret exposure point (including encoding methods) is grep-able using the consolidated pattern shown in the Encoding section;
no_std+alloccompatible
For zero-cost performance justification see ZERO_COST_WRAPPERS.md.
Quick Start
use ;
dynamic_alias!; // Dynamic<String>
fixed_alias!; // Fixed<[u8; 32]>
let mut pw: Password = "hunter2".into;
let key: Aes256Key = new;
// Scoped access — preferred; the borrow cannot outlive the closure
pw.with_secret;
// Mutable scoped access
pw.with_secret_mut;
// Direct reference — auditable escape hatch (e.g. FFI, third-party APIs)
assert_eq!;
pw.expose_secret_mut.clear;
Installation
Default (alloc enabled — Fixed<T> + Dynamic<T> + full zeroization):
[]
= "0.8.0-rc.{x}"
No-heap / embedded (Fixed<T> only — pure stack / no_std):
= { = "0.8.0-rc.{x}", = false }
Batteries-included:
= { = "0.8.0-rc.{x}", = ["full"] }
Features
| Feature | Description |
|---|---|
alloc (default) |
Heap-allocated Dynamic<T> + full zeroization of Vec/String spare capacity |
std |
Full std support (implies alloc). Use default-features = false for no-heap builds. |
rand |
from_random() (system OsRng) and fallible from_rng() for custom RNGs; no_std compatible for Fixed<T> (no heap required). Dynamic::from_random / from_rng require alloc (implicit — Dynamic<T> itself requires it). |
ct-eq |
ConstantTimeEq — timing-safe direct byte comparison (subtle) |
encoding |
Meta: all encoding sub-features (hex, base64url, bech32, bech32m); requires alloc |
encoding-hex |
ToHex / FromHexStr |
encoding-base64 |
ToBase64Url / FromBase64UrlStr |
encoding-bech32 |
ToBech32 / FromBech32Str — BIP-173 |
encoding-bech32m |
ToBech32m / FromBech32mStr — BIP-350 |
serde |
Meta: serde-deserialize + serde-serialize |
serde-deserialize |
Direct deserialization; Zeroizing-wrapped buffers; 1 MiB default limit (MAX_DESERIALIZE_BYTES); use deserialize_with_limit for custom ceilings |
serde-serialize |
Serialize secrets (requires SerializableSecret marker on inner type) |
cloneable |
CloneableSecret opt-in cloning |
secrecy-compat |
Drop-in compatibility shim for secrecy 0.8.x and 0.10.x — compat::v08 and compat::v10 modules with matching types, traits, and From conversions to native wrappers |
full |
All features combined |
no_std compatible. Fixed<T> with rand works heap-free. Dynamic<T>, encoding, and serde require alloc. Disabled features have zero overhead.
Core API
Fixed<T> (stack-allocated) and Dynamic<T> (heap-allocated, requires alloc) share the same RevealSecret / RevealSecretMut interface. Both types:
- Redact
Debugoutput to[REDACTED] - Implement
len()andis_empty()without exposing secret contents - Zeroize contents on drop (mandatory)
The preferred and recommended way to access secrets is the scoped with_secret / with_secret_mut methods. expose_secret / expose_secret_mut are escape hatches for rare cases and should be audited closely.
Preferred: scoped access
use ;
let mut key: = new;
// Read — closure borrow cannot outlive the call
let sum: u32 = key.with_secret;
// Mutate
key.with_secret_mut;
Direct reference — auditable escape hatch
// Use only when a long-lived reference is unavoidable (FFI, third-party APIs)
use ;
let key: = new;
let raw: & = key.expose_secret;
Macros for typed aliases
fixed_alias!, dynamic_alias!, fixed_generic_alias!, and dynamic_generic_alias! create typed newtype wrappers with full visibility control, optional doc strings, and compile-time zero-size guards:
use ;
fixed_alias!;
dynamic_alias!;
See [fixed_alias!], [dynamic_alias!], [fixed_generic_alias!], and [dynamic_generic_alias!] in the API docs.
Zero-size behavior note
fixed_alias!(Name, N) rejects N = 0 at compile time (via a const-eval index-out-of-bounds guard).
However, fixed_generic_alias!, dynamic_alias!, and dynamic_generic_alias! allow zero-sized types (SecretBuffer<0>, Dynamic<[u8; 0]>, Dynamic<()> etc.). These compile successfully but have no cryptographic value and should never be used in production. Always validate that the effective size is > 0 in your unit tests when using the generic or dynamic alias macros.
See also the Best Practices section in SECURITY.md for the equivalent guidance.
Polymorphic / generic code
use RevealSecret;
Encoding & Decoding
secure-gate provides symmetric, zero-overhead encoding and decoding for four formats: hex, base64url, bech32 (BIP-173), and bech32m (BIP-350). All operations are explicit and return Result on failure.
Available traits
| Format | Encode | Decode | Feature |
|---|---|---|---|
| Hex | ToHex |
FromHexStr |
encoding-hex |
| Base64URL | ToBase64Url |
FromBase64UrlStr |
encoding-base64 |
| Bech32 (BIP-173) | ToBech32 |
FromBech32Str |
encoding-bech32 |
| Bech32m (BIP-350) | ToBech32m |
FromBech32mStr |
encoding-bech32m |
Encoding (to string)
Use trait methods on the wrapper:
let key: = ...;
let hex = key.to_hex; // String
let b64 = key.to_base64url; // String
let bech32 = key.try_to_bech32?; // String with HRP
let bech32m = key.try_to_bech32m?; // String with HRP
Direct Constructors (Recommended)
Both Fixed<[u8; N]> and Dynamic<Vec<u8>> offer one-shot constructors from strings. These use panic-safe Zeroizing + pre-alloc swap internally.
| Format | Fixed<[u8; N]> |
Dynamic<Vec<u8>> |
Notes |
|---|---|---|---|
| Hex | Fixed::try_from_hex(s) |
Dynamic::try_from_hex(s) |
HexError |
| Base64URL | Fixed::try_from_base64url(s) |
Dynamic::try_from_base64url(s) |
Base64Error (unpadded, URL-safe) |
| Bech32 (BIP-173) | Fixed::try_from_bech32(s, hrp) |
Dynamic::try_from_bech32(s, hrp) |
HRP validated; Bech32Error::UnexpectedHrp |
| Bech32 (unchecked) | Fixed::try_from_bech32_unchecked(s) |
Dynamic::try_from_bech32_unchecked(s) |
No HRP; Bech32Error |
| Bech32m (BIP-350) | Fixed::try_from_bech32m(s, hrp) |
Dynamic::try_from_bech32m(s, hrp) |
HRP validated; Bech32Error::UnexpectedHrp |
| Bech32m (unchecked) | Fixed::try_from_bech32m_unchecked(s) |
Dynamic::try_from_bech32m_unchecked(s) |
No HRP; Bech32Error |
Security notes:
- Prefer HRP-validated constructors to prevent cross-protocol confusion attacks.
- Use
_uncheckedonly when HRP is validated upstream. - All constructors guarantee zeroization even on OOM panic via
Zeroizing.
Audit Surface (Secret Materialization)
Encoding and decoding methods are convenience wrappers that internally use scoped with_secret access — they do not bypass the security model, but return the fully materialized encoded value.
They exist because users who call them have already decided to reveal the secret — the wrapper reduces boilerplate and avoids long-lived raw references.
Audit every exposure point by searching your codebase for:
- Access:
expose_secret,expose_secret_mut,with_secret,with_secret_mut - Encode:
to_hex,to_base64url,try_to_bech32,try_to_bech32m - Decode:
try_from_hex,try_from_base64url,try_from_bech32*(including_unchecked)
Best practice: Prefer scoped methods (with_secret / with_secret_mut) when possible — they keep exposure minimal.
Serde
serde-deserialize decodes directly to the inner type. After deserialization completes, temporary buffers for Dynamic<Vec<u8>> and Dynamic<String> are Zeroizing-wrapped — oversized buffers are zeroized even on rejection. The default limit is MAX_DESERIALIZE_BYTES (1 MiB); call Dynamic::deserialize_with_limit to set a custom ceiling. Serialization requires the SerializableSecret marker trait.
Note:
MAX_DESERIALIZE_BYTES(anddeserialize_with_limit) is enforced after the upstream deserializer has fully materialized the payload. It is a result-length acceptance bound, not a pre-allocation DoS guard. For untrusted input, enforce size limits at the transport or parser layer upstream.
See [SerializableSecret] in the API docs for the full example.
Random Generation
from_random() uses the system RNG (OsRng), panics on failure, and is heap-free for Fixed<T> (no_std / no_alloc). from_rng fills from any TryCryptoRng + TryRngCore and returns Result (e.g. seeded StdRng in tests). Dynamic::from_random / from_rng require alloc (implicit — Dynamic<T> itself already requires it). See [Fixed::from_random], [Fixed::from_rng], [Dynamic::from_random], and [Dynamic::from_rng] in the API docs.
Security Model
- Explicit access only —
.with_secret()/.expose_secret()required; no silent leaks - Zeroize on drop — always active; inner type must implement
Zeroize - Timing-safe equality —
ct-eqfeature (.ct_eq()) - No unsafe code — enforced with
#![forbid(unsafe_code)]
Read SECURITY.md for the full threat model and mitigations.
Migrating from secrecy
Enable secrecy-compat and swap imports — your code compiles unchanged. Then replace compat types with native Dynamic<T> / Fixed<[T; N]> at your own pace using the provided From conversions.
= { = "0.8.0-rc.{x}", = ["secrecy-compat"] }
See MIGRATING_FROM_SECRECY.md for the full guide, including per-version import tables, type mappings, step-by-step instructions, and security notes for the transition period.
Contributing
MSRV & Lockfile
This crate enforces MSRV 1.70 (rust-version = "1.70" in Cargo.toml).
Important: Always use the MSRV toolchain to update Cargo.lock:
Do not use a newer toolchain (1.80+, nightly) to update the lockfile — it generates version 4 format, which Cargo 1.70 cannot read, breaking the MSRV CI job with:
lock file version `4` was found, but this version of Cargo does not understand this lock file
License
MIT OR Apache-2.0