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