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