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//! # Choosing the inner type for security
24//!
25//! Different inner types have different reallocation-residue profiles:
26//!
27//! | Inner type | Realloc surface | Use case |
28//! |---|---|---|
29//! | `Dynamic<[u8; N]>` (boxed array) | **None** — fixed size | Long-lived known-size keys held on the heap |
30//! | `Dynamic<Vec<u8>>` pre-sized | None *if* you avoid capacity-growing mutations | Variable-length secrets with a known upper bound |
31//! | `Dynamic<Vec<u8>>` growable | **Yes** — each realloc leaves the old buffer unzeroed | Convenient, but see realloc-residue warning below |
32//! | `Dynamic<String>` | Same as `Dynamic<Vec<u8>>` | Passwords, API keys |
33//!
34//! For **long-lived, known-size key material**, prefer `Dynamic<[u8; N]>` —
35//! it combines `Dynamic`'s heap-only property with zero realloc surface.
36//! See `SECURITY.md` § "Inherent Rust Limitations" for the broader discussion.
37//!
38//! # Construction
39//!
40//! | Constructor | Notes |
41//! |---|---|
42//! | [`Dynamic::new(value)`](Dynamic::new) | Ergonomic default; accepts `String`, `Vec<u8>`, `&str`, `Box<T>`, etc. |
43//! | [`Dynamic::<Vec<u8>>::new_with(f)`](Dynamic::new_with) | Scoped; for API symmetry with [`Fixed::new_with`](crate::Fixed::new_with) |
44//! | [`Dynamic::<String>::new_with(f)`](Dynamic::new_with) | Scoped; for API symmetry |
45//!
46//! Unlike [`Fixed::new_with`](crate::Fixed::new_with), `Dynamic` is already heap-only so
47//! `new_with` exists for consistent API idiom, not for stack-residue avoidance.
48//!
49//! # 3-tier access model
50//!
51//! ```rust
52//! # #[cfg(feature = "alloc")]
53//! # {
54//! use secure_gate::{Dynamic, RevealSecret, RevealSecretMut, SecretLen};
55//!
56//! let mut pw: Dynamic<String> = Dynamic::new(String::from("hunter2"));
57//!
58//! // Tier 1 — scoped (preferred): borrow is confined to the closure.
59//! let len = pw.with_secret(|s: &String| s.len());
60//! assert_eq!(len, 7);
61//!
62//! // Tier 1 mutable — scoped mutation. Prefer capacity-stable mutations for
63//! // Dynamic<String> / Dynamic<Vec<u8>>; see SECURITY.md for realloc notes.
64//! pw.with_secret_mut(|s: &mut String| s.make_ascii_uppercase());
65//!
66//! // Tier 2 — direct reference (escape hatch).
67//! assert_eq!(pw.expose_secret(), "HUNTER2");
68//!
69//! // Tier 3 — owned consumption.
70//! let owned = pw.into_inner();
71//! assert_eq!(format!("{:?}", owned), "[REDACTED]");
72//! # }
73//! ```
74//!
75//! # Warning
76//!
77//! Ensure your profile sets `panic = "unwind"` — `panic = "abort"` skips destructors
78//! and therefore skips zeroization. (`Dynamic` cannot be `static` since it requires
79//! `Box` allocation, so the static-secret warning from `Fixed` does not apply.)
80//!
81//! For `Dynamic<Vec<_>>` and `Dynamic<String>`, capacity-changing mutations can
82//! cause the standard allocator to free the previous buffer without zeroizing it.
83//! Pre-allocate to the maximum size, use capacity-stable mutations, prefer
84//! `Dynamic<[u8; N]>` (no realloc surface) for known-size heap secrets, or use
85//! [`Fixed<T>`](crate::Fixed) for known-size stack secrets. See `SECURITY.md` for
86//! the full threat-model discussion and deployment-level mitigations including
87//! the [`zeroizing-alloc`](https://crates.io/crates/zeroizing-alloc) global
88//! allocator.
89//!
90//! # See also
91//!
92//! - [`Fixed<T>`](crate::Fixed) — stack-allocated alternative for fixed-size secrets
93//!   (always available, no `alloc` required).
94
95#[cfg(feature = "alloc")]
96extern crate alloc;
97use alloc::boxed::Box;
98use alloc::string::{String, ToString};
99use alloc::vec::Vec;
100use zeroize::Zeroize;
101
102#[cfg(any(
103    feature = "encoding-hex",
104    feature = "encoding-base32",
105    feature = "encoding-base64",
106    feature = "encoding-bech32",
107    feature = "encoding-bech32m",
108    feature = "ct-eq",
109    feature = "std",
110))]
111use crate::RevealSecret;
112
113// Encoding traits
114#[cfg(feature = "encoding-base32")]
115use crate::traits::encoding::base32::ToBase32;
116#[cfg(feature = "encoding-base64")]
117use crate::traits::encoding::base64_url::ToBase64Url;
118#[cfg(feature = "encoding-bech32")]
119use crate::traits::encoding::bech32::ToBech32;
120#[cfg(feature = "encoding-bech32m")]
121use crate::traits::encoding::bech32m::ToBech32m;
122#[cfg(feature = "encoding-hex")]
123use crate::traits::encoding::hex::ToHex;
124
125#[cfg(feature = "rand")]
126use rand::{rngs::OsRng, TryCryptoRng, TryRngCore};
127
128// Dynamic<Vec<u8>> is always alloc-dependent, so the alloc-gated blanket traits
129// are always available when encoding features are enabled for this type.
130#[cfg(feature = "encoding-base32")]
131use crate::traits::decoding::base32::FromBase32Str;
132#[cfg(feature = "encoding-base64")]
133use crate::traits::decoding::base64_url::FromBase64UrlStr;
134#[cfg(feature = "encoding-bech32")]
135use crate::traits::decoding::bech32::FromBech32Str;
136#[cfg(feature = "encoding-bech32m")]
137use crate::traits::decoding::bech32m::FromBech32mStr;
138#[cfg(feature = "encoding-hex")]
139use crate::traits::decoding::hex::FromHexStr;
140
141/// Zero-cost heap-allocated wrapper for variable-length secrets.
142///
143/// `Dynamic<T>` stores a `T: Zeroize` value in a `Box<T>` and unconditionally zeroizes
144/// it on drop (including `Vec`/`String` spare capacity). There is no `Deref`, `AsRef`,
145/// or `Copy` — every access is explicit through [`RevealSecret`](crate::RevealSecret)
146/// or [`RevealSecretMut`](crate::RevealSecretMut).
147///
148/// This is **not** `Fixed<T>` — it is the heap-allocated alternative for variable-length
149/// secrets. Secret bytes never reside on the stack.
150///
151/// # Examples
152///
153/// ```rust
154/// # #[cfg(feature = "alloc")]
155/// # {
156/// use secure_gate::{Dynamic, RevealSecret, SecretLen};
157///
158/// let pw: Dynamic<String> = Dynamic::new(String::from("hunter2"));
159/// assert_eq!(pw.with_secret(|s: &String| s.len()), 7);
160/// assert_eq!(format!("{:?}", pw), "[REDACTED]");
161/// # }
162/// ```
163///
164/// # Constructors for `Dynamic<Vec<u8>>`
165///
166/// | Constructor | Feature | Notes |
167/// |---|---|---|
168/// | [`new(value)`](Self::new) | — | Accepts `Vec<u8>`, `&[u8]`, `Box<Vec<u8>>` |
169/// | [`new_with(f)`](Self::new_with) | — | Scoped closure construction |
170/// | [`try_from_hex(s)`](Self::try_from_hex) | `encoding-hex` | Constant-time hex decoding |
171/// | [`try_from_base32(s)`](Self::try_from_base32) | `encoding-base32` | Constant-time Base32 decoding |
172/// | [`try_from_base64url(s)`](Self::try_from_base64url) | `encoding-base64` | Constant-time Base64url decoding |
173/// | [`try_from_bech32(s, hrp)`](Self::try_from_bech32) | `encoding-bech32` | HRP-validated Bech32 |
174/// | [`try_from_bech32_unchecked(s)`](Self::try_from_bech32_unchecked) | `encoding-bech32` | Bech32 without HRP check |
175/// | [`try_from_bech32m(s, hrp)`](Self::try_from_bech32m) | `encoding-bech32m` | HRP-validated Bech32m |
176/// | [`try_from_bech32m_unchecked(s)`](Self::try_from_bech32m_unchecked) | `encoding-bech32m` | Bech32m without HRP check |
177/// | [`from_random(len)`](Self::from_random) | `rand` | System RNG |
178/// | [`from_rng(len, rng)`](Self::from_rng) | `rand` | Custom RNG |
179///
180/// # See also
181///
182/// - [`RevealSecret`](crate::RevealSecret) / [`RevealSecretMut`](crate::RevealSecretMut) — the 3-tier access traits.
183/// - [`Fixed<T>`](crate::Fixed) — stack-allocated alternative.
184#[must_use = "Dynamic<T> holds secret material; dropping it on the floor usually indicates a bug — bind with `let _name = ...` or chain a method call"]
185pub struct Dynamic<T: ?Sized + zeroize::Zeroize> {
186    inner: Box<T>,
187}
188
189impl<T: ?Sized + zeroize::Zeroize> Dynamic<T> {
190    /// Wraps `value` in a `Box<T>` and returns a `Dynamic<T>`.
191    ///
192    /// Accepts any type that implements `Into<Box<T>>` — including owned values,
193    /// `Box<T>`, `String`, `Vec<u8>`, `&str` (via the blanket `From<&str>` impl), etc.
194    ///
195    /// Equivalent to `Dynamic::from(value)` — `#[doc(alias = "from")]` is set so both
196    /// names appear in docs.rs search.
197    ///
198    /// Requires the `alloc` feature (which `Dynamic<T>` itself always requires).
199    #[doc(alias = "from")]
200    #[inline(always)]
201    pub fn new<U>(value: U) -> Self
202    where
203        U: Into<Box<T>>,
204    {
205        let inner = value.into();
206        Self { inner }
207    }
208}
209
210/// Zero-copy wrapping of an already-boxed value.
211impl<T: ?Sized + zeroize::Zeroize> From<Box<T>> for Dynamic<T> {
212    #[inline(always)]
213    fn from(boxed: Box<T>) -> Self {
214        Self { inner: boxed }
215    }
216}
217
218/// Copies a byte slice to the heap and wraps it.
219impl From<&[u8]> for Dynamic<Vec<u8>> {
220    #[inline(always)]
221    fn from(slice: &[u8]) -> Self {
222        Self::new(slice.to_vec())
223    }
224}
225
226/// Copies a string to the heap and wraps it.
227impl From<&str> for Dynamic<String> {
228    #[inline(always)]
229    fn from(input: &str) -> Self {
230        Self::new(input.to_string())
231    }
232}
233
234/// Boxes the value and wraps it.
235impl<T: 'static + zeroize::Zeroize> From<T> for Dynamic<T> {
236    #[inline(always)]
237    fn from(value: T) -> Self {
238        Self {
239            inner: Box::new(value),
240        }
241    }
242}
243
244// Hex encoding and decoding for Dynamic<Vec<u8>>.
245// Dynamic is always heap-allocated, so no no-alloc split is needed.
246#[cfg(feature = "encoding-hex")]
247impl Dynamic<Vec<u8>> {
248    /// Decodes a hex string (lowercase, uppercase, or mixed) into `Dynamic<Vec<u8>>`.
249    ///
250    /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
251    /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
252    pub fn try_from_hex(s: &str) -> Result<Self, crate::error::HexError> {
253        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
254            s.try_from_hex()?,
255        )))
256    }
257}
258
259// Base32 encoding and decoding for Dynamic<Vec<u8>>.
260#[cfg(feature = "encoding-base32")]
261impl Dynamic<Vec<u8>> {
262    /// Decodes an uppercase, unpadded Base32 (RFC 4648 §6) string into `Dynamic<Vec<u8>>`.
263    ///
264    /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
265    /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
266    ///
267    /// Decoding is strict about the alphabet, case, padding and length, but lenient
268    /// about non-canonical trailing bits: unused bits in the final group are ignored
269    /// rather than rejected, so `"MZ"` and `"MY"` both decode to `[0x66]`. Decoding is
270    /// therefore not injective — two distinct strings can yield the same bytes, and
271    /// only encoder-produced strings are canonical. Do not use the decoded value to
272    /// decide that two encoded strings were equal.
273    pub fn try_from_base32(s: &str) -> Result<Self, crate::error::Base32Error> {
274        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
275            s.try_from_base32()?,
276        )))
277    }
278}
279
280// Base64url encoding and decoding for Dynamic<Vec<u8>>.
281#[cfg(feature = "encoding-base64")]
282impl Dynamic<Vec<u8>> {
283    /// Decodes a Base64url (unpadded) string into `Dynamic<Vec<u8>>`.
284    ///
285    /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
286    /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
287    pub fn try_from_base64url(s: &str) -> Result<Self, crate::error::Base64Error> {
288        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
289            s.try_from_base64url()?,
290        )))
291    }
292}
293
294// Bech32 (BIP-173) encoding and decoding for Dynamic<Vec<u8>>.
295#[cfg(feature = "encoding-bech32")]
296impl Dynamic<Vec<u8>> {
297    /// Decodes a Bech32 (BIP-173) string into `Dynamic<Vec<u8>>`, validating the HRP
298    /// (case-insensitive).
299    ///
300    /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
301    /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
302    ///
303    /// HRP comparison is non-constant-time — this is intentional, as the HRP is public
304    /// metadata, not secret material.
305    pub fn try_from_bech32(s: &str, expected_hrp: &str) -> Result<Self, crate::error::Bech32Error> {
306        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
307            s.try_from_bech32(expected_hrp)?,
308        )))
309    }
310
311    /// Decodes a Bech32 (BIP-173) string into `Dynamic<Vec<u8>>` without validating the HRP.
312    ///
313    /// Use [`try_from_bech32`](Self::try_from_bech32) in security-critical code to prevent
314    /// cross-protocol confusion attacks.
315    pub fn try_from_bech32_unchecked(s: &str) -> Result<Self, crate::error::Bech32Error> {
316        let (_hrp, bytes) = s.try_from_bech32_unchecked()?;
317        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(bytes)))
318    }
319}
320
321// Bech32m (BIP-350) encoding and decoding for Dynamic<Vec<u8>>.
322#[cfg(feature = "encoding-bech32m")]
323impl Dynamic<Vec<u8>> {
324    /// Decodes a Bech32m (BIP-350) string into `Dynamic<Vec<u8>>`, validating the HRP
325    /// (case-insensitive).
326    ///
327    /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
328    /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
329    pub fn try_from_bech32m(
330        s: &str,
331        expected_hrp: &str,
332    ) -> Result<Self, crate::error::Bech32Error> {
333        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
334            s.try_from_bech32m(expected_hrp)?,
335        )))
336    }
337
338    /// Decodes a Bech32m (BIP-350) string into `Dynamic<Vec<u8>>` without validating the HRP.
339    ///
340    /// Use [`try_from_bech32m`](Self::try_from_bech32m) in security-critical code.
341    pub fn try_from_bech32m_unchecked(s: &str) -> Result<Self, crate::error::Bech32Error> {
342        let (_hrp, bytes) = s.try_from_bech32m_unchecked()?;
343        Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(bytes)))
344    }
345}
346
347/// Construction helpers and random generation for `Dynamic<Vec<u8>>`.
348impl Dynamic<Vec<u8>> {
349    /// Transfers `protected` bytes into a freshly boxed `Vec`, keeping
350    /// [`zeroize::Zeroizing`] alive across the only allocation that can panic.
351    ///
352    /// # Panic safety
353    ///
354    /// `Box::new(Vec::new())` is the sole allocation point — just the 24-byte
355    /// `Vec` header, no data buffer. If it panics (OOM), `protected` is still
356    /// in scope and `Zeroizing::drop` zeroes the secret bytes during unwind.
357    /// After the swap, `protected` holds an empty `Vec` (no-op to zeroize) and
358    /// `Dynamic::from(boxed)` is an infallible struct-field assignment.
359    ///
360    /// Note: `Box::new(*protected)` would be cleaner but does not compile —
361    /// `Zeroizing` implements `Deref` (returning `&T`), not a move-out, so
362    /// `*protected` yields a reference rather than an owned value (E0507).
363    #[inline(always)]
364    fn from_protected_bytes(mut protected: zeroize::Zeroizing<alloc::vec::Vec<u8>>) -> Self {
365        // Only fallible allocation; protected stays live across it for panic-safety
366        let mut boxed = Box::<alloc::vec::Vec<u8>>::default();
367        core::mem::swap(&mut *boxed, &mut *protected);
368        Self::from(boxed)
369    }
370
371    /// Closure-based constructor that protects against closure panics.
372    ///
373    /// The intermediate `Vec<u8>` is held inside a `Zeroizing` wrapper for the
374    /// entire duration of the closure, so any bytes the closure writes are
375    /// zeroed during stack unwinding if `f` panics. Constructed via the same
376    /// `Zeroizing` + swap pattern used by `from_protected_bytes`.
377    #[inline(always)]
378    pub fn new_with<F>(f: F) -> Self
379    where
380        F: FnOnce(&mut alloc::vec::Vec<u8>),
381    {
382        let mut v: zeroize::Zeroizing<alloc::vec::Vec<u8>> =
383            zeroize::Zeroizing::new(alloc::vec::Vec::new());
384        f(&mut v);
385        Self::from_protected_bytes(v)
386    }
387}
388
389impl Dynamic<alloc::string::String> {
390    /// Heap-only construction from a `Zeroizing<String>`. Swaps the protected
391    /// buffer into a default-initialized `Box<String>` and returns the
392    /// `Dynamic`. Panic-safe: if the `Box` allocation OOM-panics, `protected`
393    /// stays live and `Zeroizing::drop` zeroes the secret bytes during unwind.
394    #[inline(always)]
395    fn from_protected_bytes(mut protected: zeroize::Zeroizing<alloc::string::String>) -> Self {
396        // Only fallible allocation; protected stays live across it for panic-safety
397        let mut boxed = Box::<alloc::string::String>::default();
398        core::mem::swap(&mut *boxed, &mut *protected);
399        Self::from(boxed)
400    }
401
402    /// Closure-based constructor that protects against closure panics.
403    ///
404    /// The intermediate `String` is held inside a `Zeroizing` wrapper for the
405    /// entire duration of the closure, so any bytes the closure writes are
406    /// zeroed during stack unwinding if `f` panics. Constructed via the same
407    /// `Zeroizing` + swap pattern used by `from_protected_bytes`.
408    #[inline(always)]
409    pub fn new_with<F>(f: F) -> Self
410    where
411        F: FnOnce(&mut alloc::string::String),
412    {
413        let mut s: zeroize::Zeroizing<alloc::string::String> =
414            zeroize::Zeroizing::new(alloc::string::String::new());
415        f(&mut s);
416        Self::from_protected_bytes(s)
417    }
418}
419
420/// Hex encoding for `Dynamic<Vec<u8>>`; delegates via `with_secret`.
421///
422/// Deliberately **not** implemented for `Dynamic<String>` — hex-encoding
423/// textual secrets is a design smell; convert explicitly inside `with_secret`
424/// if genuinely needed. Bring the trait into scope: `use secure_gate::ToHex;`.
425///
426/// ```rust
427/// # #[cfg(feature = "encoding-hex")] {
428/// use secure_gate::{Dynamic, ToHex};
429///
430/// let token: Dynamic<Vec<u8>> = Dynamic::from(&[0xDEu8, 0xAD][..]);
431/// assert_eq!(token.to_hex(), "dead");
432/// # }
433/// ```
434#[cfg(feature = "encoding-hex")]
435impl ToHex for Dynamic<Vec<u8>> {
436    #[inline]
437    fn to_hex(&self) -> alloc::string::String {
438        self.with_secret(|s| s.to_hex())
439    }
440
441    #[inline]
442    fn to_hex_upper(&self) -> alloc::string::String {
443        self.with_secret(|s| s.to_hex_upper())
444    }
445
446    #[inline]
447    fn to_hex_zeroizing(&self) -> crate::EncodedSecret {
448        self.with_secret(|s| s.to_hex_zeroizing())
449    }
450
451    #[inline]
452    fn to_hex_upper_zeroizing(&self) -> crate::EncodedSecret {
453        self.with_secret(|s| s.to_hex_upper_zeroizing())
454    }
455}
456
457/// Base32 encoding for `Dynamic<Vec<u8>>`; delegates via `with_secret`.
458///
459/// Bring the trait into scope: `use secure_gate::ToBase32;`.
460///
461/// ```rust
462/// # #[cfg(feature = "encoding-base32")] {
463/// use secure_gate::{Dynamic, ToBase32};
464///
465/// let token: Dynamic<Vec<u8>> = Dynamic::from(&[0xABu8; 4][..]);
466/// assert_eq!(token.to_base32(), "VOV2XKY");
467/// # }
468/// ```
469#[cfg(feature = "encoding-base32")]
470impl ToBase32 for Dynamic<Vec<u8>> {
471    #[inline]
472    fn to_base32(&self) -> alloc::string::String {
473        self.with_secret(|s| s.to_base32())
474    }
475
476    #[inline]
477    fn to_base32_zeroizing(&self) -> crate::EncodedSecret {
478        self.with_secret(|s| s.to_base32_zeroizing())
479    }
480}
481
482/// Base64url encoding for `Dynamic<Vec<u8>>`; delegates via `with_secret`.
483///
484/// Bring the trait into scope: `use secure_gate::ToBase64Url;`.
485///
486/// ```rust
487/// # #[cfg(feature = "encoding-base64")] {
488/// use secure_gate::{Dynamic, ToBase64Url};
489///
490/// let token: Dynamic<Vec<u8>> = Dynamic::from(&[0xABu8; 4][..]);
491/// assert_eq!(token.to_base64url(), "q6urqw");
492/// # }
493/// ```
494#[cfg(feature = "encoding-base64")]
495impl ToBase64Url for Dynamic<Vec<u8>> {
496    #[inline]
497    fn to_base64url(&self) -> alloc::string::String {
498        self.with_secret(|s| s.to_base64url())
499    }
500
501    #[inline]
502    fn to_base64url_zeroizing(&self) -> crate::EncodedSecret {
503        self.with_secret(|s| s.to_base64url_zeroizing())
504    }
505}
506
507/// Bech32 encoding for `Dynamic<Vec<u8>>`; delegates via `with_secret`.
508///
509/// Bring the trait into scope: `use secure_gate::ToBech32;`.
510#[cfg(feature = "encoding-bech32")]
511impl ToBech32 for Dynamic<Vec<u8>> {
512    #[inline]
513    fn try_to_bech32(&self, hrp: &str) -> Result<alloc::string::String, crate::error::Bech32Error> {
514        self.with_secret(|s| s.try_to_bech32(hrp))
515    }
516
517    #[inline]
518    fn try_to_bech32_zeroizing(
519        &self,
520        hrp: &str,
521    ) -> Result<crate::EncodedSecret, crate::error::Bech32Error> {
522        self.with_secret(|s| s.try_to_bech32_zeroizing(hrp))
523    }
524}
525
526/// Bech32m encoding for `Dynamic<Vec<u8>>`; delegates via `with_secret`.
527///
528/// Bring the trait into scope: `use secure_gate::ToBech32m;`.
529#[cfg(feature = "encoding-bech32m")]
530impl ToBech32m for Dynamic<Vec<u8>> {
531    #[inline]
532    fn try_to_bech32m(
533        &self,
534        hrp: &str,
535    ) -> Result<alloc::string::String, crate::error::Bech32Error> {
536        self.with_secret(|s| s.try_to_bech32m(hrp))
537    }
538
539    #[inline]
540    fn try_to_bech32m_zeroizing(
541        &self,
542        hrp: &str,
543    ) -> Result<crate::EncodedSecret, crate::error::Bech32Error> {
544        self.with_secret(|s| s.try_to_bech32m_zeroizing(hrp))
545    }
546}
547
548// RevealSecret — one generic impl covers every inner type, including local
549// user-defined ones. Length reporting lives in the narrower `SecretLen`
550// (implemented for `Dynamic<String>` and `Dynamic<Vec<T>>`), because a generic
551// `T` has no meaningful length.
552impl<T: ?Sized + zeroize::Zeroize> crate::RevealSecret for Dynamic<T> {
553    type Inner = T;
554
555    #[inline(always)]
556    fn with_secret<F, R>(&self, f: F) -> R
557    where
558        F: FnOnce(&T) -> R,
559    {
560        f(&self.inner)
561    }
562
563    #[inline(always)]
564    fn expose_secret(&self) -> &T {
565        &self.inner
566    }
567
568    /// Consumes `self` and returns the inner value wrapped in [`crate::InnerSecret`].
569    ///
570    /// **Allocation note:** allocates one small `Box<T>` sentinel (24 bytes for
571    /// `String`/`Vec` on 64-bit) before the swap. If that allocation panics (OOM),
572    /// `self.inner` is unchanged and `Dynamic::drop` zeroizes the real secret during
573    /// unwind — confidentiality is preserved. This is the same OOM-safety pattern
574    /// used by `from_protected_bytes` and `deserialize_with_limit`.
575    ///
576    /// After ownership transfer, capacity-changing mutations on the returned
577    /// `InnerSecret<T>` have the same realloc-residue caveats as mutating
578    /// `Dynamic<T>` in place; see `SECURITY.md`.
579    ///
580    /// See [`RevealSecret::into_inner`](crate::RevealSecret::into_inner) for full
581    /// documentation including the redacted `Debug` behavior.
582    #[inline(always)]
583    fn into_inner(mut self) -> crate::InnerSecret<T>
584    where
585        Self: Sized,
586        Self::Inner: Sized + crate::SentinelValue + zeroize::Zeroize,
587    {
588        // Swap in a sentinel. If Box::new panics (OOM) before the swap, self.inner
589        // still holds the real secret and Dynamic::drop zeroizes it on unwind.
590        // After the swap, self.inner is an inert sentinel — zeroized on
591        // Dynamic::drop as a no-op. `*boxed` deref-moves the value out of the Box.
592        let boxed = core::mem::replace(
593            &mut self.inner,
594            Box::new(crate::SentinelValue::sentinel_value()),
595        );
596        crate::InnerSecret::new(*boxed)
597    }
598}
599
600// RevealSecretMut — same generic coverage as RevealSecret.
601impl<T: ?Sized + zeroize::Zeroize> crate::RevealSecretMut for Dynamic<T> {
602    #[inline(always)]
603    fn with_secret_mut<F, R>(&mut self, f: F) -> R
604    where
605        F: FnOnce(&mut T) -> R,
606    {
607        f(&mut self.inner)
608    }
609
610    #[inline(always)]
611    fn expose_secret_mut(&mut self) -> &mut T {
612        &mut self.inner
613    }
614}
615
616// Random generation
617#[cfg(feature = "rand")]
618impl Dynamic<alloc::vec::Vec<u8>> {
619    /// Fills a new `Vec<u8>` with `len` cryptographically secure random bytes and wraps it.
620    ///
621    /// Uses the system RNG ([`OsRng`](rand::rngs::OsRng)) via [`TryRngCore::try_fill_bytes`](rand::TryRngCore::try_fill_bytes).
622    /// Requires the `rand` feature (and `alloc`, which `Dynamic<Vec<u8>>` always needs).
623    ///
624    /// # Panics
625    ///
626    /// Panics if the system RNG fails to provide bytes ([`TryRngCore::try_fill_bytes`](rand::TryRngCore::try_fill_bytes)
627    /// returns `Err`). This is treated as a fatal environment error.
628    ///
629    /// # Examples
630    ///
631    /// ```rust
632    /// # #[cfg(all(feature = "alloc", feature = "rand"))]
633    /// use secure_gate::{Dynamic, RevealSecret, SecretLen};
634    ///
635    /// # #[cfg(all(feature = "alloc", feature = "rand"))]
636    /// # {
637    /// let nonce: Dynamic<Vec<u8>> = Dynamic::from_random(24);
638    /// assert_eq!(nonce.len(), 24);
639    /// # }
640    /// ```
641    #[inline]
642    pub fn from_random(len: usize) -> Self {
643        Self::new_with(|v| {
644            v.resize(len, 0u8);
645            OsRng
646                .try_fill_bytes(v)
647                .expect("OsRng failure is a program error");
648        })
649    }
650
651    /// Allocates a `Vec<u8>` of length `len`, fills it from `rng`, and wraps it.
652    ///
653    /// Accepts any [`TryCryptoRng`](rand::TryCryptoRng) + [`TryRngCore`](rand::TryRngCore) — for example,
654    /// a seeded [`StdRng`](rand::rngs::StdRng) for deterministic tests. Requires the `rand`
655    /// feature and `alloc` (implicit — [`Dynamic<T>`](crate::Dynamic) itself requires it).
656    ///
657    /// # Errors
658    ///
659    /// Returns `R::Error` if [`try_fill_bytes`](rand::TryRngCore::try_fill_bytes) fails.
660    ///
661    /// # Examples
662    ///
663    /// ```rust
664    /// # #[cfg(all(feature = "alloc", feature = "rand"))]
665    /// # {
666    /// use rand::rngs::StdRng;
667    /// use rand::SeedableRng;
668    /// use secure_gate::Dynamic;
669    ///
670    /// let mut rng = StdRng::from_seed([9u8; 32]);
671    /// let nonce: Dynamic<Vec<u8>> = Dynamic::from_rng(24, &mut rng).expect("rng fill");
672    /// # }
673    /// ```
674    #[inline]
675    pub fn from_rng<R: TryRngCore + TryCryptoRng>(
676        len: usize,
677        rng: &mut R,
678    ) -> Result<Self, R::Error> {
679        let mut result = Ok(());
680        let this = Self::new_with(|v| {
681            v.resize(len, 0u8);
682            result = rng.try_fill_bytes(v);
683        });
684        result.map(|_| this)
685    }
686}
687
688/// Constant-time equality for `Dynamic<T>` — routes through [`expose_secret()`](crate::RevealSecret::expose_secret).
689///
690/// `==` is **deliberately not implemented**. Always use `ct_eq`.
691#[cfg(feature = "ct-eq")]
692impl<T: ?Sized + zeroize::Zeroize> crate::ConstantTimeEq for Dynamic<T>
693where
694    T: crate::ConstantTimeEq,
695    Self: crate::RevealSecret<Inner = T>,
696{
697    fn ct_eq(&self, other: &Self) -> bool {
698        self.expose_secret().ct_eq(other.expose_secret())
699    }
700}
701
702/// Always prints `[REDACTED]` — secrets never appear in debug output.
703impl<T: ?Sized + zeroize::Zeroize> core::fmt::Debug for Dynamic<T> {
704    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
705        f.write_str("[REDACTED]")
706    }
707}
708
709/// Opt-in cloning — requires `cloneable` feature and [`CloneableSecret`](crate::CloneableSecret)
710/// marker. Each clone is independently zeroized on drop, but cloning increases exposure surface.
711#[cfg(feature = "cloneable")]
712impl<T: zeroize::Zeroize + crate::CloneableSecret> Clone for Dynamic<T> {
713    fn clone(&self) -> Self {
714        Self::new(self.inner.clone())
715    }
716}
717
718// ---------------------------------------------------------------------------
719// Streaming I/O (std only)
720// ---------------------------------------------------------------------------
721
722/// Streams bytes directly into the protected buffer via [`RevealSecretMut`](crate::RevealSecretMut).
723///
724/// Data flows **into** the wrapper, so no intermediate unprotected `Vec<u8>` is
725/// accumulated before wrapping.
726///
727/// # Growth wipes the outgoing buffer
728///
729/// Writing past the current capacity cannot simply delegate to `Vec`: `Vec` would
730/// reallocate, copy the secret into the new allocation, and hand the **old** one
731/// back to the allocator with the plaintext still in it. This impl grows by hand
732/// instead — it allocates the larger buffer, copies, zeroizes the old buffer
733/// (contents and spare capacity), and only then releases it.
734///
735/// Pre-sizing with `Vec::with_capacity` avoids the copy altogether and is worth
736/// doing when the length is known up front:
737///
738/// ```rust
739/// # #[cfg(feature = "std")] {
740/// use std::io::Write;
741/// use secure_gate::Dynamic;
742///
743/// let payload = b"decrypted payload";
744///
745/// // Pre-sized: no growth, so no copy and nothing to wipe.
746/// let mut secret = Dynamic::<Vec<u8>>::new(Vec::with_capacity(payload.len()));
747/// secret.write_all(payload).unwrap();
748/// # }
749/// ```
750///
751/// This addresses only the buffer this impl owns. A caller who grows the `Vec`
752/// themselves through [`with_secret_mut`](crate::RevealSecretMut::with_secret_mut)
753/// or [`expose_secret_mut`](crate::RevealSecretMut::expose_secret_mut) holds
754/// `&mut Vec<u8>` directly, and their reallocation is outside this crate's
755/// control — see the heap-reallocation residue section in `SECURITY.md`.
756#[cfg(feature = "std")]
757impl std::io::Write for Dynamic<alloc::vec::Vec<u8>> {
758    #[inline]
759    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
760        use crate::RevealSecretMut;
761
762        self.with_secret_mut(|v: &mut alloc::vec::Vec<u8>| -> std::io::Result<usize> {
763            if buf.len() > v.capacity() - v.len() {
764                // Grow by hand so the outgoing allocation can be wiped before the
765                // allocator gets it back. `Vec`'s own realloc would copy the secret
766                // into the new buffer and free the old one still holding plaintext.
767                let needed = v.len().checked_add(buf.len()).ok_or_else(|| {
768                    std::io::Error::new(std::io::ErrorKind::OutOfMemory, "capacity overflow")
769                })?;
770                // Mirror `Vec`'s amortized doubling so repeated writes stay linear.
771                let new_cap = core::cmp::max(needed, v.capacity().saturating_mul(2));
772                let mut grown = alloc::vec::Vec::with_capacity(new_cap);
773                grown.extend_from_slice(v);
774                // Wipes contents *and* spare capacity, and sets len to 0 without
775                // freeing; the assignment below then drops the zeroed allocation.
776                v.zeroize();
777                *v = grown;
778            }
779            v.extend_from_slice(buf);
780            Ok(buf.len())
781        })
782    }
783
784    #[inline]
785    fn flush(&mut self) -> std::io::Result<()> {
786        Ok(())
787    }
788}
789
790/// Cursor-like reader over a [`Dynamic<Vec<u8>>`].
791///
792/// Created by [`Dynamic::<Vec<u8>>::as_reader`]. Borrows the `Dynamic`
793/// immutably and tracks the read position internally. Each [`Read::read`]
794/// call goes through [`with_secret`](crate::RevealSecret::with_secret),
795/// preserving the crate's auditable access model.
796///
797/// # Security
798///
799/// `Read::read()` copies secret bytes into the caller-supplied buffer.
800/// The caller is responsible for zeroizing that buffer. Prefer piping
801/// directly into encrypted writers (`io::copy` into an encryptor, etc.)
802/// rather than reading into intermediate `Vec<u8>` buffers.
803///
804/// The `Dynamic` wrapper continues to zeroize its contents on drop
805/// regardless of how many bytes have been read out.
806#[cfg(feature = "std")]
807pub struct DynamicReader<'a> {
808    secret: &'a Dynamic<alloc::vec::Vec<u8>>,
809    offset: usize,
810}
811
812#[cfg(feature = "std")]
813impl std::io::Read for DynamicReader<'_> {
814    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
815        let offset = self.offset;
816        let n = self.secret.with_secret(|v| {
817            let remaining = v.len().saturating_sub(offset);
818            let n = remaining.min(buf.len());
819            buf[..n].copy_from_slice(&v[offset..offset + n]);
820            n
821        });
822        self.offset += n;
823        Ok(n)
824    }
825}
826
827#[cfg(feature = "std")]
828impl Dynamic<alloc::vec::Vec<u8>> {
829    /// Returns a [`DynamicReader`] that implements [`std::io::Read`].
830    ///
831    /// This replaces the common `with_secret` + `Cursor` boilerplate:
832    ///
833    /// ```rust
834    /// # #[cfg(feature = "std")] {
835    /// use std::io;
836    /// use secure_gate::Dynamic;
837    ///
838    /// let secret = Dynamic::<Vec<u8>>::new(vec![1, 2, 3, 4]);
839    ///
840    /// // Before: awkward closure + Cursor dance
841    /// // secret.with_secret(|b| io::copy(&mut Cursor::new(b), &mut encryptor))?;
842    ///
843    /// // After: pipe directly into an encrypted writer — no intermediate buffer
844    /// let mut encryptor = io::sink(); // stand-in for a real encryptor
845    /// io::copy(&mut secret.as_reader(), &mut encryptor).unwrap();
846    /// # }
847    /// ```
848    ///
849    /// # Security
850    ///
851    /// Each `read()` call copies secret bytes into the caller's buffer.
852    /// Prefer piping directly into encrypted writers rather than reading
853    /// into intermediate buffers. The caller is responsible for zeroizing
854    /// any destination buffer.
855    #[inline]
856    pub fn as_reader(&self) -> DynamicReader<'_> {
857        DynamicReader {
858            secret: self,
859            offset: 0,
860        }
861    }
862}
863
864/// Opt-in serialization — requires `serde-serialize` feature and
865/// [`SerializableSecret`](crate::SerializableSecret) marker. Serialization exposes the
866/// full secret — audit every impl.
867#[cfg(feature = "serde-serialize")]
868impl<T: zeroize::Zeroize + crate::SerializableSecret> serde::Serialize for Dynamic<T> {
869    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
870    where
871        S: serde::Serializer,
872    {
873        self.inner.serialize(serializer)
874    }
875}
876
877// Deserialize
878
879/// Default maximum byte length accepted when deserializing `Dynamic<Vec<u8>>` or
880/// `Dynamic<String>` via the standard `serde::Deserialize` impl (1 MiB).
881///
882/// Pass a custom value to [`Dynamic::deserialize_with_limit`] when a different
883/// ceiling is required.
884///
885/// **Important:** this limit is enforced *after* the upstream deserializer has fully
886/// materialized the payload. It is a **result-length acceptance bound**, not a
887/// pre-allocation DoS guard. For untrusted input, enforce size limits at the
888/// transport or parser layer upstream.
889#[cfg(feature = "serde-deserialize")]
890pub const MAX_DESERIALIZE_BYTES: usize = 1_048_576;
891
892#[cfg(feature = "serde-deserialize")]
893impl Dynamic<alloc::vec::Vec<u8>> {
894    /// Deserializes into `Dynamic<Vec<u8>>`, rejecting payloads larger than `limit` bytes.
895    ///
896    /// The standard [`serde::Deserialize`] impl calls this with [`MAX_DESERIALIZE_BYTES`].
897    /// Use this method directly when you need a tighter or looser ceiling.
898    ///
899    /// **Zeroization scope.** Once the upstream deserializer returns a complete
900    /// `Vec<u8>`, the value is wrapped in `Zeroizing` and stays protected for the
901    /// rest of this function: oversized buffers are zeroized before the error is
902    /// returned, and an OOM panic in the subsequent `Box` allocation triggers
903    /// zeroization on unwind. **However, this guarantee does *not* extend backwards
904    /// into the deserializer itself.** If the upstream `Vec<u8>` visitor accumulates
905    /// bytes element-by-element (e.g., a JSON sequence) and fails partway through,
906    /// the partial buffer is owned by the visitor and dropped as a plain `Vec<u8>` —
907    /// not zeroized. In the typical untrusted-input threat model the partial bytes
908    /// are attacker-controlled (the malformed payload they sent), so the practical
909    /// disclosure surface is bounded; but if your threat model includes deserialization
910    /// of trusted-but-corruptible secret material, treat the deserialize step as
911    /// outside the zeroization boundary and use `from_protected_bytes` (private API)
912    /// or `new_with` for in-process construction instead.
913    ///
914    /// **Important:** this limit is enforced *after* the upstream deserializer has fully
915    /// materialized the payload. It is a **result-length acceptance bound**, not a
916    /// pre-allocation DoS guard. For untrusted input, enforce size limits at the
917    /// transport or parser layer upstream.
918    pub fn deserialize_with_limit<'de, D>(deserializer: D, limit: usize) -> Result<Self, D::Error>
919    where
920        D: serde::Deserializer<'de>,
921    {
922        let mut buf: zeroize::Zeroizing<alloc::vec::Vec<u8>> =
923            zeroize::Zeroizing::new(serde::Deserialize::deserialize(deserializer)?);
924        if buf.len() > limit {
925            // buf drops here → Zeroizing zeros the oversized buffer before deallocation
926            return Err(serde::de::Error::custom(
927                "deserialized secret exceeds maximum size",
928            ));
929        }
930        // Only fallible allocation; protected stays live across it for panic-safety
931        let mut boxed = Box::<alloc::vec::Vec<u8>>::default();
932        core::mem::swap(&mut *boxed, &mut *buf);
933        Ok(Self::from(boxed))
934    }
935}
936
937#[cfg(feature = "serde-deserialize")]
938impl Dynamic<String> {
939    /// Deserializes into `Dynamic<String>`, rejecting payloads larger than `limit` bytes.
940    ///
941    /// The standard [`serde::Deserialize`] impl calls this with [`MAX_DESERIALIZE_BYTES`].
942    /// Use this method directly when you need a tighter or looser ceiling.
943    ///
944    /// **Zeroization scope.** Once the upstream deserializer returns a complete
945    /// `String`, the value is wrapped in `Zeroizing` and stays protected for the
946    /// rest of this function: oversized buffers are zeroized before the error is
947    /// returned, and an OOM panic in the subsequent `Box` allocation triggers
948    /// zeroization on unwind. **However, this guarantee does *not* extend backwards
949    /// into the deserializer itself.** If the upstream `String` visitor accumulates
950    /// characters and fails partway through (e.g., on an invalid UTF-8 boundary),
951    /// the partial buffer is owned by the visitor and dropped as a plain `String` —
952    /// not zeroized. In the typical untrusted-input threat model the partial bytes
953    /// are attacker-controlled (the malformed payload they sent), so the practical
954    /// disclosure surface is bounded; but if your threat model includes deserialization
955    /// of trusted-but-corruptible secret material, treat the deserialize step as
956    /// outside the zeroization boundary and use `from_protected_bytes` (private API)
957    /// or `new_with` for in-process construction instead.
958    ///
959    /// **Important:** this limit is enforced *after* the upstream deserializer has fully
960    /// materialized the payload. It is a **result-length acceptance bound**, not a
961    /// pre-allocation DoS guard. For untrusted input, enforce size limits at the
962    /// transport or parser layer upstream.
963    pub fn deserialize_with_limit<'de, D>(deserializer: D, limit: usize) -> Result<Self, D::Error>
964    where
965        D: serde::Deserializer<'de>,
966    {
967        let mut buf: zeroize::Zeroizing<alloc::string::String> =
968            zeroize::Zeroizing::new(serde::Deserialize::deserialize(deserializer)?);
969        if buf.len() > limit {
970            // buf drops here → Zeroizing zeros the oversized buffer before deallocation
971            return Err(serde::de::Error::custom(
972                "deserialized secret exceeds maximum size",
973            ));
974        }
975        // Only fallible allocation; protected stays live across it for panic-safety
976        let mut boxed = Box::<alloc::string::String>::default();
977        core::mem::swap(&mut *boxed, &mut *buf);
978        Ok(Self::from(boxed))
979    }
980}
981
982#[cfg(feature = "serde-deserialize")]
983impl<'de> serde::Deserialize<'de> for Dynamic<alloc::vec::Vec<u8>> {
984    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
985    where
986        D: serde::Deserializer<'de>,
987    {
988        Self::deserialize_with_limit(deserializer, MAX_DESERIALIZE_BYTES)
989    }
990}
991
992#[cfg(feature = "serde-deserialize")]
993impl<'de> serde::Deserialize<'de> for Dynamic<String> {
994    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
995    where
996        D: serde::Deserializer<'de>,
997    {
998        Self::deserialize_with_limit(deserializer, MAX_DESERIALIZE_BYTES)
999    }
1000}
1001
1002/// Zeroizes the inner value (including `Vec`/`String` spare capacity).
1003///
1004/// **Warning:** does not run under `panic = "abort"`.
1005impl<T: ?Sized + zeroize::Zeroize> zeroize::Zeroize for Dynamic<T> {
1006    fn zeroize(&mut self) {
1007        self.inner.zeroize();
1008    }
1009}
1010
1011/// Unconditionally zeroizes the inner value when the wrapper is dropped.
1012///
1013/// **Warning:** `Drop` does not run under `panic = "abort"`.
1014impl<T: ?Sized + zeroize::Zeroize> Drop for Dynamic<T> {
1015    fn drop(&mut self) {
1016        self.zeroize();
1017    }
1018}
1019
1020/// Marker confirming that `Dynamic<T>` always zeroizes on drop.
1021impl<T: ?Sized + zeroize::Zeroize> zeroize::ZeroizeOnDrop for Dynamic<T> {}
1022
1023impl crate::SecretLen for Dynamic<String> {
1024    #[inline(always)]
1025    fn len(&self) -> usize {
1026        self.inner.len()
1027    }
1028}
1029
1030impl<T: zeroize::Zeroize> crate::SecretLen for Dynamic<Vec<T>> {
1031    #[inline(always)]
1032    fn len(&self) -> usize {
1033        self.inner.len()
1034    }
1035
1036    #[inline(always)]
1037    fn byte_len(&self) -> usize {
1038        self.inner.len() * core::mem::size_of::<T>()
1039    }
1040}