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 #[inline(always)]
386 fn from_protected_bytes(mut protected: zeroize::Zeroizing<alloc::vec::Vec<u8>>) -> Self {
387 // Only fallible allocation; protected stays live across it for panic-safety
388 let mut boxed = Box::<alloc::vec::Vec<u8>>::default();
389 core::mem::swap(&mut *boxed, &mut *protected);
390 Self::from(boxed)
391 }
392
393 /// Closure-based constructor that protects against closure panics.
394 ///
395 /// The intermediate `Vec<u8>` is held inside a `Zeroizing` wrapper for the
396 /// entire duration of the closure, so any bytes the closure writes are
397 /// zeroed during stack unwinding if `f` panics. Constructed via the same
398 /// `Zeroizing` + swap pattern used by `from_protected_bytes`.
399 #[inline(always)]
400 pub fn new_with<F>(f: F) -> Self
401 where
402 F: FnOnce(&mut alloc::vec::Vec<u8>),
403 {
404 let mut v: zeroize::Zeroizing<alloc::vec::Vec<u8>> =
405 zeroize::Zeroizing::new(alloc::vec::Vec::new());
406 f(&mut v);
407 Self::from_protected_bytes(v)
408 }
409}
410
411impl Dynamic<alloc::string::String> {
412 /// Heap-only construction from a `Zeroizing<String>`. Swaps the protected
413 /// buffer into a default-initialized `Box<String>` and returns the
414 /// `Dynamic`. Panic-safe: if the `Box` allocation OOM-panics, `protected`
415 /// stays live and `Zeroizing::drop` zeroes the secret bytes during unwind.
416 #[inline(always)]
417 fn from_protected_bytes(mut protected: zeroize::Zeroizing<alloc::string::String>) -> Self {
418 // Only fallible allocation; protected stays live across it for panic-safety
419 let mut boxed = Box::<alloc::string::String>::default();
420 core::mem::swap(&mut *boxed, &mut *protected);
421 Self::from(boxed)
422 }
423
424 /// Closure-based constructor that protects against closure panics.
425 ///
426 /// The intermediate `String` is held inside a `Zeroizing` wrapper for the
427 /// entire duration of the closure, so any bytes the closure writes are
428 /// zeroed during stack unwinding if `f` panics. Constructed via the same
429 /// `Zeroizing` + swap pattern used by `from_protected_bytes`.
430 #[inline(always)]
431 pub fn new_with<F>(f: F) -> Self
432 where
433 F: FnOnce(&mut alloc::string::String),
434 {
435 let mut s: zeroize::Zeroizing<alloc::string::String> =
436 zeroize::Zeroizing::new(alloc::string::String::new());
437 f(&mut s);
438 Self::from_protected_bytes(s)
439 }
440}
441
442// RevealSecret
443impl crate::RevealSecret for Dynamic<String> {
444 type Inner = String;
445
446 #[inline(always)]
447 fn with_secret<F, R>(&self, f: F) -> R
448 where
449 F: FnOnce(&String) -> R,
450 {
451 f(&self.inner)
452 }
453
454 #[inline(always)]
455 fn expose_secret(&self) -> &String {
456 &self.inner
457 }
458
459 #[inline(always)]
460 fn len(&self) -> usize {
461 self.inner.len()
462 }
463
464 /// Consumes `self` and returns the inner `String` wrapped in [`crate::InnerSecret`].
465 ///
466 /// **Allocation note:** allocates one small `Box<String>` sentinel (24 bytes on
467 /// 64-bit) before the swap. If that allocation panics (OOM), `self.inner` is
468 /// unchanged and `Dynamic::drop` zeroizes the real secret during unwind —
469 /// confidentiality is preserved. This is the same OOM-safety pattern used by
470 /// `from_protected_bytes` and `deserialize_with_limit`.
471 ///
472 /// See [`RevealSecret::into_inner`] for full documentation including the
473 /// redacted `Debug` behavior.
474 #[inline(always)]
475 fn into_inner(mut self) -> crate::InnerSecret<String>
476 where
477 Self: Sized,
478 Self::Inner: Sized + Default + zeroize::Zeroize,
479 {
480 // Swap in an empty-String sentinel. If Default::default() panics (OOM) before
481 // the swap, self.inner still holds the real secret and Dynamic::drop zeroizes
482 // it on unwind. After the swap, self.inner is Box<String::new()> — zeroized
483 // on Dynamic::drop as a no-op. `*boxed` deref-moves the String out of the Box.
484 let boxed = core::mem::take(&mut self.inner);
485 crate::InnerSecret::new(*boxed)
486 }
487}
488
489impl<T: zeroize::Zeroize> crate::RevealSecret for Dynamic<Vec<T>> {
490 type Inner = Vec<T>;
491
492 #[inline(always)]
493 fn with_secret<F, R>(&self, f: F) -> R
494 where
495 F: FnOnce(&Vec<T>) -> R,
496 {
497 f(&self.inner)
498 }
499
500 #[inline(always)]
501 fn expose_secret(&self) -> &Vec<T> {
502 &self.inner
503 }
504
505 #[inline(always)]
506 fn len(&self) -> usize {
507 self.inner.len()
508 }
509
510 #[inline(always)]
511 fn byte_len(&self) -> usize {
512 self.inner.len() * core::mem::size_of::<T>()
513 }
514
515 /// Consumes `self` and returns the inner `Vec<T>` wrapped in [`crate::InnerSecret`].
516 ///
517 /// **Allocation note:** allocates one small `Box<Vec<T>>` sentinel (24 bytes on
518 /// 64-bit) before the swap. If that allocation panics (OOM), `self.inner` is
519 /// unchanged and `Dynamic::drop` zeroizes the real secret during unwind —
520 /// confidentiality is preserved. This is the same OOM-safety pattern used by
521 /// `from_protected_bytes` and `deserialize_with_limit`.
522 ///
523 /// See [`RevealSecret::into_inner`] for full documentation including the
524 /// redacted `Debug` behavior.
525 #[inline(always)]
526 fn into_inner(mut self) -> crate::InnerSecret<Vec<T>>
527 where
528 Self: Sized,
529 Self::Inner: Sized + Default + zeroize::Zeroize,
530 {
531 // Swap in an empty-Vec sentinel. If Default::default() panics (OOM) before the
532 // swap, self.inner still holds the real secret and Dynamic::drop zeroizes it on
533 // unwind. After the swap, self.inner is Box<Vec::new()> — zeroized on
534 // Dynamic::drop as a no-op. `*boxed` deref-moves the Vec out of the Box.
535 let boxed = core::mem::take(&mut self.inner);
536 crate::InnerSecret::new(*boxed)
537 }
538}
539
540// RevealSecretMut
541impl crate::RevealSecretMut for Dynamic<String> {
542 #[inline(always)]
543 fn with_secret_mut<F, R>(&mut self, f: F) -> R
544 where
545 F: FnOnce(&mut String) -> R,
546 {
547 f(&mut self.inner)
548 }
549
550 #[inline(always)]
551 fn expose_secret_mut(&mut self) -> &mut String {
552 &mut self.inner
553 }
554}
555
556impl<T: zeroize::Zeroize> crate::RevealSecretMut for Dynamic<Vec<T>> {
557 #[inline(always)]
558 fn with_secret_mut<F, R>(&mut self, f: F) -> R
559 where
560 F: FnOnce(&mut Vec<T>) -> R,
561 {
562 f(&mut self.inner)
563 }
564
565 #[inline(always)]
566 fn expose_secret_mut(&mut self) -> &mut Vec<T> {
567 &mut self.inner
568 }
569}
570
571// Random generation
572#[cfg(feature = "rand")]
573impl Dynamic<alloc::vec::Vec<u8>> {
574 /// Fills a new `Vec<u8>` with `len` cryptographically secure random bytes and wraps it.
575 ///
576 /// Uses the system RNG ([`OsRng`](rand::rngs::OsRng)) via [`TryRngCore::try_fill_bytes`](rand::TryRngCore::try_fill_bytes).
577 /// Requires the `rand` feature (and `alloc`, which `Dynamic<Vec<u8>>` always needs).
578 ///
579 /// # Panics
580 ///
581 /// Panics if the system RNG fails to provide bytes ([`TryRngCore::try_fill_bytes`](rand::TryRngCore::try_fill_bytes)
582 /// returns `Err`). This is treated as a fatal environment error.
583 ///
584 /// # Examples
585 ///
586 /// ```rust
587 /// # #[cfg(all(feature = "alloc", feature = "rand"))]
588 /// use secure_gate::{Dynamic, RevealSecret};
589 ///
590 /// # #[cfg(all(feature = "alloc", feature = "rand"))]
591 /// # {
592 /// let nonce: Dynamic<Vec<u8>> = Dynamic::from_random(24);
593 /// assert_eq!(nonce.len(), 24);
594 /// # }
595 /// ```
596 #[inline]
597 pub fn from_random(len: usize) -> Self {
598 Self::new_with(|v| {
599 v.resize(len, 0u8);
600 OsRng
601 .try_fill_bytes(v)
602 .expect("OsRng failure is a program error");
603 })
604 }
605
606 /// Allocates a `Vec<u8>` of length `len`, fills it from `rng`, and wraps it.
607 ///
608 /// Accepts any [`TryCryptoRng`](rand::TryCryptoRng) + [`TryRngCore`](rand::TryRngCore) — for example,
609 /// a seeded [`StdRng`](rand::rngs::StdRng) for deterministic tests. Requires the `rand`
610 /// feature and `alloc` (implicit — [`Dynamic<T>`](crate::Dynamic) itself requires it).
611 ///
612 /// # Errors
613 ///
614 /// Returns `R::Error` if [`try_fill_bytes`](rand::TryRngCore::try_fill_bytes) fails.
615 ///
616 /// # Examples
617 ///
618 /// ```rust
619 /// # #[cfg(all(feature = "alloc", feature = "rand"))]
620 /// # {
621 /// use rand::rngs::StdRng;
622 /// use rand::SeedableRng;
623 /// use secure_gate::Dynamic;
624 ///
625 /// let mut rng = StdRng::from_seed([9u8; 32]);
626 /// let nonce: Dynamic<Vec<u8>> = Dynamic::from_rng(24, &mut rng).expect("rng fill");
627 /// # }
628 /// ```
629 #[inline]
630 pub fn from_rng<R: TryRngCore + TryCryptoRng>(
631 len: usize,
632 rng: &mut R,
633 ) -> Result<Self, R::Error> {
634 let mut result = Ok(());
635 let this = Self::new_with(|v| {
636 v.resize(len, 0u8);
637 result = rng.try_fill_bytes(v);
638 });
639 result.map(|_| this)
640 }
641}
642
643/// Constant-time equality for `Dynamic<T>` — routes through [`expose_secret()`](crate::RevealSecret::expose_secret).
644///
645/// `==` is **deliberately not implemented**. Always use `ct_eq`.
646#[cfg(feature = "ct-eq")]
647impl<T: ?Sized + zeroize::Zeroize> crate::ConstantTimeEq for Dynamic<T>
648where
649 T: crate::ConstantTimeEq,
650 Self: crate::RevealSecret<Inner = T>,
651{
652 fn ct_eq(&self, other: &Self) -> bool {
653 self.expose_secret().ct_eq(other.expose_secret())
654 }
655}
656
657/// Always prints `[REDACTED]` — secrets never appear in debug output.
658impl<T: ?Sized + zeroize::Zeroize> core::fmt::Debug for Dynamic<T> {
659 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
660 f.write_str("[REDACTED]")
661 }
662}
663
664/// Opt-in cloning — requires `cloneable` feature and [`CloneableSecret`](crate::CloneableSecret)
665/// marker. Each clone is independently zeroized on drop, but cloning increases exposure surface.
666#[cfg(feature = "cloneable")]
667impl<T: zeroize::Zeroize + crate::CloneableSecret> Clone for Dynamic<T> {
668 fn clone(&self) -> Self {
669 Self::new(self.inner.clone())
670 }
671}
672
673// ---------------------------------------------------------------------------
674// Streaming I/O (std only)
675// ---------------------------------------------------------------------------
676
677/// Streams bytes directly into the protected buffer via [`RevealSecretMut`](crate::RevealSecretMut).
678///
679/// Data flows **into** the wrapper — this is a pure security improvement over
680/// accumulating plaintext in a bare `Vec<u8>` before wrapping.
681///
682/// # Example
683///
684/// ```rust
685/// # #[cfg(feature = "std")] {
686/// use std::io::Write;
687/// use secure_gate::Dynamic;
688///
689/// let mut secret = Dynamic::<Vec<u8>>::new(vec![]);
690/// secret.write_all(b"decrypted payload").unwrap();
691///
692/// // Secret material was protected from the first byte —
693/// // no intermediate unprotected buffer ever existed.
694/// # }
695/// ```
696#[cfg(feature = "std")]
697impl std::io::Write for Dynamic<alloc::vec::Vec<u8>> {
698 #[inline]
699 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
700 use crate::RevealSecretMut;
701 self.with_secret_mut(|v| std::io::Write::write(v, buf))
702 }
703
704 #[inline]
705 fn flush(&mut self) -> std::io::Result<()> {
706 Ok(())
707 }
708}
709
710/// Cursor-like reader over a [`Dynamic<Vec<u8>>`].
711///
712/// Created by [`Dynamic::<Vec<u8>>::as_reader`]. Borrows the `Dynamic`
713/// immutably and tracks the read position internally. Each [`Read::read`]
714/// call goes through [`with_secret`](crate::RevealSecret::with_secret),
715/// preserving the crate's auditable access model.
716///
717/// # Security
718///
719/// `Read::read()` copies secret bytes into the caller-supplied buffer.
720/// The caller is responsible for zeroizing that buffer. Prefer piping
721/// directly into encrypted writers (`io::copy` into an encryptor, etc.)
722/// rather than reading into intermediate `Vec<u8>` buffers.
723///
724/// The `Dynamic` wrapper continues to zeroize its contents on drop
725/// regardless of how many bytes have been read out.
726#[cfg(feature = "std")]
727pub struct DynamicReader<'a> {
728 secret: &'a Dynamic<alloc::vec::Vec<u8>>,
729 offset: usize,
730}
731
732#[cfg(feature = "std")]
733impl std::io::Read for DynamicReader<'_> {
734 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
735 let offset = self.offset;
736 let n = self.secret.with_secret(|v| {
737 let remaining = v.len().saturating_sub(offset);
738 let n = remaining.min(buf.len());
739 buf[..n].copy_from_slice(&v[offset..offset + n]);
740 n
741 });
742 self.offset += n;
743 Ok(n)
744 }
745}
746
747#[cfg(feature = "std")]
748impl Dynamic<alloc::vec::Vec<u8>> {
749 /// Returns a [`DynamicReader`] that implements [`std::io::Read`].
750 ///
751 /// This replaces the common `with_secret` + `Cursor` boilerplate:
752 ///
753 /// ```rust
754 /// # #[cfg(feature = "std")] {
755 /// use std::io;
756 /// use secure_gate::Dynamic;
757 ///
758 /// let secret = Dynamic::<Vec<u8>>::new(vec![1, 2, 3, 4]);
759 ///
760 /// // Before: awkward closure + Cursor dance
761 /// // secret.with_secret(|b| io::copy(&mut Cursor::new(b), &mut encryptor))?;
762 ///
763 /// // After: pipe directly into an encrypted writer — no intermediate buffer
764 /// let mut encryptor = io::sink(); // stand-in for a real encryptor
765 /// io::copy(&mut secret.as_reader(), &mut encryptor).unwrap();
766 /// # }
767 /// ```
768 ///
769 /// # Security
770 ///
771 /// Each `read()` call copies secret bytes into the caller's buffer.
772 /// Prefer piping directly into encrypted writers rather than reading
773 /// into intermediate buffers. The caller is responsible for zeroizing
774 /// any destination buffer.
775 #[inline]
776 pub fn as_reader(&self) -> DynamicReader<'_> {
777 DynamicReader {
778 secret: self,
779 offset: 0,
780 }
781 }
782}
783
784/// Opt-in serialization — requires `serde-serialize` feature and
785/// [`SerializableSecret`](crate::SerializableSecret) marker. Serialization exposes the
786/// full secret — audit every impl.
787#[cfg(feature = "serde-serialize")]
788impl<T: zeroize::Zeroize + crate::SerializableSecret> serde::Serialize for Dynamic<T> {
789 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
790 where
791 S: serde::Serializer,
792 {
793 self.inner.serialize(serializer)
794 }
795}
796
797// Deserialize
798
799/// Default maximum byte length accepted when deserializing `Dynamic<Vec<u8>>` or
800/// `Dynamic<String>` via the standard `serde::Deserialize` impl (1 MiB).
801///
802/// Pass a custom value to [`Dynamic::deserialize_with_limit`] when a different
803/// ceiling is required.
804///
805/// **Important:** this limit is enforced *after* the upstream deserializer has fully
806/// materialized the payload. It is a **result-length acceptance bound**, not a
807/// pre-allocation DoS guard. For untrusted input, enforce size limits at the
808/// transport or parser layer upstream.
809#[cfg(feature = "serde-deserialize")]
810pub const MAX_DESERIALIZE_BYTES: usize = 1_048_576;
811
812#[cfg(feature = "serde-deserialize")]
813impl Dynamic<alloc::vec::Vec<u8>> {
814 /// Deserializes into `Dynamic<Vec<u8>>`, rejecting payloads larger than `limit` bytes.
815 ///
816 /// The standard [`serde::Deserialize`] impl calls this with [`MAX_DESERIALIZE_BYTES`].
817 /// Use this method directly when you need a tighter or looser ceiling.
818 ///
819 /// **Zeroization scope.** Once the upstream deserializer returns a complete
820 /// `Vec<u8>`, the value is wrapped in `Zeroizing` and stays protected for the
821 /// rest of this function: oversized buffers are zeroized before the error is
822 /// returned, and an OOM panic in the subsequent `Box` allocation triggers
823 /// zeroization on unwind. **However, this guarantee does *not* extend backwards
824 /// into the deserializer itself.** If the upstream `Vec<u8>` visitor accumulates
825 /// bytes element-by-element (e.g., a JSON sequence) and fails partway through,
826 /// the partial buffer is owned by the visitor and dropped as a plain `Vec<u8>` —
827 /// not zeroized. In the typical untrusted-input threat model the partial bytes
828 /// are attacker-controlled (the malformed payload they sent), so the practical
829 /// disclosure surface is bounded; but if your threat model includes deserialization
830 /// of trusted-but-corruptible secret material, treat the deserialize step as
831 /// outside the zeroization boundary and use `from_protected_bytes` (private API)
832 /// or `new_with` for in-process construction instead.
833 ///
834 /// **Important:** this limit is enforced *after* the upstream deserializer has fully
835 /// materialized the payload. It is a **result-length acceptance bound**, not a
836 /// pre-allocation DoS guard. For untrusted input, enforce size limits at the
837 /// transport or parser layer upstream.
838 pub fn deserialize_with_limit<'de, D>(deserializer: D, limit: usize) -> Result<Self, D::Error>
839 where
840 D: serde::Deserializer<'de>,
841 {
842 let mut buf: zeroize::Zeroizing<alloc::vec::Vec<u8>> =
843 zeroize::Zeroizing::new(serde::Deserialize::deserialize(deserializer)?);
844 if buf.len() > limit {
845 // buf drops here → Zeroizing zeros the oversized buffer before deallocation
846 return Err(serde::de::Error::custom(
847 "deserialized secret exceeds maximum size",
848 ));
849 }
850 // Only fallible allocation; protected stays live across it for panic-safety
851 let mut boxed = Box::<alloc::vec::Vec<u8>>::default();
852 core::mem::swap(&mut *boxed, &mut *buf);
853 Ok(Self::from(boxed))
854 }
855}
856
857#[cfg(feature = "serde-deserialize")]
858impl Dynamic<String> {
859 /// Deserializes into `Dynamic<String>`, rejecting payloads larger than `limit` bytes.
860 ///
861 /// The standard [`serde::Deserialize`] impl calls this with [`MAX_DESERIALIZE_BYTES`].
862 /// Use this method directly when you need a tighter or looser ceiling.
863 ///
864 /// **Zeroization scope.** Once the upstream deserializer returns a complete
865 /// `String`, the value is wrapped in `Zeroizing` and stays protected for the
866 /// rest of this function: oversized buffers are zeroized before the error is
867 /// returned, and an OOM panic in the subsequent `Box` allocation triggers
868 /// zeroization on unwind. **However, this guarantee does *not* extend backwards
869 /// into the deserializer itself.** If the upstream `String` visitor accumulates
870 /// characters and fails partway through (e.g., on an invalid UTF-8 boundary),
871 /// the partial buffer is owned by the visitor and dropped as a plain `String` —
872 /// not zeroized. In the typical untrusted-input threat model the partial bytes
873 /// are attacker-controlled (the malformed payload they sent), so the practical
874 /// disclosure surface is bounded; but if your threat model includes deserialization
875 /// of trusted-but-corruptible secret material, treat the deserialize step as
876 /// outside the zeroization boundary and use `from_protected_bytes` (private API)
877 /// or `new_with` for in-process construction instead.
878 ///
879 /// **Important:** this limit is enforced *after* the upstream deserializer has fully
880 /// materialized the payload. It is a **result-length acceptance bound**, not a
881 /// pre-allocation DoS guard. For untrusted input, enforce size limits at the
882 /// transport or parser layer upstream.
883 pub fn deserialize_with_limit<'de, D>(deserializer: D, limit: usize) -> Result<Self, D::Error>
884 where
885 D: serde::Deserializer<'de>,
886 {
887 let mut buf: zeroize::Zeroizing<alloc::string::String> =
888 zeroize::Zeroizing::new(serde::Deserialize::deserialize(deserializer)?);
889 if buf.len() > limit {
890 // buf drops here → Zeroizing zeros the oversized buffer before deallocation
891 return Err(serde::de::Error::custom(
892 "deserialized secret exceeds maximum size",
893 ));
894 }
895 // Only fallible allocation; protected stays live across it for panic-safety
896 let mut boxed = Box::<alloc::string::String>::default();
897 core::mem::swap(&mut *boxed, &mut *buf);
898 Ok(Self::from(boxed))
899 }
900}
901
902#[cfg(feature = "serde-deserialize")]
903impl<'de> serde::Deserialize<'de> for Dynamic<alloc::vec::Vec<u8>> {
904 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
905 where
906 D: serde::Deserializer<'de>,
907 {
908 Self::deserialize_with_limit(deserializer, MAX_DESERIALIZE_BYTES)
909 }
910}
911
912#[cfg(feature = "serde-deserialize")]
913impl<'de> serde::Deserialize<'de> for Dynamic<String> {
914 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
915 where
916 D: serde::Deserializer<'de>,
917 {
918 Self::deserialize_with_limit(deserializer, MAX_DESERIALIZE_BYTES)
919 }
920}
921
922/// Zeroizes the inner value (including `Vec`/`String` spare capacity).
923///
924/// **Warning:** does not run under `panic = "abort"`.
925impl<T: ?Sized + zeroize::Zeroize> zeroize::Zeroize for Dynamic<T> {
926 fn zeroize(&mut self) {
927 self.inner.zeroize();
928 }
929}
930
931/// Unconditionally zeroizes the inner value when the wrapper is dropped.
932///
933/// **Warning:** `Drop` does not run under `panic = "abort"`.
934impl<T: ?Sized + zeroize::Zeroize> Drop for Dynamic<T> {
935 fn drop(&mut self) {
936 self.zeroize();
937 }
938}
939
940/// Marker confirming that `Dynamic<T>` always zeroizes on drop.
941impl<T: ?Sized + zeroize::Zeroize> zeroize::ZeroizeOnDrop for Dynamic<T> {}