secure-gate
Secure wrappers for secrets with explicit access and mandatory zeroization — a no_std-compatible, zero-overhead library with audit-friendly access patterns.
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)].
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;
Core Concepts
Fixed<T> (stack-allocated) and Dynamic<T> (heap, requires alloc) share the same access interface:
Debugoutput →[REDACTED].len()/.is_empty()without exposure- Zeroize on drop (always)
- Access via
.with_secret(|s| ...)(preferred) or.expose_secret()(auditable escape hatch)
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;
What You Get
- Zero-cost safety — mandatory zeroization on drop;
no_std/no_allocsupport. See ZERO_COST_WRAPPERS.md for benchmarks. - Audit-first API — secrets cannot leak via
Deref. Access requires explicitwith_secretscopes or an auditableexpose_secretescape hatch. - Type-safe wrappers — macros create newtype aliases that redact
Debugoutput automatically. - Batteries included — optional, zero-overhead support for serde, constant-time comparison (
subtle), and secure encoding (hex, base64url, bech32/m). - No unsafe code — enforced with
#![forbid(unsafe_code)].
Installation
Default (alloc enabled — Fixed<T> + Dynamic<T> + full zeroization):
[]
= "0.9.0-rc.{x}"
No-heap / embedded (Fixed<T> only — pure stack / no_std):
= { = "0.9.0-rc.{x}", = false }
Batteries-included:
= { = "0.9.0-rc.{x}", = ["full"] }
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: = ...;
// Direct on the wrapper (convenient; omit `with_secret` from audit greps)
let hex = key.to_hex;
let b64 = key.to_base64url;
let bech32 = key.try_to_bech32?;
let bech32m = key.try_to_bech32m?;
// Scoped on the inner bytes (preferred when you want `with_secret` in audit sweeps)
let hex_scoped = key.with_secret;
let b64_scoped = key.with_secret;
let bech32_scoped = key.with_secret?;
let bech32m_scoped = key.with_secret?;
Direct Constructors (Recommended)
Both Fixed<[u8; N]> and Dynamic<Vec<u8>> offer the same one-shot constructors from strings (call Fixed::… or Dynamic::… depending on which wrapper you need). These use panic-safe Zeroizing + pre-alloc swap internally.
| Format | Method (both wrappers) | Notes |
|---|---|---|
| Hex | try_from_hex(s) |
HexError |
| Base64URL | try_from_base64url(s) |
Base64Error (unpadded, URL-safe) |
| Bech32 (BIP-173) | try_from_bech32(s, hrp) |
HRP validated; Bech32Error::UnexpectedHrp |
| Bech32 (unchecked) | try_from_bech32_unchecked(s) |
No HRP; Bech32Error |
| Bech32m (BIP-350) | try_from_bech32m(s, hrp) |
HRP validated; Bech32Error::UnexpectedHrp |
| Bech32m (unchecked) | 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.
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 (SysRng), panics on failure, and is heap-free for Fixed<T> (no_std / no_alloc). from_rng fills from any TryCryptoRng + TryRng 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.
Audit Guide
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.
Read SECURITY.md for the full threat model and mitigations.
What changed in 0.9.0
Edition 2024, MSRV 1.85, rand 0.10 (OsRng → SysRng), dep bumps.
Full details in CHANGELOG.md. Users on Rust < 1.85: pin secure-gate = "0.8".
Branch support
Version 0.9.x (main) targets Rust Edition 2024 and MSRV 1.85.
For Rust < 1.85, pin secure-gate = "0.8" — the release/0.8 branch (Edition 2021, MSRV 1.75) receives security patches and important backports.
Current crates.io version: 0.9.0-rc.3 (see Cargo.toml for exact version).
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.
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.
Features
Common stacks: default (alloc), features = ["full"], or default-features = false for heap-free Fixed only.
| 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 SysRng) and fallible from_rng() for any TryRng + TryCryptoRng; 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.
Contributing
MSRV & Lockfile
This crate (main, 0.9.x) enforces MSRV 1.85 (rust-version = "1.85" in Cargo.toml). Rust 1.85 is the minimum that supports Rust edition 2024.
Always use the MSRV toolchain to update Cargo.lock:
License
MIT OR Apache-2.0