Skip to main content

secure_gate/
dynamic.rs

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