Skip to main content

secure_gate/
dynamic.rs

1//! Heap-allocated wrapper for variable-length secrets.
2//!
3//! Provides [`Dynamic<T>`], a zero-cost wrapper enforcing explicit access to sensitive data.
4//! Treat secrets as radioactive — minimize exposure surface.
5//!
6//! **Inner type must implement `Zeroize`** for automatic zeroization on drop (including spare capacity).
7//! Requires the `alloc` feature.
8//!
9//! # Examples
10//!
11//! ```rust
12//! # #[cfg(feature = "alloc")]
13//! use secure_gate::{Dynamic, RevealSecret};
14//!
15//! # #[cfg(feature = "alloc")]
16//! {
17//! let secret: Dynamic<Vec<u8>> = Dynamic::new(vec![1u8, 2, 3, 4]);
18//! let sum = secret.with_secret(|s| s.iter().sum::<u8>());
19//! assert_eq!(sum, 10);
20//! # }
21//! ```
22
23#[cfg(feature = "alloc")]
24extern crate alloc;
25use alloc::boxed::Box;
26use zeroize::Zeroize;
27
28#[cfg(any(feature = "encoding-hex", feature = "encoding-base64"))]
29use crate::RevealSecret;
30
31// Encoding traits
32#[cfg(feature = "encoding-base64")]
33use crate::traits::encoding::base64_url::ToBase64Url;
34#[cfg(feature = "encoding-hex")]
35use crate::traits::encoding::hex::ToHex;
36
37#[cfg(feature = "rand")]
38use rand::{rngs::OsRng, TryCryptoRng, TryRngCore};
39
40#[cfg(feature = "encoding-base64")]
41use crate::traits::decoding::base64_url::FromBase64UrlStr;
42#[cfg(feature = "encoding-bech32")]
43use crate::traits::decoding::bech32::FromBech32Str;
44#[cfg(feature = "encoding-bech32m")]
45use crate::traits::decoding::bech32m::FromBech32mStr;
46#[cfg(feature = "encoding-hex")]
47use crate::traits::decoding::hex::FromHexStr;
48
49/// Zero-cost heap-allocated wrapper for variable-length secrets.
50///
51/// Requires `alloc`. **Inner type must implement `Zeroize`** for automatic zeroization on drop
52/// (including spare capacity in `Vec`/`String`).
53///
54/// No `Deref`, `AsRef`, or `Copy` by default — all access requires
55/// [`expose_secret()`](crate::RevealSecret::expose_secret) or
56/// [`with_secret()`](crate::RevealSecret::with_secret) (scoped, preferred).
57/// For the common concrete types, [`Dynamic::<Vec<u8>>::new_with`](Dynamic::new_with) and
58/// [`Dynamic::<String>::new_with`](Dynamic::new_with) are the matching scoped constructors —
59/// closures that write directly into the wrapper. [`new(value)`](Dynamic::new) remains
60/// available as the ergonomic default. `Debug` always prints `[REDACTED]`.
61pub struct Dynamic<T: ?Sized + zeroize::Zeroize> {
62    inner: Box<T>,
63}
64
65impl<T: ?Sized + zeroize::Zeroize> Dynamic<T> {
66    /// Wraps `value` in a `Box<T>` and returns a `Dynamic<T>`.
67    ///
68    /// Accepts any type that implements `Into<Box<T>>` — including owned values,
69    /// `Box<T>`, `String`, `Vec<u8>`, `&str` (via the blanket `From<&str>` impl), etc.
70    ///
71    /// Equivalent to `Dynamic::from(value)` — `#[doc(alias = "from")]` is set so both
72    /// names appear in docs.rs search.
73    ///
74    /// Requires the `alloc` feature (which `Dynamic<T>` itself always requires).
75    #[doc(alias = "from")]
76    #[inline(always)]
77    pub fn new<U>(value: U) -> Self
78    where
79        U: Into<Box<T>>,
80    {
81        let inner = value.into();
82        Self { inner }
83    }
84}
85
86// From impls
87impl<T: ?Sized + zeroize::Zeroize> From<Box<T>> for Dynamic<T> {
88    #[inline(always)]
89    fn from(boxed: Box<T>) -> Self {
90        Self { inner: boxed }
91    }
92}
93
94impl From<&[u8]> for Dynamic<Vec<u8>> {
95    #[inline(always)]
96    fn from(slice: &[u8]) -> Self {
97        Self::new(slice.to_vec())
98    }
99}
100
101impl From<&str> for Dynamic<String> {
102    #[inline(always)]
103    fn from(input: &str) -> Self {
104        Self::new(input.to_string())
105    }
106}
107
108impl<T: 'static + zeroize::Zeroize> From<T> for Dynamic<T> {
109    #[inline(always)]
110    fn from(value: T) -> Self {
111        Self {
112            inner: Box::new(value),
113        }
114    }
115}
116
117// Encoding helpers for Dynamic<Vec<u8>>
118impl Dynamic<Vec<u8>> {
119    /// Encodes the secret bytes as a lowercase hex string.
120    ///
121    /// Delegates to [`ToHex::to_hex`](crate::ToHex::to_hex) on the inner `Vec<u8>`.
122    /// Requires the `encoding-hex` feature.
123    #[cfg(feature = "encoding-hex")]
124    #[inline]
125    pub fn to_hex(&self) -> alloc::string::String {
126        self.with_secret(|s: &Vec<u8>| s.to_hex())
127    }
128
129    /// Encodes the secret bytes as an uppercase hex string.
130    ///
131    /// Delegates to [`ToHex::to_hex_upper`](crate::ToHex::to_hex_upper) on the inner `Vec<u8>`.
132    /// Requires the `encoding-hex` feature.
133    #[cfg(feature = "encoding-hex")]
134    #[inline]
135    pub fn to_hex_upper(&self) -> alloc::string::String {
136        self.with_secret(|s: &Vec<u8>| s.to_hex_upper())
137    }
138
139    /// Encodes the secret bytes as an unpadded Base64url string.
140    ///
141    /// Delegates to [`ToBase64Url::to_base64url`](crate::ToBase64Url::to_base64url) on the inner `Vec<u8>`.
142    /// Requires the `encoding-base64` feature.
143    #[cfg(feature = "encoding-base64")]
144    #[inline]
145    pub fn to_base64url(&self) -> alloc::string::String {
146        self.with_secret(|s: &Vec<u8>| s.to_base64url())
147    }
148
149    /// Transfers `protected` bytes into a freshly boxed `Vec`, keeping
150    /// [`zeroize::Zeroizing`] alive across the only allocation that can panic.
151    ///
152    /// # Panic safety
153    ///
154    /// `Box::new(Vec::new())` is the sole allocation point — just the 24-byte
155    /// `Vec` header, no data buffer. If it panics (OOM), `protected` is still
156    /// in scope and `Zeroizing::drop` zeroes the secret bytes during unwind.
157    /// After the swap, `protected` holds an empty `Vec` (no-op to zeroize) and
158    /// `Dynamic::from(boxed)` is an infallible struct-field assignment.
159    ///
160    /// Note: `Box::new(*protected)` would be cleaner but does not compile —
161    /// `Zeroizing` implements `Deref` (returning `&T`), not a move-out, so
162    /// `*protected` yields a reference rather than an owned value (E0507).
163    #[cfg(any(
164        feature = "encoding-hex",
165        feature = "encoding-base64",
166        feature = "encoding-bech32",
167        feature = "encoding-bech32m",
168    ))]
169    #[inline(always)]
170    fn from_protected_bytes(mut protected: zeroize::Zeroizing<alloc::vec::Vec<u8>>) -> Self {
171        // Only fallible allocation; protected stays live across it for panic-safety
172        let mut boxed = Box::<alloc::vec::Vec<u8>>::default();
173        core::mem::swap(&mut *boxed, &mut *protected);
174        Self::from(boxed)
175    }
176
177    /// Closure-based constructor for consistent API with [`Fixed::new_with`](crate::Fixed::new_with).
178    /// The actual secret data is allocated on the heap; this method exists
179    /// for consistent security-first construction idiom across the crate.
180    #[inline(always)]
181    pub fn new_with<F>(f: F) -> Self
182    where
183        F: FnOnce(&mut alloc::vec::Vec<u8>),
184    {
185        let mut v = alloc::vec::Vec::new();
186        f(&mut v);
187        Self::new(v)
188    }
189}
190
191impl Dynamic<alloc::string::String> {
192    /// Closure-based constructor for consistent API with [`Fixed::new_with`](crate::Fixed::new_with).
193    /// The actual secret data is allocated on the heap; this method exists
194    /// for consistent security-first construction idiom across the crate.
195    #[inline(always)]
196    pub fn new_with<F>(f: F) -> Self
197    where
198        F: FnOnce(&mut alloc::string::String),
199    {
200        let mut s = alloc::string::String::new();
201        f(&mut s);
202        Self::new(s)
203    }
204}
205
206// RevealSecret
207impl crate::RevealSecret for Dynamic<String> {
208    type Inner = String;
209
210    #[inline(always)]
211    fn with_secret<F, R>(&self, f: F) -> R
212    where
213        F: FnOnce(&String) -> R,
214    {
215        f(&self.inner)
216    }
217
218    #[inline(always)]
219    fn expose_secret(&self) -> &String {
220        &self.inner
221    }
222
223    #[inline(always)]
224    fn len(&self) -> usize {
225        self.inner.len()
226    }
227
228    /// Consumes `self` and returns the inner `String` wrapped in [`crate::InnerSecret`].
229    ///
230    /// **Allocation note:** allocates one small `Box<String>` sentinel (24 bytes on
231    /// 64-bit) before the swap. If that allocation panics (OOM), `self.inner` is
232    /// unchanged and `Dynamic::drop` zeroizes the real secret during unwind —
233    /// confidentiality is preserved. This is the same OOM-safety pattern used by
234    /// `from_protected_bytes` and `deserialize_with_limit`.
235    ///
236    /// See [`RevealSecret::into_inner`] for full documentation including the
237    /// redacted `Debug` behavior.
238    #[inline(always)]
239    fn into_inner(mut self) -> crate::InnerSecret<String>
240    where
241        Self: Sized,
242        Self::Inner: Sized + Default + zeroize::Zeroize,
243    {
244        // Take inner and leave an empty-String sentinel. If allocating the default
245        // panics (OOM) before the swap, self.inner still holds the real secret and
246        // Dynamic::drop zeroizes it on unwind. After `take`, self.inner is an empty
247        // `String` in the box — zeroized on Dynamic::drop as a no-op. `*boxed`
248        // deref-moves the String out of the Box.
249        let boxed = core::mem::take(&mut self.inner);
250        crate::InnerSecret::new(*boxed)
251    }
252}
253
254impl<T: zeroize::Zeroize> crate::RevealSecret for Dynamic<Vec<T>> {
255    type Inner = Vec<T>;
256
257    #[inline(always)]
258    fn with_secret<F, R>(&self, f: F) -> R
259    where
260        F: FnOnce(&Vec<T>) -> R,
261    {
262        f(&self.inner)
263    }
264
265    #[inline(always)]
266    fn expose_secret(&self) -> &Vec<T> {
267        &self.inner
268    }
269
270    #[inline(always)]
271    fn len(&self) -> usize {
272        self.inner.len() * core::mem::size_of::<T>()
273    }
274
275    /// Consumes `self` and returns the inner `Vec<T>` wrapped in [`crate::InnerSecret`].
276    ///
277    /// **Allocation note:** allocates one small `Box<Vec<T>>` sentinel (24 bytes on
278    /// 64-bit) before the swap. If that allocation panics (OOM), `self.inner` is
279    /// unchanged and `Dynamic::drop` zeroizes the real secret during unwind —
280    /// confidentiality is preserved. This is the same OOM-safety pattern used by
281    /// `from_protected_bytes` and `deserialize_with_limit`.
282    ///
283    /// See [`RevealSecret::into_inner`] for full documentation including the
284    /// redacted `Debug` behavior.
285    #[inline(always)]
286    fn into_inner(mut self) -> crate::InnerSecret<Vec<T>>
287    where
288        Self: Sized,
289        Self::Inner: Sized + Default + zeroize::Zeroize,
290    {
291        // Take inner and leave an empty-Vec sentinel. If allocating the default
292        // panics (OOM) before the swap, self.inner still holds the real secret and
293        // Dynamic::drop zeroizes it on unwind. After `take`, self.inner is an empty
294        // `Vec` in the box — zeroized on Dynamic::drop as a no-op. `*boxed`
295        // deref-moves the Vec out of the Box.
296        let boxed = core::mem::take(&mut self.inner);
297        crate::InnerSecret::new(*boxed)
298    }
299}
300
301// RevealSecretMut
302impl crate::RevealSecretMut for Dynamic<String> {
303    #[inline(always)]
304    fn with_secret_mut<F, R>(&mut self, f: F) -> R
305    where
306        F: FnOnce(&mut String) -> R,
307    {
308        f(&mut self.inner)
309    }
310
311    #[inline(always)]
312    fn expose_secret_mut(&mut self) -> &mut String {
313        &mut self.inner
314    }
315}
316
317impl<T: zeroize::Zeroize> crate::RevealSecretMut for Dynamic<Vec<T>> {
318    #[inline(always)]
319    fn with_secret_mut<F, R>(&mut self, f: F) -> R
320    where
321        F: FnOnce(&mut Vec<T>) -> R,
322    {
323        f(&mut self.inner)
324    }
325
326    #[inline(always)]
327    fn expose_secret_mut(&mut self) -> &mut Vec<T> {
328        &mut self.inner
329    }
330}
331
332// Random generation
333#[cfg(feature = "rand")]
334impl Dynamic<alloc::vec::Vec<u8>> {
335    /// Fills a new `Vec<u8>` with `len` cryptographically secure random bytes and wraps it.
336    ///
337    /// Uses the system RNG ([`OsRng`](rand::rngs::OsRng)) via [`TryRngCore::try_fill_bytes`](rand::TryRngCore::try_fill_bytes).
338    /// In `rand` 0.9, `OsRng` is a zero-sized handle to the OS generator (not user-seedable). Requires the `rand`
339    /// feature (and `alloc`, which `Dynamic<Vec<u8>>` always needs).
340    ///
341    /// # Panics
342    ///
343    /// Panics if the system RNG fails to provide bytes ([`TryRngCore::try_fill_bytes`](rand::TryRngCore::try_fill_bytes)
344    /// returns `Err`). This is treated as a fatal environment error.
345    ///
346    /// # Examples
347    ///
348    /// ```rust
349    /// # #[cfg(all(feature = "alloc", feature = "rand"))]
350    /// use secure_gate::{Dynamic, RevealSecret};
351    ///
352    /// # #[cfg(all(feature = "alloc", feature = "rand"))]
353    /// # {
354    /// let nonce: Dynamic<Vec<u8>> = Dynamic::from_random(24);
355    /// assert_eq!(nonce.len(), 24);
356    /// # }
357    /// ```
358    #[inline]
359    pub fn from_random(len: usize) -> Self {
360        Self::new_with(|v| {
361            v.resize(len, 0u8);
362            OsRng
363                .try_fill_bytes(v)
364                .expect("OsRng failure is a program error");
365        })
366    }
367
368    /// Allocates a `Vec<u8>` of length `len`, fills it from `rng`, and wraps it.
369    ///
370    /// Accepts any [`TryCryptoRng`](rand::TryCryptoRng) + [`TryRngCore`](rand::TryRngCore).
371    /// Pass [`OsRng`](rand::rngs::OsRng) for the same system entropy as [`from_random`](Self::from_random)
372    /// with a fallible interface. **Do not use `OsRng` for deterministic tests** — in `rand` 0.9 it is a
373    /// unit struct backed by the OS and is **not** seedable; use a seedable PRNG such as
374    /// [`StdRng`](rand::rngs::StdRng) with [`SeedableRng`](rand::SeedableRng) instead. Requires the `rand`
375    /// feature and `alloc` (implicit — [`Dynamic<T>`](crate::Dynamic) itself requires it).
376    ///
377    /// # Errors
378    ///
379    /// Returns `R::Error` if [`try_fill_bytes`](rand::TryRngCore::try_fill_bytes) fails.
380    ///
381    /// # Examples
382    ///
383    /// System RNG (same source as `from_random`, `Result`-based):
384    ///
385    /// ```rust
386    /// # #[cfg(all(feature = "alloc", feature = "rand"))]
387    /// # {
388    /// use rand::rngs::OsRng;
389    /// use secure_gate::Dynamic;
390    ///
391    /// let nonce: Dynamic<Vec<u8>> = Dynamic::from_rng(24, &mut OsRng).expect("rng fill");
392    /// # }
393    /// ```
394    ///
395    /// Deterministic fill (tests) with a seedable generator:
396    ///
397    /// ```rust
398    /// # #[cfg(all(feature = "alloc", feature = "rand"))]
399    /// # {
400    /// use rand::rngs::StdRng;
401    /// use rand::SeedableRng;
402    /// use secure_gate::Dynamic;
403    ///
404    /// let mut rng = StdRng::from_seed([9u8; 32]);
405    /// let nonce: Dynamic<Vec<u8>> = Dynamic::from_rng(24, &mut rng).expect("rng fill");
406    /// # }
407    /// ```
408    #[inline]
409    pub fn from_rng<R: TryRngCore + TryCryptoRng>(len: usize, rng: &mut R) -> Result<Self, R::Error> {
410        let mut result = Ok(());
411        let this = Self::new_with(|v| {
412            v.resize(len, 0u8);
413            result = rng.try_fill_bytes(v);
414        });
415        result.map(|_| this)
416    }
417}
418
419// Decoding constructors
420#[cfg(feature = "encoding-hex")]
421impl Dynamic<alloc::vec::Vec<u8>> {
422    /// Decodes a lowercase hex string into `Dynamic<Vec<u8>>`.
423    ///
424    /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
425    /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
426    pub fn try_from_hex(s: &str) -> Result<Self, crate::error::HexError> {
427        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
428            s.try_from_hex()?,
429        )))
430    }
431}
432
433#[cfg(feature = "encoding-base64")]
434impl Dynamic<alloc::vec::Vec<u8>> {
435    /// Decodes a Base64url (unpadded) string into `Dynamic<Vec<u8>>`.
436    ///
437    /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
438    /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
439    pub fn try_from_base64url(s: &str) -> Result<Self, crate::error::Base64Error> {
440        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
441            s.try_from_base64url()?,
442        )))
443    }
444}
445
446#[cfg(feature = "encoding-bech32")]
447impl Dynamic<alloc::vec::Vec<u8>> {
448    /// Decodes a Bech32 (BIP-173) string into `Dynamic<Vec<u8>>`.
449    ///
450    /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
451    /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
452    ///
453    /// # Warning
454    ///
455    /// The HRP is **not validated** — any HRP will be accepted as long as the checksum
456    /// is valid. For security-critical code where cross-protocol confusion must be
457    /// prevented, use [`try_from_bech32`](Self::try_from_bech32).
458    pub fn try_from_bech32_unchecked(s: &str) -> Result<Self, crate::error::Bech32Error> {
459        let (_hrp, bytes) = s.try_from_bech32_unchecked()?;
460        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(bytes)))
461    }
462
463    /// Decodes a Bech32 (BIP-173) string into `Dynamic<Vec<u8>>`, validating that the HRP
464    /// matches `expected_hrp` (case-insensitive).
465    ///
466    /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
467    /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
468    ///
469    /// Prefer this over [`try_from_bech32_unchecked`](Self::try_from_bech32_unchecked) in
470    /// security-critical code to prevent cross-protocol confusion attacks.
471    pub fn try_from_bech32(s: &str, expected_hrp: &str) -> Result<Self, crate::error::Bech32Error> {
472        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
473            s.try_from_bech32(expected_hrp)?,
474        )))
475    }
476}
477
478#[cfg(feature = "encoding-bech32m")]
479impl Dynamic<alloc::vec::Vec<u8>> {
480    /// Decodes a Bech32m (BIP-350) string into `Dynamic<Vec<u8>>`.
481    ///
482    /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
483    /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
484    ///
485    /// # Warning
486    ///
487    /// The HRP is **not validated** — any HRP will be accepted as long as the checksum
488    /// is valid. For security-critical code where cross-protocol confusion must be
489    /// prevented, use [`try_from_bech32m`](Self::try_from_bech32m).
490    pub fn try_from_bech32m_unchecked(s: &str) -> Result<Self, crate::error::Bech32Error> {
491        let (_hrp, bytes) = s.try_from_bech32m_unchecked()?;
492        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(bytes)))
493    }
494
495    /// Decodes a Bech32m (BIP-350) string into `Dynamic<Vec<u8>>`, validating that the HRP
496    /// matches `expected_hrp` (case-insensitive).
497    ///
498    /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
499    /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
500    ///
501    /// Prefer this over [`try_from_bech32m_unchecked`](Self::try_from_bech32m_unchecked) in
502    /// security-critical code to prevent cross-protocol confusion attacks.
503    pub fn try_from_bech32m(
504        s: &str,
505        expected_hrp: &str,
506    ) -> Result<Self, crate::error::Bech32Error> {
507        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
508            s.try_from_bech32m(expected_hrp)?,
509        )))
510    }
511}
512
513// ConstantTimeEq
514#[cfg(feature = "ct-eq")]
515impl<T: ?Sized + zeroize::Zeroize> crate::ConstantTimeEq for Dynamic<T>
516where
517    T: crate::ConstantTimeEq,
518{
519    fn ct_eq(&self, other: &Self) -> bool {
520        self.inner.ct_eq(&other.inner)
521    }
522}
523
524// Debug
525impl<T: ?Sized + zeroize::Zeroize> core::fmt::Debug for Dynamic<T> {
526    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
527        f.write_str("[REDACTED]")
528    }
529}
530
531// Clone
532#[cfg(feature = "cloneable")]
533impl<T: zeroize::Zeroize + crate::CloneableSecret> Clone for Dynamic<T> {
534    fn clone(&self) -> Self {
535        Self::new(self.inner.clone())
536    }
537}
538
539// Serialize
540#[cfg(feature = "serde-serialize")]
541impl<T: zeroize::Zeroize + crate::SerializableSecret> serde::Serialize for Dynamic<T> {
542    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
543    where
544        S: serde::Serializer,
545    {
546        self.inner.serialize(serializer)
547    }
548}
549
550// Deserialize
551
552/// Default maximum byte length accepted when deserializing `Dynamic<Vec<u8>>` or
553/// `Dynamic<String>` via the standard `serde::Deserialize` impl (1 MiB).
554///
555/// Pass a custom value to [`Dynamic::deserialize_with_limit`] when a different
556/// ceiling is required.
557///
558/// **Important:** this limit is enforced *after* the upstream deserializer has fully
559/// materialized the payload. It is a **result-length acceptance bound**, not a
560/// pre-allocation DoS guard. For untrusted input, enforce size limits at the
561/// transport or parser layer upstream.
562#[cfg(feature = "serde-deserialize")]
563pub const MAX_DESERIALIZE_BYTES: usize = 1_048_576;
564
565#[cfg(feature = "serde-deserialize")]
566impl Dynamic<alloc::vec::Vec<u8>> {
567    /// Deserializes into `Dynamic<Vec<u8>>`, rejecting payloads larger than `limit` bytes.
568    ///
569    /// The standard [`serde::Deserialize`] impl calls this with [`MAX_DESERIALIZE_BYTES`].
570    /// Use this method directly when you need a tighter or looser ceiling.
571    ///
572    /// The intermediate buffer is kept inside a `Zeroizing` wrapper until after the `Box`
573    /// allocation completes, guaranteeing zeroization even on OOM panic. Oversized buffers
574    /// are also zeroized before the error is returned.
575    ///
576    /// **Important:** this limit is enforced *after* the upstream deserializer has fully
577    /// materialized the payload. It is a **result-length acceptance bound**, not a
578    /// pre-allocation DoS guard. For untrusted input, enforce size limits at the
579    /// transport or parser layer upstream.
580    pub fn deserialize_with_limit<'de, D>(deserializer: D, limit: usize) -> Result<Self, D::Error>
581    where
582        D: serde::Deserializer<'de>,
583    {
584        let mut buf: zeroize::Zeroizing<alloc::vec::Vec<u8>> =
585            zeroize::Zeroizing::new(serde::Deserialize::deserialize(deserializer)?);
586        if buf.len() > limit {
587            // buf drops here → Zeroizing zeros the oversized buffer before deallocation
588            return Err(serde::de::Error::custom(
589                "deserialized secret exceeds maximum size",
590            ));
591        }
592        // Only fallible allocation; protected stays live across it for panic-safety
593        let mut boxed = Box::<alloc::vec::Vec<u8>>::default();
594        core::mem::swap(&mut *boxed, &mut *buf);
595        Ok(Self::from(boxed))
596    }
597}
598
599#[cfg(feature = "serde-deserialize")]
600impl Dynamic<String> {
601    /// Deserializes into `Dynamic<String>`, rejecting payloads larger than `limit` bytes.
602    ///
603    /// The standard [`serde::Deserialize`] impl calls this with [`MAX_DESERIALIZE_BYTES`].
604    /// Use this method directly when you need a tighter or looser ceiling.
605    ///
606    /// The intermediate buffer is kept inside a `Zeroizing` wrapper until after the `Box`
607    /// allocation completes, guaranteeing zeroization even on OOM panic. Oversized buffers
608    /// are also zeroized before the error is returned.
609    ///
610    /// **Important:** this limit is enforced *after* the upstream deserializer has fully
611    /// materialized the payload. It is a **result-length acceptance bound**, not a
612    /// pre-allocation DoS guard. For untrusted input, enforce size limits at the
613    /// transport or parser layer upstream.
614    pub fn deserialize_with_limit<'de, D>(deserializer: D, limit: usize) -> Result<Self, D::Error>
615    where
616        D: serde::Deserializer<'de>,
617    {
618        let mut buf: zeroize::Zeroizing<alloc::string::String> =
619            zeroize::Zeroizing::new(serde::Deserialize::deserialize(deserializer)?);
620        if buf.len() > limit {
621            // buf drops here → Zeroizing zeros the oversized buffer before deallocation
622            return Err(serde::de::Error::custom(
623                "deserialized secret exceeds maximum size",
624            ));
625        }
626        // Only fallible allocation; protected stays live across it for panic-safety
627        let mut boxed = Box::<alloc::string::String>::default();
628        core::mem::swap(&mut *boxed, &mut *buf);
629        Ok(Self::from(boxed))
630    }
631}
632
633#[cfg(feature = "serde-deserialize")]
634impl<'de> serde::Deserialize<'de> for Dynamic<alloc::vec::Vec<u8>> {
635    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
636    where
637        D: serde::Deserializer<'de>,
638    {
639        Self::deserialize_with_limit(deserializer, MAX_DESERIALIZE_BYTES)
640    }
641}
642
643#[cfg(feature = "serde-deserialize")]
644impl<'de> serde::Deserialize<'de> for Dynamic<String> {
645    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
646    where
647        D: serde::Deserializer<'de>,
648    {
649        Self::deserialize_with_limit(deserializer, MAX_DESERIALIZE_BYTES)
650    }
651}
652
653// Zeroize + Drop (now always present with bound)
654impl<T: ?Sized + zeroize::Zeroize> zeroize::Zeroize for Dynamic<T> {
655    fn zeroize(&mut self) {
656        self.inner.zeroize();
657    }
658}
659
660impl<T: ?Sized + zeroize::Zeroize> Drop for Dynamic<T> {
661    fn drop(&mut self) {
662        self.zeroize();
663    }
664}
665
666impl<T: ?Sized + zeroize::Zeroize> zeroize::ZeroizeOnDrop for Dynamic<T> {}