Skip to main content

Crate secure_gate

Crate secure_gate 

Source
Expand description

Secure wrappers for secrets with explicit access and mandatory zeroization — a no_std-compatible, zero-overhead library with audit-friendly access patterns.

Secrets are automatically zeroized on drop (the inner type must implement Zeroize). While a secret is held in Fixed or Dynamic, there is no Deref and no AsRef: callers reach the inner secret only via RevealSecret / RevealSecretMut, and Debug always prints [REDACTED]. All access follows a 3-tier model: scoped closures (preferred), direct references (escape hatch), and owned extraction (consumption).

Extraction is a hand-off, not a third layer of protection. The output wrappers it returns (InnerSecret<T>, EncodedSecret) do implement Deref — that is their purpose. They keep zeroize-on-drop for the buffer they own and keep Debug redacted; ordinary copies you make of the derefed value (*inner, .to_string()) are not tracked. See Where accident-prevention ends.

§Which type should I use?

TypeAllocationUse caseFeature
Fixed<T>StackKeys, nonces, tokens — compile-time-known sizeAlways available
Dynamic<T>HeapPasswords, API keys, ciphertexts — variable lengthalloc (default)

Both types share the same RevealSecret / RevealSecretMut access API.

§Quick start

use secure_gate::{Fixed, RevealSecret};

// Wrap a 32-byte key
let key = Fixed::new([0x42u8; 32]);

// Tier 1 — scoped access (preferred): secret ref cannot escape the closure
let first = key.with_secret(|bytes| bytes[0]);
assert_eq!(first, 0x42);

// Tier 2 — direct reference (escape hatch for FFI / third-party APIs)
assert_eq!(key.expose_secret().len(), 32);

// Debug is always redacted
assert_eq!(format!("{:?}", key), "[REDACTED]");
// key is zeroized when dropped
use secure_gate::{Dynamic, RevealSecret};

let password: Dynamic<String> = Dynamic::new(String::from("hunter2"));
let len = password.with_secret(|s: &String| s.len());
assert_eq!(len, 7);
use secure_gate::{fixed_alias, RevealSecret};

fixed_alias!(pub Aes256Key, 32);

let key: Aes256Key = [0xABu8; 32].into();
key.with_secret(|b| assert_eq!(b.len(), 32));

§Module structure

secure_gate (lib.rs)
├── Fixed<T>              ← always available, stack-allocated
├── Dynamic<T>            ← requires `alloc`, heap-allocated
├── traits/
│   ├── RevealSecret      ← immutable access (always available)
│   ├── RevealSecretMut   ← mutable access (always available)
│   ├── revealed_secrets/
│   │   ├── InnerSecret<T>    ← owned extraction wrapper
│   │   └── EncodedSecret     ← zeroizing encoded string wrapper (alloc)
│   ├── ConstantTimeEq    ← ct-eq feature
│   ├── CloneableSecret   ← cloneable feature
│   ├── SerializableSecret← serde-serialize feature
│   ├── encoding/         ← ToHex, ToBase32, ToBase64Url, ToBech32, ToBech32m
│   └── decoding/         ← FromHexStr, FromBase32Str, FromBase64UrlStr, FromBech32Str, FromBech32mStr
├── macros/               ← fixed_alias!, fixed_newtype!, dynamic_alias!, dynamic_newtype!, etc.
└── error                 ← FromSliceError, HexError, Base32Error, Base64Error, Bech32Error, DecodingError

All public items are re-exported at the crate root. Use secure_gate::Fixed, not secure_gate::fixed::Fixed.

§Type taxonomy

CategoryTypesDeref to secret?Purpose
Secret wrappersFixed<T>, Dynamic<T>No — use RevealSecretHold live secrets; Debug → [REDACTED]
Output wrappersInnerSecret<T>, EncodedSecretYes — caller owns the dataHold extracted or encoded results
Opt-in markersCloneableSecret, SerializableSecret— (no methods)Implement on inner type T to unlock gated impls

CloneableSecret and SerializableSecret are implemented on the inner type T, not on Fixed<T> or Dynamic<T> directly. Output wrappers (InnerSecret, EncodedSecret) are not secret wrappers and do not interact with these markers.

§Where accident-prevention ends

This crate carries two separate obligations, and they end in different places.

