Skip to main content

secure_gate/
dynamic.rs

1//! Heap-allocated wrapper for variable-length secrets.
2//!
3//! > **Import path:** `use secure_gate::Dynamic;` (not `secure_gate::dynamic::Dynamic`)
4//!
5//! [`Dynamic<T>`] is a zero-cost wrapper that enforces explicit, auditable access to
6//! sensitive data stored on the heap. It is the primary secret type for variable-length
7//! material such as passwords, API keys, and ciphertexts. Requires the `alloc` feature.
8//!
9//! # Security invariants
10//!
11//! - **No `Deref`, `AsRef`, or `Copy`** — the inner value cannot leak through
12//!   implicit conversions.
13//! - **`Debug` always prints `[REDACTED]`** — secrets never appear in logs or
14//!   panic messages.
15//! - **Unconditional zeroization on drop** — includes `Vec`/`String` spare capacity.
16//! - **Heap-only** — secret bytes never reside on the stack. Inner value stored in `Box<T>`.
17//! - **Opt-in `Clone`** — requires `T: CloneableSecret` and the `cloneable` feature.
18//! - **Opt-in `Serialize`** — requires the `SerializableSecret` marker and the
19//!   `serde-serialize`/`serde-deserialize` features.
20//! - **Panic safety** — all decode constructors use the `from_protected_bytes` pattern:
21//!   a `Zeroizing` wrapper survives OOM panics from `Box::new`.
22//!
23//! # Choosing the inner type for security
24//!
25//! Different inner types have different reallocation-residue profiles:
26//!
27//! | Inner type | Realloc surface | Use case |
28//! |---|---|---|
29//! | `Dynamic<[u8; N]>` (boxed array) | **None** — fixed size | Long-lived known-size keys held on the heap |
30//! | `Dynamic<[u8]>` (boxed slice) | **None** once built — the length is set at construction and cannot change | Secrets whose length is known only at runtime but never changes afterwards |
31//! | `Dynamic<Vec<u8>>` pre-sized | None *if* you avoid capacity-growing mutations | Variable-length secrets with a known upper bound |
32//! | `Dynamic<Vec<u8>>` growable | **Yes** — each realloc leaves the old buffer unzeroed | Convenient, but see realloc-residue warning below |
33//! | `Dynamic<String>` | Same as `Dynamic<Vec<u8>>` | Passwords, API keys |
34//!
35//! For **long-lived, known-size key material**, prefer `Dynamic<[u8; N]>` —
36//! it combines `Dynamic`'s heap-only property with zero realloc surface.
37//! See `SECURITY.md` § "Inherent Rust Limitations" for the broader discussion.
38//!
39//! When the length is fixed but only known at runtime, `Dynamic<[u8]>` gives the same
40//! guarantee by construction: `with_secret_mut` hands the closure a `&mut [u8]`, which has
41//! no `push`, `extend` or `reserve`, so a reallocation is not expressible rather than merely
42//! discouraged. Two things to know before choosing it:
43//!
44//! - **Build the `Vec` at its exact final capacity.** `Dynamic::<[u8]>::new(vec)` converts
45//!   through `Box<[u8]>::from(Vec)`, which shrinks a `Vec` with excess capacity into a
46//!   newly allocated buffer and frees the old one unwiped. `vec![0u8; len]` is already
47//!   exact; a `Vec` that was grown or truncated is not.
48//! - **It is the narrowest `Dynamic` shape.** Scoped access, [`Debug`](core::fmt::Debug)
49//!   redaction, zeroize-on-drop and `ct_eq` (with `ct-eq`) work. `SecretLen::len`,
50//!   `new_with`, `from_random`, `into_inner`, the encoding traits, serde and `Clone` are
51//!   not implemented for it; read the length with `with_secret(|s| s.len())`.
52//!
53//! ```
54//! use secure_gate::{Dynamic, RevealSecret, RevealSecretMut};
55//!
56//! let len = 32; // known only at runtime
57//! let mut key: Dynamic<[u8]> = Dynamic::new(vec![0u8; len]); // exact capacity: no shrink
58//! key.with_secret_mut(|k: &mut [u8]| k.fill(0x42)); // `&mut [u8]` — cannot grow
59//! assert_eq!(key.with_secret(|k| k.len()), 32);
60//! ```
61//!
62//! # Construction
63//!
64//! | Constructor | Notes |
65//! |---|---|
66//! | [`Dynamic::<Vec<u8>>::new_with(len, f)`](Dynamic::new_with) | **Preferred.** Writes into a sized, pre-zeroed slot inside the wrapper; growth is not expressible |
67//! | [`Dynamic::<Vec<u8>>::try_new_with(len, f)`](Dynamic::try_new_with) | The same, for a fill that can fail; wipes the partial write on `Err` |
68//! | [`Dynamic::new(value)`](Dynamic::new) | **Moves** the buffer in — no copy. Only as safe as how you built the value |
69//! | [`From<&str>`](Dynamic::from) / [`From<&[u8]>`](Dynamic::from) | **Copies**; your source buffer is left unwiped |
70//!
71//! Choosing between them is about where the bytes are written, not convenience:
72//!
73//! - **Length known before the fill** — use `new_with(len, …)`. That is two allocations,
74//!   not one: the slot, of exactly `len` bytes, plus the `Box<Vec<u8>>` header every
75//!   `Dynamic<Vec<u8>>` is built around. `tests/lifecycle_trace_heap.rs` measures
76//!   `from_random(len)` — this constructor with an RNG in the closure — at exactly two,
77//!   and `Dynamic::new(owned)` at exactly one, because a move brings its buffer with it.
78//!   So the recommendation is not about allocation count; it is about the slot's shape.
79//!   Capacity equals length, so the wrapper never holds slack, and the closure is handed a
80//!   slice, so growth is not expressible at all.
81//! - **Length not known** — use `Dynamic::new(Vec::new())` and write through the
82//!   [`std::io::Write`] impl, which grows by hand and **zeroizes each abandoned buffer**
83//!   before releasing it. It is the one growth path here that does. Requires `std`.
84//! - **You already own the value** — `new(owned)` moves the allocation rather than
85//!   copying it, so the buffer you filled becomes the buffer the wrapper protects. That
86//!   is necessary but not sufficient: if you *grew* the value to build it, those
87//!   reallocations already happened and the copies are already on the heap. Build without
88//!   growth — `with_capacity` at the exact final size, or one `resize` — then move it in.
89//!
90//! `Dynamic<String>` has no closure constructor. It cannot: `String` must hold valid
91//! UTF-8 and characters have variable byte widths, so a fixed byte window is not a place
92//! you can write arbitrary text. Build a pre-sized `String` and move it in with
93//! [`Dynamic::new`].
94//!
95//! # 3-tier access model
96//!
97//! ```rust
98//! # #[cfg(feature = "alloc")]
99//! # {
100//! use secure_gate::{Dynamic, RevealSecret, RevealSecretMut, SecretLen};
101//!
102//! let mut pw: Dynamic<String> = Dynamic::new(String::from("hunter2"));
103//!
104//! // Tier 1 — scoped (preferred): borrow is confined to the closure.
105//! let len = pw.with_secret(|s: &String| s.len());
106//! assert_eq!(len, 7);
107//!
108//! // Tier 1 mutable — scoped mutation. Prefer capacity-stable mutations for
109//! // Dynamic<String> / Dynamic<Vec<u8>>; see SECURITY.md for realloc notes.
110//! pw.with_secret_mut(|s: &mut String| s.make_ascii_uppercase());
111//!
112//! // Tier 2 — direct reference (escape hatch).
113//! assert_eq!(pw.expose_secret(), "HUNTER2");
114//!
115//! // Tier 3 — transfer ownership. Protection ends here.
116//! let owned: String = pw.into_inner();
117//! assert_eq!(owned, "HUNTER2");
118//! # }
119//! ```
120//!
121//! # Warning
122//!
123//! Ensure your profile sets `panic = "unwind"` — `panic = "abort"` skips destructors
124//! and therefore skips zeroization. (`Dynamic` cannot be `static` since it requires
125//! `Box` allocation, so the static-secret warning from `Fixed` does not apply.)
126//!
127//! For `Dynamic<Vec<_>>` and `Dynamic<String>`, capacity-changing mutations can
128//! cause the standard allocator to free the previous buffer without zeroizing it.
129//! Pre-allocate to the maximum size, use capacity-stable mutations, prefer
130//! `Dynamic<[u8; N]>` (no realloc surface) for known-size heap secrets, or use
131//! [`Fixed<T>`](crate::Fixed) for known-size stack secrets. See `SECURITY.md` for
132//! the full threat-model discussion and deployment-level mitigations including
133//! the [`zeroizing-alloc`](https://crates.io/crates/zeroizing-alloc) global
134//! allocator.
135//!
136//! # See also
137//!
138//! - [`Fixed<T>`](crate::Fixed) — stack-allocated alternative for fixed-size secrets
139//!   (always available, no `alloc` required).
140
141#[cfg(feature = "alloc")]
142extern crate alloc;
143use alloc::boxed::Box;
144use alloc::string::{String, ToString};
145use alloc::vec::Vec;
146use zeroize::Zeroize;
147
148#[cfg(any(
149    feature = "encoding-hex",
150    feature = "encoding-base32",
151    feature = "encoding-base64",
152    feature = "encoding-bech32",
153    feature = "ct-eq",
154    feature = "std",
155))]
156use crate::RevealSecret;
157
158// Encoding traits
159#[cfg(feature = "encoding-base32")]
160use crate::traits::encoding::base32::ToBase32;
161#[cfg(feature = "encoding-base64")]
162use crate::traits::encoding::base64_url::ToBase64Url;
163#[cfg(feature = "encoding-bech32")]
164use crate::traits::encoding::bech32::ToBech32;
165#[cfg(feature = "encoding-bech32")]
166use crate::traits::encoding::bech32m::ToBech32m;
167#[cfg(feature = "encoding-hex")]
168use crate::traits::encoding::hex::ToHex;
169
170#[cfg(feature = "rand")]
171use rand::{TryCryptoRng, TryRng, rngs::SysRng};
172
173// Dynamic<Vec<u8>> is always alloc-dependent, so the alloc-gated blanket traits
174// are always available when encoding features are enabled for this type.
175#[cfg(feature = "encoding-base32")]
176use crate::traits::decoding::base32::FromBase32Str;
177#[cfg(feature = "encoding-base64")]
178use crate::traits::decoding::base64_url::FromBase64UrlStr;
179#[cfg(feature = "encoding-bech32")]
180use crate::traits::decoding::bech32::FromBech32Str;
181#[cfg(feature = "encoding-bech32")]
182use crate::traits::decoding::bech32m::FromBech32mStr;
183#[cfg(feature = "encoding-hex")]
184use crate::traits::decoding::hex::FromHexStr;
185
186/// Zero-cost heap-allocated wrapper for variable-length secrets.
187///
188/// `Dynamic<T>` stores a `T: Zeroize` value in a `Box<T>` and unconditionally zeroizes
189/// it on drop (including `Vec`/`String` spare capacity). There is no `Deref`, `AsRef`,
190/// or `Copy` — every access is explicit through [`RevealSecret`](crate::RevealSecret)
191/// or [`RevealSecretMut`](crate::RevealSecretMut).
192///
193/// This is **not** `Fixed<T>` — it is the heap-allocated alternative for variable-length
194/// secrets. Secret bytes never reside on the stack.
195///
196/// # Examples
197///
198/// ```rust
199/// # #[cfg(feature = "alloc")]
200/// # {
201/// use secure_gate::{Dynamic, RevealSecret, SecretLen};
202///
203/// let pw: Dynamic<String> = Dynamic::new(String::from("hunter2"));
204/// assert_eq!(pw.with_secret(|s: &String| s.len()), 7);
205/// assert_eq!(format!("{:?}", pw), "[REDACTED]");
206/// # }
207/// ```
208///
209/// # Constructors for `Dynamic<Vec<u8>>`
210///
211/// | Constructor | Feature | Notes |
212/// |---|---|---|
213/// | [`new_with(len, f)`](Self::new_with) | — | **Preferred.** Sized, pre-zeroed slot inside the wrapper |
214/// | [`try_new_with(len, f)`](Self::try_new_with) | — | The same, fallible; wipes the partial write on `Err` |
215/// | [`new(value)`](Self::new) | — | **Moves** the buffer in. Accepts `Vec<u8>`, `&[u8]`, `Box<Vec<u8>>` |
216/// | [`try_from_hex(s)`](Self::try_from_hex) | `encoding-hex` | Constant-time hex decoding |
217/// | [`try_from_base32(s)`](Self::try_from_base32) | `encoding-base32` | Constant-time Base32 decoding |
218/// | [`try_from_base64url(s)`](Self::try_from_base64url) | `encoding-base64` | Constant-time Base64url decoding |
219/// | [`try_from_bech32(s, hrp)`](Self::try_from_bech32) | `encoding-bech32` | HRP-validated Bech32 |
220/// | [`try_from_bech32_unchecked(s)`](Self::try_from_bech32_unchecked) | `encoding-bech32` | Bech32 without HRP check |
221/// | [`try_from_bech32m(s, hrp)`](Self::try_from_bech32m) | `encoding-bech32` | HRP-validated Bech32m |
222/// | [`try_from_bech32m_unchecked(s)`](Self::try_from_bech32m_unchecked) | `encoding-bech32` | Bech32m without HRP check |
223/// | [`from_random(len)`](Self::from_random) | `rand` | System RNG |
224/// | [`from_rng(len, rng)`](Self::from_rng) | `rand` | Custom RNG |
225///
226/// # See also
227///
228/// - [`RevealSecret`](crate::RevealSecret) / [`RevealSecretMut`](crate::RevealSecretMut) — the 3-tier access traits.
229/// - [`Fixed<T>`](crate::Fixed) — stack-allocated alternative.
230#[must_use = "Dynamic<T> holds secret material; dropping it on the floor usually indicates a bug — bind with `let _name = ...` or chain a method call"]
231pub struct Dynamic<T: ?Sized + zeroize::Zeroize> {
232    inner: Box<T>,
233}
234
235impl<T: ?Sized + zeroize::Zeroize> Dynamic<T> {
236    /// Wraps `value` in a `Box<T>` and returns a `Dynamic<T>`.
237    ///
238    /// Accepts any type that implements `Into<Box<T>>` — including owned values,
239    /// `Box<T>`, `String`, `Vec<u8>`, `&str` (via the blanket `From<&str>` impl), etc.
240    ///
241    /// Equivalent to `Dynamic::from(value)` — `#[doc(alias = "from")]` is set so both
242    /// names appear in docs.rs search.
243    ///
244    /// Requires the `alloc` feature (which `Dynamic<T>` itself always requires).
245    #[doc(alias = "from")]
246    #[inline(always)]
247    pub fn new<U>(value: U) -> Self
248    where
249        U: Into<Box<T>>,
250    {
251        let inner = value.into();
252        Self { inner }
253    }
254}
255
256/// Moves an already-boxed value in — zero-copy, no allocation at all. The box the caller
257/// already owns becomes the box the wrapper owns, so whatever buffer sits behind `T` never
258/// moves and never gets a second copy taken of it.
259///
260/// "Already boxed" describes the shape of the value at the moment it arrives here, not how
261/// it was built. A `Vec<u8>` pushed onto in a loop and later turned into a `Box<[u8]>`, or a
262/// `String` grown with `push_str` before being boxed, already paid for every one of those
263/// reallocations before this impl runs — and each reallocation frees the old block without
264/// zeroizing it, the same mechanism
265/// `tests/lifecycle_trace_heap.rs::check_growth_orphan_retains_secret_vec` (and its
266/// `_string` sibling) measures happening to a value already *inside* the wrapper. Nothing
267/// this impl does can reach backward and clean that up; it only ever protects the buffer it
268/// ends up holding. Build without growth — `with_capacity` at the exact final size, or one
269/// `resize` — before boxing, then move the box in.
270impl<T: ?Sized + zeroize::Zeroize> From<Box<T>> for Dynamic<T> {
271    #[inline(always)]
272    fn from(boxed: Box<T>) -> Self {
273        Self { inner: boxed }
274    }
275}
276
277/// Copies a byte slice to the heap and wraps the copy. The source slice is left exactly as
278/// it was — this impl protects the copy it makes, not the memory it copied from, and leaving
279/// that memory unwiped is the caller's problem to solve.
280///
281/// `tests/lifecycle_trace_heap.rs::check_str_and_slice_conversions_copy_then_wipe` measures
282/// both halves of that: converting `&[u8]` costs two allocations (the fresh `Vec<u8>` buffer
283/// plus the `Box<Vec<u8>>` header, against one for a move) and the source slice's bytes are
284/// unchanged after the wrapper is built and dropped. If the bytes are already in an owned
285/// `Vec<u8>` you control, [`Dynamic::new`] moves it in for one allocation and nothing of
286/// yours left behind to wipe.
287impl From<&[u8]> for Dynamic<Vec<u8>> {
288    #[inline(always)]
289    fn from(slice: &[u8]) -> Self {
290        Self::new(slice.to_vec())
291    }
292}
293
294/// Copies a string to the heap and wraps the copy, leaving the source `&str` exactly as it
295/// was — measured the same way as the `&[u8]` conversion above, by
296/// `tests/lifecycle_trace_heap.rs::check_str_and_slice_conversions_copy_then_wipe`: two
297/// allocations (the copied `String` buffer plus the `Box<String>` header, against one for a
298/// move) and a caller-side buffer whose bytes are provably unchanged once the wrapper is
299/// built and dropped.
300///
301/// This is the impl a caller now reaches for after `Dynamic::<String>::new_with` was removed
302/// outright in 0.9.0-rc.12 — an `&str` is what's already on hand, so `From<&str>` reads as
303/// the drop-in replacement. It is the **worse** one: whatever plaintext the `&str` borrows
304/// from — a literal, an owned `String`, a line read off stdin — is untouched by this call and
305/// stays live and unprotected for as long as its own scope does. If you own the `String`,
306/// size it once — `String::with_capacity` at the exact final length — and move it in with
307/// [`Dynamic::new`], which `check_string_new_moves_callers_buffer` measures as exactly one
308/// allocation (the `Box<String>` header) with the secret staying at the caller's original
309/// buffer address.
310impl From<&str> for Dynamic<String> {
311    #[inline(always)]
312    fn from(input: &str) -> Self {
313        Self::new(input.to_string())
314    }
315}
316
317/// Moves an owned value onto the heap by boxing it — the impl behind [`Dynamic::new`]'s
318/// "moves the buffer in, no copy" guarantee for `Vec<u8>`, `String`, and every other `Sized`
319/// `T`. `Box::new(value)` only relocates `value`'s own stack representation (for a `Vec<u8>`,
320/// the pointer/length/capacity triple); the heap buffer that pointer already points at is
321/// never touched, so it lands inside the wrapper at the address it already had.
322///
323/// That is necessary for "no copy" to hold, but not sufficient for it to matter: if `value`
324/// was grown to build it — pushed onto, extended, `push_str`'d past its original capacity —
325/// each of those reallocations already ran *before* this impl ever sees the value, and each
326/// one freed a heap block holding the secret, unwiped, somewhere this wrapper will never
327/// look. `check_growth_orphan_retains_secret_vec` / `_string` in
328/// `tests/lifecycle_trace_heap.rs` measure exactly that allocator behavior for growth
329/// happening *after* the value is wrapped; the same allocator runs the same way one step
330/// earlier when the growth happens during construction instead.
331/// `check_string_new_moves_callers_buffer` measures the guarantee this impl does deliver
332/// when construction is growth-free: one allocation (the `Box` header) and the secret
333/// staying at the caller's original buffer address. Build without growth — `with_capacity`
334/// at the exact final size, or one `resize` — then move the value in.
335impl<T: 'static + zeroize::Zeroize> From<T> for Dynamic<T> {
336    #[inline(always)]
337    fn from(value: T) -> Self {
338        Self {
339            inner: Box::new(value),
340        }
341    }
342}
343
344// Hex encoding and decoding for Dynamic<Vec<u8>>.
345// Dynamic is always heap-allocated, so no no-alloc split is needed.
346#[cfg(feature = "encoding-hex")]
347impl Dynamic<Vec<u8>> {
348    /// Decodes a hex string (lowercase, uppercase, or mixed) into `Dynamic<Vec<u8>>`.
349    ///
350    /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
351    /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
352    pub fn try_from_hex(s: &str) -> Result<Self, crate::error::HexError> {
353        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
354            s.try_from_hex()?,
355        )))
356    }
357}
358
359// Base32 encoding and decoding for Dynamic<Vec<u8>>.
360#[cfg(feature = "encoding-base32")]
361impl Dynamic<Vec<u8>> {
362    /// Decodes an uppercase, unpadded Base32 (RFC 4648 §6) string into `Dynamic<Vec<u8>>`.
363    ///
364    /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
365    /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
366    ///
367    /// Decoding is strict about the alphabet, case, padding and length, but lenient
368    /// about non-canonical trailing bits: unused bits in the final group are ignored
369    /// rather than rejected, so `"MZ"` and `"MY"` both decode to `[0x66]`. Decoding is
370    /// therefore not injective — two distinct strings can yield the same bytes, and
371    /// only encoder-produced strings are canonical. Do not use the decoded value to
372    /// decide that two encoded strings were equal.
373    pub fn try_from_base32(s: &str) -> Result<Self, crate::error::Base32Error> {
374        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
375            s.try_from_base32()?,
376        )))
377    }
378}
379
380// Base64url encoding and decoding for Dynamic<Vec<u8>>.
381#[cfg(feature = "encoding-base64")]
382impl Dynamic<Vec<u8>> {
383    /// Decodes a Base64url (unpadded) string into `Dynamic<Vec<u8>>`.
384    ///
385    /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
386    /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
387    pub fn try_from_base64url(s: &str) -> Result<Self, crate::error::Base64Error> {
388        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
389            s.try_from_base64url()?,
390        )))
391    }
392}
393
394// Bech32 (BIP-173) encoding and decoding for Dynamic<Vec<u8>>.
395#[cfg(feature = "encoding-bech32")]
396impl Dynamic<Vec<u8>> {
397    /// Decodes a Bech32 (BIP-173) string into `Dynamic<Vec<u8>>`, validating the HRP
398    /// (case-insensitive).
399    ///
400    /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
401    /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
402    ///
403    /// HRP comparison is non-constant-time — this is intentional, as the HRP is public
404    /// metadata, not secret material.
405    pub fn try_from_bech32(s: &str, expected_hrp: &str) -> Result<Self, crate::error::Bech32Error> {
406        Self::try_from_bech32_sized::<{ crate::BECH32_CODE_LENGTH }>(s, expected_hrp)
407    }
408
409    /// Like [`try_from_bech32`](Self::try_from_bech32), accepting strings up to `C`
410    /// characters.
411    ///
412    /// `C` is the bech32 code length: the cap on the whole encoded string. Pass the `C`
413    /// the string was encoded with, or any larger value. See
414    /// [`Bech32Sized`](crate::Bech32Sized) for what `C` above
415    /// [`BECH32_CODE_LENGTH`](crate::BECH32_CODE_LENGTH) costs.
416    pub fn try_from_bech32_sized<const C: usize>(
417        s: &str,
418        expected_hrp: &str,
419    ) -> Result<Self, crate::error::Bech32Error> {
420        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
421            s.try_from_bech32_sized::<C>(expected_hrp)?,
422        )))
423    }
424
425    /// Decodes a Bech32 (BIP-173) string into `Dynamic<Vec<u8>>` without validating the HRP.
426    ///
427    /// Use [`try_from_bech32`](Self::try_from_bech32) in security-critical code to prevent
428    /// cross-protocol confusion attacks.
429    pub fn try_from_bech32_unchecked(s: &str) -> Result<Self, crate::error::Bech32Error> {
430        Self::try_from_bech32_unchecked_sized::<{ crate::BECH32_CODE_LENGTH }>(s)
431    }
432
433    /// Like [`try_from_bech32_unchecked`](Self::try_from_bech32_unchecked), accepting
434    /// strings up to `C` characters.
435    pub fn try_from_bech32_unchecked_sized<const C: usize>(
436        s: &str,
437    ) -> Result<Self, crate::error::Bech32Error> {
438        let (_hrp, bytes) = s.try_from_bech32_unchecked_sized::<C>()?;
439        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(bytes)))
440    }
441}
442
443// Bech32m (BIP-350) encoding and decoding for Dynamic<Vec<u8>>.
444#[cfg(feature = "encoding-bech32")]
445impl Dynamic<Vec<u8>> {
446    /// Decodes a Bech32m (BIP-350) string into `Dynamic<Vec<u8>>`, validating the HRP
447    /// (case-insensitive).
448    ///
449    /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
450    /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
451    pub fn try_from_bech32m(
452        s: &str,
453        expected_hrp: &str,
454    ) -> Result<Self, crate::error::Bech32Error> {
455        Self::try_from_bech32m_sized::<{ crate::BECH32_CODE_LENGTH }>(s, expected_hrp)
456    }
457
458    /// Like [`try_from_bech32m`](Self::try_from_bech32m), accepting strings up to `C`
459    /// characters. See [`Bech32mSized`](crate::Bech32mSized) for what `C` above
460    /// [`BECH32_CODE_LENGTH`](crate::BECH32_CODE_LENGTH) costs.
461    pub fn try_from_bech32m_sized<const C: usize>(
462        s: &str,
463        expected_hrp: &str,
464    ) -> Result<Self, crate::error::Bech32Error> {
465        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
466            s.try_from_bech32m_sized::<C>(expected_hrp)?,
467        )))
468    }
469
470    /// Decodes a Bech32m (BIP-350) string into `Dynamic<Vec<u8>>` without validating the HRP.
471    ///
472    /// Use [`try_from_bech32m`](Self::try_from_bech32m) in security-critical code.
473    pub fn try_from_bech32m_unchecked(s: &str) -> Result<Self, crate::error::Bech32Error> {
474        Self::try_from_bech32m_unchecked_sized::<{ crate::BECH32_CODE_LENGTH }>(s)
475    }
476
477    /// Like [`try_from_bech32m_unchecked`](Self::try_from_bech32m_unchecked), accepting
478    /// strings up to `C` characters.
479    pub fn try_from_bech32m_unchecked_sized<const C: usize>(
480        s: &str,
481    ) -> Result<Self, crate::error::Bech32Error> {
482        let (_hrp, bytes) = s.try_from_bech32m_unchecked_sized::<C>()?;
483        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(bytes)))
484    }
485}
486
487/// Construction helpers and random generation for `Dynamic<Vec<u8>>`.
488impl Dynamic<Vec<u8>> {
489    /// Transfers `protected` bytes into a freshly boxed `Vec`, keeping
490    /// [`zeroize::Zeroizing`] alive across the only allocation that can panic.
491    ///
492    /// # Panic safety
493    ///
494    /// `Box::new(Vec::new())` is the sole allocation point — just the 24-byte
495    /// `Vec` header, no data buffer. If it panics (OOM), `protected` is still
496    /// in scope and `Zeroizing::drop` zeroes the secret bytes during unwind.
497    /// After the swap, `protected` holds an empty `Vec` (no-op to zeroize) and
498    /// `Dynamic::from(boxed)` is an infallible struct-field assignment.
499    ///
500    /// Note: `Box::new(*protected)` would be cleaner but does not compile —
501    /// `Zeroizing` implements `Deref` (returning `&T`), not a move-out, so
502    /// `*protected` yields a reference rather than an owned value (E0507).
503    #[inline(always)]
504    fn from_protected_bytes(mut protected: zeroize::Zeroizing<alloc::vec::Vec<u8>>) -> Self {
505        // Only fallible allocation; protected stays live across it for panic-safety
506        let mut boxed = Box::<alloc::vec::Vec<u8>>::default();
507        core::mem::swap(&mut *boxed, &mut *protected);
508        Self::from(boxed)
509    }
510
511    /// Writes into a **sized, pre-zeroed slot** inside the wrapper's own storage.
512    ///
513    /// The closure receives exactly `len` bytes, **every one of them zero**. That is a
514    /// guarantee, not an implementation detail: callers rely on the tail being zero as
515    /// real input — a 40-bit RC4 key is five bytes of derived material followed by
516    /// eleven zeros, and those zeros are key-schedule input rather than padding, so a
517    /// slot that merely happened to be zeroed would produce a different cipher the day
518    /// it was not.
519    ///
520    /// Two allocations, not one: the slot of exactly `len` bytes, plus the `Box<Vec<u8>>`
521    /// header the bytes are swapped into on the way out. `tests/lifecycle_trace_heap.rs`
522    /// measures `from_random(len)` — this constructor with an RNG in the closure — at
523    /// exactly two, "the payload buffer and the `Box<Vec<u8>>` header", and measures
524    /// `Dynamic::new(owned)` at exactly one, since a move carries its buffer along. By
525    /// allocation count this is therefore the joint most expensive constructor here, tied
526    /// with `From<&[u8]>`; the count is not what recommends it. What does is the shape of
527    /// the slot: capacity equals length, so the wrapper never holds slack, and the slot is
528    /// a slice, so **growth is not expressible** and the reallocation hazard cannot arise.
529    ///
530    /// # Changed in 0.9.0-rc.12 — this took no `len` before
531    ///
532    /// If you are here from `E0061: this function takes 2 arguments but 1 was supplied`,
533    /// this is why. The previous signature handed the closure a **zero-capacity
534    /// `Vec<u8>`**, so filling it reallocated — and a `Vec` reallocation frees the old
535    /// block **without zeroizing it**, abandoning a copy of the secret on the heap that
536    /// nothing would ever wipe. A constructor whose entire purpose was to avoid an
537    /// unprotected copy created one, silently, in the common case of two pushes.
538    ///
539    /// Pass the final length and fill the slot:
540    ///
541    /// ```rust
542    /// # #[cfg(feature = "alloc")] {
543    /// use secure_gate::{Dynamic, RevealSecret};
544    ///
545    /// // was: new_with(|v| { v.extend_from_slice(&material); v.resize(16, 0); })
546    /// let key = Dynamic::<Vec<u8>>::new_with(16, |slot| {
547    ///     slot[..4].copy_from_slice(&[1, 2, 3, 4]);
548    /// });
549    /// assert_eq!(key.expose_secret().len(), 16);
550    /// key.with_secret(|b| assert_eq!(&b[4..], &[0u8; 12]));
551    /// # }
552    /// ```
553    ///
554    /// # When the length is not known in advance
555    ///
556    /// Use [`Dynamic::new`] with an empty `Vec` and write through the
557    /// [`std::io::Write`] impl, which grows by hand and **zeroizes each abandoned
558    /// buffer** before releasing it — the one growth path in this crate that does.
559    /// See the [`Write`](#impl-Write-for-Dynamic<Vec<u8>>) impl for what it does and
560    /// does not cover. It requires the `std` feature.
561    ///
562    /// # Panics
563    ///
564    /// Allocating the slot is `alloc::vec![0u8; len]`, so `len` is not a hint — it is an
565    /// allocation request, and both allocator failure modes are reachable through it. A
566    /// `len` above `isize::MAX` panics with `"capacity overflow"` before anything is
567    /// allocated. A `len` the allocator cannot satisfy reaches `handle_alloc_error`, which
568    /// **aborts** rather than unwinding: no destructor runs anywhere, so no `Zeroizing` on
569    /// this stack or any frame above it gets the chance to wipe anything.
570    ///
571    /// `len` is caller-supplied, and the worked example on the fallible sibling is
572    /// `try_new_with(len, |slot| r.read_exact(slot))` — precisely the shape where `len`
573    /// arrives off the wire, out of a frame header or a manifest. Bound it before passing
574    /// it here, for the same reason `deserialize_with_limit` on this type takes a ceiling
575    /// instead of trusting the payload: a length field is attacker-controlled input until
576    /// something checks it.
577    ///
578    /// For some callers that bound is not a check that *moves* — it is a check that changes
579    /// *shape*, and the difference decides whether there is any work to do. If your length
580    /// previously flowed into a decompressor or a comparison further down, a ceiling
581    /// enforced after the fact was enough: an overstated length failed a check and you
582    /// returned an error. Here the length **is** the allocation, and
583    /// `alloc::vec![0u8; 9_000_000_000]` does not fail a comparison — it asks the allocator
584    /// for nine gigabytes before any of your code runs. A caller whose length comes from a
585    /// trusted source has nothing to do. A caller whose length comes from the file format
586    /// has acquired an allocation-sized surface where it previously had a comparison, and
587    /// needs a bound it may never have written.
588    ///
589    /// A panic *inside* `f` is safe: the slot lives in a [`zeroize::Zeroizing`] for the
590    /// whole call, so partially written bytes are wiped during unwinding.
591    ///
592    /// # See also
593    ///
594    /// - [`try_new_with`](Self::try_new_with) — when the fill can fail.
595    /// - [`SlotWriter`](crate::SlotWriter) — append to the slot without writing offsets
596    ///   by hand.
597    #[inline(always)]
598    pub fn new_with<F>(len: usize, f: F) -> Self
599    where
600        F: FnOnce(&mut [u8]),
601    {
602        let mut v: zeroize::Zeroizing<alloc::vec::Vec<u8>> =
603            zeroize::Zeroizing::new(alloc::vec![0u8; len]);
604        f(&mut v[..]);
605        Self::from_protected_bytes(v)
606    }
607
608    /// [`new_with`](Self::new_with) for a fill that can fail.
609    ///
610    /// Identical in every respect but the closure's return type. On `Err` the
611    /// partially written slot is **zeroized before the error is returned** — the
612    /// `Zeroizing` holding it is dropped on the way out, so a failed fill leaves
613    /// nothing behind.
614    ///
615    /// That is not defensive tidiness. Decoders that write into a caller-supplied
616    /// buffer routinely leave real plaintext in it when they fail; `miniz_oxide`'s
617    /// `decompress_slice_iter_to_slice` documents exactly that ("in that case the
618    /// output buffer will still contain the partial decompression"). Wrapping only on
619    /// success would leave that partial secret in an unprotected buffer on every
620    /// malformed input.
621    ///
622    /// # Errors
623    ///
624    /// Returns whatever the closure returns. The error type is the caller's; this
625    /// method neither inspects nor wraps it.
626    ///
627    /// # A short fill is not an error, and its tail is the zero fill
628    ///
629    /// The closure returns `Result<(), E>`, which carries no bytes-written count. A closure
630    /// that writes fewer than `len` bytes and returns `Ok` therefore yields a full-length
631    /// secret whose untouched tail is the pre-zeroing. Nothing here can catch that, because
632    /// nothing here is ever told how far the fill got.
633    ///
634    /// Whether that is the right answer or silent corruption depends on what is in the
635    /// slot, and the two shapes differ completely:
636    ///
637    /// - **Fixed-width key material** — the 40-bit RC4 key from [`new_with`](Self::new_with):
638    ///   five bytes of derived material followed by eleven zeros that are key-schedule
639    ///   input. The short fill *is* the intended fill, and the zero guarantee is what makes
640    ///   it reproducible.
641    /// - **A variable-content payload** — a decoded document, a decompressed record, a frame
642    ///   whose `len` came from a header that is only an upper bound or is simply wrong (a
643    ///   truncated archive, a manifest that disagrees with its contents). Here the zero tail
644    ///   is not input. It is missing plaintext in the shape of real data, and whatever
645    ///   parses the result downstream will read it as real.
646    ///
647    /// For the second shape the closure has to check its own coverage and fail, since this
648    /// method cannot do it: write through a [`SlotWriter`](crate::SlotWriter) and return
649    /// `Err` unless [`is_full()`](crate::SlotWriter::is_full) holds at the end, or keep the
650    /// count by hand and compare `bytes_written == slot.len()` before returning `Ok`. The
651    /// `read_exact` example below does not demonstrate this and cannot be read as the
652    /// pattern to copy — [`read_exact`](std::io::Read::read_exact) surfaces under-fill as
653    /// `Err` by construction, so that closure gets the check for free and never writes one.
654    ///
655    /// # Panics
656    ///
657    /// The same two allocator failures as [`new_with`](Self::new_with), for the same
658    /// reason: the body allocates the slot with the identical `alloc::vec![0u8; len]`. A
659    /// `len` above `isize::MAX` panics with `"capacity overflow"`; a `len` the allocator
660    /// cannot satisfy reaches `handle_alloc_error`, which aborts without unwinding, so no
661    /// `Zeroizing` anywhere on the stack runs. The `# Errors` section above describes the
662    /// closure's failures only — an oversized `len` never reaches the closure and never
663    /// arrives as `Err`, so a caller treating `Result` as the complete failure channel is
664    /// wrong about this one. Bound `len` before passing it; the example below takes it
665    /// straight from its caller, which in real code means straight from a frame header.
666    ///
667    /// A panic *inside* `f` is safe, exactly as in `new_with`: the slot is held in a
668    /// [`zeroize::Zeroizing`] for the whole call and is wiped during unwinding.
669    ///
670    /// # Examples
671    ///
672    /// ```rust
673    /// # #[cfg(all(feature = "alloc", feature = "std"))] {
674    /// use secure_gate::{Dynamic, RevealSecret};
675    /// use std::io::Read;
676    ///
677    /// fn read_exact_secret<R: Read>(r: &mut R, len: usize) -> std::io::Result<Dynamic<Vec<u8>>> {
678    ///     Dynamic::<Vec<u8>>::try_new_with(len, |slot| r.read_exact(slot))
679    /// }
680    ///
681    /// let secret = read_exact_secret(&mut [7u8; 32].as_slice(), 32).unwrap();
682    /// assert_eq!(secret.expose_secret().len(), 32);
683    ///
684    /// // A short read fails, and yields no secret at all.
685    /// assert!(read_exact_secret(&mut [7u8; 8].as_slice(), 32).is_err());
686    /// # }
687    /// ```
688    #[inline(always)]
689    pub fn try_new_with<F, E>(len: usize, f: F) -> Result<Self, E>
690    where
691        F: FnOnce(&mut [u8]) -> Result<(), E>,
692    {
693        let mut v: zeroize::Zeroizing<alloc::vec::Vec<u8>> =
694            zeroize::Zeroizing::new(alloc::vec![0u8; len]);
695        // On the error path `v` is dropped here, so `Zeroizing` wipes whatever the
696        // closure wrote before it gave up. That is why the buffer is built before the
697        // fill rather than assembled after it.
698        f(&mut v[..])?;
699        Ok(Self::from_protected_bytes(v))
700    }
701}
702
703// `new_with` was removed here in 0.9.0-rc.12, deliberately and with no replacement.
704//
705// It handed the closure a zero-capacity `String`, so filling it reallocated and each
706// reallocation freed the old buffer unwiped — the same defect the `Vec<u8>` arm had,
707// in the arm most likely to hold a password. The `Vec` arm was fixed by handing a
708// sized slot instead; no such slot exists for text. A `&mut str` cannot be usefully
709// filled: `String` must hold valid UTF-8 and characters have variable byte widths, so
710// a fixed byte window is not a place you can write arbitrary text into.
711//
712// The replacement is to build a pre-sized `String` yourself and move it in with
713// `Dynamic::new`, which transfers the buffer rather than copying it — so the
714// allocation you filled is the allocation the wrapper protects. `From<&str>` copies
715// and leaves your source unwiped; prefer `new` for secret material.
716//
717// That leaves this impl block with no unconditional member: `from_protected_bytes`
718// below exists solely for `deserialize_with_limit`, so — unlike the `Vec<u8>` arm's
719// copy of the same helper, which `new_with`/`try_new_with` keep alive unconditionally
720// — it is `#[cfg]`-gated on `serde-deserialize` rather than left to go dead under
721// `alloc` alone.
722#[cfg(feature = "serde-deserialize")]
723impl Dynamic<alloc::string::String> {
724    /// Heap-only construction from a `Zeroizing<String>`. Swaps the protected
725    /// buffer into a default-initialized `Box<String>` and returns the
726    /// `Dynamic`. Panic-safe: if the `Box` allocation OOM-panics, `protected`
727    /// stays live and `Zeroizing::drop` zeroes the secret bytes during unwind.
728    #[inline(always)]
729    fn from_protected_bytes(mut protected: zeroize::Zeroizing<alloc::string::String>) -> Self {
730        // Only fallible allocation; protected stays live across it for panic-safety
731        let mut boxed = Box::<alloc::string::String>::default();
732        core::mem::swap(&mut *boxed, &mut *protected);
733        Self::from(boxed)
734    }
735}
736
737/// Hex encoding for `Dynamic<Vec<u8>>`; delegates via `with_secret`.
738///
739/// Deliberately **not** implemented for `Dynamic<String>` — hex-encoding
740/// textual secrets is a design smell; convert explicitly inside `with_secret`
741/// if genuinely needed. Bring the trait into scope: `use secure_gate::ToHex;`.
742///
743/// ```rust
744/// # #[cfg(feature = "encoding-hex")] {
745/// use secure_gate::{Dynamic, ToHex};
746///
747/// let token: Dynamic<Vec<u8>> = Dynamic::from(&[0xDEu8, 0xAD][..]);
748/// assert_eq!(&*token.to_hex(), "dead");
749/// # }
750/// ```
751#[cfg(feature = "encoding-hex")]
752impl ToHex for Dynamic<Vec<u8>> {
753    #[inline]
754    fn to_hex(&self) -> crate::EncodedSecret {
755        self.with_secret(|s| s.to_hex())
756    }
757
758    #[inline]
759    fn to_hex_upper(&self) -> crate::EncodedSecret {
760        self.with_secret(|s| s.to_hex_upper())
761    }
762}
763
764/// Base32 encoding for `Dynamic<Vec<u8>>`; delegates via `with_secret`.
765///
766/// Bring the trait into scope: `use secure_gate::ToBase32;`.
767///
768/// ```rust
769/// # #[cfg(feature = "encoding-base32")] {
770/// use secure_gate::{Dynamic, ToBase32};
771///
772/// let token: Dynamic<Vec<u8>> = Dynamic::from(&[0xABu8; 4][..]);
773/// assert_eq!(&*token.to_base32(), "VOV2XKY");
774/// # }
775/// ```
776#[cfg(feature = "encoding-base32")]
777impl ToBase32 for Dynamic<Vec<u8>> {
778    #[inline]
779    fn to_base32(&self) -> crate::EncodedSecret {
780        self.with_secret(|s| s.to_base32())
781    }
782}
783
784/// Base64url encoding for `Dynamic<Vec<u8>>`; delegates via `with_secret`.
785///
786/// Bring the trait into scope: `use secure_gate::ToBase64Url;`.
787///
788/// ```rust
789/// # #[cfg(feature = "encoding-base64")] {
790/// use secure_gate::{Dynamic, ToBase64Url};
791///
792/// let token: Dynamic<Vec<u8>> = Dynamic::from(&[0xABu8; 4][..]);
793/// assert_eq!(&*token.to_base64url(), "q6urqw");
794/// # }
795/// ```
796#[cfg(feature = "encoding-base64")]
797impl ToBase64Url for Dynamic<Vec<u8>> {
798    #[inline]
799    fn to_base64url(&self) -> crate::EncodedSecret {
800        self.with_secret(|s| s.to_base64url())
801    }
802}
803
804/// Bech32 encoding for `Dynamic<Vec<u8>>`; delegates via `with_secret`.
805///
806/// Bring the trait into scope: `use secure_gate::ToBech32;`.
807#[cfg(feature = "encoding-bech32")]
808impl ToBech32 for Dynamic<Vec<u8>> {
809    #[inline]
810    fn try_to_bech32(
811        &self,
812        hrp: &str,
813        case: crate::Case,
814    ) -> Result<crate::EncodedSecret, crate::error::Bech32Error> {
815        self.with_secret(|s| s.try_to_bech32(hrp, case))
816    }
817
818    #[inline]
819    fn try_to_bech32_sized<const C: usize>(
820        &self,
821        hrp: &str,
822        case: crate::Case,
823    ) -> Result<crate::EncodedSecret, crate::error::Bech32Error> {
824        self.with_secret(|s| s.try_to_bech32_sized::<C>(hrp, case))
825    }
826}
827
828/// Bech32m encoding for `Dynamic<Vec<u8>>`; delegates via `with_secret`.
829///
830/// Bring the trait into scope: `use secure_gate::ToBech32m;`.
831#[cfg(feature = "encoding-bech32")]
832impl ToBech32m for Dynamic<Vec<u8>> {
833    #[inline]
834    fn try_to_bech32m(
835        &self,
836        hrp: &str,
837        case: crate::Case,
838    ) -> Result<crate::EncodedSecret, crate::error::Bech32Error> {
839        self.with_secret(|s| s.try_to_bech32m(hrp, case))
840    }
841
842    #[inline]
843    fn try_to_bech32m_sized<const C: usize>(
844        &self,
845        hrp: &str,
846        case: crate::Case,
847    ) -> Result<crate::EncodedSecret, crate::error::Bech32Error> {
848        self.with_secret(|s| s.try_to_bech32m_sized::<C>(hrp, case))
849    }
850}
851
852// RevealSecret — one generic impl covers every inner type, including local
853// user-defined ones. Length reporting lives in the narrower `SecretLen`
854// (implemented for `Dynamic<String>` and `Dynamic<Vec<T>>`), because a generic
855// `T` has no meaningful length.
856impl<T: ?Sized + zeroize::Zeroize> crate::RevealSecret for Dynamic<T> {
857    type Inner = T;
858
859    #[inline(always)]
860    fn with_secret<F, R>(&self, f: F) -> R
861    where
862        F: FnOnce(&T) -> R,
863    {
864        f(&self.inner)
865    }
866
867    #[inline(always)]
868    fn expose_secret(&self) -> &T {
869        &self.inner
870    }
871
872    /// Consumes `self` and transfers ownership of the plain inner value.
873    ///
874    /// **Allocation note:** allocates one small `Box<T>` sentinel (24 bytes for
875    /// `String`/`Vec` on 64-bit) before the swap. If that allocation panics (OOM),
876    /// `self.inner` is unchanged and `Dynamic::drop` zeroizes the real secret during
877    /// unwind — confidentiality is preserved. This is the same OOM-safety pattern
878    /// used by `from_protected_bytes` and `deserialize_with_limit`.
879    ///
880    /// See [`RevealSecret::into_inner`](crate::RevealSecret::into_inner) for the full
881    /// contract. Protection ends at this call: the returned value is an ordinary
882    /// `String` / `Vec<T>`, moved out without copying.
883    #[inline(always)]
884    fn into_inner(mut self) -> T
885    where
886        Self: Sized,
887        Self::Inner: Sized + crate::SentinelValue + zeroize::Zeroize,
888    {
889        // Swap in a sentinel. If Box::new panics (OOM) before the swap, self.inner
890        // still holds the real secret and Dynamic::drop zeroizes it on unwind.
891        // After the swap, self.inner is an inert sentinel — zeroized on
892        // Dynamic::drop as a no-op. `*boxed` deref-moves the value out of the Box.
893        let boxed = core::mem::replace(
894            &mut self.inner,
895            Box::new(crate::SentinelValue::sentinel_value()),
896        );
897        *boxed
898    }
899}
900
901// RevealSecretMut — same generic coverage as RevealSecret.
902impl<T: ?Sized + zeroize::Zeroize> crate::RevealSecretMut for Dynamic<T> {
903    #[inline(always)]
904    fn with_secret_mut<F, R>(&mut self, f: F) -> R
905    where
906        F: FnOnce(&mut T) -> R,
907    {
908        f(&mut self.inner)
909    }
910
911    #[inline(always)]
912    fn expose_secret_mut(&mut self) -> &mut T {
913        &mut self.inner
914    }
915}
916
917// Random generation
918#[cfg(feature = "rand")]
919impl Dynamic<alloc::vec::Vec<u8>> {
920    /// Fills a new `Vec<u8>` with `len` cryptographically secure random bytes and wraps it.
921    ///
922    /// Uses the system RNG ([`SysRng`](rand::rngs::SysRng)). Requires the `rand` feature (and
923    /// `alloc`, which `Dynamic<Vec<u8>>` always needs).
924    ///
925    /// # Panics
926    ///
927    /// Panics if the system RNG fails to provide bytes ([`TryRng::try_fill_bytes`](rand::TryRng::try_fill_bytes)
928    /// returns `Err`). This is treated as a fatal environment error.
929    ///
930    /// # Examples
931    ///
932    /// ```rust
933    /// # #[cfg(all(feature = "alloc", feature = "rand"))]
934    /// use secure_gate::{Dynamic, RevealSecret, SecretLen};
935    ///
936    /// # #[cfg(all(feature = "alloc", feature = "rand"))]
937    /// # {
938    /// let nonce: Dynamic<Vec<u8>> = Dynamic::from_random(24);
939    /// assert_eq!(nonce.len(), 24);
940    /// # }
941    /// ```
942    #[inline]
943    pub fn from_random(len: usize) -> Self {
944        Self::new_with(len, |slot| {
945            SysRng
946                .try_fill_bytes(slot)
947                .expect("SysRng failure is a program error");
948        })
949    }
950
951    /// Allocates a `Vec<u8>` of length `len`, fills it from `rng`, and wraps it.
952    ///
953    /// Accepts any [`TryCryptoRng`](rand::TryCryptoRng) + [`TryRng`](rand::TryRng) — for example,
954    /// a seeded `StdRng` for deterministic tests. Requires the `rand`
955    /// feature and `alloc` (implicit — [`Dynamic<T>`](crate::Dynamic) itself requires it).
956    ///
957    /// # Errors
958    ///
959    /// Returns `R::Error` if [`try_fill_bytes`](rand::TryRng::try_fill_bytes) fails.
960    ///
961    /// # Examples
962    ///
963    /// ```rust
964    /// # #[cfg(all(feature = "alloc", feature = "rand"))]
965    /// # {
966    /// use rand::rngs::StdRng;
967    /// use rand::SeedableRng;
968    /// use secure_gate::Dynamic;
969    ///
970    /// let mut rng = StdRng::from_seed([9u8; 32]);
971    /// let nonce: Dynamic<Vec<u8>> = Dynamic::from_rng(24, &mut rng).expect("rng fill");
972    /// # }
973    /// ```
974    #[inline]
975    pub fn from_rng<R: TryRng + TryCryptoRng>(len: usize, rng: &mut R) -> Result<Self, R::Error> {
976        // This body was the captured-local workaround `try_new_with` exists to retire:
977        // `new_with` could not report failure, so the error was written to a local and
978        // checked afterwards. One expression now — and one observable behaviour change,
979        // not none.
980        //
981        // The old body was `Vec::new()` followed by `v.resize(len, 0u8)`, and `resize`
982        // grows through `RawVec::grow_amortized`, which floors capacity at
983        // `MIN_NON_ZERO_CAP` — 8 for one-byte elements. So `from_rng(4, ..)` used to return
984        // a buffer of length 4 with capacity 8, while `alloc::vec![0u8; len]` allocates the
985        // exact size and returns capacity 4. That is visible through
986        // `with_secret(Vec::capacity)`, which this crate's tests read directly, and it
987        // tightens the `io::Write` impl's spare-room branch
988        // (`buf.len() > v.capacity() - v.len()`): a short random secret no longer carries
989        // up to four free bytes that could absorb a small write without growing. Holding no
990        // slack is the whole point of the new constructor, so the change stays — it is just
991        // not behaviour-neutral, and a 0.8 backport that assumes it is will be wrong about
992        // any capacity assertion below 8.
993        //
994        // Everything else is unchanged: same `R::Error`, same wipe-on-`Err` (the
995        // `Zeroizing` drops on the way out), same allocation count.
996        Self::try_new_with(len, |slot| rng.try_fill_bytes(slot))
997    }
998}
999
1000/// Constant-time equality for `Dynamic<T>` — routes through [`expose_secret()`](crate::RevealSecret::expose_secret).
1001///
1002/// `==` is **deliberately not implemented**. Always use `ct_eq`.
1003#[cfg(feature = "ct-eq")]
1004impl<T: ?Sized + zeroize::Zeroize> crate::ConstantTimeEq for Dynamic<T>
1005where
1006    T: crate::ConstantTimeEq,
1007    Self: crate::RevealSecret<Inner = T>,
1008{
1009    fn ct_eq(&self, other: &Self) -> bool {
1010        self.expose_secret().ct_eq(other.expose_secret())
1011    }
1012}
1013
1014/// Always prints `[REDACTED]` — secrets never appear in debug output.
1015impl<T: ?Sized + zeroize::Zeroize> core::fmt::Debug for Dynamic<T> {
1016    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1017        f.write_str("[REDACTED]")
1018    }
1019}
1020
1021/// Opt-in cloning — requires `cloneable` feature and [`CloneableSecret`](crate::CloneableSecret)
1022/// marker. Each clone is independently zeroized on drop, but cloning increases exposure surface.
1023#[cfg(feature = "cloneable")]
1024impl<T: zeroize::Zeroize + crate::CloneableSecret> Clone for Dynamic<T> {
1025    fn clone(&self) -> Self {
1026        Self::new(self.inner.clone())
1027    }
1028}
1029
1030// ---------------------------------------------------------------------------
1031// Streaming I/O (std only)
1032// ---------------------------------------------------------------------------
1033
1034/// Streams bytes directly into the protected buffer via [`RevealSecretMut`](crate::RevealSecretMut).
1035///
1036/// Data flows **into** the wrapper, so no intermediate unprotected `Vec<u8>` is
1037/// accumulated before wrapping.
1038///
1039/// # Growth wipes the outgoing buffer
1040///
1041/// Writing past the current capacity cannot simply delegate to `Vec`: `Vec` would
1042/// reallocate, copy the secret into the new allocation, and hand the **old** one
1043/// back to the allocator with the plaintext still in it. This impl grows by hand
1044/// instead — it allocates the larger buffer, copies, zeroizes the old buffer
1045/// (contents and spare capacity), and only then releases it.
1046///
1047/// Pre-sizing with `Vec::with_capacity` avoids the copy altogether and is worth
1048/// doing when the length is known up front:
1049///
1050/// ```rust
1051/// # #[cfg(feature = "std")] {
1052/// use std::io::Write;
1053/// use secure_gate::Dynamic;
1054///
1055/// let payload = b"decrypted payload";
1056///
1057/// // Pre-sized: no growth, so no copy and nothing to wipe.
1058/// let mut secret = Dynamic::<Vec<u8>>::new(Vec::with_capacity(payload.len()));
1059/// secret.write_all(payload).unwrap();
1060/// # }
1061/// ```
1062///
1063/// # This is the sanctioned path for a secret whose length you do not know
1064///
1065/// **Every buffer this impl abandons is zeroized before it is released.** Growth is done
1066/// by hand — allocate, copy, wipe the old buffer including its spare capacity, and only
1067/// then drop it — rather than through `Vec`'s own reallocation, which would free the old
1068/// block still holding plaintext. Doubling is amortized, so streaming stays linear.
1069///
1070/// That makes the pair complete: **length known before the fill, use
1071/// [`new_with(len, …)`](Dynamic::new_with); length not known, use `Write`.** Neither
1072/// leaves an unwiped copy behind, and there is no third case that needs one.
1073///
1074/// ## What it does not cover
1075///
1076/// The guarantee is about the buffer this impl owns, and three things sit outside it:
1077///
1078/// - **A caller who grows the `Vec` themselves**, through
1079///   [`with_secret_mut`](crate::RevealSecretMut::with_secret_mut) or
1080///   [`expose_secret_mut`](crate::RevealSecretMut::expose_secret_mut), holds
1081///   `&mut Vec<u8>` directly and their reallocation is `Vec`'s, not this one's. See the
1082///   heap-reallocation residue section in `SECURITY.md`.
1083/// - **Whatever fed the bytes in.** Driving this with [`std::io::copy`] passes every byte
1084///   through `copy`'s own transfer buffer, and [`std::io::Stdin`] keeps a
1085///   process-lifetime internal buffer. Nothing here can reach either. To close the
1086///   transfer buffer too, read into a [`Fixed<[u8; N]>`](crate::Fixed) chunk and
1087///   `write_all` each chunk through this impl.
1088/// - **The `std` feature**, which gates this impl. Without it the recommended path for
1089///   unknown-length secrets is simply absent, and the growable alternatives are not.
1090///
1091/// What it eliminates is the **unbounded chain** of unwiped reallocations — the residue
1092/// that scales with input size, leaving an intermediate copy on the heap for every
1093/// growth. That is the defect; the buffers above are bounded and do not accumulate.
1094#[cfg(feature = "std")]
1095impl std::io::Write for Dynamic<alloc::vec::Vec<u8>> {
1096    #[inline]
1097    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1098        use crate::RevealSecretMut;
1099        use zeroize::Zeroize;
1100
1101        self.with_secret_mut(|v: &mut alloc::vec::Vec<u8>| -> std::io::Result<usize> {
1102            if buf.len() > v.capacity() - v.len() {
1103                // Grow by hand so the outgoing allocation can be wiped before the
1104                // allocator gets it back. `Vec`'s own realloc would copy the secret
1105                // into the new buffer and free the old one still holding plaintext.
1106                let needed = v.len().checked_add(buf.len()).ok_or_else(|| {
1107                    std::io::Error::new(std::io::ErrorKind::OutOfMemory, "capacity overflow")
1108                })?;
1109                // Mirror `Vec`'s amortized doubling so repeated writes stay linear.
1110                let new_cap = core::cmp::max(needed, v.capacity().saturating_mul(2));
1111                let mut grown = alloc::vec::Vec::with_capacity(new_cap);
1112                grown.extend_from_slice(v);
1113                // Wipes contents *and* spare capacity, and sets len to 0 without
1114                // freeing; the assignment below then drops the zeroed allocation.
1115                v.zeroize();
1116                *v = grown;
1117            }
1118            v.extend_from_slice(buf);
1119            Ok(buf.len())
1120        })
1121    }
1122
1123    #[inline]
1124    fn flush(&mut self) -> std::io::Result<()> {
1125        Ok(())
1126    }
1127}
1128
1129/// Cursor-like reader over a [`Dynamic<Vec<u8>>`].
1130///
1131/// Created by [`Dynamic::<Vec<u8>>::as_reader`]. Borrows the `Dynamic`
1132/// immutably and tracks the read position internally. Each [`Read::read`](std::io::Read::read)
1133/// call goes through [`with_secret`](crate::RevealSecret::with_secret),
1134/// preserving the crate's auditable access model.
1135///
1136/// # Security
1137///
1138/// `Read::read()` copies secret bytes into the caller-supplied buffer.
1139/// The caller is responsible for zeroizing that buffer. Prefer piping
1140/// directly into encrypted writers (`io::copy` into an encryptor, etc.)
1141/// rather than reading into intermediate `Vec<u8>` buffers.
1142///
1143/// The `Dynamic` wrapper continues to zeroize its contents on drop
1144/// regardless of how many bytes have been read out.
1145#[cfg(feature = "std")]
1146pub struct DynamicReader<'a> {
1147    secret: &'a Dynamic<alloc::vec::Vec<u8>>,
1148    offset: usize,
1149}
1150
1151#[cfg(feature = "std")]
1152impl std::io::Read for DynamicReader<'_> {
1153    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1154        let offset = self.offset;
1155        let n = self.secret.with_secret(|v| {
1156            let remaining = v.len().saturating_sub(offset);
1157            let n = remaining.min(buf.len());
1158            buf[..n].copy_from_slice(&v[offset..offset + n]);
1159            n
1160        });
1161        self.offset += n;
1162        Ok(n)
1163    }
1164}
1165
1166#[cfg(feature = "std")]
1167impl Dynamic<alloc::vec::Vec<u8>> {
1168    /// Returns a [`DynamicReader`] that implements [`std::io::Read`].
1169    ///
1170    /// This replaces the common `with_secret` + `Cursor` boilerplate:
1171    ///
1172    /// ```rust
1173    /// # #[cfg(feature = "std")] {
1174    /// use std::io;
1175    /// use secure_gate::Dynamic;
1176    ///
1177    /// let secret = Dynamic::<Vec<u8>>::new(vec![1, 2, 3, 4]);
1178    ///
1179    /// // Before: awkward closure + Cursor dance
1180    /// // secret.with_secret(|b| io::copy(&mut Cursor::new(b), &mut encryptor))?;
1181    ///
1182    /// // After: pipe directly into an encrypted writer — no intermediate buffer
1183    /// let mut encryptor = io::sink(); // stand-in for a real encryptor
1184    /// io::copy(&mut secret.as_reader(), &mut encryptor).unwrap();
1185    /// # }
1186    /// ```
1187    ///
1188    /// # Security
1189    ///
1190    /// Each `read()` call copies secret bytes into the caller's buffer.
1191    /// Prefer piping directly into encrypted writers rather than reading
1192    /// into intermediate buffers. The caller is responsible for zeroizing
1193    /// any destination buffer.
1194    #[inline]
1195    pub fn as_reader(&self) -> DynamicReader<'_> {
1196        DynamicReader {
1197            secret: self,
1198            offset: 0,
1199        }
1200    }
1201}
1202
1203/// Opt-in serialization — requires `serde-serialize` feature and
1204/// [`SerializableSecret`](crate::SerializableSecret) marker. Serialization exposes the
1205/// full secret — audit every impl.
1206#[cfg(feature = "serde-serialize")]
1207impl<T: zeroize::Zeroize + crate::SerializableSecret> serde::Serialize for Dynamic<T> {
1208    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1209    where
1210        S: serde::Serializer,
1211    {
1212        self.inner.serialize(serializer)
1213    }
1214}
1215
1216// Deserialize
1217
1218/// Default maximum byte length accepted when deserializing `Dynamic<Vec<u8>>` or
1219/// `Dynamic<String>` via the standard `serde::Deserialize` impl (1 MiB).
1220///
1221/// Pass a custom value to [`Dynamic::deserialize_with_limit`] when a different
1222/// ceiling is required.
1223///
1224/// **Important:** this limit is enforced *after* the upstream deserializer has fully
1225/// materialized the payload. It is a **result-length acceptance bound**, not a
1226/// pre-allocation DoS guard. For untrusted input, enforce size limits at the
1227/// transport or parser layer upstream.
1228#[cfg(feature = "serde-deserialize")]
1229pub const MAX_DESERIALIZE_BYTES: usize = 1_048_576;
1230
1231#[cfg(feature = "serde-deserialize")]
1232impl Dynamic<alloc::vec::Vec<u8>> {
1233    /// Deserializes into `Dynamic<Vec<u8>>`, rejecting payloads larger than `limit` bytes.
1234    ///
1235    /// The standard [`serde::Deserialize`] impl calls this with [`MAX_DESERIALIZE_BYTES`].
1236    /// Use this method directly when you need a tighter or looser ceiling.
1237    ///
1238    /// **Zeroization scope.** Once the upstream deserializer returns a complete
1239    /// `Vec<u8>`, the value is wrapped in `Zeroizing` and stays protected for the
1240    /// rest of this function: oversized buffers are zeroized before the error is
1241    /// returned, and an OOM panic in the subsequent `Box` allocation triggers
1242    /// zeroization on unwind. **However, this guarantee does *not* extend backwards
1243    /// into the deserializer itself.** If the upstream `Vec<u8>` visitor accumulates
1244    /// bytes element-by-element (e.g., a JSON sequence) and fails partway through,
1245    /// the partial buffer is owned by the visitor and dropped as a plain `Vec<u8>` —
1246    /// not zeroized. In the typical untrusted-input threat model the partial bytes
1247    /// are attacker-controlled (the malformed payload they sent), so the practical
1248    /// disclosure surface is bounded; but if your threat model includes deserialization
1249    /// of trusted-but-corruptible secret material, treat the deserialize step as
1250    /// outside the zeroization boundary and build in-process instead:
1251    /// [`new_with(len, …)`](Dynamic::new_with) (or [`try_new_with`](Dynamic::try_new_with)
1252    /// for a fill that can fail) writes straight into the wrapper's own pre-zeroed slot,
1253    /// so there is no separate buffer for a visitor to accumulate into and abandon unwiped.
1254    ///
1255    /// **Important:** this limit is enforced *after* the upstream deserializer has fully
1256    /// materialized the payload. It is a **result-length acceptance bound**, not a
1257    /// pre-allocation DoS guard. For untrusted input, enforce size limits at the
1258    /// transport or parser layer upstream.
1259    pub fn deserialize_with_limit<'de, D>(deserializer: D, limit: usize) -> Result<Self, D::Error>
1260    where
1261        D: serde::Deserializer<'de>,
1262    {
1263        let mut buf: zeroize::Zeroizing<alloc::vec::Vec<u8>> =
1264            zeroize::Zeroizing::new(serde::Deserialize::deserialize(deserializer)?);
1265        if buf.len() > limit {
1266            // buf drops here → Zeroizing zeros the oversized buffer before deallocation
1267            return Err(serde::de::Error::custom(
1268                "deserialized secret exceeds maximum size",
1269            ));
1270        }
1271        // Only fallible allocation; protected stays live across it for panic-safety
1272        let mut boxed = Box::<alloc::vec::Vec<u8>>::default();
1273        core::mem::swap(&mut *boxed, &mut *buf);
1274        Ok(Self::from(boxed))
1275    }
1276}
1277
1278#[cfg(feature = "serde-deserialize")]
1279impl Dynamic<String> {
1280    /// Deserializes into `Dynamic<String>`, rejecting payloads larger than `limit` bytes.
1281    ///
1282    /// The standard [`serde::Deserialize`] impl calls this with [`MAX_DESERIALIZE_BYTES`].
1283    /// Use this method directly when you need a tighter or looser ceiling.
1284    ///
1285    /// **Zeroization scope.** Once the upstream deserializer returns a complete
1286    /// `String`, the value is wrapped in `Zeroizing` and stays protected for the
1287    /// rest of this function: oversized buffers are zeroized before the error is
1288    /// returned, and an OOM panic in the subsequent `Box` allocation triggers
1289    /// zeroization on unwind. **However, this guarantee does *not* extend backwards
1290    /// into the deserializer itself.** If the upstream `String` visitor accumulates
1291    /// characters and fails partway through (e.g., on an invalid UTF-8 boundary),
1292    /// the partial buffer is owned by the visitor and dropped as a plain `String` —
1293    /// not zeroized. In the typical untrusted-input threat model the partial bytes
1294    /// are attacker-controlled (the malformed payload they sent), so the practical
1295    /// disclosure surface is bounded; but if your threat model includes deserialization
1296    /// of trusted-but-corruptible secret material, treat the deserialize step as
1297    /// outside the zeroization boundary and build in-process instead. `String` has no
1298    /// sized-slot constructor of its own — it must hold valid UTF-8 and characters are
1299    /// variable-width, so there is no fixed byte window arbitrary text can be written
1300    /// into. Size a `String` to its exact final length yourself (`String::with_capacity`,
1301    /// filled without triggering a reallocation) and move it in with
1302    /// [`Dynamic::new`](Dynamic::new), which moves the buffer rather than copying it.
1303    ///
1304    /// **Important:** this limit is enforced *after* the upstream deserializer has fully
1305    /// materialized the payload. It is a **result-length acceptance bound**, not a
1306    /// pre-allocation DoS guard. For untrusted input, enforce size limits at the
1307    /// transport or parser layer upstream.
1308    pub fn deserialize_with_limit<'de, D>(deserializer: D, limit: usize) -> Result<Self, D::Error>
1309    where
1310        D: serde::Deserializer<'de>,
1311    {
1312        let buf: zeroize::Zeroizing<alloc::string::String> =
1313            zeroize::Zeroizing::new(serde::Deserialize::deserialize(deserializer)?);
1314        if buf.len() > limit {
1315            // buf drops here → Zeroizing zeros the oversized buffer before deallocation
1316            return Err(serde::de::Error::custom(
1317                "deserialized secret exceeds maximum size",
1318            ));
1319        }
1320        Ok(Self::from_protected_bytes(buf))
1321    }
1322}
1323
1324#[cfg(feature = "serde-deserialize")]
1325impl<'de> serde::Deserialize<'de> for Dynamic<alloc::vec::Vec<u8>> {
1326    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1327    where
1328        D: serde::Deserializer<'de>,
1329    {
1330        Self::deserialize_with_limit(deserializer, MAX_DESERIALIZE_BYTES)
1331    }
1332}
1333
1334#[cfg(feature = "serde-deserialize")]
1335impl<'de> serde::Deserialize<'de> for Dynamic<String> {
1336    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1337    where
1338        D: serde::Deserializer<'de>,
1339    {
1340        Self::deserialize_with_limit(deserializer, MAX_DESERIALIZE_BYTES)
1341    }
1342}
1343
1344/// Zeroizes the inner value (including `Vec`/`String` spare capacity).
1345///
1346/// **Warning:** does not run under `panic = "abort"`.
1347impl<T: ?Sized + zeroize::Zeroize> zeroize::Zeroize for Dynamic<T> {
1348    fn zeroize(&mut self) {
1349        self.inner.zeroize();
1350    }
1351}
1352
1353/// Unconditionally zeroizes the inner value when the wrapper is dropped.
1354///
1355/// **Warning:** `Drop` does not run under `panic = "abort"`.
1356impl<T: ?Sized + zeroize::Zeroize> Drop for Dynamic<T> {
1357    fn drop(&mut self) {
1358        self.zeroize();
1359    }
1360}
1361
1362/// Marker confirming that `Dynamic<T>` always zeroizes on drop.
1363impl<T: ?Sized + zeroize::Zeroize> zeroize::ZeroizeOnDrop for Dynamic<T> {}
1364
1365impl crate::SecretLen for Dynamic<String> {
1366    #[inline(always)]
1367    fn len(&self) -> usize {
1368        self.inner.len()
1369    }
1370}
1371
1372impl<T: zeroize::Zeroize> crate::SecretLen for Dynamic<Vec<T>> {
1373    #[inline(always)]
1374    fn len(&self) -> usize {
1375        self.inner.len()
1376    }
1377
1378    #[inline(always)]
1379    fn byte_len(&self) -> usize {
1380        self.inner.len() * core::mem::size_of::<T>()
1381    }
1382}