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`/`Deserialize`** — requires marker traits 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//! # Construction
24//!
25//! | Constructor | Notes |
26//! |---|---|
27//! | [`Dynamic::new(value)`](Dynamic::new) | Ergonomic default; accepts `String`, `Vec<u8>`, `&str`, `Box<T>`, etc. |
28//! | [`Dynamic::<Vec<u8>>::new_with(f)`](Dynamic::new_with) | Scoped; for API symmetry with [`Fixed::new_with`](crate::Fixed::new_with) |
29//! | [`Dynamic::<String>::new_with(f)`](Dynamic::new_with) | Scoped; for API symmetry |
30//!
31//! Unlike [`Fixed::new_with`](crate::Fixed::new_with), `Dynamic` is already heap-only so
32//! `new_with` exists for consistent API idiom, not for stack-residue avoidance.
33//!
34//! # 3-tier access model
35//!
36//! ```rust
37//! # #[cfg(feature = "alloc")]
38//! # {
39//! use secure_gate::{Dynamic, RevealSecret, RevealSecretMut};
40//!
41//! let mut pw: Dynamic<String> = Dynamic::new(String::from("hunter2"));
42//!
43//! // Tier 1 — scoped (preferred): borrow is confined to the closure.
44//! let len = pw.with_secret(|s: &String| s.len());
45//! assert_eq!(len, 7);
46//!
47//! // Tier 1 mutable — scoped mutation.
48//! pw.with_secret_mut(|s: &mut String| s.push('!'));
49//!
50//! // Tier 2 — direct reference (escape hatch).
51//! assert_eq!(pw.expose_secret(), "hunter2!");
52//!
53//! // Tier 3 — owned consumption.
54//! let owned = pw.into_inner();
55//! assert_eq!(format!("{:?}", owned), "[REDACTED]");
56//! # }
57//! ```
58//!
59//! # Warning
60//!
61//! Ensure your profile sets `panic = "unwind"` — `panic = "abort"` skips destructors
62//! and therefore skips zeroization. (`Dynamic` cannot be `static` since it requires
63//! `Box` allocation, so the static-secret warning from `Fixed` does not apply.)
64//!
65//! # See also
66//!
67//! - [`Fixed<T>`](crate::Fixed) — stack-allocated alternative for fixed-size secrets
68//!   (always available, no `alloc` required).
69
70#[cfg(feature = "alloc")]
71extern crate alloc;
72use alloc::boxed::Box;
73use zeroize::Zeroize;
74
75#[cfg(any(
76    feature = "encoding-hex",
77    feature = "encoding-base64",
78    feature = "encoding-bech32",
79    feature = "encoding-bech32m",
80    feature = "ct-eq",
81))]
82use crate::RevealSecret;
83
84// Encoding traits
85#[cfg(feature = "encoding-base64")]
86use crate::traits::encoding::base64_url::ToBase64Url;
87#[cfg(feature = "encoding-bech32")]
88use crate::traits::encoding::bech32::ToBech32;
89#[cfg(feature = "encoding-bech32m")]
90use crate::traits::encoding::bech32m::ToBech32m;
91#[cfg(feature = "encoding-hex")]
92use crate::traits::encoding::hex::ToHex;
93
94#[cfg(feature = "rand")]
95use rand::{rngs::OsRng, TryCryptoRng, TryRngCore};
96
97// Dynamic<Vec<u8>> is always alloc-dependent, so the alloc-gated blanket traits
98// are always available when encoding features are enabled for this type.
99#[cfg(feature = "encoding-base64")]
100use crate::traits::decoding::base64_url::FromBase64UrlStr;
101#[cfg(feature = "encoding-bech32")]
102use crate::traits::decoding::bech32::FromBech32Str;
103#[cfg(feature = "encoding-bech32m")]
104use crate::traits::decoding::bech32m::FromBech32mStr;
105#[cfg(feature = "encoding-hex")]
106use crate::traits::decoding::hex::FromHexStr;
107
108/// Zero-cost heap-allocated wrapper for variable-length secrets.
109///
110/// `Dynamic<T>` stores a `T: Zeroize` value in a `Box<T>` and unconditionally zeroizes
111/// it on drop (including `Vec`/`String` spare capacity). There is no `Deref`, `AsRef`,
112/// or `Copy` — every access is explicit through [`RevealSecret`](crate::RevealSecret)
113/// or [`RevealSecretMut`](crate::RevealSecretMut).
114///
115/// This is **not** `Fixed<T>` — it is the heap-allocated alternative for variable-length
116/// secrets. Secret bytes never reside on the stack.
117///
118/// # Examples
119///
120/// ```rust
121/// # #[cfg(feature = "alloc")]
122/// # {
123/// use secure_gate::{Dynamic, RevealSecret};
124///
125/// let pw: Dynamic<String> = Dynamic::new(String::from("hunter2"));
126/// assert_eq!(pw.with_secret(|s: &String| s.len()), 7);
127/// assert_eq!(format!("{:?}", pw), "[REDACTED]");
128/// # }
129/// ```
130///
131/// # Constructors for `Dynamic<Vec<u8>>`
132///
133/// | Constructor | Feature | Notes |
134/// |---|---|---|
135/// | [`new(value)`](Self::new) | — | Accepts `Vec<u8>`, `&[u8]`, `Box<Vec<u8>>` |
136/// | [`new_with(f)`](Self::new_with) | — | Scoped closure construction |
137/// | [`try_from_hex(s)`](Self::try_from_hex) | `encoding-hex` | Constant-time hex decoding |
138/// | [`try_from_base64url(s)`](Self::try_from_base64url) | `encoding-base64` | Constant-time Base64url decoding |
139/// | [`try_from_bech32(s, hrp)`](Self::try_from_bech32) | `encoding-bech32` | HRP-validated Bech32 |
140/// | [`try_from_bech32_unchecked(s)`](Self::try_from_bech32_unchecked) | `encoding-bech32` | Bech32 without HRP check |
141/// | [`try_from_bech32m(s, hrp)`](Self::try_from_bech32m) | `encoding-bech32m` | HRP-validated Bech32m |
142/// | [`try_from_bech32m_unchecked(s)`](Self::try_from_bech32m_unchecked) | `encoding-bech32m` | Bech32m without HRP check |
143/// | [`from_random(len)`](Self::from_random) | `rand` | System RNG |
144/// | [`from_rng(len, rng)`](Self::from_rng) | `rand` | Custom RNG |
145///
146/// # See also
147///
148/// - [`RevealSecret`](crate::RevealSecret) / [`RevealSecretMut`](crate::RevealSecretMut) — the 3-tier access traits.
149/// - [`Fixed<T>`](crate::Fixed) — stack-allocated alternative.
150pub struct Dynamic<T: ?Sized + zeroize::Zeroize> {
151    inner: Box<T>,
152}
153
154impl<T: ?Sized + zeroize::Zeroize> Dynamic<T> {
155    /// Wraps `value` in a `Box<T>` and returns a `Dynamic<T>`.
156    ///
157    /// Accepts any type that implements `Into<Box<T>>` — including owned values,
158    /// `Box<T>`, `String`, `Vec<u8>`, `&str` (via the blanket `From<&str>` impl), etc.
159    ///
160    /// Equivalent to `Dynamic::from(value)` — `#[doc(alias = "from")]` is set so both
161    /// names appear in docs.rs search.
162    ///
163    /// Requires the `alloc` feature (which `Dynamic<T>` itself always requires).
164    #[doc(alias = "from")]
165    #[inline(always)]
166    pub fn new<U>(value: U) -> Self
167    where
168        U: Into<Box<T>>,
169    {
170        let inner = value.into();
171        Self { inner }
172    }
173}
174
175/// Zero-copy wrapping of an already-boxed value.
176impl<T: ?Sized + zeroize::Zeroize> From<Box<T>> for Dynamic<T> {
177    #[inline(always)]
178    fn from(boxed: Box<T>) -> Self {
179        Self { inner: boxed }
180    }
181}
182
183/// Copies a byte slice to the heap and wraps it.
184impl From<&[u8]> for Dynamic<Vec<u8>> {
185    #[inline(always)]
186    fn from(slice: &[u8]) -> Self {
187        Self::new(slice.to_vec())
188    }
189}
190
191/// Copies a string to the heap and wraps it.
192impl From<&str> for Dynamic<String> {
193    #[inline(always)]
194    fn from(input: &str) -> Self {
195        Self::new(input.to_string())
196    }
197}
198
199/// Boxes the value and wraps it.
200impl<T: 'static + zeroize::Zeroize> From<T> for Dynamic<T> {
201    #[inline(always)]
202    fn from(value: T) -> Self {
203        Self {
204            inner: Box::new(value),
205        }
206    }
207}
208
209// Hex encoding and decoding for Dynamic<Vec<u8>>.
210// Dynamic is always heap-allocated, so no no-alloc split is needed.
211#[cfg(feature = "encoding-hex")]
212impl Dynamic<Vec<u8>> {
213    /// Encodes the secret bytes as a lowercase hex string.
214    #[inline]
215    pub fn to_hex(&self) -> alloc::string::String {
216        self.with_secret(|s: &Vec<u8>| s.to_hex())
217    }
218
219    /// Encodes the secret bytes as an uppercase hex string.
220    #[inline]
221    pub fn to_hex_upper(&self) -> alloc::string::String {
222        self.with_secret(|s: &Vec<u8>| s.to_hex_upper())
223    }
224
225    /// Encodes the secret bytes as a lowercase hex string, returning
226    /// [`EncodedSecret`](crate::EncodedSecret) to preserve zeroization.
227    #[inline]
228    pub fn to_hex_zeroizing(&self) -> crate::EncodedSecret {
229        self.with_secret(|s: &Vec<u8>| s.to_hex_zeroizing())
230    }
231
232    /// Encodes the secret bytes as an uppercase hex string, returning
233    /// [`EncodedSecret`](crate::EncodedSecret) to preserve zeroization.
234    #[inline]
235    pub fn to_hex_upper_zeroizing(&self) -> crate::EncodedSecret {
236        self.with_secret(|s: &Vec<u8>| s.to_hex_upper_zeroizing())
237    }
238
239    /// Decodes a hex string (lowercase, uppercase, or mixed) into `Dynamic<Vec<u8>>`.
240    ///
241    /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
242    /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
243    pub fn try_from_hex(s: &str) -> Result<Self, crate::error::HexError> {
244        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
245            s.try_from_hex()?,
246        )))
247    }
248}
249
250// Base64url encoding and decoding for Dynamic<Vec<u8>>.
251#[cfg(feature = "encoding-base64")]
252impl Dynamic<Vec<u8>> {
253    /// Encodes the secret bytes as an unpadded Base64url string (RFC 4648, URL-safe alphabet).
254    #[inline]
255    pub fn to_base64url(&self) -> alloc::string::String {
256        self.with_secret(|s: &Vec<u8>| s.to_base64url())
257    }
258
259    /// Encodes the secret bytes as an unpadded Base64url string, returning
260    /// [`EncodedSecret`](crate::EncodedSecret) to preserve zeroization.
261    #[inline]
262    pub fn to_base64url_zeroizing(&self) -> crate::EncodedSecret {
263        self.with_secret(|s: &Vec<u8>| s.to_base64url_zeroizing())
264    }
265
266    /// Decodes a Base64url (unpadded) string into `Dynamic<Vec<u8>>`.
267    ///
268    /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
269    /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
270    pub fn try_from_base64url(s: &str) -> Result<Self, crate::error::Base64Error> {
271        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
272            s.try_from_base64url()?,
273        )))
274    }
275}
276
277// Bech32 (BIP-173) encoding and decoding for Dynamic<Vec<u8>>.
278#[cfg(feature = "encoding-bech32")]
279impl Dynamic<Vec<u8>> {
280    /// Encodes the secret bytes as a Bech32 (BIP-173) string with the given HRP.
281    #[inline]
282    pub fn try_to_bech32(
283        &self,
284        hrp: &str,
285    ) -> Result<alloc::string::String, crate::error::Bech32Error> {
286        self.with_secret(|s: &Vec<u8>| s.try_to_bech32(hrp))
287    }
288
289    /// Encodes the secret bytes as a Bech32 string, returning
290    /// [`EncodedSecret`](crate::EncodedSecret) to preserve zeroization.
291    #[inline]
292    pub fn try_to_bech32_zeroizing(
293        &self,
294        hrp: &str,
295    ) -> Result<crate::EncodedSecret, crate::error::Bech32Error> {
296        self.with_secret(|s: &Vec<u8>| s.try_to_bech32_zeroizing(hrp))
297    }
298
299    /// Decodes a Bech32 (BIP-173) string into `Dynamic<Vec<u8>>`, validating the HRP
300    /// (case-insensitive).
301    ///
302    /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
303    /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
304    ///
305    /// HRP comparison is non-constant-time — this is intentional, as the HRP is public
306    /// metadata, not secret material.
307    pub fn try_from_bech32(s: &str, expected_hrp: &str) -> Result<Self, crate::error::Bech32Error> {
308        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
309            s.try_from_bech32(expected_hrp)?,
310        )))
311    }
312
313    /// Decodes a Bech32 (BIP-173) string into `Dynamic<Vec<u8>>` without validating the HRP.
314    ///
315    /// Use [`try_from_bech32`](Self::try_from_bech32) in security-critical code to prevent
316    /// cross-protocol confusion attacks.
317    pub fn try_from_bech32_unchecked(s: &str) -> Result<Self, crate::error::Bech32Error> {
318        let (_hrp, bytes) = s.try_from_bech32_unchecked()?;
319        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(bytes)))
320    }
321}
322
323// Bech32m (BIP-350) encoding and decoding for Dynamic<Vec<u8>>.
324#[cfg(feature = "encoding-bech32m")]
325impl Dynamic<Vec<u8>> {
326    /// Encodes the secret bytes as a Bech32m (BIP-350) string with the given HRP.
327    #[inline]
328    pub fn try_to_bech32m(
329        &self,
330        hrp: &str,
331    ) -> Result<alloc::string::String, crate::error::Bech32Error> {
332        self.with_secret(|s: &Vec<u8>| s.try_to_bech32m(hrp))
333    }
334
335    /// Encodes the secret bytes as a Bech32m string, returning
336    /// [`EncodedSecret`](crate::EncodedSecret) to preserve zeroization.
337    #[inline]
338    pub fn try_to_bech32m_zeroizing(
339        &self,
340        hrp: &str,
341    ) -> Result<crate::EncodedSecret, crate::error::Bech32Error> {
342        self.with_secret(|s: &Vec<u8>| s.try_to_bech32m_zeroizing(hrp))
343    }
344
345    /// Decodes a Bech32m (BIP-350) string into `Dynamic<Vec<u8>>`, validating the HRP
346    /// (case-insensitive).
347    ///
348    /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
349    /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
350    pub fn try_from_bech32m(
351        s: &str,
352        expected_hrp: &str,
353    ) -> Result<Self, crate::error::Bech32Error> {
354        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
355            s.try_from_bech32m(expected_hrp)?,
356        )))
357    }
358
359    /// Decodes a Bech32m (BIP-350) string into `Dynamic<Vec<u8>>` without validating the HRP.
360    ///
361    /// Use [`try_from_bech32m`](Self::try_from_bech32m) in security-critical code.
362    pub fn try_from_bech32m_unchecked(s: &str) -> Result<Self, crate::error::Bech32Error> {
363        let (_hrp, bytes) = s.try_from_bech32m_unchecked()?;
364        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(bytes)))
365    }
366}
367
368/// Construction helpers and random generation for `Dynamic<Vec<u8>>`.
369impl Dynamic<Vec<u8>> {
370    /// Transfers `protected` bytes into a freshly boxed `Vec`, keeping
371    /// [`zeroize::Zeroizing`] alive across the only allocation that can panic.
372    ///
373    /// # Panic safety
374    ///
375    /// `Box::new(Vec::new())` is the sole allocation point — just the 24-byte
376    /// `Vec` header, no data buffer. If it panics (OOM), `protected` is still
377    /// in scope and `Zeroizing::drop` zeroes the secret bytes during unwind.
378    /// After the swap, `protected` holds an empty `Vec` (no-op to zeroize) and
379    /// `Dynamic::from(boxed)` is an infallible struct-field assignment.
380    ///
381    /// Note: `Box::new(*protected)` would be cleaner but does not compile —
382    /// `Zeroizing` implements `Deref` (returning `&T`), not a move-out, so
383    /// `*protected` yields a reference rather than an owned value (E0507).
384    #[cfg(any(
385        feature = "encoding-hex",
386        feature = "encoding-base64",
387        feature = "encoding-bech32",
388        feature = "encoding-bech32m",
389    ))]
390    #[inline(always)]
391    fn from_protected_bytes(mut protected: zeroize::Zeroizing<alloc::vec::Vec<u8>>) -> Self {
392        // Only fallible allocation; protected stays live across it for panic-safety
393        let mut boxed = Box::<alloc::vec::Vec<u8>>::default();
394        core::mem::swap(&mut *boxed, &mut *protected);
395        Self::from(boxed)
396    }
397
398    /// Closure-based constructor for consistent API with [`Fixed::new_with`](crate::Fixed::new_with).
399    /// The actual secret data is allocated on the heap; this method exists
400    /// for consistent security-first construction idiom across the crate.
401    #[inline(always)]
402    pub fn new_with<F>(f: F) -> Self
403    where
404        F: FnOnce(&mut alloc::vec::Vec<u8>),
405    {
406        let mut v = alloc::vec::Vec::new();
407        f(&mut v);
408        Self::new(v)
409    }
410}
411
412impl Dynamic<alloc::string::String> {
413    /// Closure-based constructor for consistent API with [`Fixed::new_with`](crate::Fixed::new_with).
414    /// The actual secret data is allocated on the heap; this method exists
415    /// for consistent security-first construction idiom across the crate.
416    #[inline(always)]
417    pub fn new_with<F>(f: F) -> Self
418    where
419        F: FnOnce(&mut alloc::string::String),
420    {
421        let mut s = alloc::string::String::new();
422        f(&mut s);
423        Self::new(s)
424    }
425}
426
427// RevealSecret
428impl crate::RevealSecret for Dynamic<String> {
429    type Inner = String;
430
431    #[inline(always)]
432    fn with_secret<F, R>(&self, f: F) -> R
433    where
434        F: FnOnce(&String) -> R,
435    {
436        f(&self.inner)
437    }
438
439    #[inline(always)]
440    fn expose_secret(&self) -> &String {
441        &self.inner
442    }
443
444    #[inline(always)]
445    fn len(&self) -> usize {
446        self.inner.len()
447    }
448
449    /// Consumes `self` and returns the inner `String` wrapped in [`crate::InnerSecret`].
450    ///
451    /// **Allocation note:** allocates one small `Box<String>` sentinel (24 bytes on
452    /// 64-bit) before the swap. If that allocation panics (OOM), `self.inner` is
453    /// unchanged and `Dynamic::drop` zeroizes the real secret during unwind —
454    /// confidentiality is preserved. This is the same OOM-safety pattern used by
455    /// `from_protected_bytes` and `deserialize_with_limit`.
456    ///
457    /// See [`RevealSecret::into_inner`] for full documentation including the
458    /// redacted `Debug` behavior.
459    #[inline(always)]
460    fn into_inner(mut self) -> crate::InnerSecret<String>
461    where
462        Self: Sized,
463        Self::Inner: Sized + Default + zeroize::Zeroize,
464    {
465        // Swap in an empty-String sentinel. If Default::default() panics (OOM) before
466        // the swap, self.inner still holds the real secret and Dynamic::drop zeroizes
467        // it on unwind. After the swap, self.inner is Box<String::new()> — zeroized
468        // on Dynamic::drop as a no-op. `*boxed` deref-moves the String out of the Box.
469        let boxed = core::mem::take(&mut self.inner);
470        crate::InnerSecret::new(*boxed)
471    }
472}
473
474impl<T: zeroize::Zeroize> crate::RevealSecret for Dynamic<Vec<T>> {
475    type Inner = Vec<T>;
476
477    #[inline(always)]
478    fn with_secret<F, R>(&self, f: F) -> R
479    where
480        F: FnOnce(&Vec<T>) -> R,
481    {
482        f(&self.inner)
483    }
484
485    #[inline(always)]
486    fn expose_secret(&self) -> &Vec<T> {
487        &self.inner
488    }
489
490    #[inline(always)]
491    fn len(&self) -> usize {
492        self.inner.len() * core::mem::size_of::<T>()
493    }
494
495    /// Consumes `self` and returns the inner `Vec<T>` wrapped in [`crate::InnerSecret`].
496    ///
497    /// **Allocation note:** allocates one small `Box<Vec<T>>` sentinel (24 bytes on
498    /// 64-bit) before the swap. If that allocation panics (OOM), `self.inner` is
499    /// unchanged and `Dynamic::drop` zeroizes the real secret during unwind —
500    /// confidentiality is preserved. This is the same OOM-safety pattern used by
501    /// `from_protected_bytes` and `deserialize_with_limit`.
502    ///
503    /// See [`RevealSecret::into_inner`] for full documentation including the
504    /// redacted `Debug` behavior.
505    #[inline(always)]
506    fn into_inner(mut self) -> crate::InnerSecret<Vec<T>>
507    where
508        Self: Sized,
509        Self::Inner: Sized + Default + zeroize::Zeroize,
510    {
511        // Swap in an empty-Vec sentinel. If Default::default() panics (OOM) before the
512        // swap, self.inner still holds the real secret and Dynamic::drop zeroizes it on
513        // unwind. After the swap, self.inner is Box<Vec::new()> — zeroized on
514        // Dynamic::drop as a no-op. `*boxed` deref-moves the Vec out of the Box.
515        let boxed = core::mem::take(&mut self.inner);
516        crate::InnerSecret::new(*boxed)
517    }
518}
519
520// RevealSecretMut
521impl crate::RevealSecretMut for Dynamic<String> {
522    #[inline(always)]
523    fn with_secret_mut<F, R>(&mut self, f: F) -> R
524    where
525        F: FnOnce(&mut String) -> R,
526    {
527        f(&mut self.inner)
528    }
529
530    #[inline(always)]
531    fn expose_secret_mut(&mut self) -> &mut String {
532        &mut self.inner
533    }
534}
535
536impl<T: zeroize::Zeroize> crate::RevealSecretMut for Dynamic<Vec<T>> {
537    #[inline(always)]
538    fn with_secret_mut<F, R>(&mut self, f: F) -> R
539    where
540        F: FnOnce(&mut Vec<T>) -> R,
541    {
542        f(&mut self.inner)
543    }
544
545    #[inline(always)]
546    fn expose_secret_mut(&mut self) -> &mut Vec<T> {
547        &mut self.inner
548    }
549}
550
551// Random generation
552#[cfg(feature = "rand")]
553impl Dynamic<alloc::vec::Vec<u8>> {
554    /// Fills a new `Vec<u8>` with `len` cryptographically secure random bytes and wraps it.
555    ///
556    /// Uses the system RNG ([`OsRng`](rand::rngs::OsRng)) via [`TryRngCore::try_fill_bytes`](rand::TryRngCore::try_fill_bytes).
557    /// Requires the `rand` feature (and `alloc`, which `Dynamic<Vec<u8>>` always needs).
558    ///
559    /// # Panics
560    ///
561    /// Panics if the system RNG fails to provide bytes ([`TryRngCore::try_fill_bytes`](rand::TryRngCore::try_fill_bytes)
562    /// returns `Err`). This is treated as a fatal environment error.
563    ///
564    /// # Examples
565    ///
566    /// ```rust
567    /// # #[cfg(all(feature = "alloc", feature = "rand"))]
568    /// use secure_gate::{Dynamic, RevealSecret};
569    ///
570    /// # #[cfg(all(feature = "alloc", feature = "rand"))]
571    /// # {
572    /// let nonce: Dynamic<Vec<u8>> = Dynamic::from_random(24);
573    /// assert_eq!(nonce.len(), 24);
574    /// # }
575    /// ```
576    #[inline]
577    pub fn from_random(len: usize) -> Self {
578        Self::new_with(|v| {
579            v.resize(len, 0u8);
580            OsRng
581                .try_fill_bytes(v)
582                .expect("OsRng failure is a program error");
583        })
584    }
585
586    /// Allocates a `Vec<u8>` of length `len`, fills it from `rng`, and wraps it.
587    ///
588    /// Accepts any [`TryCryptoRng`](rand::TryCryptoRng) + [`TryRngCore`](rand::TryRngCore) — for example,
589    /// a seeded [`StdRng`](rand::rngs::StdRng) for deterministic tests. Requires the `rand`
590    /// feature and `alloc` (implicit — [`Dynamic<T>`](crate::Dynamic) itself requires it).
591    ///
592    /// # Errors
593    ///
594    /// Returns `R::Error` if [`try_fill_bytes`](rand::TryRngCore::try_fill_bytes) fails.
595    ///
596    /// # Examples
597    ///
598    /// ```rust
599    /// # #[cfg(all(feature = "alloc", feature = "rand"))]
600    /// # {
601    /// use rand::rngs::StdRng;
602    /// use rand::SeedableRng;
603    /// use secure_gate::Dynamic;
604    ///
605    /// let mut rng = StdRng::from_seed([9u8; 32]);
606    /// let nonce: Dynamic<Vec<u8>> = Dynamic::from_rng(24, &mut rng).expect("rng fill");
607    /// # }
608    /// ```
609    #[inline]
610    pub fn from_rng<R: TryRngCore + TryCryptoRng>(
611        len: usize,
612        rng: &mut R,
613    ) -> Result<Self, R::Error> {
614        let mut result = Ok(());
615        let this = Self::new_with(|v| {
616            v.resize(len, 0u8);
617            result = rng.try_fill_bytes(v);
618        });
619        result.map(|_| this)
620    }
621}
622
623/// Constant-time equality for `Dynamic<T>` — routes through [`expose_secret()`](crate::RevealSecret::expose_secret).
624///
625/// `==` is **deliberately not implemented**. Always use `ct_eq`.
626#[cfg(feature = "ct-eq")]
627impl<T: ?Sized + zeroize::Zeroize> crate::ConstantTimeEq for Dynamic<T>
628where
629    T: crate::ConstantTimeEq,
630    Self: crate::RevealSecret<Inner = T>,
631{
632    fn ct_eq(&self, other: &Self) -> bool {
633        self.expose_secret().ct_eq(other.expose_secret())
634    }
635}
636
637/// Always prints `[REDACTED]` — secrets never appear in debug output.
638impl<T: ?Sized + zeroize::Zeroize> core::fmt::Debug for Dynamic<T> {
639    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
640        f.write_str("[REDACTED]")
641    }
642}
643
644/// Opt-in cloning — requires `cloneable` feature and [`CloneableSecret`](crate::CloneableSecret)
645/// marker. Each clone is independently zeroized on drop, but cloning increases exposure surface.
646#[cfg(feature = "cloneable")]
647impl<T: zeroize::Zeroize + crate::CloneableSecret> Clone for Dynamic<T> {
648    fn clone(&self) -> Self {
649        Self::new(self.inner.clone())
650    }
651}
652
653/// Opt-in serialization — requires `serde-serialize` feature and
654/// [`SerializableSecret`](crate::SerializableSecret) marker. Serialization exposes the
655/// full secret — audit every impl.
656#[cfg(feature = "serde-serialize")]
657impl<T: zeroize::Zeroize + crate::SerializableSecret> serde::Serialize for Dynamic<T> {
658    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
659    where
660        S: serde::Serializer,
661    {
662        self.inner.serialize(serializer)
663    }
664}
665
666// Deserialize
667
668/// Default maximum byte length accepted when deserializing `Dynamic<Vec<u8>>` or
669/// `Dynamic<String>` via the standard `serde::Deserialize` impl (1 MiB).
670///
671/// Pass a custom value to [`Dynamic::deserialize_with_limit`] when a different
672/// ceiling is required.
673///
674/// **Important:** this limit is enforced *after* the upstream deserializer has fully
675/// materialized the payload. It is a **result-length acceptance bound**, not a
676/// pre-allocation DoS guard. For untrusted input, enforce size limits at the
677/// transport or parser layer upstream.
678#[cfg(feature = "serde-deserialize")]
679pub const MAX_DESERIALIZE_BYTES: usize = 1_048_576;
680
681#[cfg(feature = "serde-deserialize")]
682impl Dynamic<alloc::vec::Vec<u8>> {
683    /// Deserializes into `Dynamic<Vec<u8>>`, rejecting payloads larger than `limit` bytes.
684    ///
685    /// The standard [`serde::Deserialize`] impl calls this with [`MAX_DESERIALIZE_BYTES`].
686    /// Use this method directly when you need a tighter or looser ceiling.
687    ///
688    /// The intermediate buffer is kept inside a `Zeroizing` wrapper until after the `Box`
689    /// allocation completes, guaranteeing zeroization even on OOM panic. Oversized buffers
690    /// are also zeroized before the error is returned.
691    ///
692    /// **Important:** this limit is enforced *after* the upstream deserializer has fully
693    /// materialized the payload. It is a **result-length acceptance bound**, not a
694    /// pre-allocation DoS guard. For untrusted input, enforce size limits at the
695    /// transport or parser layer upstream.
696    pub fn deserialize_with_limit<'de, D>(deserializer: D, limit: usize) -> Result<Self, D::Error>
697    where
698        D: serde::Deserializer<'de>,
699    {
700        let mut buf: zeroize::Zeroizing<alloc::vec::Vec<u8>> =
701            zeroize::Zeroizing::new(serde::Deserialize::deserialize(deserializer)?);
702        if buf.len() > limit {
703            // buf drops here → Zeroizing zeros the oversized buffer before deallocation
704            return Err(serde::de::Error::custom(
705                "deserialized secret exceeds maximum size",
706            ));
707        }
708        // Only fallible allocation; protected stays live across it for panic-safety
709        let mut boxed = Box::<alloc::vec::Vec<u8>>::default();
710        core::mem::swap(&mut *boxed, &mut *buf);
711        Ok(Self::from(boxed))
712    }
713}
714
715#[cfg(feature = "serde-deserialize")]
716impl Dynamic<String> {
717    /// Deserializes into `Dynamic<String>`, rejecting payloads larger than `limit` bytes.
718    ///
719    /// The standard [`serde::Deserialize`] impl calls this with [`MAX_DESERIALIZE_BYTES`].
720    /// Use this method directly when you need a tighter or looser ceiling.
721    ///
722    /// The intermediate buffer is kept inside a `Zeroizing` wrapper until after the `Box`
723    /// allocation completes, guaranteeing zeroization even on OOM panic. Oversized buffers
724    /// are also zeroized before the error is returned.
725    ///
726    /// **Important:** this limit is enforced *after* the upstream deserializer has fully
727    /// materialized the payload. It is a **result-length acceptance bound**, not a
728    /// pre-allocation DoS guard. For untrusted input, enforce size limits at the
729    /// transport or parser layer upstream.
730    pub fn deserialize_with_limit<'de, D>(deserializer: D, limit: usize) -> Result<Self, D::Error>
731    where
732        D: serde::Deserializer<'de>,
733    {
734        let mut buf: zeroize::Zeroizing<alloc::string::String> =
735            zeroize::Zeroizing::new(serde::Deserialize::deserialize(deserializer)?);
736        if buf.len() > limit {
737            // buf drops here → Zeroizing zeros the oversized buffer before deallocation
738            return Err(serde::de::Error::custom(
739                "deserialized secret exceeds maximum size",
740            ));
741        }
742        // Only fallible allocation; protected stays live across it for panic-safety
743        let mut boxed = Box::<alloc::string::String>::default();
744        core::mem::swap(&mut *boxed, &mut *buf);
745        Ok(Self::from(boxed))
746    }
747}
748
749#[cfg(feature = "serde-deserialize")]
750impl<'de> serde::Deserialize<'de> for Dynamic<alloc::vec::Vec<u8>> {
751    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
752    where
753        D: serde::Deserializer<'de>,
754    {
755        Self::deserialize_with_limit(deserializer, MAX_DESERIALIZE_BYTES)
756    }
757}
758
759#[cfg(feature = "serde-deserialize")]
760impl<'de> serde::Deserialize<'de> for Dynamic<String> {
761    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
762    where
763        D: serde::Deserializer<'de>,
764    {
765        Self::deserialize_with_limit(deserializer, MAX_DESERIALIZE_BYTES)
766    }
767}
768
769/// Zeroizes the inner value (including `Vec`/`String` spare capacity).
770///
771/// **Warning:** does not run under `panic = "abort"`.
772impl<T: ?Sized + zeroize::Zeroize> zeroize::Zeroize for Dynamic<T> {
773    fn zeroize(&mut self) {
774        self.inner.zeroize();
775    }
776}
777
778/// Unconditionally zeroizes the inner value when the wrapper is dropped.
779///
780/// **Warning:** `Drop` does not run under `panic = "abort"`.
781impl<T: ?Sized + zeroize::Zeroize> Drop for Dynamic<T> {
782    fn drop(&mut self) {
783        self.zeroize();
784    }
785}
786
787/// Marker confirming that `Dynamic<T>` always zeroizes on drop.
788impl<T: ?Sized + zeroize::Zeroize> zeroize::ZeroizeOnDrop for Dynamic<T> {}