Skip to main content

secure_gate/
lib.rs

1// #![doc = include_str!("../README.md")] //uncomment for doctest runs
2
3// no_std by default; the `std` feature opts back into the standard library.
4// Verified in CI by cross-building for a bare-metal target.
5#![cfg_attr(not(feature = "std"), no_std)]
6// Forbid unsafe code unconditionally
7#![forbid(unsafe_code)]
8#![warn(missing_docs)]
9
10//! Secure wrappers for secrets with **explicit access** and **mandatory zeroization** — a
11//! `no_std`-compatible, zero-overhead library with audit-friendly access patterns.
12//!
13//! Secrets are **automatically zeroized on drop** (the inner type must implement
14//! [`Zeroize`](zeroize::Zeroize)). No `Deref`, no accidental leaks — callers access
15//! the inner secret only via [`RevealSecret`] / [`RevealSecretMut`]. `Debug` always
16//! prints `[REDACTED]`. All access follows a **3-tier model**: scoped closures (preferred),
17//! direct references (escape hatch), and owned extraction (consumption).
18//!
19//! # Which type should I use?
20//!
21//! | Type | Allocation | Use case | Feature |
22//! |------|-----------|----------|---------|
23//! | [`Fixed<T>`] | Stack | Keys, nonces, tokens — compile-time-known size | Always available |
24//! | [`Dynamic<T>`] | Heap | Passwords, API keys, ciphertexts — variable length | `alloc` (default) |
25//!
26//! Both types share the same [`RevealSecret`] / [`RevealSecretMut`] access API.
27//!
28//! # Quick start
29//!
30//! ```rust
31//! use secure_gate::{Fixed, RevealSecret};
32//!
33//! // Wrap a 32-byte key
34//! let key = Fixed::new([0x42u8; 32]);
35//!
36//! // Tier 1 — scoped access (preferred): secret ref cannot escape the closure
37//! let first = key.with_secret(|bytes| bytes[0]);
38//! assert_eq!(first, 0x42);
39//!
40//! // Tier 2 — direct reference (escape hatch for FFI / third-party APIs)
41//! assert_eq!(key.expose_secret().len(), 32);
42//!
43//! // Debug is always redacted
44//! assert_eq!(format!("{:?}", key), "[REDACTED]");
45//! // key is zeroized when dropped
46//! ```
47//!
48//! ```rust
49//! # #[cfg(feature = "alloc")]
50//! # {
51//! use secure_gate::{Dynamic, RevealSecret};
52//!
53//! let password: Dynamic<String> = Dynamic::new(String::from("hunter2"));
54//! let len = password.with_secret(|s: &String| s.len());
55//! assert_eq!(len, 7);
56//! # }
57//! ```
58//!
59//! ```rust
60//! use secure_gate::{fixed_alias, RevealSecret};
61//!
62//! fixed_alias!(pub Aes256Key, 32);
63//!
64//! let key: Aes256Key = [0xABu8; 32].into();
65//! key.with_secret(|b| assert_eq!(b.len(), 32));
66//! ```
67//!
68//! # Module structure
69//!
70//! ```text
71//! secure_gate (lib.rs)
72//! ├── Fixed<T>              ← always available, stack-allocated
73//! ├── Dynamic<T>            ← requires `alloc`, heap-allocated
74//! ├── traits/
75//! │   ├── RevealSecret      ← immutable access (always available)
76//! │   ├── RevealSecretMut   ← mutable access (always available)
77//! │   ├── revealed_secrets/
78//! │   │   ├── InnerSecret<T>    ← owned extraction wrapper
79//! │   │   └── EncodedSecret     ← zeroizing encoded string wrapper (alloc)
80//! │   ├── ConstantTimeEq    ← ct-eq feature
81//! │   ├── CloneableSecret   ← cloneable feature
82//! │   ├── SerializableSecret← serde-serialize feature
83//! │   ├── encoding/         ← ToHex, ToBase64Url, ToBech32, ToBech32m
84//! │   └── decoding/         ← FromHexStr, FromBase64UrlStr, FromBech32Str, FromBech32mStr
85//! ├── macros/               ← fixed_alias!, dynamic_alias!, etc.
86//! └── error                 ← FromSliceError, HexError, Base64Error, Bech32Error, DecodingError
87//! ```
88//!
89//! All public items are re-exported at the crate root. Use `secure_gate::Fixed`,
90//! not `secure_gate::fixed::Fixed`.
91//!
92//! # Type taxonomy
93//!
94//! | Category | Types | `Deref` to secret? | Purpose |
95//! |----------|-------|-------------------|---------|
96//! | **Secret wrappers** | [`Fixed<T>`], [`Dynamic<T>`] | No — use [`RevealSecret`] | Hold live secrets; `Debug` → `[REDACTED]` |
97//! | **Output wrappers** | [`InnerSecret<T>`], [`EncodedSecret`] | Yes — caller owns the data | Hold extracted or encoded results |
98//! | **Opt-in markers** | [`CloneableSecret`], [`SerializableSecret`] | — (no methods) | Implement on inner type `T` to unlock gated impls |
99//!
100//! `CloneableSecret` and `SerializableSecret` are implemented on the **inner type `T`**,
101//! not on `Fixed<T>` or `Dynamic<T>` directly. Output wrappers ([`InnerSecret`],
102//! [`EncodedSecret`]) are not secret wrappers and do not interact with these markers.
103//!
104//! # Import paths
105//!
106//! ```rust
107//! // ✅ Correct — always import from the crate root
108//! use secure_gate::{Fixed, RevealSecret};
109//!
110//! // ❌ Wrong — these internal paths compile but are not the public API
111//! // use secure_gate::traits::reveal_secret::RevealSecret;
112//! // use secure_gate::traits::encoding::hex::ToHex;
113//! ```
114//!
115//! # Method resolution: wrapper methods vs trait methods
116//!
117//! Encoding methods exist at **two levels** — both produce identical results:
118//!
119//! | Call style | Example | Appears in audit sweep? |
120//! |-----------|---------|------------------------|
121//! | **Wrapper inherent** (ergonomic) | `key.to_hex()` | No — grep for `to_hex` directly |
122//! | **Trait via scoped access** (audit-friendly) | `key.with_secret(\|b\| b.to_hex())` | Yes — `with_secret` is grep-able |
123//!
124//! The wrapper methods ([`Fixed::to_hex`], [`Dynamic::to_hex`](Dynamic::to_hex)) internally call
125//! `self.with_secret(|s| s.to_hex())` — they are convenience shorthands, not
126//! separate implementations.
127//!
128//! # Feature flags
129//!
130//! | Feature | Default | Description |
131//! |---------|---------|-------------|
132//! | `alloc` | **yes** | Heap types ([`Dynamic<T>`]), `Vec`/`String` zeroization |
133//! | `std` | no | Full `std` support (implies `alloc`) |
134//! | | | **Cryptographic** |
135//! | `ct-eq` | no | [`ConstantTimeEq`] via `subtle` — timing-safe comparison |
136//! | `rand` | no | `from_random()` / `from_rng()` — `no_std` for [`Fixed`] |
137//! | | | **Serialization** |
138//! | `serde-serialize` | no | Serde `Serialize` (requires [`SerializableSecret`] marker) |
139//! | `serde-deserialize` | no | Serde `Deserialize` with 1 MiB default limit |
140//! | `serde` | no | Both directions |
141//! | | | **Encoding** |
142//! | `encoding-hex` | no | [`ToHex`] / [`FromHexStr`] via `base16ct` (constant-time) |
143//! | `encoding-base64` | no | [`ToBase64Url`] / [`FromBase64UrlStr`] via `base64ct` (constant-time) |
144//! | `encoding-bech32` | no | [`ToBech32`] / [`FromBech32Str`] — BIP-173, extended ~5 KB limit |
145//! | `encoding-bech32m` | no | [`ToBech32m`] / [`FromBech32mStr`] — BIP-350, standard 90-byte limit |
146//! | `encoding` | no | All encoding features |
147//! | | | **Meta** |
148//! | `cloneable` | no | [`CloneableSecret`] opt-in cloning |
149//! | `full` | no | Everything |
150//!
151//! # What's available without `alloc`?
152//!
153//! With `default-features = false`:
154//! - [`Fixed<T>`], [`RevealSecret`], [`RevealSecretMut`], [`InnerSecret`]
155//! - [`Fixed::try_from_hex`](Fixed::try_from_hex), [`Fixed::try_from_base64url`](Fixed::try_from_base64url),
156//!   [`Fixed::try_from_bech32`](Fixed::try_from_bech32), [`Fixed::try_from_bech32m`](Fixed::try_from_bech32m)
157//!   (no-alloc stack-based decoding)
158//! - [`fixed_alias!`], [`fixed_generic_alias!`]
159//! - [`FromSliceError`]
160//!
161//! **Not** available without `alloc`: [`Dynamic<T>`], [`EncodedSecret`],
162//! encoding traits ([`ToHex`], etc.), decoding traits ([`FromHexStr`], etc.),
163//! [`dynamic_alias!`], [`dynamic_generic_alias!`], serde support.
164//!
165//! # `no_std`
166//!
167//! `no_std` compatible (`#![no_std]` unless the `std` feature is enabled — verified in CI
168//! by cross-building for `thumbv7em-none-eabihf`). [`Fixed<T>`] works without `alloc`.
169//! Enable `alloc` (default) for [`Dynamic<T>`]. For pure stack / embedded builds, use
170//! `default-features = false`. MSRV: **1.85** (Rust edition 2024).
171//!
172//! One caveat: the `rand` feature compiles without `std` or `alloc`, but
173//! [`Fixed::from_random`] relies on `getrandom`, which needs a platform entropy backend.
174//! On bare-metal targets you must configure one (see the `getrandom` crate's
175//! "custom backend" documentation); without it, builds enabling `rand` fail at link
176//! time on such targets. [`Fixed::from_rng`] with a caller-supplied RNG has no such
177//! requirement.
178//!
179//! # Security
180//!
181//! This crate has **not** undergone an independent security audit. No unsafe code —
182//! enforced with `#![forbid(unsafe_code)]`. Prefer scoped access ([`RevealSecret::with_secret`])
183//! over direct references. Prefer zeroizing encoding variants (`to_hex_zeroizing`, etc.)
184//! when the encoded form is sensitive. See
185//! [SECURITY.md](https://github.com/Slurp9187/secure-gate/blob/main/secure-gate-core/SECURITY.md)
186//! for the full threat model.
187//!
188//! # Inherent Rust Limitations
189//!
190//! Three universal limits apply to any in-memory secret library in Rust (and in
191//! C, C++, and Go) — `secure-gate` documents them honestly rather than
192//! overclaiming:
193//!
194//! - **Stack-move residue**: moving a [`Fixed<T>`] by value leaves the prior stack
195//!   slot uncleared until a later frame overwrites it. Prefer
196//!   [`Fixed::new_with`](Fixed::new_with), pass by reference, or use
197//!   [`Dynamic<T>`] for long-lived secrets.
198//! - **Heap-reallocation residue**: `Vec` / `String` realloc frees the old buffer
199//!   unzeroed. Pre-size, use `Dynamic<[u8; N]>` (boxed array — no realloc surface),
200//!   or install a zero-on-dealloc global allocator such as `zeroizing-alloc` at
201//!   the binary level.
202//! - **Swap / core dumps**: process memory paged to disk or written on crash is
203//!   outside any in-process library's reach. Use OS facilities (`mlock`,
204//!   encrypted swap, disabled core dumps).
205//!
206//! Full discussion in
207//! [SECURITY.md § Inherent Rust Limitations](https://github.com/Slurp9187/secure-gate/blob/main/secure-gate-core/SECURITY.md#inherent-rust-limitations).
208//!
209//! See the [README](https://github.com/Slurp9187/secure-gate/blob/main/README.md) and
210//! [SECURITY.md](https://github.com/Slurp9187/secure-gate/blob/main/SECURITY.md) for full details.
211
212#[cfg(feature = "alloc")]
213extern crate alloc;
214
215#[cfg(feature = "alloc")]
216mod dynamic;
217
218/// Fixed-size secret wrapper types - always available with zero dependencies.
219/// These provide fundamental secure storage abstractions for fixed-size data.
220mod fixed;
221
222/// Centralized error types - always available.
223mod error;
224
225/// Core traits for wrapper polymorphism - always available.
226pub mod traits;
227
228/// Heap-allocated secret wrapper with explicit access and automatic zeroization on drop.
229///
230/// Variable-length secrets (passwords, API keys, ciphertexts). Inner type must implement
231/// `Zeroize`. Secret bytes live on the heap only — never on the stack. Requires `alloc`.
232///
233/// See [`Fixed<T>`] for the stack-allocated alternative.
234///
235/// ```rust
236/// # #[cfg(feature = "alloc")]
237/// # {
238/// use secure_gate::{Dynamic, RevealSecret};
239///
240/// let pw: Dynamic<String> = Dynamic::new(String::from("hunter2"));
241/// assert_eq!(pw.with_secret(|s: &String| s.len()), 7);
242/// assert_eq!(format!("{:?}", pw), "[REDACTED]");
243/// # }
244/// ```
245#[cfg(feature = "alloc")]
246pub use dynamic::Dynamic;
247
248/// Cursor-like reader over [`Dynamic<Vec<u8>>`] — see [`Dynamic::as_reader`].
249#[cfg(feature = "std")]
250pub use dynamic::DynamicReader;
251
252#[cfg(all(feature = "alloc", feature = "serde-deserialize"))]
253/// Default maximum byte length for `Dynamic<Vec<u8>>` / `Dynamic<String>` deserialization (1 MiB).
254///
255/// The standard `serde::Deserialize` impl for both types rejects payloads exceeding this value.
256/// Pass a custom ceiling to [`Dynamic::deserialize_with_limit`] when a different limit is needed.
257///
258/// **Important:** this limit is enforced *after* the upstream deserializer has fully
259/// materialized the payload. It is a **result-length acceptance bound**, not a
260/// pre-allocation DoS guard. For untrusted input, enforce size limits at the
261/// transport or parser layer upstream.
262pub use dynamic::MAX_DESERIALIZE_BYTES;
263
264/// Stack-allocated secret wrapper with explicit access and automatic zeroization on drop.
265///
266/// Fixed-size secrets (keys, nonces, tokens). Inner type must implement `Zeroize`.
267/// Always available — works without `alloc`. Prefer [`new_with`](Fixed::new_with) over
268/// [`new`](Fixed::new) when minimizing stack residue matters.
269///
270/// See [`Dynamic<T>`] for the heap-allocated alternative.
271///
272/// ```rust
273/// use secure_gate::{Fixed, RevealSecret};
274///
275/// let key = Fixed::new([0xABu8; 32]);
276/// key.with_secret(|b| assert_eq!(b[0], 0xAB));
277/// assert_eq!(format!("{:?}", key), "[REDACTED]");
278/// ```
279pub use fixed::Fixed;
280
281/// Marker trait that opts a secret type into cloning. No methods — gates the `Clone`
282/// impl on [`Fixed`] and [`Dynamic`]. Each clone is independently zeroized on drop,
283/// but increases the in-memory exposure surface. Requires `cloneable` feature.
284///
285/// Implement this on your inner type `T`; `Fixed<T>` and `Dynamic<T>` then gain the
286/// gated `Clone` impl automatically. **This marker is deliberately not implemented by
287/// default** on `Fixed<T>` or `Dynamic<T>` — cloning is an opt-in risk that must be
288/// explicitly enabled. Without the `cloneable` feature this type does not exist at all.
289///
290/// **Coherence note:** Rust's orphan rule prevents downstream crates from implementing
291/// this marker for foreign types such as `String`, `Vec<u8>`, or `[u8; N]` — so
292/// `Dynamic<String>` and `Fixed<[u8; N]>` themselves can never become `Clone`. To opt
293/// a secret into cloning, define a newtype inner type in your crate (deriving
294/// `Zeroize` and `Clone`) and implement the marker on it. This is intentional: it keeps
295/// every cloneable secret type visible in *your* code. See the trait-level docs for a
296/// worked example.
297///
298/// See also [`SerializableSecret`] (the other opt-in marker trait).
299#[cfg(feature = "cloneable")]
300pub use traits::CloneableSecret;
301
302/// Constant-time equality for secrets — prevents timing side-channel attacks.
303///
304/// Provides [`ct_eq()`](ConstantTimeEq::ct_eq) via the `subtle` crate. `==` is
305/// **deliberately not implemented** on [`Fixed`] / [`Dynamic`] — always use `ct_eq`.
306/// Requires `ct-eq` feature.
307///
308/// ```rust
309/// # #[cfg(feature = "ct-eq")]
310/// # {
311/// use secure_gate::{Fixed, ConstantTimeEq};
312///
313/// let a = Fixed::new([1u8; 32]);
314/// let b = Fixed::new([1u8; 32]);
315/// assert!(a.ct_eq(&b));
316/// # }
317/// ```
318#[cfg(feature = "ct-eq")]
319pub use traits::ConstantTimeEq;
320
321/// Explicit immutable access to secret contents (3-tier access model).
322///
323/// - **Tier 1** (preferred): [`with_secret()`](RevealSecret::with_secret) — scoped closure,
324///   borrow cannot escape.
325/// - **Tier 2** (escape hatch): [`expose_secret()`](RevealSecret::expose_secret) — direct
326///   `&T` reference for FFI / third-party APIs.
327/// - **Tier 3** (consumption): [`into_inner()`](RevealSecret::into_inner) — returns
328///   [`InnerSecret<T>`] with zeroization transferred to caller.
329/// - **Metadata**: [`len()`](RevealSecret::len) / [`is_empty()`](RevealSecret::is_empty) —
330///   no secret exposure.
331///
332/// See [`RevealSecretMut`] for the mutable counterpart.
333pub use traits::RevealSecret;
334
335/// Explicit mutable access to secret contents.
336///
337/// Extends [`RevealSecret`]. Prefer [`with_secret_mut()`](RevealSecretMut::with_secret_mut)
338/// (Tier 1) over [`expose_secret_mut()`](RevealSecretMut::expose_secret_mut) (Tier 2).
339/// Only [`Fixed`] and [`Dynamic`] implement this — read-only wrappers deliberately do not.
340pub use traits::RevealSecretMut;
341
342/// Owned extraction **output wrapper** returned by [`RevealSecret::into_inner`] (Tier 3 access).
343///
344/// Wraps [`Zeroizing<T>`](zeroize::Zeroizing) with `Debug` → `[REDACTED]`. Implements
345/// `Deref<Target = T>` for ergonomic access (the **only** type in this crate that derefs
346/// to the secret — [`Fixed`] and [`Dynamic`] deliberately do not).
347///
348/// This is an **output wrapper**, not a secret wrapper like [`Fixed`]/[`Dynamic`] — it
349/// holds the owned result of Tier 3 extraction, with zeroization transferred to the
350/// caller. See also [`EncodedSecret`] (the other output wrapper, for encoded strings).
351/// Use [`into_zeroizing()`](InnerSecret::into_zeroizing) when an API requires
352/// `Zeroizing<T>` directly.
353pub use traits::InnerSecret;
354
355/// Placeholder values left behind by [`RevealSecret::into_inner`] (Tier 3 access).
356///
357/// When `into_inner` moves the real secret out of a wrapper, it must leave *something*
358/// behind for the wrapper's `Drop` impl to zeroize. `SentinelValue::sentinel_value()`
359/// produces that inert placeholder (an all-default array, empty `String`, or empty
360/// `Vec`). Implemented for `[T; N]` (any `N`, `T: Default`), `String`, and `Vec<T>`.
361///
362/// Implement this for your own inner types to make `into_inner` available on wrappers
363/// around them. A sentinel must never contain secret material.
364pub use traits::SentinelValue;
365
366/// Encoded string **output wrapper** for zeroizing encoded output.
367///
368/// This is an **output wrapper** — it exists *only* to keep encoded data zeroized until
369/// it drops. It is **not** a secret wrapper like [`Fixed`]/[`Dynamic`] and does not
370/// accept [`CloneableSecret`] or [`SerializableSecret`] markers. See also
371/// [`InnerSecret`] (the other output wrapper, for owned secret extraction).
372///
373/// Returned by all `*_zeroizing` encoding methods (`to_hex_zeroizing`,
374/// `to_base64url_zeroizing`, `try_to_bech32_zeroizing`, etc.). Wraps
375/// `Zeroizing<String>` with `Debug` → `[REDACTED]`. Implements `Deref<Target = str>`
376/// and `Display`.
377///
378/// Use [`into_inner()`](EncodedSecret::into_inner) to extract a plain `String`
379/// (ends zeroization) or [`into_zeroizing()`](EncodedSecret::into_zeroizing) to
380/// preserve it.
381///
382/// Requires `alloc` feature.
383#[cfg(feature = "alloc")]
384pub use traits::EncodedSecret;
385
386/// Marker trait that opts a secret type into Serde serialization. No methods — gates the
387/// `Serialize` impl on [`Fixed`] and [`Dynamic`]. Serialization exposes the full secret;
388/// audit every impl. Requires `serde-serialize` feature.
389///
390/// **`Deserialize` does NOT require this marker** — it is gated separately by the
391/// `serde-deserialize` feature with its own impls on the wrapper types directly. This
392/// marker controls `Serialize` only.
393///
394/// Implement this on your inner type `T`; `Fixed<T>` and `Dynamic<T>` then gain the
395/// gated `Serialize` impl automatically. **This marker is deliberately not implemented
396/// by default** on `Fixed<T>` or `Dynamic<T>` — serialization is an opt-in risk that
397/// must be explicitly enabled. Without the `serde-serialize` feature this type does not
398/// exist at all.
399///
400/// **Coherence note:** Rust's orphan rule prevents downstream crates from implementing
401/// this marker for foreign types such as `String`, `Vec<u8>`, or `[u8; N]` — so
402/// `Fixed<[u8; N]>` and `Dynamic<String>` themselves can never become `Serialize`. To
403/// opt a secret into serialization, define a newtype inner type in your crate (deriving
404/// `Zeroize` and `Serialize`) and implement the marker on it. This is intentional: it
405/// keeps every serializable secret type visible in *your* code. See the trait-level
406/// docs for a worked example.
407///
408/// See also [`CloneableSecret`] (the other opt-in marker trait).
409#[cfg(feature = "serde-serialize")]
410pub use traits::SerializableSecret;
411
412// Type alias macros (always available)
413mod macros;
414
415/// Decodes Base64url strings (`&str`) to `Vec<u8>`. Blanket impl for `AsRef<str>`.
416/// Requires `encoding-base64` + `alloc`. See [`ToBase64Url`] for the encoding counterpart.
417#[cfg(all(feature = "encoding-base64", feature = "alloc"))]
418pub use traits::FromBase64UrlStr;
419
420/// Decodes Bech32 (BIP-173) strings to `Vec<u8>` with HRP validation.
421/// Requires `encoding-bech32` + `alloc`. See [`ToBech32`] for the encoding counterpart.
422#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
423pub use traits::FromBech32Str;
424
425/// Decodes Bech32m (BIP-350) strings to `Vec<u8>` with HRP validation.
426/// Requires `encoding-bech32m` + `alloc`. See [`ToBech32m`] for the encoding counterpart.
427#[cfg(all(feature = "encoding-bech32m", feature = "alloc"))]
428pub use traits::FromBech32mStr;
429
430/// Decodes hex strings (`&str`) to `Vec<u8>`. Blanket impl for `AsRef<str>`.
431/// Requires `encoding-hex` + `alloc`. See [`ToHex`] for the encoding counterpart.
432#[cfg(all(feature = "encoding-hex", feature = "alloc"))]
433pub use traits::FromHexStr;
434
435/// Encodes byte data as Base64url strings (RFC 4648, URL-safe, no padding).
436/// Blanket impl for `AsRef<[u8]>`. Requires `encoding-base64` + `alloc`.
437/// See [`FromBase64UrlStr`] for the decoding counterpart.
438#[cfg(all(feature = "encoding-base64", feature = "alloc"))]
439pub use traits::ToBase64Url;
440
441/// Encodes byte data as Bech32 (BIP-173) strings with extended ~5 KB payload limit.
442/// Blanket impl for `AsRef<[u8]>`. Requires `encoding-bech32` + `alloc`.
443/// See [`FromBech32Str`] for the decoding counterpart.
444#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
445pub use traits::ToBech32;
446
447/// Encodes byte data as Bech32m (BIP-350) strings with standard 90-byte payload limit.
448/// Blanket impl for `AsRef<[u8]>`. Requires `encoding-bech32m` + `alloc`.
449/// See [`FromBech32mStr`] for the decoding counterpart.
450#[cfg(all(feature = "encoding-bech32m", feature = "alloc"))]
451pub use traits::ToBech32m;
452
453/// Encodes byte data as hexadecimal strings (constant-time via `base16ct`).
454/// Blanket impl for `AsRef<[u8]>`. Provides `to_hex()`, `to_hex_upper()`, and
455/// zeroizing variants. Requires `encoding-hex` + `alloc`.
456/// See [`FromHexStr`] for the decoding counterpart.
457#[cfg(all(feature = "encoding-hex", feature = "alloc"))]
458pub use traits::ToHex;
459
460/// Marker trait for types that support secure decoding (`AsRef<str>`). No methods —
461/// enables blanket impls of [`FromHexStr`], [`FromBase64UrlStr`], etc.
462#[cfg(any(
463    feature = "encoding-hex",
464    feature = "encoding-base64",
465    feature = "encoding-bech32",
466    feature = "encoding-bech32m",
467))]
468pub use traits::SecureDecoding;
469
470/// Marker trait for types that support secure encoding (`AsRef<[u8]>`). No methods —
471/// enables blanket impls of [`ToHex`], [`ToBase64Url`], etc.
472#[cfg(any(
473    feature = "encoding-hex",
474    feature = "encoding-base64",
475    feature = "encoding-bech32",
476    feature = "encoding-bech32m",
477))]
478pub use traits::SecureEncoding;
479
480/// Errors from Bech32 (BIP-173) and Bech32m (BIP-350) decoding.
481/// Variant shapes are identical in debug and release builds; no secret material
482/// (payload bytes, HRP strings) is ever carried in an error.
483#[cfg(any(feature = "encoding-bech32", feature = "encoding-bech32m"))]
484pub use error::Bech32Error;
485
486/// Errors from Base64url decoding. Variant shapes are identical in debug and
487/// release builds; only numeric length metadata is carried.
488#[cfg(feature = "encoding-base64")]
489pub use error::Base64Error;
490
491/// Errors from hex decoding. Variant shapes are identical in debug and
492/// release builds; only numeric length metadata is carried.
493#[cfg(feature = "encoding-hex")]
494pub use error::HexError;
495
496/// Unified error type wrapping format-specific decoding errors ([`HexError`],
497/// [`Base64Error`], [`Bech32Error`]). Always available; variants depend on enabled features.
498pub use error::DecodingError;
499
500/// Error returned when a byte slice cannot be converted to `Fixed<[u8; N]>` due to
501/// length mismatch. Produced by `Fixed::try_from(&[u8])`.
502pub use error::FromSliceError;