Skip to main content

secure_gate/
lib.rs

1// #![doc = include_str!("../README.md")] //uncomment for doctest runs
2
3// Forbid unsafe code unconditionally
4#![forbid(unsafe_code)]
5#![warn(missing_docs)]
6
7//! Secure wrappers for secrets with **explicit access** and **mandatory zeroization** — a
8//! `no_std`-compatible, zero-overhead library with audit-friendly access patterns.
9//!
10//! Secrets are **automatically zeroized on drop** (the inner type must implement
11//! [`Zeroize`](zeroize::Zeroize)). No `Deref`, no accidental leaks — callers access
12//! the inner secret only via [`RevealSecret`] / [`RevealSecretMut`]. `Debug` always
13//! prints `[REDACTED]`. All access follows a **3-tier model**: scoped closures (preferred),
14//! direct references (escape hatch), and owned extraction (consumption).
15//!
16//! # Which type should I use?
17//!
18//! | Type | Allocation | Use case | Feature |
19//! |------|-----------|----------|---------|
20//! | [`Fixed<T>`] | Stack | Keys, nonces, tokens — compile-time-known size | Always available |
21//! | [`Dynamic<T>`] | Heap | Passwords, API keys, ciphertexts — variable length | `alloc` (default) |
22//!
23//! Both types share the same [`RevealSecret`] / [`RevealSecretMut`] access API.
24//!
25//! # Quick start
26//!
27//! ```rust
28//! use secure_gate::{Fixed, RevealSecret};
29//!
30//! // Wrap a 32-byte key
31//! let key = Fixed::new([0x42u8; 32]);
32//!
33//! // Tier 1 — scoped access (preferred): secret ref cannot escape the closure
34//! let first = key.with_secret(|bytes| bytes[0]);
35//! assert_eq!(first, 0x42);
36//!
37//! // Tier 2 — direct reference (escape hatch for FFI / third-party APIs)
38//! assert_eq!(key.expose_secret().len(), 32);
39//!
40//! // Debug is always redacted
41//! assert_eq!(format!("{:?}", key), "[REDACTED]");
42//! // key is zeroized when dropped
43//! ```
44//!
45//! ```rust
46//! # #[cfg(feature = "alloc")]
47//! # {
48//! use secure_gate::{Dynamic, RevealSecret};
49//!
50//! let password: Dynamic<String> = Dynamic::new(String::from("hunter2"));
51//! let len = password.with_secret(|s: &String| s.len());
52//! assert_eq!(len, 7);
53//! # }
54//! ```
55//!
56//! ```rust
57//! use secure_gate::{fixed_alias, RevealSecret};
58//!
59//! fixed_alias!(pub Aes256Key, 32);
60//!
61//! let key: Aes256Key = [0xABu8; 32].into();
62//! key.with_secret(|b| assert_eq!(b.len(), 32));
63//! ```
64//!
65//! # Module structure
66//!
67//! ```text
68//! secure_gate (lib.rs)
69//! ├── Fixed<T>              ← always available, stack-allocated
70//! ├── Dynamic<T>            ← requires `alloc`, heap-allocated
71//! ├── traits/
72//! │   ├── RevealSecret      ← immutable access (always available)
73//! │   ├── RevealSecretMut   ← mutable access (always available)
74//! │   ├── revealed_secrets/
75//! │   │   ├── InnerSecret<T>    ← owned extraction wrapper
76//! │   │   └── EncodedSecret     ← zeroizing encoded string wrapper (alloc)
77//! │   ├── ConstantTimeEq    ← ct-eq feature
78//! │   ├── CloneableSecret   ← cloneable feature
79//! │   ├── SerializableSecret← serde-serialize feature
80//! │   ├── encoding/         ← ToHex, ToBase64Url, ToBech32, ToBech32m
81//! │   └── decoding/         ← FromHexStr, FromBase64UrlStr, FromBech32Str, FromBech32mStr
82//! ├── macros/               ← fixed_alias!, dynamic_alias!, etc.
83//! └── error                 ← FromSliceError, HexError, Base64Error, Bech32Error, DecodingError
84//! ```
85//!
86//! All public items are re-exported at the crate root. Use `secure_gate::Fixed`,
87//! not `secure_gate::fixed::Fixed`.
88//!
89//! # Import paths
90//!
91//! ```rust
92//! // ✅ Correct — always import from the crate root
93//! use secure_gate::{Fixed, RevealSecret};
94//!
95//! // ❌ Wrong — these internal paths compile but are not the public API
96//! // use secure_gate::traits::reveal_secret::RevealSecret;
97//! // use secure_gate::traits::encoding::hex::ToHex;
98//! ```
99//!
100//! # Method resolution: wrapper methods vs trait methods
101//!
102//! Encoding methods exist at **two levels** — both produce identical results:
103//!
104//! | Call style | Example | Appears in audit sweep? |
105//! |-----------|---------|------------------------|
106//! | **Wrapper inherent** (ergonomic) | `key.to_hex()` | No — grep for `to_hex` directly |
107//! | **Trait via scoped access** (audit-friendly) | `key.with_secret(\|b\| b.to_hex())` | Yes — `with_secret` is grep-able |
108//!
109//! The wrapper methods ([`Fixed::to_hex`], [`Dynamic::to_hex`](Dynamic::to_hex)) internally call
110//! `self.with_secret(|s| s.to_hex())` — they are convenience shorthands, not
111//! separate implementations.
112//!
113//! # Feature flags
114//!
115//! | Feature | Default | Description |
116//! |---------|---------|-------------|
117//! | `alloc` | **yes** | Heap types ([`Dynamic<T>`]), `Vec`/`String` zeroization |
118//! | `std` | no | Full `std` support (implies `alloc`) |
119//! | | | **Cryptographic** |
120//! | `ct-eq` | no | [`ConstantTimeEq`] via `subtle` — timing-safe comparison |
121//! | `rand` | no | `from_random()` / `from_rng()` — `no_std` for [`Fixed`] |
122//! | | | **Serialization** |
123//! | `serde-serialize` | no | Serde `Serialize` (requires [`SerializableSecret`] marker) |
124//! | `serde-deserialize` | no | Serde `Deserialize` with 1 MiB default limit |
125//! | `serde` | no | Both directions |
126//! | | | **Encoding** |
127//! | `encoding-hex` | no | [`ToHex`] / [`FromHexStr`] via `base16ct` (constant-time) |
128//! | `encoding-base64` | no | [`ToBase64Url`] / [`FromBase64UrlStr`] via `base64ct` (constant-time) |
129//! | `encoding-bech32` | no | [`ToBech32`] / [`FromBech32Str`] — BIP-173, extended ~5 KB limit |
130//! | `encoding-bech32m` | no | [`ToBech32m`] / [`FromBech32mStr`] — BIP-350, standard 90-byte limit |
131//! | `encoding` | no | All encoding features |
132//! | | | **Meta** |
133//! | `cloneable` | no | [`CloneableSecret`] opt-in cloning |
134//! | `full` | no | Everything |
135//!
136//! # What's available without `alloc`?
137//!
138//! With `default-features = false`:
139//! - [`Fixed<T>`], [`RevealSecret`], [`RevealSecretMut`], [`InnerSecret`]
140//! - [`Fixed::try_from_hex`](Fixed::try_from_hex), [`Fixed::try_from_base64url`](Fixed::try_from_base64url),
141//!   [`Fixed::try_from_bech32`](Fixed::try_from_bech32), [`Fixed::try_from_bech32m`](Fixed::try_from_bech32m)
142//!   (no-alloc stack-based decoding)
143//! - [`fixed_alias!`], [`fixed_generic_alias!`]
144//! - [`FromSliceError`]
145//!
146//! **Not** available without `alloc`: [`Dynamic<T>`], [`EncodedSecret`],
147//! encoding traits ([`ToHex`], etc.), decoding traits ([`FromHexStr`], etc.),
148//! [`dynamic_alias!`], [`dynamic_generic_alias!`], serde support.
149//!
150//! # `no_std`
151//!
152//! `no_std` compatible. [`Fixed<T>`] works without `alloc`. Enable `alloc` (default) for
153//! [`Dynamic<T>`]. For pure stack / embedded builds, use `default-features = false`.
154//! MSRV: **1.85** (Rust edition 2024).
155//!
156//! # Security
157//!
158//! This crate has **not** undergone an independent security audit. No unsafe code —
159//! enforced with `#![forbid(unsafe_code)]`. Prefer scoped access ([`RevealSecret::with_secret`])
160//! over direct references. Prefer zeroizing encoding variants (`to_hex_zeroizing`, etc.)
161//! when the encoded form is sensitive. See
162//! [SECURITY.md](https://github.com/Slurp9187/secure-gate/blob/main/secure-gate-core/SECURITY.md)
163//! for the full threat model.
164//!
165//! See the [README](https://github.com/Slurp9187/secure-gate/blob/main/README.md) and
166//! [SECURITY.md](https://github.com/Slurp9187/secure-gate/blob/main/SECURITY.md) for full details.
167
168#[cfg(feature = "alloc")]
169extern crate alloc;
170
171#[cfg(feature = "alloc")]
172mod dynamic;
173
174/// Fixed-size secret wrapper types - always available with zero dependencies.
175/// These provide fundamental secure storage abstractions for fixed-size data.
176mod fixed;
177
178/// Centralized error types - always available.
179mod error;
180
181/// Core traits for wrapper polymorphism - always available.
182pub mod traits;
183
184/// Heap-allocated secret wrapper with explicit access and automatic zeroization on drop.
185///
186/// Variable-length secrets (passwords, API keys, ciphertexts). Inner type must implement
187/// `Zeroize`. Secret bytes live on the heap only — never on the stack. Requires `alloc`.
188///
189/// See [`Fixed<T>`] for the stack-allocated alternative.
190///
191/// ```rust
192/// # #[cfg(feature = "alloc")]
193/// # {
194/// use secure_gate::{Dynamic, RevealSecret};
195///
196/// let pw: Dynamic<String> = Dynamic::new(String::from("hunter2"));
197/// assert_eq!(pw.with_secret(|s: &String| s.len()), 7);
198/// assert_eq!(format!("{:?}", pw), "[REDACTED]");
199/// # }
200/// ```
201#[cfg(feature = "alloc")]
202pub use dynamic::Dynamic;
203
204/// Cursor-like reader over [`Dynamic<Vec<u8>>`] — see [`Dynamic::as_reader`].
205#[cfg(feature = "std")]
206pub use dynamic::DynamicReader;
207
208#[cfg(all(feature = "alloc", feature = "serde-deserialize"))]
209/// Default maximum byte length for `Dynamic<Vec<u8>>` / `Dynamic<String>` deserialization (1 MiB).
210///
211/// The standard `serde::Deserialize` impl for both types rejects payloads exceeding this value.
212/// Pass a custom ceiling to [`Dynamic::deserialize_with_limit`] when a different limit is needed.
213///
214/// **Important:** this limit is enforced *after* the upstream deserializer has fully
215/// materialized the payload. It is a **result-length acceptance bound**, not a
216/// pre-allocation DoS guard. For untrusted input, enforce size limits at the
217/// transport or parser layer upstream.
218pub use dynamic::MAX_DESERIALIZE_BYTES;
219
220/// Stack-allocated secret wrapper with explicit access and automatic zeroization on drop.
221///
222/// Fixed-size secrets (keys, nonces, tokens). Inner type must implement `Zeroize`.
223/// Always available — works without `alloc`. Prefer [`new_with`](Fixed::new_with) over
224/// [`new`](Fixed::new) when minimizing stack residue matters.
225///
226/// See [`Dynamic<T>`] for the heap-allocated alternative.
227///
228/// ```rust
229/// use secure_gate::{Fixed, RevealSecret};
230///
231/// let key = Fixed::new([0xABu8; 32]);
232/// key.with_secret(|b| assert_eq!(b[0], 0xAB));
233/// assert_eq!(format!("{:?}", key), "[REDACTED]");
234/// ```
235pub use fixed::Fixed;
236
237/// Marker trait that opts a secret type into cloning. No methods — gates the `Clone`
238/// impl on [`Fixed`] and [`Dynamic`]. Each clone is independently zeroized on drop,
239/// but increases the in-memory exposure surface. Requires `cloneable` feature.
240///
241/// See also [`SerializableSecret`] (the other opt-in marker trait).
242#[cfg(feature = "cloneable")]
243pub use traits::CloneableSecret;
244
245/// Constant-time equality for secrets — prevents timing side-channel attacks.
246///
247/// Provides [`ct_eq()`](ConstantTimeEq::ct_eq) via the `subtle` crate. `==` is
248/// **deliberately not implemented** on [`Fixed`] / [`Dynamic`] — always use `ct_eq`.
249/// Requires `ct-eq` feature.
250///
251/// ```rust
252/// # #[cfg(feature = "ct-eq")]
253/// # {
254/// use secure_gate::{Fixed, ConstantTimeEq};
255///
256/// let a = Fixed::new([1u8; 32]);
257/// let b = Fixed::new([1u8; 32]);
258/// assert!(a.ct_eq(&b));
259/// # }
260/// ```
261#[cfg(feature = "ct-eq")]
262pub use traits::ConstantTimeEq;
263
264/// Explicit immutable access to secret contents (3-tier access model).
265///
266/// - **Tier 1** (preferred): [`with_secret()`](RevealSecret::with_secret) — scoped closure,
267///   borrow cannot escape.
268/// - **Tier 2** (escape hatch): [`expose_secret()`](RevealSecret::expose_secret) — direct
269///   `&T` reference for FFI / third-party APIs.
270/// - **Tier 3** (consumption): [`into_inner()`](RevealSecret::into_inner) — returns
271///   [`InnerSecret<T>`] with zeroization transferred to caller.
272/// - **Metadata**: [`len()`](RevealSecret::len) / [`is_empty()`](RevealSecret::is_empty) —
273///   no secret exposure.
274///
275/// See [`RevealSecretMut`] for the mutable counterpart.
276pub use traits::RevealSecret;
277
278/// Explicit mutable access to secret contents.
279///
280/// Extends [`RevealSecret`]. Prefer [`with_secret_mut()`](RevealSecretMut::with_secret_mut)
281/// (Tier 1) over [`expose_secret_mut()`](RevealSecretMut::expose_secret_mut) (Tier 2).
282/// Only [`Fixed`] and [`Dynamic`] implement this — read-only wrappers deliberately do not.
283pub use traits::RevealSecretMut;
284
285/// Owned, redacted wrapper returned by [`RevealSecret::into_inner`] (Tier 3 access).
286///
287/// Wraps [`Zeroizing<T>`](zeroize::Zeroizing) with `Debug` → `[REDACTED]`. Implements
288/// `Deref<Target = T>` for ergonomic access (the **only** type in this crate that derefs
289/// to the secret — [`Fixed`] and [`Dynamic`] deliberately do not).
290///
291/// This is **not** a secret wrapper like `Fixed`/`Dynamic` — it is the owned extraction
292/// result. Use [`into_zeroizing()`](InnerSecret::into_zeroizing) when an API requires
293/// `Zeroizing<T>` directly.
294pub use traits::InnerSecret;
295
296/// Owned, redacted wrapper for zeroizing encoded strings.
297///
298/// Returned by all `*_zeroizing` encoding methods (`to_hex_zeroizing`,
299/// `to_base64url_zeroizing`, `try_to_bech32_zeroizing`, etc.). Wraps
300/// `Zeroizing<String>` with `Debug` → `[REDACTED]`. Implements `Deref<Target = str>`
301/// and `Display`.
302///
303/// This is **not** a secret wrapper like `Fixed`/`Dynamic` — it is a zeroizing `String`
304/// wrapper for encoded output. Use [`into_inner()`](EncodedSecret::into_inner) to extract
305/// a plain `String` (ends zeroization) or [`into_zeroizing()`](EncodedSecret::into_zeroizing)
306/// to preserve it.
307///
308/// Requires `alloc` feature.
309#[cfg(feature = "alloc")]
310pub use traits::EncodedSecret;
311
312/// Marker trait that opts a secret type into Serde serialization. No methods — gates the
313/// `Serialize` impl on [`Fixed`] and [`Dynamic`]. Serialization exposes the full secret;
314/// audit every impl. Requires `serde-serialize` feature.
315///
316/// `Deserialize` is gated separately by the `serde-deserialize` feature and does **not**
317/// require this marker — it has its own feature-gated impls on the wrapper types directly.
318///
319/// See also [`CloneableSecret`] (the other opt-in marker trait).
320#[cfg(feature = "serde-serialize")]
321pub use traits::SerializableSecret;
322
323// Type alias macros (always available)
324mod macros;
325
326/// Decodes Base64url strings (`&str`) to `Vec<u8>`. Blanket impl for `AsRef<str>`.
327/// Requires `encoding-base64` + `alloc`. See [`ToBase64Url`] for the encoding counterpart.
328#[cfg(all(feature = "encoding-base64", feature = "alloc"))]
329pub use traits::FromBase64UrlStr;
330
331/// Decodes Bech32 (BIP-173) strings to `Vec<u8>` with HRP validation.
332/// Requires `encoding-bech32` + `alloc`. See [`ToBech32`] for the encoding counterpart.
333#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
334pub use traits::FromBech32Str;
335
336/// Decodes Bech32m (BIP-350) strings to `Vec<u8>` with HRP validation.
337/// Requires `encoding-bech32m` + `alloc`. See [`ToBech32m`] for the encoding counterpart.
338#[cfg(all(feature = "encoding-bech32m", feature = "alloc"))]
339pub use traits::FromBech32mStr;
340
341/// Decodes hex strings (`&str`) to `Vec<u8>`. Blanket impl for `AsRef<str>`.
342/// Requires `encoding-hex` + `alloc`. See [`ToHex`] for the encoding counterpart.
343#[cfg(all(feature = "encoding-hex", feature = "alloc"))]
344pub use traits::FromHexStr;
345
346/// Encodes byte data as Base64url strings (RFC 4648, URL-safe, no padding).
347/// Blanket impl for `AsRef<[u8]>`. Requires `encoding-base64` + `alloc`.
348/// See [`FromBase64UrlStr`] for the decoding counterpart.
349#[cfg(all(feature = "encoding-base64", feature = "alloc"))]
350pub use traits::ToBase64Url;
351
352/// Encodes byte data as Bech32 (BIP-173) strings with extended ~5 KB payload limit.
353/// Blanket impl for `AsRef<[u8]>`. Requires `encoding-bech32` + `alloc`.
354/// See [`FromBech32Str`] for the decoding counterpart.
355#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
356pub use traits::ToBech32;
357
358/// Encodes byte data as Bech32m (BIP-350) strings with standard 90-byte payload limit.
359/// Blanket impl for `AsRef<[u8]>`. Requires `encoding-bech32m` + `alloc`.
360/// See [`FromBech32mStr`] for the decoding counterpart.
361#[cfg(all(feature = "encoding-bech32m", feature = "alloc"))]
362pub use traits::ToBech32m;
363
364/// Encodes byte data as hexadecimal strings (constant-time via `base16ct`).
365/// Blanket impl for `AsRef<[u8]>`. Provides `to_hex()`, `to_hex_upper()`, and
366/// zeroizing variants. Requires `encoding-hex` + `alloc`.
367/// See [`FromHexStr`] for the decoding counterpart.
368#[cfg(all(feature = "encoding-hex", feature = "alloc"))]
369pub use traits::ToHex;
370
371/// Marker trait for types that support secure decoding (`AsRef<str>`). No methods —
372/// enables blanket impls of [`FromHexStr`], [`FromBase64UrlStr`], etc.
373#[cfg(any(
374    feature = "encoding-hex",
375    feature = "encoding-base64",
376    feature = "encoding-bech32",
377    feature = "encoding-bech32m",
378))]
379pub use traits::SecureDecoding;
380
381/// Marker trait for types that support secure encoding (`AsRef<[u8]>`). No methods —
382/// enables blanket impls of [`ToHex`], [`ToBase64Url`], etc.
383#[cfg(any(
384    feature = "encoding-hex",
385    feature = "encoding-base64",
386    feature = "encoding-bech32",
387    feature = "encoding-bech32m",
388))]
389pub use traits::SecureEncoding;
390
391/// Errors from Bech32 (BIP-173) and Bech32m (BIP-350) decoding.
392/// Debug builds include detailed context; release builds use generic messages.
393#[cfg(any(feature = "encoding-bech32", feature = "encoding-bech32m"))]
394pub use error::Bech32Error;
395
396/// Errors from Base64url decoding. Debug builds include detailed context;
397/// release builds use generic messages.
398#[cfg(feature = "encoding-base64")]
399pub use error::Base64Error;
400
401/// Errors from hex decoding. Debug builds include detailed context;
402/// release builds use generic messages.
403#[cfg(feature = "encoding-hex")]
404pub use error::HexError;
405
406/// Unified error type wrapping format-specific decoding errors ([`HexError`],
407/// [`Base64Error`], [`Bech32Error`]). Always available; variants depend on enabled features.
408pub use error::DecodingError;
409
410/// Error returned when a byte slice cannot be converted to `Fixed<[u8; N]>` due to
411/// length mismatch. Produced by `Fixed::try_from(&[u8])`.
412pub use error::FromSliceError;