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