Accidents must not compile. This applies while the secret is held in Fixed/Dynamic, and it ends at the named extraction. into_inner, expose_secret, and to_*_zeroizing are the exits: you typed a name, the call site is grep-able, and ownership moves to you.

Documented behavior must be accurate. This never ends, and it is why the taxonomy table above says Deref: Yes for output wrappers.

What an output wrapper still guarantees: the buffer it owns is zeroized on drop, and its Debug prints [REDACTED]. What it does not guarantee: copies you make through Deref are ordinary values. *inner on a Copy type, str::to_string(), and .to_owned() all produce untracked plaintext. That is what extraction is for — the crate is not trying to follow the bytes into your TLS stack.

Two consequences worth knowing:

  • format!("{:?}", &*inner) prints the secret. Redaction lives on the wrapper, not on T; dereferencing first opts out of it.
  • into_zeroizing() returns zeroize::Zeroizing<T>, whose Debug is not redacted. It is a deliberate hand-off to a foreign type, not an equivalent wrapper.

§Import paths

// ✅ Correct — always import from the crate root
use secure_gate::{Fixed, RevealSecret};

// ❌ Wrong — these internal paths compile but are not the public API
// use secure_gate::traits::reveal_secret::RevealSecret;
// use secure_gate::traits::encoding::hex::ToHex;

§Method resolution: wrapper methods vs trait methods

Encoding methods exist at two levels — both produce identical results:

Call styleExampleAppears in audit sweep?
Wrapper trait impl (ergonomic)key.to_hex() (needs use secure_gate::ToHex)No — grep for to_hex directly
Trait via scoped access (audit-friendly)key.with_secret(|b| b.to_hex())Yes — with_secret is grep-able

Both levels are impls of the same trait (ToHex, ToBase32, ToBase64Url, ToBech32, ToBech32m): a blanket impl covers the raw bytes inside with_secret, and per-wrapper impls on Fixed<[u8; N]> / Dynamic<Vec<u8>> delegate through with_secret internally. One trait also means one bound: fn fingerprint<S: ToHex>(s: &S) accepts wrappers and forwarding newtypes alike.

§Feature flags

FeatureDefaultDescription
allocyesHeap types (Dynamic<T>), Vec/String zeroization
stdnoFull std support (implies alloc)
Cryptographic
ct-eqnoConstantTimeEq via subtle — timing-safe comparison
randnofrom_random() / from_rng() — no_std for Fixed
Serialization
serde-serializenoSerde Serialize (requires SerializableSecret marker)
serde-deserializenoSerde Deserialize with 1 MiB default limit
serdenoBoth directions
Encoding
encoding-hexnoToHex / FromHexStr via base16ct (constant-time)
encoding-base32noToBase32 / FromBase32Str via base32ct (constant-time)
encoding-base64noToBase64Url / FromBase64UrlStr via base64ct (constant-time)
encoding-bech32noToBech32 / FromBech32Str — BIP-173, extended ~5 KB limit
encoding-bech32mnoToBech32m / FromBech32mStr — BIP-350, standard 90-byte limit
encodingnoAll encoding features
Meta
cloneablenoCloneableSecret opt-in cloning
fullnoEverything

§What’s available without alloc?

With default-features = false:

Not available without alloc: Dynamic<T>, EncodedSecret, encoding traits (ToHex, etc.), decoding traits (FromHexStr, etc.), dynamic_alias!, dynamic_generic_alias!, dynamic_newtype!, serde support.

§no_std

