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<[u8]>` (boxed slice) | **None** once built — the length is set at construction and cannot change | Secrets whose length is known only at runtime but never changes afterwards |
31//! | `Dynamic<Vec<u8>>` pre-sized | None *if* you avoid capacity-growing mutations | Variable-length secrets with a known upper bound |
32//! | `Dynamic<Vec<u8>>` growable | **Yes** — each realloc leaves the old buffer unzeroed | Convenient, but see realloc-residue warning below |
33//! | `Dynamic<String>` | Same as `Dynamic<Vec<u8>>` | Passwords, API keys |
34//!
35//! For **long-lived, known-size key material**, prefer `Dynamic<[u8; N]>` —
36//! it combines `Dynamic`'s heap-only property with zero realloc surface.
37//! See `SECURITY.md` § "Inherent Rust Limitations" for the broader discussion.
38//!
39//! When the length is fixed but only known at runtime, `Dynamic<[u8]>` gives the same
40//! guarantee by construction: `with_secret_mut` hands the closure a `&mut [u8]`, which has
41//! no `push`, `extend` or `reserve`, so a reallocation is not expressible rather than merely
42//! discouraged. Two things to know before choosing it:
43//!
44//! - **Build the `Vec` at its exact final capacity.** `Dynamic::<[u8]>::new(vec)` converts
45//! through `Box<[u8]>::from(Vec)`, which shrinks a `Vec` with excess capacity into a
46//! newly allocated buffer and frees the old one unwiped. `vec![0u8; len]` is already
47//! exact; a `Vec` that was grown or truncated is not.
48//! - **It is the narrowest `Dynamic` shape.** Scoped access, [`Debug`](core::fmt::Debug)
49//! redaction, zeroize-on-drop and `ct_eq` (with `ct-eq`) work. `SecretLen::len`,
50//! `new_with`, `from_random`, `into_inner`, the encoding traits, serde and `Clone` are
51//! not implemented for it; read the length with `with_secret(|s| s.len())`.
52//!
53//! ```
54//! use secure_gate::{Dynamic, RevealSecret, RevealSecretMut};
55//!
56//! let len = 32; // known only at runtime
57//! let mut key: Dynamic<[u8]> = Dynamic::new(vec![0u8; len]); // exact capacity: no shrink
58//! key.with_secret_mut(|k: &mut [u8]| k.fill(0x42)); // `&mut [u8]` — cannot grow
59//! assert_eq!(key.with_secret(|k| k.len()), 32);
60//! ```
61//!
62//! # Construction
63//!
64//! | Constructor | Notes |
65//! |---|---|
66//! | [`Dynamic::<Vec<u8>>::new_with(len, f)`](Dynamic::new_with) | **Preferred.** Writes into a sized, pre-zeroed slot inside the wrapper; growth is not expressible |
67//! | [`Dynamic::<Vec<u8>>::try_new_with(len, f)`](Dynamic::try_new_with) | The same, for a fill that can fail; wipes the partial write on `Err` |
68//! | [`Dynamic::new(value)`](Dynamic::new) | **Moves** the buffer in — no copy. Only as safe as how you built the value |
69//! | [`From<&str>`](Dynamic::from) / [`From<&[u8]>`](Dynamic::from) | **Copies**; your source buffer is left unwiped |
70//!
71//! Choosing between them is about where the bytes are written, not convenience:
72//!
73//! - **Length known before the fill** — use `new_with(len, …)`. That is two allocations,
74//! not one: the slot, of exactly `len` bytes, plus the `Box<Vec<u8>>` header every
75//! `Dynamic<Vec<u8>>` is built around. `tests/lifecycle_trace_heap.rs` measures
76//! `from_random(len)` — this constructor with an RNG in the closure — at exactly two,
77//! and `Dynamic::new(owned)` at exactly one, because a move brings its buffer with it.
78//! So the recommendation is not about allocation count; it is about the slot's shape.
79//! Capacity equals length, so the wrapper never holds slack, and the closure is handed a
80//! slice, so growth is not expressible at all.
81//! - **Length not known** — use `Dynamic::new(Vec::new())` and write through the
82//! [`std::io::Write`] impl, which grows by hand and **zeroizes each abandoned buffer**
83//! before releasing it. It is the one growth path here that does. Requires `std`.
84//! - **You already own the value** — `new(owned)` moves the allocation rather than
85//! copying it, so the buffer you filled becomes the buffer the wrapper protects. That
86//! is necessary but not sufficient: if you *grew* the value to build it, those
87//! reallocations already happened and the copies are already on the heap. Build without
88//! growth — `with_capacity` at the exact final size, or one `resize` — then move it in.
89//!
90//! `Dynamic<String>` has no closure constructor. It cannot: `String` must hold valid
91//! UTF-8 and characters have variable byte widths, so a fixed byte window is not a place
92//! you can write arbitrary text. Build a pre-sized `String` and move it in with
93//! [`Dynamic::new`].
94//!
95//! # 3-tier access model
96//!
97//! ```rust
98//! # #[cfg(feature = "alloc")]
99//! # {
100//! use secure_gate::{Dynamic, RevealSecret, RevealSecretMut, SecretLen};
101//!
102//! let mut pw: Dynamic<String> = Dynamic::new(String::from("hunter2"));
103//!
104//! // Tier 1 — scoped (preferred): borrow is confined to the closure.
105//! let len = pw.with_secret(|s: &String| s.len());
106//! assert_eq!(len, 7);
107//!
108//! // Tier 1 mutable — scoped mutation. Prefer capacity-stable mutations for
109//! // Dynamic<String> / Dynamic<Vec<u8>>; see SECURITY.md for realloc notes.
110//! pw.with_secret_mut(|s: &mut String| s.make_ascii_uppercase());
111//!
112//! // Tier 2 — direct reference (escape hatch).
113//! assert_eq!(pw.expose_secret(), "HUNTER2");
114//!
115//! // Tier 3 — transfer ownership. Protection ends here.
116//! let owned: String = pw.into_inner();
117//! assert_eq!(owned, "HUNTER2");
118//! # }
119//! ```
120//!
121//! # Warning
122//!
123//! Ensure your profile sets `panic = "unwind"` — `panic = "abort"` skips destructors
124//! and therefore skips zeroization. (`Dynamic` cannot be `static` since it requires
125//! `Box` allocation, so the static-secret warning from `Fixed` does not apply.)
126//!
127//! For `Dynamic<Vec<_>>` and `Dynamic<String>`, capacity-changing mutations can
128//! cause the standard allocator to free the previous buffer without zeroizing it.
129//! Pre-allocate to the maximum size, use capacity-stable mutations, prefer
130//! `Dynamic<[u8; N]>` (no realloc surface) for known-size heap secrets, or use
131//! [`Fixed<T>`](crate::Fixed) for known-size stack secrets. See `SECURITY.md` for
132//! the full threat-model discussion and deployment-level mitigations including
133//! the [`zeroizing-alloc`](https://crates.io/crates/zeroizing-alloc) global
134//! allocator.
135//!
136//! # See also
137//!
138//! - [`Fixed<T>`](crate::Fixed) — stack-allocated alternative for fixed-size secrets
139//! (always available, no `alloc` required).
140
141#[cfg(feature = "alloc")]
142extern crate alloc;
143use alloc::boxed::Box;
144use alloc::string::{String, ToString};
145use alloc::vec::Vec;
146use zeroize::Zeroize;
147
148#[cfg(any(
149 feature = "encoding-hex",
150 feature = "encoding-base32",
151 feature = "encoding-base64",
152 feature = "encoding-bech32",
153 feature = "ct-eq",
154 feature = "std",
155))]
156use crate::RevealSecret;
157
158// Encoding traits
159#[cfg(feature = "encoding-base32")]
160use crate::traits::encoding::base32::ToBase32;
161#[cfg(feature = "encoding-base64")]
162use crate::traits::encoding::base64_url::ToBase64Url;
163#[cfg(feature = "encoding-bech32")]
164use crate::traits::encoding::bech32::ToBech32;
165#[cfg(feature = "encoding-bech32")]
166use crate::traits::encoding::bech32m::ToBech32m;
167#[cfg(feature = "encoding-hex")]
168use crate::traits::encoding::hex::ToHex;
169
170#[cfg(feature = "rand")]
171use rand::{TryCryptoRng, TryRng, rngs::SysRng};
172
173// Dynamic<Vec<u8>> is always alloc-dependent, so the alloc-gated blanket traits
174// are always available when encoding features are enabled for this type.
175#[cfg(feature = "encoding-base32")]
176use crate::traits::decoding::base32::FromBase32Str;
177#[cfg(feature = "encoding-base64")]
178use crate::traits::decoding::base64_url::FromBase64UrlStr;
179#[cfg(feature = "encoding-bech32")]
180use crate::traits::decoding::bech32::FromBech32Str;
181#[cfg(feature = "encoding-bech32")]
182use crate::traits::decoding::bech32m::FromBech32mStr;
183#[cfg(feature = "encoding-hex")]
184use crate::traits::decoding::hex::FromHexStr;
185
186/// Zero-cost heap-allocated wrapper for variable-length secrets.
187///
188/// `Dynamic<T>` stores a `T: Zeroize` value in a `Box<T>` and unconditionally zeroizes
189/// it on drop (including `Vec`/`String` spare capacity). There is no `Deref`, `AsRef`,
190/// or `Copy` — every access is explicit through [`RevealSecret`](crate::RevealSecret)
191/// or [`RevealSecretMut`](crate::RevealSecretMut).
192///
193/// This is **not** `Fixed<T>` — it is the heap-allocated alternative for variable-length
194/// secrets. Secret bytes never reside on the stack.
195///
196/// # Examples
197///
198/// ```rust
199/// # #[cfg(feature = "alloc")]
200/// # {
201/// use secure_gate::{Dynamic, RevealSecret, SecretLen};
202///
203/// let pw: Dynamic<String> = Dynamic::new(String::from("hunter2"));
204/// assert_eq!(pw.with_secret(|s: &String| s.len()), 7);
205/// assert_eq!(format!("{:?}", pw), "[REDACTED]");
206/// # }
207/// ```
208///
209/// # Constructors for `Dynamic<Vec<u8>>`
210///
211/// | Constructor | Feature | Notes |
212/// |---|---|---|
213/// | [`new_with(len, f)`](Self::new_with) | — | **Preferred.** Sized, pre-zeroed slot inside the wrapper |
214/// | [`try_new_with(len, f)`](Self::try_new_with) | — | The same, fallible; wipes the partial write on `Err` |
215/// | [`new(value)`](Self::new) | — | **Moves** the buffer in. Accepts `Vec<u8>`, `&[u8]`, `Box<Vec<u8>>` |
216/// | [`try_from_hex(s)`](Self::try_from_hex) | `encoding-hex` | Constant-time hex decoding |
217/// | [`try_from_base32(s)`](Self::try_from_base32) | `encoding-base32` | Constant-time Base32 decoding |
218/// | [`try_from_base64url(s)`](Self::try_from_base64url) | `encoding-base64` | Constant-time Base64url decoding |
219/// | [`try_from_bech32(s, hrp)`](Self::try_from_bech32) | `encoding-bech32` | HRP-validated Bech32 |
220/// | [`try_from_bech32_unchecked(s)`](Self::try_from_bech32_unchecked) | `encoding-bech32` | Bech32 without HRP check |
221/// | [`try_from_bech32m(s, hrp)`](Self::try_from_bech32m) | `encoding-bech32` | HRP-validated Bech32m |
222/// | [`try_from_bech32m_unchecked(s)`](Self::try_from_bech32m_unchecked) | `encoding-bech32` | Bech32m without HRP check |
223/// | [`from_random(len)`](Self::from_random) | `rand` | System RNG |
224/// | [`from_rng(len, rng)`](Self::from_rng) | `rand` | Custom RNG |
225///
226/// # See also
227///
228/// - [`RevealSecret`](crate::RevealSecret) / [`RevealSecretMut`](crate::RevealSecretMut) — the 3-tier access traits.
229/// - [`Fixed<T>`](crate::Fixed) — stack-allocated alternative.
230#[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"]
231pub struct Dynamic<T: ?Sized + zeroize::Zeroize> {
232 inner: Box<T>,
233}
234
235impl<T: ?Sized + zeroize::Zeroize> Dynamic<T> {
236 /// Wraps `value` in a `Box<T>` and returns a `Dynamic<T>`.
237 ///
238 /// Accepts any type that implements `Into<Box<T>>` — including owned values,
239 /// `Box<T>`, `String`, `Vec<u8>`, `&str` (via the blanket `From<&str>` impl), etc.
240 ///
241 /// Equivalent to `Dynamic::from(value)` — `#[doc(alias = "from")]` is set so both
242 /// names appear in docs.rs search.
243 ///
244 /// Requires the `alloc` feature (which `Dynamic<T>` itself always requires).
245 #[doc(alias = "from")]
246 #[inline(always)]
247 pub fn new<U>(value: U) -> Self
248 where
249 U: Into<Box<T>>,
250 {
251 let inner = value.into();
252 Self { inner }
253 }
254}
255
256/// Moves an already-boxed value in — zero-copy, no allocation at all. The box the caller
257/// already owns becomes the box the wrapper owns, so whatever buffer sits behind `T` never
258/// moves and never gets a second copy taken of it.
259///
260/// "Already boxed" describes the shape of the value at the moment it arrives here, not how
261/// it was built. A `Vec<u8>` pushed onto in a loop and later turned into a `Box<[u8]>`, or a
262/// `String` grown with `push_str` before being boxed, already paid for every one of those
263/// reallocations before this impl runs — and each reallocation frees the old block without
264/// zeroizing it, the same mechanism
265/// `tests/lifecycle_trace_heap.rs::check_growth_orphan_retains_secret_vec` (and its
266/// `_string` sibling) measures happening to a value already *inside* the wrapper. Nothing
267/// this impl does can reach backward and clean that up; it only ever protects the buffer it
268/// ends up holding. Build without growth — `with_capacity` at the exact final size, or one
269/// `resize` — before boxing, then move the box in.
270impl<T: ?Sized + zeroize::Zeroize> From<Box<T>> for Dynamic<T> {
271 #[inline(always)]
272 fn from(boxed: Box<T>) -> Self {
273 Self { inner: boxed }
274 }
275}
276
277/// Copies a byte slice to the heap and wraps the copy. The source slice is left exactly as
278/// it was — this impl protects the copy it makes, not the memory it copied from, and leaving
279/// that memory unwiped is the caller's problem to solve.
280///
281/// `tests/lifecycle_trace_heap.rs::check_str_and_slice_conversions_copy_then_wipe` measures
282/// both halves of that: converting `&[u8]` costs two allocations (the fresh `Vec<u8>` buffer
283/// plus the `Box<Vec<u8>>` header, against one for a move) and the source slice's bytes are
284/// unchanged after the wrapper is built and dropped. If the bytes are already in an owned
285/// `Vec<u8>` you control, [`Dynamic::new`] moves it in for one allocation and nothing of
286/// yours left behind to wipe.
287impl From<&[u8]> for Dynamic<Vec<u8>> {
288 #[inline(always)]
289 fn from(slice: &[u8]) -> Self {
290 Self::new(slice.to_vec())
291 }
292}
293
294/// Copies a string to the heap and wraps the copy, leaving the source `&str` exactly as it
295/// was — measured the same way as the `&[u8]` conversion above, by
296/// `tests/lifecycle_trace_heap.rs::check_str_and_slice_conversions_copy_then_wipe`: two
297/// allocations (the copied `String` buffer plus the `Box<String>` header, against one for a
298/// move) and a caller-side buffer whose bytes are provably unchanged once the wrapper is
299/// built and dropped.
300///
301/// This is the impl a caller now reaches for after `Dynamic::<String>::new_with` was removed
302/// outright in 0.9.0-rc.12 — an `&str` is what's already on hand, so `From<&str>` reads as
303/// the drop-in replacement. It is the **worse** one: whatever plaintext the `&str` borrows
304/// from — a literal, an owned `String`, a line read off stdin — is untouched by this call and
305/// stays live and unprotected for as long as its own scope does. If you own the `String`,
306/// size it once — `String::with_capacity` at the exact final length — and move it in with
307/// [`Dynamic::new`], which `check_string_new_moves_callers_buffer` measures as exactly one
308/// allocation (the `Box<String>` header) with the secret staying at the caller's original
309/// buffer address.
310impl From<&str> for Dynamic<String> {
311 #[inline(always)]
312 fn from(input: &str) -> Self {
313 Self::new(input.to_string())
314 }
315}
316
317/// Moves an owned value onto the heap by boxing it — the impl behind [`Dynamic::new`]'s
318/// "moves the buffer in, no copy" guarantee for `Vec<u8>`, `String`, and every other `Sized`
319/// `T`. `Box::new(value)` only relocates `value`'s own stack representation (for a `Vec<u8>`,
320/// the pointer/length/capacity triple); the heap buffer that pointer already points at is
321/// never touched, so it lands inside the wrapper at the address it already had.
322///
323/// That is necessary for "no copy" to hold, but not sufficient for it to matter: if `value`
324/// was grown to build it — pushed onto, extended, `push_str`'d past its original capacity —
325/// each of those reallocations already ran *before* this impl ever sees the value, and each
326/// one freed a heap block holding the secret, unwiped, somewhere this wrapper will never
327/// look. `check_growth_orphan_retains_secret_vec` / `_string` in
328/// `tests/lifecycle_trace_heap.rs` measure exactly that allocator behavior for growth
329/// happening *after* the value is wrapped; the same allocator runs the same way one step
330/// earlier when the growth happens during construction instead.
331/// `check_string_new_moves_callers_buffer` measures the guarantee this impl does deliver
332/// when construction is growth-free: one allocation (the `Box` header) and the secret
333/// staying at the caller's original buffer address. Build without growth — `with_capacity`
334/// at the exact final size, or one `resize` — then move the value in.
335impl<T: 'static + zeroize::Zeroize> From<T> for Dynamic<T> {
336 #[inline(always)]
337 fn from(value: T) -> Self {
338 Self {
339 inner: Box::new(value),
340 }
341 }
342}
343
344// Hex encoding and decoding for Dynamic<Vec<u8>>.
345// Dynamic is always heap-allocated, so no no-alloc split is needed.
346#[cfg(feature = "encoding-hex")]
347impl Dynamic<Vec<u8>> {
348 /// Decodes a hex string (lowercase, uppercase, or mixed) into `Dynamic<Vec<u8>>`.
349 ///
350 /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
351 /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
352 pub fn try_from_hex(s: &str) -> Result<Self, crate::error::HexError> {
353 Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
354 s.try_from_hex()?,
355 )))
356 }
357}
358
359// Base32 encoding and decoding for Dynamic<Vec<u8>>.
360#[cfg(feature = "encoding-base32")]
361impl Dynamic<Vec<u8>> {
362 /// Decodes an uppercase, unpadded Base32 (RFC 4648 §6) string into `Dynamic<Vec<u8>>`.
363 ///
364 /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
365 /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
366 ///
367 /// Decoding is strict about the alphabet, case, padding and length, but lenient
368 /// about non-canonical trailing bits: unused bits in the final group are ignored
369 /// rather than rejected, so `"MZ"` and `"MY"` both decode to `[0x66]`. Decoding is
370 /// therefore not injective — two distinct strings can yield the same bytes, and
371 /// only encoder-produced strings are canonical. Do not use the decoded value to
372 /// decide that two encoded strings were equal.
373 pub fn try_from_base32(s: &str) -> Result<Self, crate::error::Base32Error> {
374 Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
375 s.try_from_base32()?,
376 )))
377 }
378}
379
380// Base64url encoding and decoding for Dynamic<Vec<u8>>.
381#[cfg(feature = "encoding-base64")]
382impl Dynamic<Vec<u8>> {
383 /// Decodes a Base64url (unpadded) string into `Dynamic<Vec<u8>>`.
384 ///
385 /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
386 /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
387 pub fn try_from_base64url(s: &str) -> Result<Self, crate::error::Base64Error> {
388 Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
389 s.try_from_base64url()?,
390 )))
391 }
392}
393
394// Bech32 (BIP-173) encoding and decoding for Dynamic<Vec<u8>>.
395#[cfg(feature = "encoding-bech32")]
396impl Dynamic<Vec<u8>> {
397 /// Decodes a Bech32 (BIP-173) string into `Dynamic<Vec<u8>>`, validating the HRP
398 /// (case-insensitive).
399 ///
400 /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
401 /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
402 ///
403 /// HRP comparison is non-constant-time — this is intentional, as the HRP is public
404 /// metadata, not secret material.
405 pub fn try_from_bech32(s: &str, expected_hrp: &str) -> Result<Self, crate::error::Bech32Error> {
406 Self::try_from_bech32_sized::<{ crate::BECH32_CODE_LENGTH }>(s, expected_hrp)
407 }
408
409 /// Like [`try_from_bech32`](Self::try_from_bech32), accepting strings up to `C`
410 /// characters.
411 ///
412 /// `C` is the bech32 code length: the cap on the whole encoded string. Pass the `C`
413 /// the string was encoded with, or any larger value. See
414 /// [`Bech32Sized`](crate::Bech32Sized) for what `C` above
415 /// [`BECH32_CODE_LENGTH`](crate::BECH32_CODE_LENGTH) costs.
416 pub fn try_from_bech32_sized<const C: usize>(
417 s: &str,
418 expected_hrp: &str,
419 ) -> Result<Self, crate::error::Bech32Error> {
420 Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
421 s.try_from_bech32_sized::<C>(expected_hrp)?,
422 )))
423 }
424
425 /// Decodes a Bech32 (BIP-173) string into `Dynamic<Vec<u8>>` without validating the HRP.
426 ///
427 /// Use [`try_from_bech32`](Self::try_from_bech32) in security-critical code to prevent
428 /// cross-protocol confusion attacks.
429 pub fn try_from_bech32_unchecked(s: &str) -> Result<Self, crate::error::Bech32Error> {
430 Self::try_from_bech32_unchecked_sized::<{ crate::BECH32_CODE_LENGTH }>(s)
431 }
432
433 /// Like [`try_from_bech32_unchecked`](Self::try_from_bech32_unchecked), accepting
434 /// strings up to `C` characters.
435 pub fn try_from_bech32_unchecked_sized<const C: usize>(
436 s: &str,
437 ) -> Result<Self, crate::error::Bech32Error> {
438 let (_hrp, bytes) = s.try_from_bech32_unchecked_sized::<C>()?;
439 Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(bytes)))
440 }
441}
442
443// Bech32m (BIP-350) encoding and decoding for Dynamic<Vec<u8>>.
444#[cfg(feature = "encoding-bech32")]
445impl Dynamic<Vec<u8>> {
446 /// Decodes a Bech32m (BIP-350) string into `Dynamic<Vec<u8>>`, validating the HRP
447 /// (case-insensitive).
448 ///
449 /// The decoded buffer is kept inside a `Zeroizing` wrapper until after the
450 /// `Box` allocation completes, guaranteeing zeroization even on OOM panic.
451 pub fn try_from_bech32m(
452 s: &str,
453 expected_hrp: &str,
454 ) -> Result<Self, crate::error::Bech32Error> {
455 Self::try_from_bech32m_sized::<{ crate::BECH32_CODE_LENGTH }>(s, expected_hrp)
456 }
457
458 /// Like [`try_from_bech32m`](Self::try_from_bech32m), accepting strings up to `C`
459 /// characters. See [`Bech32mSized`](crate::Bech32mSized) for what `C` above
460 /// [`BECH32_CODE_LENGTH`](crate::BECH32_CODE_LENGTH) costs.
461 pub fn try_from_bech32m_sized<const C: usize>(
462 s: &str,
463 expected_hrp: &str,
464 ) -> Result<Self, crate::error::Bech32Error> {
465 Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(
466 s.try_from_bech32m_sized::<C>(expected_hrp)?,
467 )))
468 }
469
470 /// Decodes a Bech32m (BIP-350) string into `Dynamic<Vec<u8>>` without validating the HRP.
471 ///
472 /// Use [`try_from_bech32m`](Self::try_from_bech32m) in security-critical code.
473 pub fn try_from_bech32m_unchecked(s: &str) -> Result<Self, crate::error::Bech32Error> {
474 Self::try_from_bech32m_unchecked_sized::<{ crate::BECH32_CODE_LENGTH }>(s)
475 }
476
477 /// Like [`try_from_bech32m_unchecked`](Self::try_from_bech32m_unchecked), accepting
478 /// strings up to `C` characters.
479 pub fn try_from_bech32m_unchecked_sized<const C: usize>(
480 s: &str,
481 ) -> Result<Self, crate::error::Bech32Error> {
482 let (_hrp, bytes) = s.try_from_bech32m_unchecked_sized::<C>()?;
483 Ok(Self::from_protected_bytes(zeroize::Zeroizing::new(bytes)))
484 }
485}
486
487/// Construction helpers and random generation for `Dynamic<Vec<u8>>`.
488impl Dynamic<Vec<u8>> {
489 /// Transfers `protected` bytes into a freshly boxed `Vec`, keeping
490 /// [`zeroize::Zeroizing`] alive across the only allocation that can panic.
491 ///
492 /// # Panic safety
493 ///
494 /// `Box::new(Vec::new())` is the sole allocation point — just the 24-byte
495 /// `Vec` header, no data buffer. If it panics (OOM), `protected` is still
496 /// in scope and `Zeroizing::drop` zeroes the secret bytes during unwind.
497 /// After the swap, `protected` holds an empty `Vec` (no-op to zeroize) and
498 /// `Dynamic::from(boxed)` is an infallible struct-field assignment.
499 ///
500 /// Note: `Box::new(*protected)` would be cleaner but does not compile —
501 /// `Zeroizing` implements `Deref` (returning `&T`), not a move-out, so
502 /// `*protected` yields a reference rather than an owned value (E0507).
503 #[inline(always)]
504 fn from_protected_bytes(mut protected: zeroize::Zeroizing<alloc::vec::Vec<u8>>) -> Self {
505 // Only fallible allocation; protected stays live across it for panic-safety
506 let mut boxed = Box::<alloc::vec::Vec<u8>>::default();
507 core::mem::swap(&mut *boxed, &mut *protected);
508 Self::from(boxed)
509 }
510
511 /// Writes into a **sized, pre-zeroed slot** inside the wrapper's own storage.
512 ///
513 /// The closure receives exactly `len` bytes, **every one of them zero**. That is a
514 /// guarantee, not an implementation detail: callers rely on the tail being zero as
515 /// real input — a 40-bit RC4 key is five bytes of derived material followed by
516 /// eleven zeros, and those zeros are key-schedule input rather than padding, so a
517 /// slot that merely happened to be zeroed would produce a different cipher the day
518 /// it was not.
519 ///
520 /// Two allocations, not one: the slot of exactly `len` bytes, plus the `Box<Vec<u8>>`
521 /// header the bytes are swapped into on the way out. `tests/lifecycle_trace_heap.rs`
522 /// measures `from_random(len)` — this constructor with an RNG in the closure — at
523 /// exactly two, "the payload buffer and the `Box<Vec<u8>>` header", and measures
524 /// `Dynamic::new(owned)` at exactly one, since a move carries its buffer along. By
525 /// allocation count this is therefore the joint most expensive constructor here, tied
526 /// with `From<&[u8]>`; the count is not what recommends it. What does is the shape of
527 /// the slot: capacity equals length, so the wrapper never holds slack, and the slot is
528 /// a slice, so **growth is not expressible** and the reallocation hazard cannot arise.
529 ///
530 /// # Changed in 0.9.0-rc.12 — this took no `len` before
531 ///
532 /// If you are here from `E0061: this function takes 2 arguments but 1 was supplied`,
533 /// this is why. The previous signature handed the closure a **zero-capacity
534 /// `Vec<u8>`**, so filling it reallocated — and a `Vec` reallocation frees the old
535 /// block **without zeroizing it**, abandoning a copy of the secret on the heap that
536 /// nothing would ever wipe. A constructor whose entire purpose was to avoid an
537 /// unprotected copy created one, silently, in the common case of two pushes.
538 ///
539 /// Pass the final length and fill the slot:
540 ///
541 /// ```rust
542 /// # #[cfg(feature = "alloc")] {
543 /// use secure_gate::{Dynamic, RevealSecret};
544 ///
545 /// // was: new_with(|v| { v.extend_from_slice(&material); v.resize(16, 0); })
546 /// let key = Dynamic::<Vec<u8>>::new_with(16, |slot| {
547 /// slot[..4].copy_from_slice(&[1, 2, 3, 4]);
548 /// });
549 /// assert_eq!(key.expose_secret().len(), 16);
550 /// key.with_secret(|b| assert_eq!(&b[4..], &[0u8; 12]));
551 /// # }
552 /// ```
553 ///
554 /// # When the length is not known in advance
555 ///
556 /// Use [`Dynamic::new`] with an empty `Vec` and write through the
557 /// [`std::io::Write`] impl, which grows by hand and **zeroizes each abandoned
558 /// buffer** before releasing it — the one growth path in this crate that does.
559 /// See the [`Write`](#impl-Write-for-Dynamic<Vec<u8>>) impl for what it does and
560 /// does not cover. It requires the `std` feature.
561 ///
562 /// # Panics
563 ///
564 /// Allocating the slot is `alloc::vec![0u8; len]`, so `len` is not a hint — it is an
565 /// allocation request, and both allocator failure modes are reachable through it. A
566 /// `len` above `isize::MAX` panics with `"capacity overflow"` before anything is
567 /// allocated. A `len` the allocator cannot satisfy reaches `handle_alloc_error`, which
568 /// **aborts** rather than unwinding: no destructor runs anywhere, so no `Zeroizing` on
569 /// this stack or any frame above it gets the chance to wipe anything.
570 ///
571 /// `len` is caller-supplied, and the worked example on the fallible sibling is
572 /// `try_new_with(len, |slot| r.read_exact(slot))` — precisely the shape where `len`
573 /// arrives off the wire, out of a frame header or a manifest. Bound it before passing
574 /// it here, for the same reason `deserialize_with_limit` on this type takes a ceiling
575 /// instead of trusting the payload: a length field is attacker-controlled input until
576 /// something checks it.
577 ///
578 /// For some callers that bound is not a check that *moves* — it is a check that changes
579 /// *shape*, and the difference decides whether there is any work to do. If your length
580 /// previously flowed into a decompressor or a comparison further down, a ceiling
581 /// enforced after the fact was enough: an overstated length failed a check and you
582 /// returned an error. Here the length **is** the allocation, and
583 /// `alloc::vec![0u8; 9_000_000_000]` does not fail a comparison — it asks the allocator
584 /// for nine gigabytes before any of your code runs. A caller whose length comes from a
585 /// trusted source has nothing to do. A caller whose length comes from the file format
586 /// has acquired an allocation-sized surface where it previously had a comparison, and
587 /// needs a bound it may never have written.
588 ///
589 /// A panic *inside* `f` is safe: the slot lives in a [`zeroize::Zeroizing`] for the
590 /// whole call, so partially written bytes are wiped during unwinding.
591 ///
592 /// # See also
593 ///
594 /// - [`try_new_with`](Self::try_new_with) — when the fill can fail.
595 /// - [`SlotWriter`](crate::SlotWriter) — append to the slot without writing offsets
596 /// by hand.
597 #[inline(always)]
598 pub fn new_with<F>(len: usize, f: F) -> Self
599 where
600 F: FnOnce(&mut [u8]),
601 {
602 let mut v: zeroize::Zeroizing<alloc::vec::Vec<u8>> =
603 zeroize::Zeroizing::new(alloc::vec![0u8; len]);
604 f(&mut v[..]);
605 Self::from_protected_bytes(v)
606 }
607
608 /// [`new_with`](Self::new_with) for a fill that can fail.
609 ///
610 /// Identical in every respect but the closure's return type. On `Err` the
611 /// partially written slot is **zeroized before the error is returned** — the
612 /// `Zeroizing` holding it is dropped on the way out, so a failed fill leaves
613 /// nothing behind.
614 ///
615 /// That is not defensive tidiness. Decoders that write into a caller-supplied
616 /// buffer routinely leave real plaintext in it when they fail; `miniz_oxide`'s
617 /// `decompress_slice_iter_to_slice` documents exactly that ("in that case the
618 /// output buffer will still contain the partial decompression"). Wrapping only on
619 /// success would leave that partial secret in an unprotected buffer on every
620 /// malformed input.
621 ///
622 /// # Errors
623 ///
624 /// Returns whatever the closure returns. The error type is the caller's; this
625 /// method neither inspects nor wraps it.
626 ///
627 /// # A short fill is not an error, and its tail is the zero fill
628 ///
629 /// The closure returns `Result<(), E>`, which carries no bytes-written count. A closure
630 /// that writes fewer than `len` bytes and returns `Ok` therefore yields a full-length
631 /// secret whose untouched tail is the pre-zeroing. Nothing here can catch that, because
632 /// nothing here is ever told how far the fill got.
633 ///
634 /// Whether that is the right answer or silent corruption depends on what is in the
635 /// slot, and the two shapes differ completely:
636 ///
637 /// - **Fixed-width key material** — the 40-bit RC4 key from [`new_with`](Self::new_with):
638 /// five bytes of derived material followed by eleven zeros that are key-schedule
639 /// input. The short fill *is* the intended fill, and the zero guarantee is what makes
640 /// it reproducible.
641 /// - **A variable-content payload** — a decoded document, a decompressed record, a frame
642 /// whose `len` came from a header that is only an upper bound or is simply wrong (a
643 /// truncated archive, a manifest that disagrees with its contents). Here the zero tail
644 /// is not input. It is missing plaintext in the shape of real data, and whatever
645 /// parses the result downstream will read it as real.
646 ///
647 /// For the second shape the closure has to check its own coverage and fail, since this
648 /// method cannot do it: write through a [`SlotWriter`](crate::SlotWriter) and return
649 /// `Err` unless [`is_full()`](crate::SlotWriter::is_full) holds at the end, or keep the
650 /// count by hand and compare `bytes_written == slot.len()` before returning `Ok`. The
651 /// `read_exact` example below does not demonstrate this and cannot be read as the
652 /// pattern to copy — [`read_exact`](std::io::Read::read_exact) surfaces under-fill as
653 /// `Err` by construction, so that closure gets the check for free and never writes one.
654 ///
655 /// # Panics
656 ///
657 /// The same two allocator failures as [`new_with`](Self::new_with), for the same
658 /// reason: the body allocates the slot with the identical `alloc::vec![0u8; len]`. A
659 /// `len` above `isize::MAX` panics with `"capacity overflow"`; a `len` the allocator
660 /// cannot satisfy reaches `handle_alloc_error`, which aborts without unwinding, so no
661 /// `Zeroizing` anywhere on the stack runs. The `# Errors` section above describes the
662 /// closure's failures only — an oversized `len` never reaches the closure and never
663 /// arrives as `Err`, so a caller treating `Result` as the complete failure channel is
664 /// wrong about this one. Bound `len` before passing it; the example below takes it
665 /// straight from its caller, which in real code means straight from a frame header.
666 ///
667 /// A panic *inside* `f` is safe, exactly as in `new_with`: the slot is held in a
668 /// [`zeroize::Zeroizing`] for the whole call and is wiped during unwinding.
669 ///
670 /// # Examples
671 ///
672 /// ```rust
673 /// # #[cfg(all(feature = "alloc", feature = "std"))] {
674 /// use secure_gate::{Dynamic, RevealSecret};
675 /// use std::io::Read;
676 ///
677 /// fn read_exact_secret<R: Read>(r: &mut R, len: usize) -> std::io::Result<Dynamic<Vec<u8>>> {
678 /// Dynamic::<Vec<u8>>::try_new_with(len, |slot| r.read_exact(slot))
679 /// }
680 ///
681 /// let secret = read_exact_secret(&mut [7u8; 32].as_slice(), 32).unwrap();
682 /// assert_eq!(secret.expose_secret().len(), 32);
683 ///
684 /// // A short read fails, and yields no secret at all.
685 /// assert!(read_exact_secret(&mut [7u8; 8].as_slice(), 32).is_err());
686 /// # }
687 /// ```
688 #[inline(always)]
689 pub fn try_new_with<F, E>(len: usize, f: F) -> Result<Self, E>
690 where
691 F: FnOnce(&mut [u8]) -> Result<(), E>,
692 {
693 let mut v: zeroize::Zeroizing<alloc::vec::Vec<u8>> =
694 zeroize::Zeroizing::new(alloc::vec![0u8; len]);
695 // On the error path `v` is dropped here, so `Zeroizing` wipes whatever the
696 // closure wrote before it gave up. That is why the buffer is built before the
697 // fill rather than assembled after it.
698 f(&mut v[..])?;
699 Ok(Self::from_protected_bytes(v))
700 }
701}
702
703// `new_with` was removed here in 0.9.0-rc.12, deliberately and with no replacement.
704//
705// It handed the closure a zero-capacity `String`, so filling it reallocated and each
706// reallocation freed the old buffer unwiped — the same defect the `Vec<u8>` arm had,
707// in the arm most likely to hold a password. The `Vec` arm was fixed by handing a
708// sized slot instead; no such slot exists for text. A `&mut str` cannot be usefully
709// filled: `String` must hold valid UTF-8 and characters have variable byte widths, so
710// a fixed byte window is not a place you can write arbitrary text into.
711//
712// The replacement is to build a pre-sized `String` yourself and move it in with
713// `Dynamic::new`, which transfers the buffer rather than copying it — so the
714// allocation you filled is the allocation the wrapper protects. `From<&str>` copies
715// and leaves your source unwiped; prefer `new` for secret material.
716//
717// That leaves this impl block with no unconditional member: `from_protected_bytes`
718// below exists solely for `deserialize_with_limit`, so — unlike the `Vec<u8>` arm's
719// copy of the same helper, which `new_with`/`try_new_with` keep alive unconditionally
720// — it is `#[cfg]`-gated on `serde-deserialize` rather than left to go dead under
721// `alloc` alone.
722#[cfg(feature = "serde-deserialize")]
723impl Dynamic<alloc::string::String> {
724 /// Heap-only construction from a `Zeroizing<String>`. Swaps the protected
725 /// buffer into a default-initialized `Box<String>` and returns the
726 /// `Dynamic`. Panic-safe: if the `Box` allocation OOM-panics, `protected`
727 /// stays live and `Zeroizing::drop` zeroes the secret bytes during unwind.
728 #[inline(always)]
729 fn from_protected_bytes(mut protected: zeroize::Zeroizing<alloc::string::String>) -> Self {
730 // Only fallible allocation; protected stays live across it for panic-safety
731 let mut boxed = Box::<alloc::string::String>::default();
732 core::mem::swap(&mut *boxed, &mut *protected);
733 Self::from(boxed)
734 }
735}
736
737/// Hex encoding for `Dynamic<Vec<u8>>`; delegates via `with_secret`.
738///
739/// Deliberately **not** implemented for `Dynamic<String>` — hex-encoding
740/// textual secrets is a design smell; convert explicitly inside `with_secret`
741/// if genuinely needed. Bring the trait into scope: `use secure_gate::ToHex;`.
742///
743/// ```rust
744/// # #[cfg(feature = "encoding-hex")] {
745/// use secure_gate::{Dynamic, ToHex};
746///
747/// let token: Dynamic<Vec<u8>> = Dynamic::from(&[0xDEu8, 0xAD][..]);
748/// assert_eq!(&*token.to_hex(), "dead");
749/// # }
750/// ```
751#[cfg(feature = "encoding-hex")]
752impl ToHex for Dynamic<Vec<u8>> {
753 #[inline]
754 fn to_hex(&self) -> crate::EncodedSecret {
755 self.with_secret(|s| s.to_hex())
756 }
757
758 #[inline]
759 fn to_hex_upper(&self) -> crate::EncodedSecret {
760 self.with_secret(|s| s.to_hex_upper())
761 }
762}
763
764/// Base32 encoding for `Dynamic<Vec<u8>>`; delegates via `with_secret`.
765///
766/// Bring the trait into scope: `use secure_gate::ToBase32;`.
767///
768/// ```rust
769/// # #[cfg(feature = "encoding-base32")] {
770/// use secure_gate::{Dynamic, ToBase32};
771///
772/// let token: Dynamic<Vec<u8>> = Dynamic::from(&[0xABu8; 4][..]);
773/// assert_eq!(&*token.to_base32(), "VOV2XKY");
774/// # }
775/// ```
776#[cfg(feature = "encoding-base32")]
777impl ToBase32 for Dynamic<Vec<u8>> {
778 #[inline]
779 fn to_base32(&self) -> crate::EncodedSecret {
780 self.with_secret(|s| s.to_base32())
781 }
782}
783
784/// Base64url encoding for `Dynamic<Vec<u8>>`; delegates via `with_secret`.
785///
786/// Bring the trait into scope: `use secure_gate::ToBase64Url;`.
787///
788/// ```rust
789/// # #[cfg(feature = "encoding-base64")] {
790/// use secure_gate::{Dynamic, ToBase64Url};
791///
792/// let token: Dynamic<Vec<u8>> = Dynamic::from(&[0xABu8; 4][..]);
793/// assert_eq!(&*token.to_base64url(), "q6urqw");
794/// # }
795/// ```
796#[cfg(feature = "encoding-base64")]
797impl ToBase64Url for Dynamic<Vec<u8>> {
798 #[inline]
799 fn to_base64url(&self) -> crate::EncodedSecret {
800 self.with_secret(|s| s.to_base64url())
801 }
802}
803
804/// Bech32 encoding for `Dynamic<Vec<u8>>`; delegates via `with_secret`.
805///
806/// Bring the trait into scope: `use secure_gate::ToBech32;`.
807#[cfg(feature = "encoding-bech32")]
808impl ToBech32 for Dynamic<Vec<u8>> {
809 #[inline]
810 fn try_to_bech32(
811 &self,
812 hrp: &str,
813 case: crate::Case,
814 ) -> Result<crate::EncodedSecret, crate::error::Bech32Error> {
815 self.with_secret(|s| s.try_to_bech32(hrp, case))
816 }
817
818 #[inline]
819 fn try_to_bech32_sized<const C: usize>(
820 &self,
821 hrp: &str,
822 case: crate::Case,
823 ) -> Result<crate::EncodedSecret, crate::error::Bech32Error> {
824 self.with_secret(|s| s.try_to_bech32_sized::<C>(hrp, case))
825 }
826}
827
828/// Bech32m encoding for `Dynamic<Vec<u8>>`; delegates via `with_secret`.
829///
830/// Bring the trait into scope: `use secure_gate::ToBech32m;`.
831#[cfg(feature = "encoding-bech32")]
832impl ToBech32m for Dynamic<Vec<u8>> {
833 #[inline]
834 fn try_to_bech32m(
835 &self,
836 hrp: &str,
837 case: crate::Case,
838 ) -> Result<crate::EncodedSecret, crate::error::Bech32Error> {
839 self.with_secret(|s| s.try_to_bech32m(hrp, case))
840 }
841
842 #[inline]
843 fn try_to_bech32m_sized<const C: usize>(
844 &self,
845 hrp: &str,
846 case: crate::Case,
847 ) -> Result<crate::EncodedSecret, crate::error::Bech32Error> {
848 self.with_secret(|s| s.try_to_bech32m_sized::<C>(hrp, case))
849 }
850}
851
852// RevealSecret — one generic impl covers every inner type, including local
853// user-defined ones. Length reporting lives in the narrower `SecretLen`
854// (implemented for `Dynamic<String>` and `Dynamic<Vec<T>>`), because a generic
855// `T` has no meaningful length.
856impl<T: ?Sized + zeroize::Zeroize> crate::RevealSecret for Dynamic<T> {
857 type Inner = T;
858
859 #[inline(always)]
860 fn with_secret<F, R>(&self, f: F) -> R
861 where
862 F: FnOnce(&T) -> R,
863 {
864 f(&self.inner)
865 }
866
867 #[inline(always)]
868 fn expose_secret(&self) -> &T {
869 &self.inner
870 }
871
872 /// Consumes `self` and transfers ownership of the plain inner value.
873 ///
874 /// **Allocation note:** allocates one small `Box<T>` sentinel (24 bytes for
875 /// `String`/`Vec` on 64-bit) before the swap. If that allocation panics (OOM),
876 /// `self.inner` is unchanged and `Dynamic::drop` zeroizes the real secret during
877 /// unwind — confidentiality is preserved. This is the same OOM-safety pattern
878 /// used by `from_protected_bytes` and `deserialize_with_limit`.
879 ///
880 /// See [`RevealSecret::into_inner`](crate::RevealSecret::into_inner) for the full
881 /// contract. Protection ends at this call: the returned value is an ordinary
882 /// `String` / `Vec<T>`, moved out without copying.
883 #[inline(always)]
884 fn into_inner(mut self) -> T
885 where
886 Self: Sized,
887 Self::Inner: Sized + crate::SentinelValue + zeroize::Zeroize,
888 {
889 // Swap in a sentinel. If Box::new panics (OOM) before the swap, self.inner
890 // still holds the real secret and Dynamic::drop zeroizes it on unwind.
891 // After the swap, self.inner is an inert sentinel — zeroized on
892 // Dynamic::drop as a no-op. `*boxed` deref-moves the value out of the Box.
893 let boxed = core::mem::replace(
894 &mut self.inner,
895 Box::new(crate::SentinelValue::sentinel_value()),
896 );
897 *boxed
898 }
899}
900
901// RevealSecretMut — same generic coverage as RevealSecret.
902impl<T: ?Sized + zeroize::Zeroize> crate::RevealSecretMut for Dynamic<T> {
903 #[inline(always)]
904 fn with_secret_mut<F, R>(&mut self, f: F) -> R
905 where
906 F: FnOnce(&mut T) -> R,
907 {
908 f(&mut self.inner)
909 }
910
911 #[inline(always)]
912 fn expose_secret_mut(&mut self) -> &mut T {
913 &mut self.inner
914 }
915}
916
917// Random generation
918#[cfg(feature = "rand")]
919impl Dynamic<alloc::vec::Vec<u8>> {
920 /// Fills a new `Vec<u8>` with `len` cryptographically secure random bytes and wraps it.
921 ///
922 /// Uses the system RNG ([`SysRng`](rand::rngs::SysRng)). Requires the `rand` feature (and
923 /// `alloc`, which `Dynamic<Vec<u8>>` always needs).
924 ///
925 /// # Panics
926 ///
927 /// Panics if the system RNG fails to provide bytes ([`TryRng::try_fill_bytes`](rand::TryRng::try_fill_bytes)
928 /// returns `Err`). This is treated as a fatal environment error.
929 ///
930 /// # Examples
931 ///
932 /// ```rust
933 /// # #[cfg(all(feature = "alloc", feature = "rand"))]
934 /// use secure_gate::{Dynamic, RevealSecret, SecretLen};
935 ///
936 /// # #[cfg(all(feature = "alloc", feature = "rand"))]
937 /// # {
938 /// let nonce: Dynamic<Vec<u8>> = Dynamic::from_random(24);
939 /// assert_eq!(nonce.len(), 24);
940 /// # }
941 /// ```
942 #[inline]
943 pub fn from_random(len: usize) -> Self {
944 Self::new_with(len, |slot| {
945 SysRng
946 .try_fill_bytes(slot)
947 .expect("SysRng failure is a program error");
948 })
949 }
950
951 /// Allocates a `Vec<u8>` of length `len`, fills it from `rng`, and wraps it.
952 ///
953 /// Accepts any [`TryCryptoRng`](rand::TryCryptoRng) + [`TryRng`](rand::TryRng) — for example,
954 /// a seeded `StdRng` for deterministic tests. Requires the `rand`
955 /// feature and `alloc` (implicit — [`Dynamic<T>`](crate::Dynamic) itself requires it).
956 ///
957 /// # Errors
958 ///
959 /// Returns `R::Error` if [`try_fill_bytes`](rand::TryRng::try_fill_bytes) fails.
960 ///
961 /// # Examples
962 ///
963 /// ```rust
964 /// # #[cfg(all(feature = "alloc", feature = "rand"))]
965 /// # {
966 /// use rand::rngs::StdRng;
967 /// use rand::SeedableRng;
968 /// use secure_gate::Dynamic;
969 ///
970 /// let mut rng = StdRng::from_seed([9u8; 32]);
971 /// let nonce: Dynamic<Vec<u8>> = Dynamic::from_rng(24, &mut rng).expect("rng fill");
972 /// # }
973 /// ```
974 #[inline]
975 pub fn from_rng<R: TryRng + TryCryptoRng>(len: usize, rng: &mut R) -> Result<Self, R::Error> {
976 // This body was the captured-local workaround `try_new_with` exists to retire:
977 // `new_with` could not report failure, so the error was written to a local and
978 // checked afterwards. One expression now — and one observable behaviour change,
979 // not none.
980 //
981 // The old body was `Vec::new()` followed by `v.resize(len, 0u8)`, and `resize`
982 // grows through `RawVec::grow_amortized`, which floors capacity at
983 // `MIN_NON_ZERO_CAP` — 8 for one-byte elements. So `from_rng(4, ..)` used to return
984 // a buffer of length 4 with capacity 8, while `alloc::vec![0u8; len]` allocates the
985 // exact size and returns capacity 4. That is visible through
986 // `with_secret(Vec::capacity)`, which this crate's tests read directly, and it
987 // tightens the `io::Write` impl's spare-room branch
988 // (`buf.len() > v.capacity() - v.len()`): a short random secret no longer carries
989 // up to four free bytes that could absorb a small write without growing. Holding no
990 // slack is the whole point of the new constructor, so the change stays — it is just
991 // not behaviour-neutral, and a 0.8 backport that assumes it is will be wrong about
992 // any capacity assertion below 8.
993 //
994 // Everything else is unchanged: same `R::Error`, same wipe-on-`Err` (the
995 // `Zeroizing` drops on the way out), same allocation count.
996 Self::try_new_with(len, |slot| rng.try_fill_bytes(slot))
997 }
998}
999
1000/// Constant-time equality for `Dynamic<T>` — routes through [`expose_secret()`](crate::RevealSecret::expose_secret).
1001///
1002/// `==` is **deliberately not implemented**. Always use `ct_eq`.
1003#[cfg(feature = "ct-eq")]
1004impl<T: ?Sized + zeroize::Zeroize> crate::ConstantTimeEq for Dynamic<T>
1005where
1006 T: crate::ConstantTimeEq,
1007 Self: crate::RevealSecret<Inner = T>,
1008{
1009 fn ct_eq(&self, other: &Self) -> bool {
1010 self.expose_secret().ct_eq(other.expose_secret())
1011 }
1012}
1013
1014/// Always prints `[REDACTED]` — secrets never appear in debug output.
1015impl<T: ?Sized + zeroize::Zeroize> core::fmt::Debug for Dynamic<T> {
1016 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1017 f.write_str("[REDACTED]")
1018 }
1019}
1020
1021/// Opt-in cloning — requires `cloneable` feature and [`CloneableSecret`](crate::CloneableSecret)
1022/// marker. Each clone is independently zeroized on drop, but cloning increases exposure surface.
1023#[cfg(feature = "cloneable")]
1024impl<T: zeroize::Zeroize + crate::CloneableSecret> Clone for Dynamic<T> {
1025 fn clone(&self) -> Self {
1026 Self::new(self.inner.clone())
1027 }
1028}
1029
1030// ---------------------------------------------------------------------------
1031// Streaming I/O (std only)
1032// ---------------------------------------------------------------------------
1033
1034/// Streams bytes directly into the protected buffer via [`RevealSecretMut`](crate::RevealSecretMut).
1035///
1036/// Data flows **into** the wrapper, so no intermediate unprotected `Vec<u8>` is
1037/// accumulated before wrapping.
1038///
1039/// # Growth wipes the outgoing buffer
1040///
1041/// Writing past the current capacity cannot simply delegate to `Vec`: `Vec` would
1042/// reallocate, copy the secret into the new allocation, and hand the **old** one
1043/// back to the allocator with the plaintext still in it. This impl grows by hand
1044/// instead — it allocates the larger buffer, copies, zeroizes the old buffer
1045/// (contents and spare capacity), and only then releases it.
1046///
1047/// Pre-sizing with `Vec::with_capacity` avoids the copy altogether and is worth
1048/// doing when the length is known up front:
1049///
1050/// ```rust
1051/// # #[cfg(feature = "std")] {
1052/// use std::io::Write;
1053/// use secure_gate::Dynamic;
1054///
1055/// let payload = b"decrypted payload";
1056///
1057/// // Pre-sized: no growth, so no copy and nothing to wipe.
1058/// let mut secret = Dynamic::<Vec<u8>>::new(Vec::with_capacity(payload.len()));
1059/// secret.write_all(payload).unwrap();
1060/// # }
1061/// ```
1062///
1063/// # This is the sanctioned path for a secret whose length you do not know
1064///
1065/// **Every buffer this impl abandons is zeroized before it is released.** Growth is done
1066/// by hand — allocate, copy, wipe the old buffer including its spare capacity, and only
1067/// then drop it — rather than through `Vec`'s own reallocation, which would free the old
1068/// block still holding plaintext. Doubling is amortized, so streaming stays linear.
1069///
1070/// That makes the pair complete: **length known before the fill, use
1071/// [`new_with(len, …)`](Dynamic::new_with); length not known, use `Write`.** Neither
1072/// leaves an unwiped copy behind, and there is no third case that needs one.
1073///
1074/// ## What it does not cover
1075///
1076/// The guarantee is about the buffer this impl owns, and three things sit outside it:
1077///
1078/// - **A caller who grows the `Vec` themselves**, through
1079/// [`with_secret_mut`](crate::RevealSecretMut::with_secret_mut) or
1080/// [`expose_secret_mut`](crate::RevealSecretMut::expose_secret_mut), holds
1081/// `&mut Vec<u8>` directly and their reallocation is `Vec`'s, not this one's. See the
1082/// heap-reallocation residue section in `SECURITY.md`.
1083/// - **Whatever fed the bytes in.** Driving this with [`std::io::copy`] passes every byte
1084/// through `copy`'s own transfer buffer, and [`std::io::Stdin`] keeps a
1085/// process-lifetime internal buffer. Nothing here can reach either. To close the
1086/// transfer buffer too, read into a [`Fixed<[u8; N]>`](crate::Fixed) chunk and
1087/// `write_all` each chunk through this impl.
1088/// - **The `std` feature**, which gates this impl. Without it the recommended path for
1089/// unknown-length secrets is simply absent, and the growable alternatives are not.
1090///
1091/// What it eliminates is the **unbounded chain** of unwiped reallocations — the residue
1092/// that scales with input size, leaving an intermediate copy on the heap for every
1093/// growth. That is the defect; the buffers above are bounded and do not accumulate.
1094#[cfg(feature = "std")]
1095impl std::io::Write for Dynamic<alloc::vec::Vec<u8>> {
1096 #[inline]
1097 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1098 use crate::RevealSecretMut;
1099 use zeroize::Zeroize;
1100
1101 self.with_secret_mut(|v: &mut alloc::vec::Vec<u8>| -> std::io::Result<usize> {
1102 if buf.len() > v.capacity() - v.len() {
1103 // Grow by hand so the outgoing allocation can be wiped before the
1104 // allocator gets it back. `Vec`'s own realloc would copy the secret
1105 // into the new buffer and free the old one still holding plaintext.
1106 let needed = v.len().checked_add(buf.len()).ok_or_else(|| {
1107 std::io::Error::new(std::io::ErrorKind::OutOfMemory, "capacity overflow")
1108 })?;
1109 // Mirror `Vec`'s amortized doubling so repeated writes stay linear.
1110 let new_cap = core::cmp::max(needed, v.capacity().saturating_mul(2));
1111 let mut grown = alloc::vec::Vec::with_capacity(new_cap);
1112 grown.extend_from_slice(v);
1113 // Wipes contents *and* spare capacity, and sets len to 0 without
1114 // freeing; the assignment below then drops the zeroed allocation.
1115 v.zeroize();
1116 *v = grown;
1117 }
1118 v.extend_from_slice(buf);
1119 Ok(buf.len())
1120 })
1121 }
1122
1123 #[inline]
1124 fn flush(&mut self) -> std::io::Result<()> {
1125 Ok(())
1126 }
1127}
1128
1129/// Cursor-like reader over a [`Dynamic<Vec<u8>>`].
1130///
1131/// Created by [`Dynamic::<Vec<u8>>::as_reader`]. Borrows the `Dynamic`
1132/// immutably and tracks the read position internally. Each [`Read::read`](std::io::Read::read)
1133/// call goes through [`with_secret`](crate::RevealSecret::with_secret),
1134/// preserving the crate's auditable access model.
1135///
1136/// # Security
1137///
1138/// `Read::read()` copies secret bytes into the caller-supplied buffer.
1139/// The caller is responsible for zeroizing that buffer. Prefer piping
1140/// directly into encrypted writers (`io::copy` into an encryptor, etc.)
1141/// rather than reading into intermediate `Vec<u8>` buffers.
1142///
1143/// The `Dynamic` wrapper continues to zeroize its contents on drop
1144/// regardless of how many bytes have been read out.
1145#[cfg(feature = "std")]
1146pub struct DynamicReader<'a> {
1147 secret: &'a Dynamic<alloc::vec::Vec<u8>>,
1148 offset: usize,
1149}
1150
1151#[cfg(feature = "std")]
1152impl std::io::Read for DynamicReader<'_> {
1153 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1154 let offset = self.offset;
1155 let n = self.secret.with_secret(|v| {
1156 let remaining = v.len().saturating_sub(offset);
1157 let n = remaining.min(buf.len());
1158 buf[..n].copy_from_slice(&v[offset..offset + n]);
1159 n
1160 });
1161 self.offset += n;
1162 Ok(n)
1163 }
1164}
1165
1166#[cfg(feature = "std")]
1167impl Dynamic<alloc::vec::Vec<u8>> {
1168 /// Returns a [`DynamicReader`] that implements [`std::io::Read`].
1169 ///
1170 /// This replaces the common `with_secret` + `Cursor` boilerplate:
1171 ///
1172 /// ```rust
1173 /// # #[cfg(feature = "std")] {
1174 /// use std::io;
1175 /// use secure_gate::Dynamic;
1176 ///
1177 /// let secret = Dynamic::<Vec<u8>>::new(vec![1, 2, 3, 4]);
1178 ///
1179 /// // Before: awkward closure + Cursor dance
1180 /// // secret.with_secret(|b| io::copy(&mut Cursor::new(b), &mut encryptor))?;
1181 ///
1182 /// // After: pipe directly into an encrypted writer — no intermediate buffer
1183 /// let mut encryptor = io::sink(); // stand-in for a real encryptor
1184 /// io::copy(&mut secret.as_reader(), &mut encryptor).unwrap();
1185 /// # }
1186 /// ```
1187 ///
1188 /// # Security
1189 ///
1190 /// Each `read()` call copies secret bytes into the caller's buffer.
1191 /// Prefer piping directly into encrypted writers rather than reading
1192 /// into intermediate buffers. The caller is responsible for zeroizing
1193 /// any destination buffer.
1194 #[inline]
1195 pub fn as_reader(&self) -> DynamicReader<'_> {
1196 DynamicReader {
1197 secret: self,
1198 offset: 0,
1199 }
1200 }
1201}
1202
1203/// Opt-in serialization — requires `serde-serialize` feature and
1204/// [`SerializableSecret`](crate::SerializableSecret) marker. Serialization exposes the
1205/// full secret — audit every impl.
1206#[cfg(feature = "serde-serialize")]
1207impl<T: zeroize::Zeroize + crate::SerializableSecret> serde::Serialize for Dynamic<T> {
1208 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1209 where
1210 S: serde::Serializer,
1211 {
1212 self.inner.serialize(serializer)
1213 }
1214}
1215
1216// Deserialize
1217
1218/// Default maximum byte length accepted when deserializing `Dynamic<Vec<u8>>` or
1219/// `Dynamic<String>` via the standard `serde::Deserialize` impl (1 MiB).
1220///
1221/// Pass a custom value to [`Dynamic::deserialize_with_limit`] when a different
1222/// ceiling is required.
1223///
1224/// **Important:** this limit is enforced *after* the upstream deserializer has fully
1225/// materialized the payload. It is a **result-length acceptance bound**, not a
1226/// pre-allocation DoS guard. For untrusted input, enforce size limits at the
1227/// transport or parser layer upstream.
1228#[cfg(feature = "serde-deserialize")]
1229pub const MAX_DESERIALIZE_BYTES: usize = 1_048_576;
1230
1231#[cfg(feature = "serde-deserialize")]
1232impl Dynamic<alloc::vec::Vec<u8>> {
1233 /// Deserializes into `Dynamic<Vec<u8>>`, rejecting payloads larger than `limit` bytes.
1234 ///
1235 /// The standard [`serde::Deserialize`] impl calls this with [`MAX_DESERIALIZE_BYTES`].
1236 /// Use this method directly when you need a tighter or looser ceiling.
1237 ///
1238 /// **Zeroization scope.** Once the upstream deserializer returns a complete
1239 /// `Vec<u8>`, the value is wrapped in `Zeroizing` and stays protected for the
1240 /// rest of this function: oversized buffers are zeroized before the error is
1241 /// returned, and an OOM panic in the subsequent `Box` allocation triggers
1242 /// zeroization on unwind. **However, this guarantee does *not* extend backwards
1243 /// into the deserializer itself.** If the upstream `Vec<u8>` visitor accumulates
1244 /// bytes element-by-element (e.g., a JSON sequence) and fails partway through,
1245 /// the partial buffer is owned by the visitor and dropped as a plain `Vec<u8>` —
1246 /// not zeroized. In the typical untrusted-input threat model the partial bytes
1247 /// are attacker-controlled (the malformed payload they sent), so the practical
1248 /// disclosure surface is bounded; but if your threat model includes deserialization
1249 /// of trusted-but-corruptible secret material, treat the deserialize step as
1250 /// outside the zeroization boundary and build in-process instead:
1251 /// [`new_with(len, …)`](Dynamic::new_with) (or [`try_new_with`](Dynamic::try_new_with)
1252 /// for a fill that can fail) writes straight into the wrapper's own pre-zeroed slot,
1253 /// so there is no separate buffer for a visitor to accumulate into and abandon unwiped.
1254 ///
1255 /// **Important:** this limit is enforced *after* the upstream deserializer has fully
1256 /// materialized the payload. It is a **result-length acceptance bound**, not a
1257 /// pre-allocation DoS guard. For untrusted input, enforce size limits at the
1258 /// transport or parser layer upstream.
1259 pub fn deserialize_with_limit<'de, D>(deserializer: D, limit: usize) -> Result<Self, D::Error>
1260 where
1261 D: serde::Deserializer<'de>,
1262 {
1263 let mut buf: zeroize::Zeroizing<alloc::vec::Vec<u8>> =
1264 zeroize::Zeroizing::new(serde::Deserialize::deserialize(deserializer)?);
1265 if buf.len() > limit {
1266 // buf drops here → Zeroizing zeros the oversized buffer before deallocation
1267 return Err(serde::de::Error::custom(
1268 "deserialized secret exceeds maximum size",
1269 ));
1270 }
1271 // Only fallible allocation; protected stays live across it for panic-safety
1272 let mut boxed = Box::<alloc::vec::Vec<u8>>::default();
1273 core::mem::swap(&mut *boxed, &mut *buf);
1274 Ok(Self::from(boxed))
1275 }
1276}
1277
1278#[cfg(feature = "serde-deserialize")]
1279impl Dynamic<String> {
1280 /// Deserializes into `Dynamic<String>`, rejecting payloads larger than `limit` bytes.
1281 ///
1282 /// The standard [`serde::Deserialize`] impl calls this with [`MAX_DESERIALIZE_BYTES`].
1283 /// Use this method directly when you need a tighter or looser ceiling.
1284 ///
1285 /// **Zeroization scope.** Once the upstream deserializer returns a complete
1286 /// `String`, the value is wrapped in `Zeroizing` and stays protected for the
1287 /// rest of this function: oversized buffers are zeroized before the error is
1288 /// returned, and an OOM panic in the subsequent `Box` allocation triggers
1289 /// zeroization on unwind. **However, this guarantee does *not* extend backwards
1290 /// into the deserializer itself.** If the upstream `String` visitor accumulates
1291 /// characters and fails partway through (e.g., on an invalid UTF-8 boundary),
1292 /// the partial buffer is owned by the visitor and dropped as a plain `String` —
1293 /// not zeroized. In the typical untrusted-input threat model the partial bytes
1294 /// are attacker-controlled (the malformed payload they sent), so the practical
1295 /// disclosure surface is bounded; but if your threat model includes deserialization
1296 /// of trusted-but-corruptible secret material, treat the deserialize step as
1297 /// outside the zeroization boundary and build in-process instead. `String` has no
1298 /// sized-slot constructor of its own — it must hold valid UTF-8 and characters are
1299 /// variable-width, so there is no fixed byte window arbitrary text can be written
1300 /// into. Size a `String` to its exact final length yourself (`String::with_capacity`,
1301 /// filled without triggering a reallocation) and move it in with
1302 /// [`Dynamic::new`](Dynamic::new), which moves the buffer rather than copying it.
1303 ///
1304 /// **Important:** this limit is enforced *after* the upstream deserializer has fully
1305 /// materialized the payload. It is a **result-length acceptance bound**, not a
1306 /// pre-allocation DoS guard. For untrusted input, enforce size limits at the
1307 /// transport or parser layer upstream.
1308 pub fn deserialize_with_limit<'de, D>(deserializer: D, limit: usize) -> Result<Self, D::Error>
1309 where
1310 D: serde::Deserializer<'de>,
1311 {
1312 let buf: zeroize::Zeroizing<alloc::string::String> =
1313 zeroize::Zeroizing::new(serde::Deserialize::deserialize(deserializer)?);
1314 if buf.len() > limit {
1315 // buf drops here → Zeroizing zeros the oversized buffer before deallocation
1316 return Err(serde::de::Error::custom(
1317 "deserialized secret exceeds maximum size",
1318 ));
1319 }
1320 Ok(Self::from_protected_bytes(buf))
1321 }
1322}
1323
1324#[cfg(feature = "serde-deserialize")]
1325impl<'de> serde::Deserialize<'de> for Dynamic<alloc::vec::Vec<u8>> {
1326 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1327 where
1328 D: serde::Deserializer<'de>,
1329 {
1330 Self::deserialize_with_limit(deserializer, MAX_DESERIALIZE_BYTES)
1331 }
1332}
1333
1334#[cfg(feature = "serde-deserialize")]
1335impl<'de> serde::Deserialize<'de> for Dynamic<String> {
1336 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1337 where
1338 D: serde::Deserializer<'de>,
1339 {
1340 Self::deserialize_with_limit(deserializer, MAX_DESERIALIZE_BYTES)
1341 }
1342}
1343
1344/// Zeroizes the inner value (including `Vec`/`String` spare capacity).
1345///
1346/// **Warning:** does not run under `panic = "abort"`.
1347impl<T: ?Sized + zeroize::Zeroize> zeroize::Zeroize for Dynamic<T> {
1348 fn zeroize(&mut self) {
1349 self.inner.zeroize();
1350 }
1351}
1352
1353/// Unconditionally zeroizes the inner value when the wrapper is dropped.
1354///
1355/// **Warning:** `Drop` does not run under `panic = "abort"`.
1356impl<T: ?Sized + zeroize::Zeroize> Drop for Dynamic<T> {
1357 fn drop(&mut self) {
1358 self.zeroize();
1359 }
1360}
1361
1362/// Marker confirming that `Dynamic<T>` always zeroizes on drop.
1363impl<T: ?Sized + zeroize::Zeroize> zeroize::ZeroizeOnDrop for Dynamic<T> {}
1364
1365impl crate::SecretLen for Dynamic<String> {
1366 #[inline(always)]
1367 fn len(&self) -> usize {
1368 self.inner.len()
1369 }
1370}
1371
1372impl<T: zeroize::Zeroize> crate::SecretLen for Dynamic<Vec<T>> {
1373 #[inline(always)]
1374 fn len(&self) -> usize {
1375 self.inner.len()
1376 }
1377
1378 #[inline(always)]
1379 fn byte_len(&self) -> usize {
1380 self.inner.len() * core::mem::size_of::<T>()
1381 }
1382}