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#[cfg(all(feature = "alloc", feature = "serde-deserialize"))]
205/// Default maximum byte length for `Dynamic<Vec<u8>>` / `Dynamic<String>` deserialization (1 MiB).
206///
207/// The standard `serde::Deserialize` impl for both types rejects payloads exceeding this value.
208/// Pass a custom ceiling to [`Dynamic::deserialize_with_limit`] when a different limit is needed.
209///
210/// **Important:** this limit is enforced *after* the upstream deserializer has fully
211/// materialized the payload. It is a **result-length acceptance bound**, not a
212/// pre-allocation DoS guard. For untrusted input, enforce size limits at the
213/// transport or parser layer upstream.
214pub use dynamic::MAX_DESERIALIZE_BYTES;
215
216/// Stack-allocated secret wrapper with explicit access and automatic zeroization on drop.
217///
218/// Fixed-size secrets (keys, nonces, tokens). Inner type must implement `Zeroize`.
219/// Always available — works without `alloc`. Prefer [`new_with`](Fixed::new_with) over
220/// [`new`](Fixed::new) when minimizing stack residue matters.
221///
222/// See [`Dynamic<T>`] for the heap-allocated alternative.
223///
224/// ```rust
225/// use secure_gate::{Fixed, RevealSecret};
226///
227/// let key = Fixed::new([0xABu8; 32]);
228/// key.with_secret(|b| assert_eq!(b[0], 0xAB));
229/// assert_eq!(format!("{:?}", key), "[REDACTED]");
230/// ```
231pub use fixed::Fixed;
232
233/// Marker trait that opts a secret type into cloning. No methods — gates the `Clone`
234/// impl on [`Fixed`] and [`Dynamic`]. Each clone is independently zeroized on drop,
235/// but increases the in-memory exposure surface. Requires `cloneable` feature.
236///
237/// See also [`SerializableSecret`] (the other opt-in marker trait).
238#[cfg(feature = "cloneable")]
239pub use traits::CloneableSecret;
240
241/// Constant-time equality for secrets — prevents timing side-channel attacks.
242///
243/// Provides [`ct_eq()`](ConstantTimeEq::ct_eq) via the `subtle` crate. `==` is
244/// **deliberately not implemented** on [`Fixed`] / [`Dynamic`] — always use `ct_eq`.
245/// Requires `ct-eq` feature.
246///
247/// ```rust
248/// # #[cfg(feature = "ct-eq")]
249/// # {
250/// use secure_gate::{Fixed, ConstantTimeEq};
251///
252/// let a = Fixed::new([1u8; 32]);
253/// let b = Fixed::new([1u8; 32]);
254/// assert!(a.ct_eq(&b));
255/// # }
256/// ```
257#[cfg(feature = "ct-eq")]
258pub use traits::ConstantTimeEq;
259
260/// Explicit immutable access to secret contents (3-tier access model).
261///
262/// - **Tier 1** (preferred): [`with_secret()`](RevealSecret::with_secret) — scoped closure,
263/// borrow cannot escape.
264/// - **Tier 2** (escape hatch): [`expose_secret()`](RevealSecret::expose_secret) — direct
265/// `&T` reference for FFI / third-party APIs.
266/// - **Tier 3** (consumption): [`into_inner()`](RevealSecret::into_inner) — returns
267/// [`InnerSecret<T>`] with zeroization transferred to caller.
268/// - **Metadata**: [`len()`](RevealSecret::len) / [`is_empty()`](RevealSecret::is_empty) —
269/// no secret exposure.
270///
271/// See [`RevealSecretMut`] for the mutable counterpart.
272pub use traits::RevealSecret;
273
274/// Explicit mutable access to secret contents.
275///
276/// Extends [`RevealSecret`]. Prefer [`with_secret_mut()`](RevealSecretMut::with_secret_mut)
277/// (Tier 1) over [`expose_secret_mut()`](RevealSecretMut::expose_secret_mut) (Tier 2).
278/// Only [`Fixed`] and [`Dynamic`] implement this — read-only wrappers deliberately do not.
279pub use traits::RevealSecretMut;
280
281/// Owned, redacted wrapper returned by [`RevealSecret::into_inner`] (Tier 3 access).
282///
283/// Wraps [`Zeroizing<T>`](zeroize::Zeroizing) with `Debug` → `[REDACTED]`. Implements
284/// `Deref<Target = T>` for ergonomic access (the **only** type in this crate that derefs
285/// to the secret — [`Fixed`] and [`Dynamic`] deliberately do not).
286///
287/// This is **not** a secret wrapper like `Fixed`/`Dynamic` — it is the owned extraction
288/// result. Use [`into_zeroizing()`](InnerSecret::into_zeroizing) when an API requires
289/// `Zeroizing<T>` directly.
290pub use traits::InnerSecret;
291
292/// Owned, redacted wrapper for zeroizing encoded strings.
293///
294/// Returned by all `*_zeroizing` encoding methods (`to_hex_zeroizing`,
295/// `to_base64url_zeroizing`, `try_to_bech32_zeroizing`, etc.). Wraps
296/// `Zeroizing<String>` with `Debug` → `[REDACTED]`. Implements `Deref<Target = str>`
297/// and `Display`.
298///
299/// This is **not** a secret wrapper like `Fixed`/`Dynamic` — it is a zeroizing `String`
300/// wrapper for encoded output. Use [`into_inner()`](EncodedSecret::into_inner) to extract
301/// a plain `String` (ends zeroization) or [`into_zeroizing()`](EncodedSecret::into_zeroizing)
302/// to preserve it.
303///
304/// Requires `alloc` feature.
305#[cfg(feature = "alloc")]
306pub use traits::EncodedSecret;
307
308/// Marker trait that opts a secret type into Serde serialization. No methods — gates the
309/// `Serialize` impl on [`Fixed`] and [`Dynamic`]. Serialization exposes the full secret;
310/// audit every impl. Requires `serde-serialize` feature.
311///
312/// `Deserialize` is gated separately by the `serde-deserialize` feature and does **not**
313/// require this marker — it has its own feature-gated impls on the wrapper types directly.
314///
315/// See also [`CloneableSecret`] (the other opt-in marker trait).
316#[cfg(feature = "serde-serialize")]
317pub use traits::SerializableSecret;
318
319// Type alias macros (always available)
320mod macros;
321
322/// Decodes Base64url strings (`&str`) to `Vec<u8>`. Blanket impl for `AsRef<str>`.
323/// Requires `encoding-base64` + `alloc`. See [`ToBase64Url`] for the encoding counterpart.
324#[cfg(all(feature = "encoding-base64", feature = "alloc"))]
325pub use traits::FromBase64UrlStr;
326
327/// Decodes Bech32 (BIP-173) strings to `Vec<u8>` with HRP validation.
328/// Requires `encoding-bech32` + `alloc`. See [`ToBech32`] for the encoding counterpart.
329#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
330pub use traits::FromBech32Str;
331
332/// Decodes Bech32m (BIP-350) strings to `Vec<u8>` with HRP validation.
333/// Requires `encoding-bech32m` + `alloc`. See [`ToBech32m`] for the encoding counterpart.
334#[cfg(all(feature = "encoding-bech32m", feature = "alloc"))]
335pub use traits::FromBech32mStr;
336
337/// Decodes hex strings (`&str`) to `Vec<u8>`. Blanket impl for `AsRef<str>`.
338/// Requires `encoding-hex` + `alloc`. See [`ToHex`] for the encoding counterpart.
339#[cfg(all(feature = "encoding-hex", feature = "alloc"))]
340pub use traits::FromHexStr;
341
342/// Encodes byte data as Base64url strings (RFC 4648, URL-safe, no padding).
343/// Blanket impl for `AsRef<[u8]>`. Requires `encoding-base64` + `alloc`.
344/// See [`FromBase64UrlStr`] for the decoding counterpart.
345#[cfg(all(feature = "encoding-base64", feature = "alloc"))]
346pub use traits::ToBase64Url;
347
348/// Encodes byte data as Bech32 (BIP-173) strings with extended ~5 KB payload limit.
349/// Blanket impl for `AsRef<[u8]>`. Requires `encoding-bech32` + `alloc`.
350/// See [`FromBech32Str`] for the decoding counterpart.
351#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
352pub use traits::ToBech32;
353
354/// Encodes byte data as Bech32m (BIP-350) strings with standard 90-byte payload limit.
355/// Blanket impl for `AsRef<[u8]>`. Requires `encoding-bech32m` + `alloc`.
356/// See [`FromBech32mStr`] for the decoding counterpart.
357#[cfg(all(feature = "encoding-bech32m", feature = "alloc"))]
358pub use traits::ToBech32m;
359
360/// Encodes byte data as hexadecimal strings (constant-time via `base16ct`).
361/// Blanket impl for `AsRef<[u8]>`. Provides `to_hex()`, `to_hex_upper()`, and
362/// zeroizing variants. Requires `encoding-hex` + `alloc`.
363/// See [`FromHexStr`] for the decoding counterpart.
364#[cfg(all(feature = "encoding-hex", feature = "alloc"))]
365pub use traits::ToHex;
366
367/// Marker trait for types that support secure decoding (`AsRef<str>`). No methods —
368/// enables blanket impls of [`FromHexStr`], [`FromBase64UrlStr`], etc.
369#[cfg(any(
370 feature = "encoding-hex",
371 feature = "encoding-base64",
372 feature = "encoding-bech32",
373 feature = "encoding-bech32m",
374))]
375pub use traits::SecureDecoding;
376
377/// Marker trait for types that support secure encoding (`AsRef<[u8]>`). No methods —
378/// enables blanket impls of [`ToHex`], [`ToBase64Url`], etc.
379#[cfg(any(
380 feature = "encoding-hex",
381 feature = "encoding-base64",
382 feature = "encoding-bech32",
383 feature = "encoding-bech32m",
384))]
385pub use traits::SecureEncoding;
386
387/// Errors from Bech32 (BIP-173) and Bech32m (BIP-350) decoding.
388/// Debug builds include detailed context; release builds use generic messages.
389#[cfg(any(feature = "encoding-bech32", feature = "encoding-bech32m"))]
390pub use error::Bech32Error;
391
392/// Errors from Base64url decoding. Debug builds include detailed context;
393/// release builds use generic messages.
394#[cfg(feature = "encoding-base64")]
395pub use error::Base64Error;
396
397/// Errors from hex decoding. Debug builds include detailed context;
398/// release builds use generic messages.
399#[cfg(feature = "encoding-hex")]
400pub use error::HexError;
401
402/// Unified error type wrapping format-specific decoding errors ([`HexError`],
403/// [`Base64Error`], [`Bech32Error`]). Always available; variants depend on enabled features.
404pub use error::DecodingError;
405
406/// Error returned when a byte slice cannot be converted to `Fixed<[u8; N]>` due to
407/// length mismatch. Produced by `Fixed::try_from(&[u8])`.
408pub use error::FromSliceError;