secure_gate/lib.rs
1// no_std by default; the `std` feature opts back into the standard library.
2// Verified in CI by cross-building for a bare-metal target.
3#![cfg_attr(not(feature = "std"), no_std)]
4// Forbid unsafe code unconditionally
5#![forbid(unsafe_code)]
6#![warn(missing_docs)]
7// `Cargo.toml` tells docs.rs to build with `--cfg docsrs`. This is what reads it:
8// `doc_cfg` annotates every feature-gated item with the feature that enables it, so
9// the docs people read say "Available on crate feature `encoding-hex`" instead of
10// leaving the reader to infer it. Nightly-only, which is fine — docs.rs builds on
11// nightly, and the only other build that sets `docsrs` is the CI step that exists to
12// catch this gate being renamed. It has been renamed before: `doc_auto_cfg` was the
13// spelling until 1.92 removed it and merged it into `doc_cfg`.
14#![cfg_attr(docsrs, feature(doc_cfg))]
15
16//! Secure wrappers for secrets with **explicit access** and **mandatory zeroization** — a
17//! `no_std`-compatible, zero-overhead library with audit-friendly access patterns.
18//!
19//! Secrets are **automatically zeroized on drop** (the inner type must implement
20//! [`Zeroize`](zeroize::Zeroize)). While a secret is held in [`Fixed`] or [`Dynamic`],
21//! there is no `Deref` and no `AsRef`: callers reach the inner secret only via
22//! [`RevealSecret`] / [`RevealSecretMut`], and `Debug` on a wrapper always prints
23//! `[REDACTED]`. [`SlotWriter`], which borrows a wrapper's storage rather than holding it,
24//! prints its cursor instead — position, remaining, length — and never the bytes.
25//! Access has two shapes: **borrow** it, or **take** it.
26//! [`with_secret()`](RevealSecret::with_secret) and
27//! [`expose_secret()`](RevealSecret::expose_secret) lend you a reference while the
28//! wrapper keeps ownership and keeps wiping. [`into_inner()`](RevealSecret::into_inner)
29//! transfers ownership: you get the plain value and the protection ends with the call.
30//!
31//! Those two shapes are what the **3-tier access model** in `SECURITY.md` counts in
32//! threes: `with_secret` (Tier 1) and `expose_secret` (Tier 2) are both the borrow
33//! shape, differing only in how tightly the lifetime is pinned, and `into_inner`
34//! (Tier 3) is the take. Two shapes, three volumes — the tiers say how loudly you
35//! have to announce which one you picked, not how many protections there are.
36//!
37//! Extraction is a hand-off, not a third layer of protection. Encoding output is the
38//! one case that keeps a wrapper — [`EncodedSecret`] **does** implement `Deref`, because
39//! its job is to keep an encoded *copy* of the secret wiped until it drops. Ordinary
40//! copies you make of the derefed value (`.to_string()`) are not tracked. See
41//! [Where accident-prevention ends](#where-accident-prevention-ends).
42//!
43//! # Which type should I use?
44//!
45//! | Type | Allocation | Use case | Feature |
46//! |------|-----------|----------|---------|
47//! | [`Fixed<T>`] | Stack | Keys, nonces, tokens — compile-time-known size | Always available |
48//! | [`Dynamic<T>`] | Heap | Passwords, API keys, ciphertexts — variable length | `alloc` (default) |
49# / [`SealedDynamic<T>`](SealedDynamic) | Encrypted in RAM while idle, under a guarded, locked [`Prekey`] | Long-held keys in a process whose memory may be read while they idle | `sealed` |"
52)]
53#![cfg_attr(
54 not(feature = "sealed"),
55 doc = "| `SealedFixed<N>` / `SealedDynamic<T>` | Encrypted in RAM while idle, under a guarded, locked `Prekey` | Long-held keys in a process whose memory may be read while they idle | `sealed` |"
56)]
57//!
58//! `Fixed` and `Dynamic` share the same [`RevealSecret`] / [`RevealSecretMut`] access API.
59//! The sealed types are opened only through scoped `with_secret` calls (no `expose_secret`),
60//! cost about 8 µs per access, and are for keys held across a session, not hot loops; their
61//! module has the threat model and the measurements.
62//!
63//! (The row above is a real intra-doc link where `sealed` is enabled, as on docs.rs, and
64//! plain text where it is not, so builds without the feature have no broken link.)
65//!
66//! # Quick start
67//!
68//! ```rust
69//! use secure_gate::{Fixed, RevealSecret};
70//!
71//! // Wrap a 32-byte key
72//! let key = Fixed::new([0x42u8; 32]);
73//!
74//! // Tier 1 — scoped access (preferred): secret ref cannot escape the closure
75//! let first = key.with_secret(|bytes| bytes[0]);
76//! assert_eq!(first, 0x42);
77//!
78//! // Tier 2 — direct reference (escape hatch for FFI / third-party APIs)
79//! assert_eq!(key.expose_secret().len(), 32);
80//!
81//! // Debug is always redacted
82//! assert_eq!(format!("{:?}", key), "[REDACTED]");
83//! // key is zeroized when dropped
84//! ```
85//!
86//! ```rust
87//! # #[cfg(feature = "alloc")]
88//! # {
89//! use secure_gate::{Dynamic, RevealSecret};
90//!
91//! let password: Dynamic<String> = Dynamic::new(String::from("hunter2"));
92//! let len = password.with_secret(|s: &String| s.len());
93//! assert_eq!(len, 7);
94//! # }
95//! ```
96//!
97//! ```rust
98//! use secure_gate::{fixed_newtype, RevealSecret};
99//!
100//! fixed_newtype!(pub Aes256Key, 32);
101//!
102//! let key = Aes256Key::new([0xABu8; 32]);
103//! key.with_secret(|b| assert_eq!(b.len(), 32));
104//! ```
105//!
106//! # Module structure
107//!
108//! ```text
109//! secure_gate (lib.rs)
110//! ├── Fixed<T> ← always available, stack-allocated
111//! ├── Dynamic<T> ← requires `alloc`, heap-allocated
112//! ├── traits/
113//! │ ├── RevealSecret ← immutable access (always available)
114//! │ ├── RevealSecretMut ← mutable access (always available)
115//! │ ├── revealed_secrets/
116//! │ │ └── EncodedSecret ← zeroizing encoded string wrapper (alloc)
117//! │ ├── ConstantTimeEq ← ct-eq feature
118//! │ ├── CloneableSecret ← cloneable feature
119//! │ ├── SerializableSecret← serde-serialize feature
120//! │ ├── encoding/ ← ToHex, ToBase32, ToBase64Url, ToBech32, ToBech32m
121//! │ └── decoding/ ← FromHexStr, FromBase32Str, FromBase64UrlStr, FromBech32Str, FromBech32mStr
122//! ├── macros/ ← fixed_newtype!, dynamic_newtype!
123//! └── error ← FromSliceError, HexError, Base32Error, Base64Error, Bech32Error
124//! ```
125//!
126//! All public items are re-exported at the crate root. Use `secure_gate::Fixed`,
127//! not `secure_gate::fixed::Fixed`.
128//!
129//! # Type taxonomy
130//!
131//! | Category | Types | `Deref` to secret? | Purpose |
132//! |----------|-------|-------------------|---------|
133//! | **Secret wrappers** | [`Fixed<T>`], [`Dynamic<T>`] | No — use [`RevealSecret`] | Hold live secrets; `Debug` → `[REDACTED]` |
134//! | **Output wrapper** | [`EncodedSecret`] | Yes — yields `&str`; copies you make are yours, the buffer stays the wrapper's | Holds encoded output, wiped on drop |
135//! | **Borrowed slot cursor** | [`SlotWriter`] | No — it writes, it does not read out | Appends into a slot a sized `new_with` handed out; `Debug` → cursor state only, never bytes |
136//! | **Opt-in markers** | [`CloneableSecret`], [`SerializableSecret`] | — (no methods) | Implement on inner type `T` to unlock gated impls |
137//!
138//! `CloneableSecret` and `SerializableSecret` are implemented on the **inner type `T`**,
139//! not on `Fixed<T>` or `Dynamic<T>` directly. [`EncodedSecret`] is not a secret
140//! wrapper and does not interact with these markers.
141//!
142//! # Where accident-prevention ends
143//!
144//! This crate carries two separate obligations, and they end in different places.
145//!
146//! **Accidents must not compile.** This applies while the secret is held in
147//! [`Fixed`]/[`Dynamic`], and it ends at the named extraction. `into_inner` and
148//! `EncodedSecret::into_inner` are the exits that transfer ownership: you typed a
149//! name, the call site is grep-able, and the wrapper is consumed. `expose_secret` is
150//! a named *borrow* — the wrapper still owns the secret and still wipes it — but it
151//! hands out a reference with no lifetime bound to a call site, so it is audited
152//! alongside them.
153//!
154//! **Documented behavior must be accurate.** This never ends, and it is why the
155//! taxonomy table above says `Deref: Yes` for the output wrapper.
156//!
157//! What [`EncodedSecret`] still guarantees: the `String` it owns is zeroized on drop,
158//! and its `Debug` prints `[REDACTED]`. What it does not guarantee: copies you make
159//! through `Deref` are ordinary values. `str::to_string()` and `.to_owned()` both
160//! produce untracked plaintext. That is what extraction is for — the crate is not
161//! trying to follow the bytes into your TLS stack. `into_inner` gives up even the first
162//! guarantee, by design: it is the named end of protection.
163//!
164//! Two consequences worth knowing:
165//!
166//! - `format!("{:?}", &*encoded)` prints the encoded secret. Redaction lives on the
167//! wrapper, not on `str`; dereferencing first opts out of it.
168//! - [`EncodedSecret::into_zeroizing`] returns a `Zeroizing<String>`, whose `Debug` is
169//! **not** redacted. It is a deliberate hand-off to a foreign type, not an equivalent
170//! wrapper.
171//!
172//! # Import paths
173//!
174//! ```rust
175//! // ✅ Correct — always import from the crate root
176//! use secure_gate::{Fixed, RevealSecret};
177//!
178//! // ❌ Wrong — these internal paths compile but are not the public API
179//! // use secure_gate::traits::reveal_secret::RevealSecret;
180//! // use secure_gate::traits::encoding::hex::ToHex;
181//! ```
182//!
183//! # Method resolution: wrapper methods vs trait methods
184//!
185//! Encoding methods exist at **two levels** — both produce identical results:
186//!
187//! | Call style | Example | Appears in audit sweep? |
188//! |-----------|---------|------------------------|
189//! | **Wrapper trait impl** (ergonomic) | `key.to_hex()` (needs `use secure_gate::ToHex`) | No — grep for `to_hex` directly |
190//! | **Trait via scoped access** (audit-friendly) | `key.with_secret(\|b\| b.to_hex())` | Yes — `with_secret` is grep-able |
191//!
192//! Both levels are impls of the **same** trait ([`ToHex`], [`ToBase32`], [`ToBase64Url`],
193//! [`ToBech32`], [`ToBech32m`]): a blanket impl covers the raw bytes inside
194//! `with_secret`, and per-wrapper impls on `Fixed<[u8; N]>` / `Dynamic<Vec<u8>>`
195//! delegate through `with_secret` internally. One trait also means one bound:
196//! `fn fingerprint<S: ToHex>(s: &S)` accepts wrappers and forwarding newtypes
197//! alike.
198//!
199//! # Feature flags
200//!
201//! | Feature | Default | Description |
202//! |---------|---------|-------------|
203//! | `alloc` | **yes** | Heap types ([`Dynamic<T>`]), `Vec`/`String` zeroization |
204//! | `std` | no | Full `std` support (implies `alloc`) |
205//! | | | **Cryptographic** |
206//! | `ct-eq` | no | [`ConstantTimeEq`] via `subtle` — timing-safe comparison |
207//! | `rand` | no | `from_random()` / `from_rng()` — `no_std` for [`Fixed`] |
208//! | `sealed` | no | `SealedFixed` / `SealedDynamic`: secrets encrypted in RAM while idle, under a locked `Prekey` (implies `std`, `rand`) |
209//! | | | **Serialization** |
210//! | `serde-serialize` | no | Serde `Serialize` (requires [`SerializableSecret`] marker) |
211//! | `serde-deserialize` | no | Serde `Deserialize` with 1 MiB default limit |
212//! | `serde` | no | Both directions |
213//! | | | **Encoding** |
214//! | `encoding-hex` | no | [`ToHex`] / [`FromHexStr`] via `base16ct` (constant-time) |
215//! | `encoding-base32` | no | [`ToBase32`] / [`FromBase32Str`] via `base32ct` (constant-time) |
216//! | `encoding-base64` | no | [`ToBase64Url`] / [`FromBase64UrlStr`] via `base64ct` (constant-time) |
217//! | `encoding-bech32` | no | [`ToBech32`] / [`FromBech32Str`] (BIP-173) **and** [`ToBech32m`] / [`FromBech32mStr`] (BIP-350); `_sized::<N>` for a caller-chosen code length |
218//! | `encoding` | no | All encoding features |
219//! | | | **Meta** |
220//! | `cloneable` | no | [`CloneableSecret`] opt-in cloning |
221//! | `full` | no | Everything except `std` and `sealed` |
222//!
223//! # What's available without `alloc`?
224//!
225//! With `default-features = false`:
226//! - [`Fixed<T>`], [`RevealSecret`], [`RevealSecretMut`]
227//! - [`Fixed::try_from_hex`](Fixed::try_from_hex), [`Fixed::try_from_base32`](Fixed::try_from_base32),
228//! [`Fixed::try_from_base64url`](Fixed::try_from_base64url),
229//! [`Fixed::try_from_bech32`](Fixed::try_from_bech32), [`Fixed::try_from_bech32m`](Fixed::try_from_bech32m)
230//! (no-alloc stack-based decoding)
231//! - [`fixed_newtype!`] (distinct nominal types — two keys of the same size that
232//! the compiler keeps apart); a plain `type` alias over [`Fixed<T>`] needs nothing
233//! at all
234//! - [`FromSliceError`]
235//!
236//! **Not** available without `alloc`: [`Dynamic<T>`], [`EncodedSecret`],
237//! encoding traits ([`ToHex`], etc.), decoding traits ([`FromHexStr`], etc.),
238//! [`dynamic_newtype!`], serde support.
239//!
240//! # `no_std`
241//!
242//! `no_std` compatible (`#![no_std]` unless the `std` feature is enabled — verified in CI
243//! by cross-building for `thumbv7em-none-eabihf`). [`Fixed<T>`] works without `alloc`.
244//! Enable `alloc` (default) for [`Dynamic<T>`]. For pure stack / embedded builds, use
245//! `default-features = false`. MSRV: **1.85** (Rust edition 2024).
246//!
247//! One caveat: the `rand` feature compiles without `std` or `alloc`, but
248//! [`Fixed::from_random`] relies on `getrandom`, which needs a platform entropy backend.
249//! On bare-metal targets you must configure one (see the `getrandom` crate's
250//! "custom backend" documentation); without it, builds enabling `rand` fail at link
251//! time on such targets. [`Fixed::from_rng`] with a caller-supplied RNG has no such
252//! requirement.
253//!
254//! # Security
255//!
256//! This crate has **not** undergone an independent security audit. No unsafe code —
257//! enforced with `#![forbid(unsafe_code)]`. Prefer scoped access ([`RevealSecret::with_secret`])
258//! over direct references. Encoders return [`EncodedSecret`], which stays wiped until it
259//! drops; `EncodedSecret::into_inner` is the named call that ends that. See
260//! [SECURITY.md](https://github.com/Slurp9187/secure-gate/blob/main/secure-gate-core/SECURITY.md)
261//! for the full threat model.
262//!
263//! # Inherent Rust Limitations
264//!
265//! Three universal limits apply to any in-memory secret library in Rust (and in
266//! C, C++, and Go) — `secure-gate` documents them honestly rather than
267//! overclaiming:
268//!
269//! - **Stack-move residue**: moving a [`Fixed<T>`] by value leaves the prior stack
270//! slot uncleared until a later frame overwrites it. Prefer
271//! [`Fixed::new_with`](Fixed::new_with), pass by reference, or use
272//! [`Dynamic<T>`] for long-lived secrets.
273//! - **Heap-reallocation residue**: `Vec` / `String` realloc frees the old buffer
274//! unzeroed. Pre-size, use `Dynamic<[u8; N]>` (boxed array — no realloc surface),
275//! or install a zero-on-dealloc global allocator such as `zeroizing-alloc` at
276//! the binary level.
277//! - **Swap / core dumps**: process memory paged to disk or written on crash is
278//! outside any in-process library's reach. Use OS facilities (`mlock`,
279//! encrypted swap, disabled core dumps).
280//!
281//! Full discussion in
282//! [SECURITY.md § Inherent Rust Limitations](https://github.com/Slurp9187/secure-gate/blob/main/secure-gate-core/SECURITY.md#inherent-rust-limitations).
283//!
284//! See the [README](https://github.com/Slurp9187/secure-gate/blob/main/README.md) and
285//! [SECURITY.md](https://github.com/Slurp9187/secure-gate/blob/main/secure-gate-core/SECURITY.md) for full details.
286
287#[cfg(feature = "alloc")]
288extern crate alloc;
289
290// Documentation-only. Several doc comments link to `std` items this crate touches through
291// gated impls (`io::Write` on `Dynamic<Vec<u8>>` and on `SlotWriter`, `io::Read` on
292// `DynamicReader`). Those links are correct and resolve on docs.rs, which builds with
293// `all-features`, and in CI, whose rustdoc job does the same. They do NOT resolve under
294// `--features=full`, which does not enable `std`: without that feature the name `std` is
295// not in scope at all, so rustdoc has nothing to point at and `-D warnings` turns each one
296// into an error. That is a plain `cargo doc --features full` — something a contributor
297// runs and CI does not. This brings the name into scope for doc builds only: it is
298// `cfg(doc)`, set by rustdoc and nothing else, so no real build is affected, `no_std` or
299// otherwise, and the links resolve in every feature combination rather than only the one
300// docs.rs happens to use.
301#[cfg(doc)]
302extern crate std;
303
304#[cfg(feature = "alloc")]
305mod dynamic;
306
307mod slot_writer;
308
309/// Fixed-size secret wrapper types - always available with zero dependencies.
310/// These provide fundamental secure storage abstractions for fixed-size data.
311mod fixed;
312
313/// Centralized error types - always available.
314mod error;
315
316/// Core traits for wrapper polymorphism - always available.
317pub mod traits;
318
319#[cfg(feature = "sealed")]
320pub mod sealed;
321
322#[cfg(feature = "sealed")]
323pub mod scoped;
324
325/// Implementation detail of the `*_newtype!` macros — not a public API.
326///
327/// Generated code needs to name `Zeroize`, `serde` traits, and `alloc` types
328/// without requiring the caller to depend on those crates directly, and
329/// `::alloc::…` paths do not resolve in an ordinary `std` crate. Re-exporting
330/// them here keeps expansions self-contained via `$crate::__private::…`.
331///
332/// Semver-exempt: items here may change or disappear in any release.
333#[doc(hidden)]
334pub mod __private {
335 #[cfg(feature = "alloc")]
336 pub use alloc::{boxed::Box, string::String, vec::Vec};
337 #[cfg(feature = "rand")]
338 pub use rand::{TryCryptoRng, TryRng};
339 #[cfg(feature = "serde-deserialize")]
340 pub use serde::{Deserialize, Deserializer};
341 #[cfg(feature = "serde-serialize")]
342 pub use serde::{Serialize, Serializer};
343 pub use zeroize::{Zeroize, ZeroizeOnDrop};
344
345 #[cfg(feature = "sealed")]
346 pub use alloc::sync::Arc;
347
348 /// A `fixed_newtype!` byte role, convertible to and from the `Fixed` it wraps without
349 /// copying the bytes out by hand. Lets `sealed_fixed_newtype!` seal a role directly.
350 #[cfg(feature = "sealed")]
351 pub trait FixedRole: Sized {
352 /// The wrapped `Fixed<[u8; N]>`.
353 type Fixed: SealableFixed;
354 /// Unwraps the role.
355 fn into_fixed(self) -> Self::Fixed;
356 /// Wraps a `Fixed` as the role.
357 fn from_fixed(fixed: Self::Fixed) -> Self;
358 }
359
360 /// Maps `Fixed<[u8; N]>` to `SealedFixed<N>` without naming `N`.
361 #[cfg(feature = "sealed")]
362 pub trait SealableFixed {
363 /// `SealedFixed<N>`.
364 type Sealed;
365 }
366
367 #[cfg(feature = "sealed")]
368 impl<const N: usize> SealableFixed for crate::Fixed<[u8; N]> {
369 type Sealed = crate::SealedFixed<N>;
370 }
371
372 /// The `SealedFixed<N>` a role seals into.
373 #[cfg(feature = "sealed")]
374 pub type SealedOf<R> = <<R as FixedRole>::Fixed as SealableFixed>::Sealed;
375}
376
377/// Heap-allocated secret wrapper with explicit access and automatic zeroization on drop.
378///
379/// Variable-length secrets (passwords, API keys, ciphertexts). Inner type must implement
380/// `Zeroize`. Secret bytes live on the heap only — never on the stack. Requires `alloc`.
381///
382/// See [`Fixed<T>`] for the stack-allocated alternative.
383///
384/// ```rust
385/// # #[cfg(feature = "alloc")]
386/// # {
387/// use secure_gate::{Dynamic, RevealSecret};
388///
389/// let pw: Dynamic<String> = Dynamic::new(String::from("hunter2"));
390/// assert_eq!(pw.with_secret(|s: &String| s.len()), 7);
391/// assert_eq!(format!("{:?}", pw), "[REDACTED]");
392/// # }
393/// ```
394#[cfg(feature = "alloc")]
395pub use dynamic::Dynamic;
396
397/// A fixed-size byte secret held encrypted in RAM while idle — see the [`sealed`] module.
398/// Requires the `sealed` feature.
399#[cfg(feature = "sealed")]
400pub use sealed::SealedFixed;
401
402/// A `Vec<u8>` or `String` secret held encrypted in RAM while idle — see the [`sealed`]
403/// module. Requires the `sealed` feature.
404#[cfg(feature = "sealed")]
405pub use sealed::SealedDynamic;
406
407/// The locked, wipeable pre-key every sealing key is derived from — see the [`sealed`]
408/// module. Requires the `sealed` feature.
409#[cfg(feature = "sealed")]
410pub use sealed::Prekey;
411
412/// Why a [`Prekey`]'s pages are not locked in RAM. Requires the `sealed` feature.
413#[cfg(feature = "sealed")]
414pub use sealed::LockError;
415
416/// Cursor-like reader over [`Dynamic<Vec<u8>>`] — see [`Dynamic::as_reader`].
417#[cfg(feature = "std")]
418pub use dynamic::DynamicReader;
419
420/// Append-shaped writing into the fixed slot handed to a sized `new_with`.
421///
422/// The sized constructors hand out a slot whose length is already decided, which is what
423/// makes growth — and therefore the reallocation hazard — inexpressible. `SlotWriter`
424/// restores the append shape on top of that, so concatenating a wire format does not mean
425/// writing offsets by hand. [`push_slice`](SlotWriter::push_slice) panics on overrun and
426/// leaves the slot untouched; the `std`-gated [`Write`](std::io::Write) path cannot do that
427/// and stay inside `Write`'s contract, so it truncates and reports the refusal on a later
428/// call. `Debug` shows the cursor — position, remaining, length — and never the bytes.
429pub use slot_writer::SlotWriter;
430
431#[cfg(all(feature = "alloc", feature = "serde-deserialize"))]
432/// Default maximum byte length for `Dynamic<Vec<u8>>` / `Dynamic<String>` deserialization (1 MiB).
433///
434/// The standard `serde::Deserialize` impl for both types rejects payloads exceeding this value.
435/// Pass a custom ceiling to [`Dynamic::deserialize_with_limit`] when a different limit is needed.
436///
437/// **Important:** this limit is enforced *after* the upstream deserializer has fully
438/// materialized the payload. It is a **result-length acceptance bound**, not a
439/// pre-allocation DoS guard. For untrusted input, enforce size limits at the
440/// transport or parser layer upstream.
441pub use dynamic::MAX_DESERIALIZE_BYTES;
442
443/// Stack-allocated secret wrapper with explicit access and automatic zeroization on drop.
444///
445/// Fixed-size secrets (keys, nonces, tokens). Inner type must implement `Zeroize`.
446/// Always available — works without `alloc`. Prefer [`new_with`](Fixed::new_with) over
447/// [`new`](Fixed::new) when minimizing stack residue matters.
448///
449/// See [`Dynamic<T>`] for the heap-allocated alternative.
450///
451/// ```rust
452/// use secure_gate::{Fixed, RevealSecret};
453///
454/// let key = Fixed::new([0xABu8; 32]);
455/// key.with_secret(|b| assert_eq!(b[0], 0xAB));
456/// assert_eq!(format!("{:?}", key), "[REDACTED]");
457/// ```
458pub use fixed::Fixed;
459
460/// Marker trait that opts a secret type into cloning. No methods — gates the `Clone`
461/// impl on [`Fixed`] and [`Dynamic`]. Each clone is independently zeroized on drop,
462/// but increases the in-memory exposure surface. Requires `cloneable` feature.
463///
464/// Implement this on your inner type `T`; `Fixed<T>` and `Dynamic<T>` then gain the
465/// gated `Clone` impl automatically. **This marker is deliberately not implemented by
466/// default** on `Fixed<T>` or `Dynamic<T>` — cloning is an opt-in risk that must be
467/// explicitly enabled. Without the `cloneable` feature this type does not exist at all.
468///
469/// **Coherence note:** Rust's orphan rule prevents downstream crates from implementing
470/// this marker for foreign types such as `String`, `Vec<u8>`, or `[u8; N]` — so
471/// `Dynamic<String>` and `Fixed<[u8; N]>` themselves can never become `Clone`. To opt
472/// a secret into cloning, define a newtype inner type in your crate (deriving
473/// `Zeroize` and `Clone`) and implement the marker on it. This is intentional: it keeps
474/// every cloneable secret type visible in *your* code. See the trait-level docs for a
475/// worked example.
476///
477/// See also [`SerializableSecret`] (the other opt-in marker trait).
478#[cfg(feature = "cloneable")]
479pub use traits::CloneableSecret;
480
481/// Constant-time equality for secrets — prevents timing side-channel attacks.
482///
483/// Provides [`ct_eq()`](ConstantTimeEq::ct_eq) via the `subtle` crate. `==` is
484/// **deliberately not implemented** on [`Fixed`] / [`Dynamic`] — always use `ct_eq`.
485/// Requires `ct-eq` feature.
486///
487/// ```rust
488/// # #[cfg(feature = "ct-eq")]
489/// # {
490/// use secure_gate::{Fixed, ConstantTimeEq};
491///
492/// let a = Fixed::new([1u8; 32]);
493/// let b = Fixed::new([1u8; 32]);
494/// assert!(a.ct_eq(&b));
495/// # }
496/// ```
497#[cfg(feature = "ct-eq")]
498pub use traits::ConstantTimeEq;
499
500/// Explicit immutable access to secret contents (3-tier access model).
501///
502/// - **Tier 1** (preferred): [`with_secret()`](RevealSecret::with_secret) — scoped closure,
503/// borrow cannot escape.
504/// - **Tier 2** (escape hatch): [`expose_secret()`](RevealSecret::expose_secret) — direct
505/// `&T` reference for FFI / third-party APIs.
506/// - **Tier 3** (consumption): [`into_inner()`](RevealSecret::into_inner) — transfers
507/// ownership of the plain value; protection ends with the call.
508///
509/// Length metadata lives in the separate [`SecretLen`] trait. See
510/// [`RevealSecretMut`] for the mutable counterpart.
511// Kept as a separate `use` from `SecretLen` on purpose: rustdoc 1.70 (the 0.8
512// line's MSRV toolchain) ICEs resolving intra-doc links on a grouped
513// `pub use a::{B, C};` re-export, and the two lines share this file.
514pub use traits::RevealSecret;
515
516/// Length metadata for secrets whose inner type has a meaningful length.
517///
518/// [`len()`](SecretLen::len) / [`byte_len()`](SecretLen::byte_len) /
519/// [`is_empty()`](SecretLen::is_empty) do not expose contents, but length itself
520/// can be sensitive for variable-length secrets — see the trait's Security
521/// section. Implemented for `Fixed<[T; N]>`, `Dynamic<String>`, and
522/// `Dynamic<Vec<T>>`; deliberately not for custom inner types.
523pub use traits::SecretLen;
524
525/// Explicit mutable access to secret contents.
526///
527/// Extends [`RevealSecret`]. Prefer [`with_secret_mut()`](RevealSecretMut::with_secret_mut)
528/// (Tier 1) over [`expose_secret_mut()`](RevealSecretMut::expose_secret_mut) (Tier 2).
529/// Implemented by [`Fixed`], [`Dynamic`], and the generated newtypes. [`EncodedSecret`]
530/// implements neither this nor [`RevealSecret`] — it is an output wrapper, not a secret one.
531pub use traits::RevealSecretMut;
532
533/// Placeholder values left behind by [`RevealSecret::into_inner`] (Tier 3 access).
534///
535/// When `into_inner` moves the real secret out of a wrapper, it must leave *something*
536/// behind for the wrapper's `Drop` impl to zeroize. `SentinelValue::sentinel_value()`
537/// produces that inert placeholder (an all-default array, empty `String`, or empty
538/// `Vec`). Implemented for `[T; N]` (any `N`, `T: Default`), `String`, and `Vec<T>`.
539///
540/// Implement this for your own inner types to make `into_inner` available on wrappers
541/// around them. A sentinel must never contain secret material.
542///
543/// Leaving it unimplemented is a deliberate position, not a gap: `into_inner` is then
544/// uncallable for that inner type, so a secret with no safe inert placeholder simply
545/// cannot be extracted by ownership transfer. `with_secret` and `expose_secret` still
546/// work. That is the safe default rather than a missing feature.
547pub use traits::SentinelValue;
548
549/// Asserts that an inner type owns no heap allocation.
550///
551/// Required by [`Fixed::new`], which is what makes `SECURITY.md`'s claim that `Fixed<T>`
552/// "has no realloc surface" true rather than aspirational. Before this bound existed,
553/// `Fixed<Vec<u8>>`, `Fixed<String>` and `fixed_newtype!(pub Name, generic Vec<u8>)` all
554/// compiled, and measured, they abandoned an unwiped buffer holding the whole secret on
555/// any capacity change — the same weakness `Dynamic` documents, minus `Dynamic`'s
556/// safe-growth `std::io::Write` path.
557///
558/// Implemented for the primitives, `[T; N]` for any `N`, tuples up to four elements, and
559/// `Option<T>`. Nothing heap-owning is implemented: `Vec<T>`, `String` and `Box<[T]>` are
560/// all absent, because the residue does not need a capacity change — replacing the whole
561/// value abandons the allocation too, measured at 1024 of 1024 bytes through a
562/// `Fixed<Box<[u8]>>`. For a heap-backed secret use [`Dynamic`], which documents the
563/// residue and offers the one safe growth path.
564///
565/// A custom inner type writes `impl FixedStorage for MyKey {}`. Like [`CloneableSecret`],
566/// the compiler checks that you made the claim, not that it is true — see the trait docs
567/// for the test to apply and for why an assertion is the right trade here.
568pub use traits::FixedStorage;
569
570/// Encoded string **output wrapper** for zeroizing encoded output.
571///
572/// This is an **output wrapper** — it exists *only* to keep encoded data zeroized until
573/// it drops. It is **not** a secret wrapper like [`Fixed`]/[`Dynamic`] and does not
574/// accept [`CloneableSecret`] or [`SerializableSecret`] markers.
575///
576/// Returned by every encoding method (`to_hex`, `to_hex_upper`, `to_base32`,
577/// `to_base64url`, `try_to_bech32`, `try_to_bech32m`, and the `_sized::<N>` forms of
578/// the last two — only bech32 and bech32m take a code length).
579/// Wraps `Zeroizing<String>` with `Debug` → `[REDACTED]`. Its one accessor is
580/// `Deref<Target = str>`; there are deliberately no `AsRef` impls, because `Deref`
581/// already opens that door. Deliberately **no** `Display` either: `{}` on this type is
582/// a compile error, so `Debug` redaction cannot mislead a caller into logging the
583/// encoded secret. Write it out with `&*encoded`.
584///
585/// Use [`into_inner()`](EncodedSecret::into_inner) to extract a plain `String`
586/// (ends zeroization) or [`into_zeroizing()`](EncodedSecret::into_zeroizing) to keep
587/// the wiping. `into_zeroizing` is a partial downgrade: zeroize-on-drop survives, the
588/// redacted `Debug` does not, because `zeroize::Zeroizing` derives its own.
589///
590/// Requires `alloc` feature.
591#[cfg(feature = "alloc")]
592pub use traits::EncodedSecret;
593
594/// Marker trait that opts a secret type into Serde serialization. No methods — gates the
595/// `Serialize` impl on [`Fixed`] and [`Dynamic`]. Serialization exposes the full secret;
596/// audit every impl. Requires `serde-serialize` feature.
597///
598/// **`Deserialize` does NOT require this marker** — it is gated separately by the
599/// `serde-deserialize` feature with its own impls on the wrapper types directly. This
600/// marker controls `Serialize` only.
601///
602/// Implement this on your inner type `T`; `Fixed<T>` and `Dynamic<T>` then gain the
603/// gated `Serialize` impl automatically. **This marker is deliberately not implemented
604/// by default** on `Fixed<T>` or `Dynamic<T>` — serialization is an opt-in risk that
605/// must be explicitly enabled. Without the `serde-serialize` feature this type does not
606/// exist at all.
607///
608/// **Coherence note:** Rust's orphan rule prevents downstream crates from implementing
609/// this marker for foreign types such as `String`, `Vec<u8>`, or `[u8; N]` — so
610/// `Fixed<[u8; N]>` and `Dynamic<String>` themselves can never become `Serialize`. To
611/// opt a secret into serialization, define a newtype inner type in your crate (deriving
612/// `Zeroize` and `Serialize`) and implement the marker on it. This is intentional: it
613/// keeps every serializable secret type visible in *your* code. See the trait-level
614/// docs for a worked example.
615///
616/// See also [`CloneableSecret`] (the other opt-in marker trait).
617#[cfg(feature = "serde-serialize")]
618pub use traits::SerializableSecret;
619
620// Newtype macros (`dynamic_newtype!` needs `alloc`)
621mod macros;
622
623/// Decodes Base32 strings (`&str`) to `Vec<u8>`. Blanket impl for `AsRef<str>`.
624/// Requires `encoding-base32` + `alloc`. See [`ToBase32`] for the encoding counterpart.
625#[cfg(all(feature = "encoding-base32", feature = "alloc"))]
626pub use traits::FromBase32Str;
627
628/// Decodes Base64url strings (`&str`) to `Vec<u8>`. Blanket impl for `AsRef<str>`.
629/// Requires `encoding-base64` + `alloc`. See [`ToBase64Url`] for the encoding counterpart.
630#[cfg(all(feature = "encoding-base64", feature = "alloc"))]
631pub use traits::FromBase64UrlStr;
632
633/// Decodes Bech32 (BIP-173) strings to `Vec<u8>` with HRP validation.
634/// Requires `encoding-bech32` + `alloc`. See [`ToBech32`] for the encoding counterpart.
635#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
636pub use traits::FromBech32Str;
637
638/// Decodes Bech32m (BIP-350) strings to `Vec<u8>` with HRP validation.
639/// Requires `encoding-bech32` + `alloc`. See [`ToBech32m`] for the encoding counterpart.
640#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
641pub use traits::FromBech32mStr;
642
643/// Decodes hex strings (`&str`) to `Vec<u8>`. Blanket impl for `AsRef<str>`.
644/// Requires `encoding-hex` + `alloc`. See [`ToHex`] for the encoding counterpart.
645#[cfg(all(feature = "encoding-hex", feature = "alloc"))]
646pub use traits::FromHexStr;
647
648/// Encodes byte data as Base32 strings (RFC 4648 §6, uppercase, no padding).
649/// Blanket impl for `AsRef<[u8]> + `[`EncodableBytes`]. Returns [`EncodedSecret`].
650/// Requires `encoding-base32` + `alloc`.
651/// See [`FromBase32Str`] for the decoding counterpart.
652#[cfg(all(feature = "encoding-base32", feature = "alloc"))]
653pub use traits::ToBase32;
654
655/// Encodes byte data as Base64url strings (RFC 4648, URL-safe, no padding).
656/// Blanket impl for `AsRef<[u8]> + `[`EncodableBytes`]. Returns [`EncodedSecret`].
657/// Requires `encoding-base64` + `alloc`.
658/// See [`FromBase64UrlStr`] for the decoding counterpart.
659#[cfg(all(feature = "encoding-base64", feature = "alloc"))]
660pub use traits::ToBase64Url;
661
662/// Encodes byte data as Bech32 (BIP-173) strings. Blanket impl for
663/// `AsRef<[u8]> + `[`EncodableBytes`]; returns `Result<`[`EncodedSecret`]`, _>`.
664/// The plain methods use [`BECH32_CODE_LENGTH`]; `try_to_bech32_sized::<N>` takes the
665/// code length as a parameter. Requires `encoding-bech32` + `alloc`.
666/// See [`FromBech32Str`] for the decoding counterpart.
667#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
668pub use traits::ToBech32;
669
670/// Encodes byte data as Bech32m (BIP-350) strings. Blanket impl for
671/// `AsRef<[u8]> + `[`EncodableBytes`]; returns `Result<`[`EncodedSecret`]`, _>`.
672/// The plain methods use [`BECH32_CODE_LENGTH`]; `try_to_bech32m_sized::<N>` takes the
673/// code length as a parameter. Requires `encoding-bech32` + `alloc`.
674/// See [`FromBech32mStr`] for the decoding counterpart.
675#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
676pub use traits::ToBech32m;
677
678/// Opt-in marker for byte-shaped encoding inputs. Every `To*` trait is blanket
679/// implemented for `AsRef<[u8]> + EncodableBytes`; the second bound keeps string-shaped
680/// types out, so an already-encoded value cannot be silently encoded again. Implement it
681/// for your own byte newtype to make it encodable.
682#[cfg(feature = "encoding-bech32")]
683pub use traits::Case;
684
685#[cfg(any(
686 feature = "encoding-hex",
687 feature = "encoding-base32",
688 feature = "encoding-base64",
689 feature = "encoding-bech32",
690))]
691pub use traits::EncodableBytes;
692
693/// Bech32 (BIP-173) checksum with a caller-chosen code length. `N` caps the length of
694/// the whole encoded string and never enters the checksum. Above [`BECH32_CODE_LENGTH`]
695/// the BCH error-detection guarantee no longer holds - see the type docs.
696#[cfg(feature = "encoding-bech32")]
697pub use traits::{Bech32Sized, Bech32Standard};
698
699/// Bech32m (BIP-350) checksum with a caller-chosen code length - the bech32m twin of
700/// [`Bech32Sized`], with the same rules and the same guarantee boundary.
701#[cfg(feature = "encoding-bech32")]
702pub use traits::{Bech32mSized, Bech32mStandard};
703
704/// The bech32 BCH code length (1023) used by every non-`_sized` method, and the helper
705/// that sizes an `N` from an HRP length and a payload byte count.
706#[cfg(feature = "encoding-bech32")]
707pub use traits::{BECH32_CODE_LENGTH, bech32_code_length};
708
709/// Encodes byte data as hexadecimal strings (constant-time via `base16ct`).
710/// Blanket impl for `AsRef<[u8]> + `[`EncodableBytes`]. Provides `to_hex()` and
711/// `to_hex_upper()`, both returning [`EncodedSecret`].
712/// Requires `encoding-hex` + `alloc`.
713/// See [`FromHexStr`] for the decoding counterpart.
714#[cfg(all(feature = "encoding-hex", feature = "alloc"))]
715pub use traits::ToHex;
716
717/// Errors from Bech32 (BIP-173) and Bech32m (BIP-350) decoding.
718/// Variant shapes are identical in debug and release builds; no secret material
719/// (payload bytes, HRP strings) is ever carried in an error.
720#[cfg(feature = "encoding-bech32")]
721pub use error::Bech32Error;
722
723/// Errors from Base32 (RFC 4648 §6, uppercase, unpadded) decoding. Variant shapes are
724/// identical in debug and release builds; only numeric length metadata is carried.
725#[cfg(feature = "encoding-base32")]
726pub use error::Base32Error;
727
728/// Errors from Base64url decoding. Variant shapes are identical in debug and
729/// release builds; only numeric length metadata is carried.
730#[cfg(feature = "encoding-base64")]
731pub use error::Base64Error;
732
733/// Errors from hex decoding. Variant shapes are identical in debug and
734/// release builds; only numeric length metadata is carried.
735#[cfg(feature = "encoding-hex")]
736pub use error::HexError;
737
738/// Error returned when a byte slice cannot be converted to `Fixed<[u8; N]>` due to
739/// length mismatch. Produced by `Fixed::try_from(&[u8])`.
740pub use error::FromSliceError;
741
742// The README's examples are compiled and run by `cargo test --doc`, so they cannot
743// silently rot. `cfg(doctest)` is set only while rustdoc *collects doctests* — never
744// while it *builds documentation* — so this item never reaches docs.rs or `cargo doc`
745// output, and the README is not duplicated onto the crate page.
746//
747// Gated on `full` as well: the README documents the fully-featured crate, so its
748// examples use `alloc` and the encoding traits unconditionally. Without this gate the
749// `--no-default-features` doctest job in CI would fail on imports the README does not
750// feature-gate. CI's `--features full` and `--all-features` entries do run them.
751//
752// It must live at the end of the file: an item here would otherwise sit ahead of the
753// `//!` crate docs above, which is not allowed.
754#[cfg(all(doctest, feature = "full"))]
755#[doc = include_str!("../README.md")]
756pub struct ReadmeDoctests;