no_std compatible (#![no_std] unless the std feature is enabled — verified in CI by cross-building for thumbv7em-none-eabihf). Fixed<T> works without alloc. Enable alloc (default) for Dynamic<T>. For pure stack / embedded builds, use default-features = false. MSRV: 1.70 (Rust edition 2021, LTS line).

One caveat: the rand feature compiles without std or alloc, but Fixed::from_random relies on getrandom, which needs a platform entropy backend. On bare-metal targets you must configure one (see the getrandom crate’s “custom backend” documentation); without it, builds enabling rand fail at link time on such targets. Fixed::from_rng with a caller-supplied RNG has no such requirement.

§Security

This crate has not undergone an independent security audit. No unsafe code — enforced with #![forbid(unsafe_code)]. Prefer scoped access (RevealSecret::with_secret) over direct references. Prefer zeroizing encoding variants (to_hex_zeroizing, etc.) when the encoded form is sensitive. See SECURITY.md for the full threat model.

§Inherent Rust Limitations

Three universal limits apply to any in-memory secret library in Rust (and in C, C++, and Go) — secure-gate documents them honestly rather than overclaiming:

  • Stack-move residue: moving a Fixed<T> by value leaves the prior stack slot uncleared until a later frame overwrites it. Prefer Fixed::new_with, pass by reference, or use Dynamic<T> for long-lived secrets.
  • Heap-reallocation residue: Vec / String realloc frees the old buffer unzeroed. Pre-size, use Dynamic<[u8; N]> (boxed array — no realloc surface), or install a zero-on-dealloc global allocator such as zeroizing-alloc at the binary level.
  • Swap / core dumps: process memory paged to disk or written on crash is outside any in-process library’s reach. Use OS facilities (mlock, encrypted swap, disabled core dumps).

Full discussion in SECURITY.md § Inherent Rust Limitations.

See the README and SECURITY.md for full details.

Re-exports§

pub use traits::CloneableSecret;
pub use traits::ConstantTimeEq;
pub use traits::RevealSecret;
pub use traits::SecretLen;
pub use traits::RevealSecretMut;
pub use traits::InnerSecret;
pub use traits::SentinelValue;
pub use traits::EncodedSecret;
pub use traits::SerializableSecret;
pub use traits::FromBase32Str;
pub use traits::FromBase64UrlStr;
pub use traits::FromBech32Str;
pub use traits::FromBech32mStr;
pub use traits::FromHexStr;
pub use traits::ToBase32;
pub use traits::ToBase64Url;
pub use traits::ToBech32;
pub use traits::ToBech32m;
pub use traits::ToHex;
pub use traits::SecureDecoding;
pub use traits::SecureEncoding;

Modules§

traits
Core traits for wrapper polymorphism - always available. Traits for polymorphic secret handling.

Macros§

dynamic_alias
Creates a type alias for Dynamic<T>.
dynamic_generic_alias
Creates a generic type alias Name<T> for Dynamic<T>.
dynamic_newtype
Creates a distinct nominal type wrapping Dynamic<T>.
fixed_alias
Creates a type alias for Fixed<[u8; N]>.
fixed_generic_alias
Creates a const-generic type alias Name<const N: usize> for Fixed<[u8; N]>.
fixed_newtype
Creates a distinct nominal type wrapping Fixed<[u8; N]>.

Structs§

Dynamic
Heap-allocated secret wrapper with explicit access and automatic zeroization on drop.
DynamicReader
Cursor-like reader over Dynamic<Vec<u8>> — see Dynamic::as_reader. Cursor-like reader over a Dynamic<Vec<u8>>.
Fixed
Stack-allocated secret wrapper with explicit access and automatic zeroization on drop.

Enums§

Base32Error
Errors from Base32 (RFC 4648 §6, uppercase, unpadded) decoding. Variant shapes are identical in debug and release builds; only numeric length metadata is carried. Errors produced when decoding Base32 (RFC 4648 §6, uppercase, unpadded) strings.
Base64Error
Errors from Base64url decoding. Variant shapes are identical in debug and release builds; only numeric length metadata is carried. Errors produced when decoding base64url strings.
Bech32Error
Errors from Bech32 (BIP-173) and Bech32m (BIP-350) decoding. Variant shapes are identical in debug and release builds; no secret material (payload bytes, HRP strings) is ever carried in an error. Errors produced when decoding Bech32 (BIP-173) or Bech32m (BIP-350) strings.
DecodingError
Unified error type wrapping format-specific decoding errors (HexError, Base32Error, Base64Error, Bech32Error). Always available; variants depend on enabled features. Unified error type for multi-format decoding operations.
FromSliceError
Error returned when a byte slice cannot be converted to Fixed<[u8; N]> due to length mismatch. Produced by Fixed::try_from(&[u8]). Error returned when a byte slice cannot be converted to a fixed-size array.
HexError
Errors from hex decoding. Variant shapes are identical in debug and release builds; only numeric length metadata is carried. Errors produced when decoding hexadecimal strings.

Constants§

MAX_DESERIALIZE_BYTES
Default maximum byte length for Dynamic<Vec<u8>> / Dynamic<String> deserialization (1 MiB).