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