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]. Access has two shapes: borrow it, or take it. with_secret() and expose_secret() lend you a reference while the wrapper keeps ownership and keeps wiping. into_inner() transfers ownership: you get the plain value and the protection ends with the call.

Those two shapes are what the 3-tier access model in SECURITY.md counts in threes: with_secret (Tier 1) and expose_secret (Tier 2) are both the borrow shape, differing only in how tightly the lifetime is pinned, and into_inner (Tier 3) is the take. Two shapes, three volumes — the tiers say how loudly you have to announce which one you picked, not how many protections there are.

Extraction is a hand-off, not a third layer of protection. Encoding output is the one case that keeps a wrapper — EncodedSecret does implement Deref, because its job is to keep an encoded copy of the secret wiped until it drops. Ordinary copies you make of the derefed value (.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_newtype, RevealSecret};

fixed_newtype!(pub Aes256Key, 32);

let key = Aes256Key::new([0xABu8; 32]);
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/
│   │   └── 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_newtype!, dynamic_newtype!
└── error                 ← FromSliceError, HexError, Base32Error, Base64Error, Bech32Error

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 wrapperEncodedSecretYes — yields &str; copies you make are yours, the buffer stays the wrapper’sHolds encoded output, wiped on drop
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. EncodedSecret is not a secret wrapper and does 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 and EncodedSecret::into_inner are the exits that transfer ownership: you typed a name, the call site is grep-able, and the wrapper is consumed. expose_secret is a named borrow — the wrapper still owns the secret and still wipes it — but it hands out a reference with no lifetime bound to a call site, so it is audited alongside them.

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

What EncodedSecret still guarantees: the String it owns is zeroized on drop, and its Debug prints [REDACTED]. What it does not guarantee: copies you make through Deref are ordinary values. str::to_string() and .to_owned() both produce untracked plaintext. That is what extraction is for — the crate is not trying to follow the bytes into your TLS stack. into_inner gives up even the first guarantee, by design: it is the named end of protection.

Two consequences worth knowing:

  • format!("{:?}", &*encoded) prints the encoded secret. Redaction lives on the wrapper, not on str; dereferencing first opts out of it.
  • EncodedSecret::into_zeroizing returns a Zeroizing<String>, 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) and ToBech32m / FromBech32mStr (BIP-350); _sized::<N> for a caller-chosen code length
encodingnoAll encoding features
Meta
cloneablenoCloneableSecret opt-in cloning
fullnoEverything except std

§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_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 — the MSRV 1.70 line, same API as main).

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. Encoders return EncodedSecret, which stays wiped until it drops; EncodedSecret::into_inner is the named call that ends that. 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::SentinelValue;
pub use traits::FixedStorage;
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::Case;
pub use traits::EncodableBytes;
pub use traits::Bech32Sized;
pub use traits::Bech32Standard;
pub use traits::Bech32mSized;
pub use traits::Bech32mStandard;
pub use traits::BECH32_CODE_LENGTH;
pub use traits::bech32_code_length;
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_newtype
Creates a distinct nominal type wrapping Dynamic<T>.
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.
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).