Skip to main content

Dynamic

Struct Dynamic 

Source
pub struct Dynamic<T: ?Sized + Zeroize> { /* private fields */ }
Available on crate feature alloc only.
Expand description

Heap-allocated secret wrapper with explicit access and automatic zeroization on drop.

Variable-length secrets (passwords, API keys, ciphertexts). Inner type must implement Zeroize. Secret bytes live on the heap only — never on the stack. Requires alloc.

See Fixed<T> for the stack-allocated alternative.

use secure_gate::{Dynamic, RevealSecret};

let pw: Dynamic<String> = Dynamic::new(String::from("hunter2"));
assert_eq!(pw.with_secret(|s: &String| s.len()), 7);
assert_eq!(format!("{:?}", pw), "[REDACTED]");

Zero-cost heap-allocated wrapper for variable-length secrets.

Dynamic<T> stores a T: Zeroize value in a Box<T> and unconditionally zeroizes it on drop (including Vec/String spare capacity). There is no Deref, AsRef, or Copy — every access is explicit through RevealSecret or RevealSecretMut.

This is not Fixed<T> — it is the heap-allocated alternative for variable-length secrets. Secret bytes never reside on the stack.

§Examples

use secure_gate::{Dynamic, RevealSecret, SecretLen};

let pw: Dynamic<String> = Dynamic::new(String::from("hunter2"));
assert_eq!(pw.with_secret(|s: &String| s.len()), 7);
assert_eq!(format!("{:?}", pw), "[REDACTED]");

§Constructors for Dynamic<Vec<u8>>

ConstructorFeatureNotes
new_with(len, f)—Preferred. Sized, pre-zeroed slot inside the wrapper
try_new_with(len, f)—The same, fallible; wipes the partial write on Err
new(value)—Moves the buffer in. Accepts Vec<u8>, &[u8], Box<Vec<u8>>
try_from_hex(s)encoding-hexConstant-time hex decoding
try_from_base32(s)encoding-base32Constant-time Base32 decoding
try_from_base64url(s)encoding-base64Constant-time Base64url decoding
try_from_bech32(s, hrp)encoding-bech32HRP-validated Bech32
try_from_bech32_unchecked(s)encoding-bech32Bech32 without HRP check
try_from_bech32m(s, hrp)encoding-bech32HRP-validated Bech32m
try_from_bech32m_unchecked(s)encoding-bech32Bech32m without HRP check
from_random(len)randSystem RNG
from_rng(len, rng)randCustom RNG

§See also

Implementations§

Source§

impl<T: ?Sized + Zeroize> Dynamic<T>

Source

pub fn new<U>(value: U) -> Self
where U: Into<Box<T>>,

Wraps value in a Box<T> and returns a Dynamic<T>.

Accepts any type that implements Into<Box<T>> — including owned values, Box<T>, String, Vec<u8>, &str (via the blanket From<&str> impl), etc.

Equivalent to Dynamic::from(value) — #[doc(alias = "from")] is set so both names appear in docs.rs search.

Requires the alloc feature (which Dynamic<T> itself always requires).

Source§

impl Dynamic<Vec<u8>>

Source

pub fn try_from_hex(s: &str) -> Result<Self, HexError>

Available on crate feature encoding-hex only.

Decodes a hex string (lowercase, uppercase, or mixed) into Dynamic<Vec<u8>>.

The decoded buffer is kept inside a Zeroizing wrapper until after the Box allocation completes, guaranteeing zeroization even on OOM panic.

Source§

impl Dynamic<Vec<u8>>

Source

pub fn try_from_base32(s: &str) -> Result<Self, Base32Error>

Available on crate feature encoding-base32 only.

Decodes an uppercase, unpadded Base32 (RFC 4648 §6) string into Dynamic<Vec<u8>>.

The decoded buffer is kept inside a Zeroizing wrapper until after the Box allocation completes, guaranteeing zeroization even on OOM panic.

Decoding is strict about the alphabet, case, padding and length, but lenient about non-canonical trailing bits: unused bits in the final group are ignored rather than rejected, so "MZ" and "MY" both decode to [0x66]. Decoding is therefore not injective — two distinct strings can yield the same bytes, and only encoder-produced strings are canonical. Do not use the decoded value to decide that two encoded strings were equal.

Source§

impl Dynamic<Vec<u8>>

Source

pub fn try_from_base64url(s: &str) -> Result<Self, Base64Error>

Available on crate feature encoding-base64 only.

Decodes a Base64url (unpadded) string into Dynamic<Vec<u8>>.

The decoded buffer is kept inside a Zeroizing wrapper until after the Box allocation completes, guaranteeing zeroization even on OOM panic.

Source§

impl Dynamic<Vec<u8>>

Source

pub fn try_from_bech32(s: &str, expected_hrp: &str) -> Result<Self, Bech32Error>

Available on crate feature encoding-bech32 only.

Decodes a Bech32 (BIP-173) string into Dynamic<Vec<u8>>, validating the HRP (case-insensitive).

The decoded buffer is kept inside a Zeroizing wrapper until after the Box allocation completes, guaranteeing zeroization even on OOM panic.

HRP comparison is non-constant-time — this is intentional, as the HRP is public metadata, not secret material.

Source

pub fn try_from_bech32_sized<const C: usize>( s: &str, expected_hrp: &str, ) -> Result<Self, Bech32Error>

Available on crate feature encoding-bech32 only.

Like try_from_bech32, accepting strings up to C characters.

C is the bech32 code length: the cap on the whole encoded string. Pass the C the string was encoded with, or any larger value. See Bech32Sized for what C above BECH32_CODE_LENGTH costs.

Source

pub fn try_from_bech32_unchecked(s: &str) -> Result<Self, Bech32Error>

Available on crate feature encoding-bech32 only.

Decodes a Bech32 (BIP-173) string into Dynamic<Vec<u8>> without validating the HRP.

Use try_from_bech32 in security-critical code to prevent cross-protocol confusion attacks.

Source

pub fn try_from_bech32_unchecked_sized<const C: usize>( s: &str, ) -> Result<Self, Bech32Error>

Available on crate feature encoding-bech32 only.

Like try_from_bech32_unchecked, accepting strings up to C characters.

Source§

impl Dynamic<Vec<u8>>

Source

pub fn try_from_bech32m( s: &str, expected_hrp: &str, ) -> Result<Self, Bech32Error>

Available on crate feature encoding-bech32 only.

Decodes a Bech32m (BIP-350) string into Dynamic<Vec<u8>>, validating the HRP (case-insensitive).

The decoded buffer is kept inside a Zeroizing wrapper until after the Box allocation completes, guaranteeing zeroization even on OOM panic.

Source

pub fn try_from_bech32m_sized<const C: usize>( s: &str, expected_hrp: &str, ) -> Result<Self, Bech32Error>

Available on crate feature encoding-bech32 only.

Like try_from_bech32m, accepting strings up to C characters. See Bech32mSized for what C above BECH32_CODE_LENGTH costs.

Source

pub fn try_from_bech32m_unchecked(s: &str) -> Result<Self, Bech32Error>

Available on crate feature encoding-bech32 only.

Decodes a Bech32m (BIP-350) string into Dynamic<Vec<u8>> without validating the HRP.

Use try_from_bech32m in security-critical code.

Source

pub fn try_from_bech32m_unchecked_sized<const C: usize>( s: &str, ) -> Result<Self, Bech32Error>

Available on crate feature encoding-bech32 only.

Like try_from_bech32m_unchecked, accepting strings up to C characters.

Source§

impl Dynamic<Vec<u8>>

Construction helpers and random generation for Dynamic<Vec<u8>>.

Source

pub fn new_with<F>(len: usize, f: F) -> Self
where F: FnOnce(&mut [u8]),

Writes into a sized, pre-zeroed slot inside the wrapper’s own storage.

The closure receives exactly len bytes, every one of them zero. That is a guarantee, not an implementation detail: callers rely on the tail being zero as real input — a 40-bit RC4 key is five bytes of derived material followed by eleven zeros, and those zeros are key-schedule input rather than padding, so a slot that merely happened to be zeroed would produce a different cipher the day it was not.

Two allocations, not one: the slot of exactly len bytes, plus the Box<Vec<u8>> header the bytes are swapped into on the way out. tests/lifecycle_trace_heap.rs measures from_random(len) — this constructor with an RNG in the closure — at exactly two, “the payload buffer and the Box<Vec<u8>> header”, and measures Dynamic::new(owned) at exactly one, since a move carries its buffer along. By allocation count this is therefore the joint most expensive constructor here, tied with From<&[u8]>; the count is not what recommends it. What does is the shape of the slot: capacity equals length, so the wrapper never holds slack, and the slot is a slice, so growth is not expressible and the reallocation hazard cannot arise.

§Changed in 0.9.0-rc.12 — this took no len before

If you are here from E0061: this function takes 2 arguments but 1 was supplied, this is why. The previous signature handed the closure a zero-capacity Vec<u8>, so filling it reallocated — and a Vec reallocation frees the old block without zeroizing it, abandoning a copy of the secret on the heap that nothing would ever wipe. A constructor whose entire purpose was to avoid an unprotected copy created one, silently, in the common case of two pushes.

Pass the final length and fill the slot:

use secure_gate::{Dynamic, RevealSecret};

// was: new_with(|v| { v.extend_from_slice(&material); v.resize(16, 0); })
let key = Dynamic::<Vec<u8>>::new_with(16, |slot| {
    slot[..4].copy_from_slice(&[1, 2, 3, 4]);
});
assert_eq!(key.expose_secret().len(), 16);
key.with_secret(|b| assert_eq!(&b[4..], &[0u8; 12]));
§When the length is not known in advance

Use Dynamic::new with an empty Vec and write through the std::io::Write impl, which grows by hand and zeroizes each abandoned buffer before releasing it — the one growth path in this crate that does. See the Write impl for what it does and does not cover. It requires the std feature.

§Panics

Allocating the slot is alloc::vec![0u8; len], so len is not a hint — it is an allocation request, and both allocator failure modes are reachable through it. A len above isize::MAX panics with "capacity overflow" before anything is allocated. A len the allocator cannot satisfy reaches handle_alloc_error, which aborts rather than unwinding: no destructor runs anywhere, so no Zeroizing on this stack or any frame above it gets the chance to wipe anything.

len is caller-supplied, and the worked example on the fallible sibling is try_new_with(len, |slot| r.read_exact(slot)) — precisely the shape where len arrives off the wire, out of a frame header or a manifest. Bound it before passing it here, for the same reason deserialize_with_limit on this type takes a ceiling instead of trusting the payload: a length field is attacker-controlled input until something checks it.

For some callers that bound is not a check that moves — it is a check that changes shape, and the difference decides whether there is any work to do. If your length previously flowed into a decompressor or a comparison further down, a ceiling enforced after the fact was enough: an overstated length failed a check and you returned an error. Here the length is the allocation, and alloc::vec![0u8; 9_000_000_000] does not fail a comparison — it asks the allocator for nine gigabytes before any of your code runs. A caller whose length comes from a trusted source has nothing to do. A caller whose length comes from the file format has acquired an allocation-sized surface where it previously had a comparison, and needs a bound it may never have written.

A panic inside f is safe: the slot lives in a zeroize::Zeroizing for the whole call, so partially written bytes are wiped during unwinding.

§See also
Source

pub fn try_new_with<F, E>(len: usize, f: F) -> Result<Self, E>
where F: FnOnce(&mut [u8]) -> Result<(), E>,

new_with for a fill that can fail.

Identical in every respect but the closure’s return type. On Err the partially written slot is zeroized before the error is returned — the Zeroizing holding it is dropped on the way out, so a failed fill leaves nothing behind.

That is not defensive tidiness. Decoders that write into a caller-supplied buffer routinely leave real plaintext in it when they fail; miniz_oxide’s decompress_slice_iter_to_slice documents exactly that (“in that case the output buffer will still contain the partial decompression”). Wrapping only on success would leave that partial secret in an unprotected buffer on every malformed input.

§Errors

Returns whatever the closure returns. The error type is the caller’s; this method neither inspects nor wraps it.

§A short fill is not an error, and its tail is the zero fill

The closure returns Result<(), E>, which carries no bytes-written count. A closure that writes fewer than len bytes and returns Ok therefore yields a full-length secret whose untouched tail is the pre-zeroing. Nothing here can catch that, because nothing here is ever told how far the fill got.

Whether that is the right answer or silent corruption depends on what is in the slot, and the two shapes differ completely:

  • Fixed-width key material — the 40-bit RC4 key from new_with: five bytes of derived material followed by eleven zeros that are key-schedule input. The short fill is the intended fill, and the zero guarantee is what makes it reproducible.
  • A variable-content payload — a decoded document, a decompressed record, a frame whose len came from a header that is only an upper bound or is simply wrong (a truncated archive, a manifest that disagrees with its contents). Here the zero tail is not input. It is missing plaintext in the shape of real data, and whatever parses the result downstream will read it as real.

For the second shape the closure has to check its own coverage and fail, since this method cannot do it: write through a SlotWriter and return Err unless is_full() holds at the end, or keep the count by hand and compare bytes_written == slot.len() before returning Ok. The read_exact example below does not demonstrate this and cannot be read as the pattern to copy — read_exact surfaces under-fill as Err by construction, so that closure gets the check for free and never writes one.

§Panics

The same two allocator failures as new_with, for the same reason: the body allocates the slot with the identical alloc::vec![0u8; len]. A len above isize::MAX panics with "capacity overflow"; a len the allocator cannot satisfy reaches handle_alloc_error, which aborts without unwinding, so no Zeroizing anywhere on the stack runs. The # Errors section above describes the closure’s failures only — an oversized len never reaches the closure and never arrives as Err, so a caller treating Result as the complete failure channel is wrong about this one. Bound len before passing it; the example below takes it straight from its caller, which in real code means straight from a frame header.

A panic inside f is safe, exactly as in new_with: the slot is held in a zeroize::Zeroizing for the whole call and is wiped during unwinding.

§Examples
use secure_gate::{Dynamic, RevealSecret};
use std::io::Read;

fn read_exact_secret<R: Read>(r: &mut R, len: usize) -> std::io::Result<Dynamic<Vec<u8>>> {
    Dynamic::<Vec<u8>>::try_new_with(len, |slot| r.read_exact(slot))
}

let secret = read_exact_secret(&mut [7u8; 32].as_slice(), 32).unwrap();
assert_eq!(secret.expose_secret().len(), 32);

// A short read fails, and yields no secret at all.
assert!(read_exact_secret(&mut [7u8; 8].as_slice(), 32).is_err());
Source§

impl Dynamic<Vec<u8>>

Source

pub fn from_random(len: usize) -> Self

Available on crate feature rand only.

Fills a new Vec<u8> with len cryptographically secure random bytes and wraps it.

Uses the system RNG (SysRng). Requires the rand feature (and alloc, which Dynamic<Vec<u8>> always needs).

§Panics

Panics if the system RNG fails to provide bytes (TryRng::try_fill_bytes returns Err). This is treated as a fatal environment error.

§Examples
use secure_gate::{Dynamic, RevealSecret, SecretLen};

let nonce: Dynamic<Vec<u8>> = Dynamic::from_random(24);
assert_eq!(nonce.len(), 24);
Source

pub fn from_rng<R: TryRng + TryCryptoRng>( len: usize, rng: &mut R, ) -> Result<Self, R::Error>

Available on crate feature rand only.

Allocates a Vec<u8> of length len, fills it from rng, and wraps it.

Accepts any TryCryptoRng + TryRng — for example, a seeded StdRng for deterministic tests. Requires the rand feature and alloc (implicit — Dynamic<T> itself requires it).

§Errors

Returns R::Error if try_fill_bytes fails.

§Examples
use rand::rngs::StdRng;
use rand::SeedableRng;
use secure_gate::Dynamic;

let mut rng = StdRng::from_seed([9u8; 32]);
let nonce: Dynamic<Vec<u8>> = Dynamic::from_rng(24, &mut rng).expect("rng fill");
Source§

impl Dynamic<Vec<u8>>

Source

pub fn as_reader(&self) -> DynamicReader<'_> ⓘ

Available on crate feature std only.

Returns a DynamicReader that implements std::io::Read.

This replaces the common with_secret + Cursor boilerplate:

use std::io;
use secure_gate::Dynamic;

let secret = Dynamic::<Vec<u8>>::new(vec![1, 2, 3, 4]);

// Before: awkward closure + Cursor dance
// secret.with_secret(|b| io::copy(&mut Cursor::new(b), &mut encryptor))?;

// After: pipe directly into an encrypted writer — no intermediate buffer
let mut encryptor = io::sink(); // stand-in for a real encryptor
io::copy(&mut secret.as_reader(), &mut encryptor).unwrap();
§Security

Each read() call copies secret bytes into the caller’s buffer. Prefer piping directly into encrypted writers rather than reading into intermediate buffers. The caller is responsible for zeroizing any destination buffer.

Source§

impl Dynamic<Vec<u8>>

Source

pub fn deserialize_with_limit<'de, D>( deserializer: D, limit: usize, ) -> Result<Self, D::Error>
where D: Deserializer<'de>,

Available on crate feature serde-deserialize only.

Deserializes into Dynamic<Vec<u8>>, rejecting payloads larger than limit bytes.

The standard serde::Deserialize impl calls this with MAX_DESERIALIZE_BYTES. Use this method directly when you need a tighter or looser ceiling.

Zeroization scope. Once the upstream deserializer returns a complete Vec<u8>, the value is wrapped in Zeroizing and stays protected for the rest of this function: oversized buffers are zeroized before the error is returned, and an OOM panic in the subsequent Box allocation triggers zeroization on unwind. However, this guarantee does not extend backwards into the deserializer itself. If the upstream Vec<u8> visitor accumulates bytes element-by-element (e.g., a JSON sequence) and fails partway through, the partial buffer is owned by the visitor and dropped as a plain Vec<u8> — not zeroized. In the typical untrusted-input threat model the partial bytes are attacker-controlled (the malformed payload they sent), so the practical disclosure surface is bounded; but if your threat model includes deserialization of trusted-but-corruptible secret material, treat the deserialize step as outside the zeroization boundary and build in-process instead: new_with(len, …) (or try_new_with for a fill that can fail) writes straight into the wrapper’s own pre-zeroed slot, so there is no separate buffer for a visitor to accumulate into and abandon unwiped.

Important: this limit is enforced after the upstream deserializer has fully materialized the payload. It is a result-length acceptance bound, not a pre-allocation DoS guard. For untrusted input, enforce size limits at the transport or parser layer upstream.

Source§

impl Dynamic<String>

Source

pub fn deserialize_with_limit<'de, D>( deserializer: D, limit: usize, ) -> Result<Self, D::Error>
where D: Deserializer<'de>,

Available on crate feature serde-deserialize only.

Deserializes into Dynamic<String>, rejecting payloads larger than limit bytes.

The standard serde::Deserialize impl calls this with MAX_DESERIALIZE_BYTES. Use this method directly when you need a tighter or looser ceiling.

Zeroization scope. Once the upstream deserializer returns a complete String, the value is wrapped in Zeroizing and stays protected for the rest of this function: oversized buffers are zeroized before the error is returned, and an OOM panic in the subsequent Box allocation triggers zeroization on unwind. However, this guarantee does not extend backwards into the deserializer itself. If the upstream String visitor accumulates characters and fails partway through (e.g., on an invalid UTF-8 boundary), the partial buffer is owned by the visitor and dropped as a plain String — not zeroized. In the typical untrusted-input threat model the partial bytes are attacker-controlled (the malformed payload they sent), so the practical disclosure surface is bounded; but if your threat model includes deserialization of trusted-but-corruptible secret material, treat the deserialize step as outside the zeroization boundary and build in-process instead. String has no sized-slot constructor of its own — it must hold valid UTF-8 and characters are variable-width, so there is no fixed byte window arbitrary text can be written into. Size a String to its exact final length yourself (String::with_capacity, filled without triggering a reallocation) and move it in with Dynamic::new, which moves the buffer rather than copying it.

Important: this limit is enforced after the upstream deserializer has fully materialized the payload. It is a result-length acceptance bound, not a pre-allocation DoS guard. For untrusted input, enforce size limits at the transport or parser layer upstream.

Trait Implementations§

Source§

impl<T: Zeroize + CloneableSecret> Clone for Dynamic<T>

Available on crate feature cloneable only.

Opt-in cloning — requires cloneable feature and CloneableSecret marker. Each clone is independently zeroized on drop, but cloning increases exposure surface.

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl<T> ConstantTimeEq for Dynamic<T>
where T: ConstantTimeEq + ?Sized + Zeroize, Self: RevealSecret<Inner = T>,

Available on crate feature ct-eq only.

Constant-time equality for Dynamic<T> — routes through expose_secret().

== is deliberately not implemented. Always use ct_eq.

Source§

fn ct_eq(&self, other: &Self) -> bool

Performs equality comparison in constant time. Read more
Source§

impl<T: ?Sized + Zeroize> Debug for Dynamic<T>

Always prints [REDACTED] — secrets never appear in debug output.

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<'de> Deserialize<'de> for Dynamic<Vec<u8>>

Available on crate feature serde-deserialize only.
Source§

fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl<'de> Deserialize<'de> for Dynamic<String>

Available on crate feature serde-deserialize only.
Source§

fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl<T: ?Sized + Zeroize> Drop for Dynamic<T>

Unconditionally zeroizes the inner value when the wrapper is dropped.

Warning: Drop does not run under panic = "abort".

Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more
Source§

impl From<&[u8]> for Dynamic<Vec<u8>>

Copies a byte slice to the heap and wraps the copy. The source slice is left exactly as it was — this impl protects the copy it makes, not the memory it copied from, and leaving that memory unwiped is the caller’s problem to solve.

tests/lifecycle_trace_heap.rs::check_str_and_slice_conversions_copy_then_wipe measures both halves of that: converting &[u8] costs two allocations (the fresh Vec<u8> buffer plus the Box<Vec<u8>> header, against one for a move) and the source slice’s bytes are unchanged after the wrapper is built and dropped. If the bytes are already in an owned Vec<u8> you control, Dynamic::new moves it in for one allocation and nothing of yours left behind to wipe.

Source§

fn from(slice: &[u8]) -> Self

Converts to this type from the input type.
Source§

impl From<&str> for Dynamic<String>

Copies a string to the heap and wraps the copy, leaving the source &str exactly as it was — measured the same way as the &[u8] conversion above, by tests/lifecycle_trace_heap.rs::check_str_and_slice_conversions_copy_then_wipe: two allocations (the copied String buffer plus the Box<String> header, against one for a move) and a caller-side buffer whose bytes are provably unchanged once the wrapper is built and dropped.

This is the impl a caller now reaches for after Dynamic::<String>::new_with was removed outright in 0.9.0-rc.12 — an &str is what’s already on hand, so From<&str> reads as the drop-in replacement. It is the worse one: whatever plaintext the &str borrows from — a literal, an owned String, a line read off stdin — is untouched by this call and stays live and unprotected for as long as its own scope does. If you own the String, size it once — String::with_capacity at the exact final length — and move it in with Dynamic::new, which check_string_new_moves_callers_buffer measures as exactly one allocation (the Box<String> header) with the secret staying at the caller’s original buffer address.

Source§

fn from(input: &str) -> Self

Converts to this type from the input type.
Source§

impl<T: ?Sized + Zeroize> From<Box<T>> for Dynamic<T>

Moves an already-boxed value in — zero-copy, no allocation at all. The box the caller already owns becomes the box the wrapper owns, so whatever buffer sits behind T never moves and never gets a second copy taken of it.

“Already boxed” describes the shape of the value at the moment it arrives here, not how it was built. A Vec<u8> pushed onto in a loop and later turned into a Box<[u8]>, or a String grown with push_str before being boxed, already paid for every one of those reallocations before this impl runs — and each reallocation frees the old block without zeroizing it, the same mechanism tests/lifecycle_trace_heap.rs::check_growth_orphan_retains_secret_vec (and its _string sibling) measures happening to a value already inside the wrapper. Nothing this impl does can reach backward and clean that up; it only ever protects the buffer it ends up holding. Build without growth — with_capacity at the exact final size, or one resize — before boxing, then move the box in.

Source§

fn from(boxed: Box<T>) -> Self

Converts to this type from the input type.
Source§

impl<T: 'static + Zeroize> From<T> for Dynamic<T>

Moves an owned value onto the heap by boxing it — the impl behind Dynamic::new’s “moves the buffer in, no copy” guarantee for Vec<u8>, String, and every other Sized T. Box::new(value) only relocates value’s own stack representation (for a Vec<u8>, the pointer/length/capacity triple); the heap buffer that pointer already points at is never touched, so it lands inside the wrapper at the address it already had.

That is necessary for “no copy” to hold, but not sufficient for it to matter: if value was grown to build it — pushed onto, extended, push_str’d past its original capacity — each of those reallocations already ran before this impl ever sees the value, and each one freed a heap block holding the secret, unwiped, somewhere this wrapper will never look. check_growth_orphan_retains_secret_vec / _string in tests/lifecycle_trace_heap.rs measure exactly that allocator behavior for growth happening after the value is wrapped; the same allocator runs the same way one step earlier when the growth happens during construction instead. check_string_new_moves_callers_buffer measures the guarantee this impl does deliver when construction is growth-free: one allocation (the Box header) and the secret staying at the caller’s original buffer address. Build without growth — with_capacity at the exact final size, or one resize — then move the value in.

Source§

fn from(value: T) -> Self

Converts to this type from the input type.
Source§

impl<T: ?Sized + Zeroize> RevealSecret for Dynamic<T>

Source§

fn into_inner(self) -> T
where Self: Sized, Self::Inner: Sized + SentinelValue + Zeroize,

Consumes self and transfers ownership of the plain inner value.

Allocation note: allocates one small Box<T> sentinel (24 bytes for String/Vec on 64-bit) before the swap. If that allocation panics (OOM), self.inner is unchanged and Dynamic::drop zeroizes the real secret during unwind — confidentiality is preserved. This is the same OOM-safety pattern used by from_protected_bytes and deserialize_with_limit.

See RevealSecret::into_inner for the full contract. Protection ends at this call: the returned value is an ordinary String / Vec<T>, moved out without copying.

Source§

type Inner = T

The inner secret type being revealed. Read more
Source§

fn with_secret<F, R>(&self, f: F) -> R
where F: FnOnce(&T) -> R,

Provides scoped (recommended) read-only access to the secret. Read more
Source§

fn expose_secret(&self) -> &T

Returns a direct (auditable) read-only reference to the secret. Read more
Source§

impl<T: ?Sized + Zeroize> RevealSecretMut for Dynamic<T>

Source§

fn with_secret_mut<F, R>(&mut self, f: F) -> R
where F: FnOnce(&mut T) -> R,

Provides scoped (recommended) mutable access to the secret. Read more
Source§

fn expose_secret_mut(&mut self) -> &mut T

Returns a direct (auditable) mutable reference to the secret. Read more
Source§

impl SecretLen for Dynamic<String>

Source§

fn len(&self) -> usize

Returns the number of elements in the secret, matching the underlying container’s len() — element count for Vec<T> and [T; N], byte count for String (where the element is a byte).
Source§

fn byte_len(&self) -> usize

Returns the total size of the secret in bytes. Read more
Source§

fn is_empty(&self) -> bool

Returns true if the secret is empty.
Source§

impl<T: Zeroize> SecretLen for Dynamic<Vec<T>>

Source§

fn len(&self) -> usize

Returns the number of elements in the secret, matching the underlying container’s len() — element count for Vec<T> and [T; N], byte count for String (where the element is a byte).
Source§

fn byte_len(&self) -> usize

Returns the total size of the secret in bytes. Read more
Source§

fn is_empty(&self) -> bool

Returns true if the secret is empty.
Source§

impl<T: Zeroize + SerializableSecret> Serialize for Dynamic<T>

Available on crate feature serde-serialize only.

Opt-in serialization — requires serde-serialize feature and SerializableSecret marker. Serialization exposes the full secret — audit every impl.

Source§

fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer,

Serialize this value into the given Serde serializer. Read more
Source§

impl ToBase32 for Dynamic<Vec<u8>>

Available on crate feature encoding-base32 only.

Base32 encoding for Dynamic<Vec<u8>>; delegates via with_secret.

Bring the trait into scope: use secure_gate::ToBase32;.

use secure_gate::{Dynamic, ToBase32};

let token: Dynamic<Vec<u8>> = Dynamic::from(&[0xABu8; 4][..]);
assert_eq!(&*token.to_base32(), "VOV2XKY");
Source§

fn to_base32(&self) -> EncodedSecret

Encode bytes as Base32 (RFC 4648 §6 alphabet, uppercase, no padding).
Source§

impl ToBase64Url for Dynamic<Vec<u8>>

Available on crate feature encoding-base64 only.

Base64url encoding for Dynamic<Vec<u8>>; delegates via with_secret.

Bring the trait into scope: use secure_gate::ToBase64Url;.

use secure_gate::{Dynamic, ToBase64Url};

let token: Dynamic<Vec<u8>> = Dynamic::from(&[0xABu8; 4][..]);
assert_eq!(&*token.to_base64url(), "q6urqw");
Source§

fn to_base64url(&self) -> EncodedSecret

Encode bytes as URL-safe base64 (no padding).
Source§

impl ToBech32 for Dynamic<Vec<u8>>

Available on crate feature encoding-bech32 only.

Bech32 encoding for Dynamic<Vec<u8>>; delegates via with_secret.

Bring the trait into scope: use secure_gate::ToBech32;.

Source§

fn try_to_bech32( &self, hrp: &str, case: Case, ) -> Result<EncodedSecret, Bech32Error>

Fallibly encodes bytes as a Bech32 (BIP-173) string with the given HRP. Read more
Source§

fn try_to_bech32_sized<const C: usize>( &self, hrp: &str, case: Case, ) -> Result<EncodedSecret, Bech32Error>

Like try_to_bech32, with a caller-chosen code length N. Read more
Source§

impl ToBech32m for Dynamic<Vec<u8>>

Available on crate feature encoding-bech32 only.

Bech32m encoding for Dynamic<Vec<u8>>; delegates via with_secret.

Bring the trait into scope: use secure_gate::ToBech32m;.

Source§

fn try_to_bech32m( &self, hrp: &str, case: Case, ) -> Result<EncodedSecret, Bech32Error>

Fallibly encodes bytes as a Bech32m (BIP-350) string with the given HRP. Read more
Source§

fn try_to_bech32m_sized<const C: usize>( &self, hrp: &str, case: Case, ) -> Result<EncodedSecret, Bech32Error>

Like try_to_bech32m, with a caller-chosen code length N. Read more
Source§

impl ToHex for Dynamic<Vec<u8>>

Available on crate feature encoding-hex only.

Hex encoding for Dynamic<Vec<u8>>; delegates via with_secret.

Deliberately not implemented for Dynamic<String> — hex-encoding textual secrets is a design smell; convert explicitly inside with_secret if genuinely needed. Bring the trait into scope: use secure_gate::ToHex;.

use secure_gate::{Dynamic, ToHex};

let token: Dynamic<Vec<u8>> = Dynamic::from(&[0xDEu8, 0xAD][..]);
assert_eq!(&*token.to_hex(), "dead");
Source§

fn to_hex(&self) -> EncodedSecret

Encode bytes as lowercase hexadecimal.
Source§

fn to_hex_upper(&self) -> EncodedSecret

Encode bytes as uppercase hexadecimal.
Source§

impl Write for Dynamic<Vec<u8>>

Available on crate feature std only.

Streams bytes directly into the protected buffer via RevealSecretMut.

Data flows into the wrapper, so no intermediate unprotected Vec<u8> is accumulated before wrapping.

§Growth wipes the outgoing buffer

Writing past the current capacity cannot simply delegate to Vec: Vec would reallocate, copy the secret into the new allocation, and hand the old one back to the allocator with the plaintext still in it. This impl grows by hand instead — it allocates the larger buffer, copies, zeroizes the old buffer (contents and spare capacity), and only then releases it.

Pre-sizing with Vec::with_capacity avoids the copy altogether and is worth doing when the length is known up front:

use std::io::Write;
use secure_gate::Dynamic;

let payload = b"decrypted payload";

// Pre-sized: no growth, so no copy and nothing to wipe.
let mut secret = Dynamic::<Vec<u8>>::new(Vec::with_capacity(payload.len()));
secret.write_all(payload).unwrap();

§This is the sanctioned path for a secret whose length you do not know

Every buffer this impl abandons is zeroized before it is released. Growth is done by hand — allocate, copy, wipe the old buffer including its spare capacity, and only then drop it — rather than through Vec’s own reallocation, which would free the old block still holding plaintext. Doubling is amortized, so streaming stays linear.

That makes the pair complete: length known before the fill, use new_with(len, …); length not known, use Write. Neither leaves an unwiped copy behind, and there is no third case that needs one.

§What it does not cover

The guarantee is about the buffer this impl owns, and three things sit outside it:

  • A caller who grows the Vec themselves, through with_secret_mut or expose_secret_mut, holds &mut Vec<u8> directly and their reallocation is Vec’s, not this one’s. See the heap-reallocation residue section in SECURITY.md.
  • Whatever fed the bytes in. Driving this with std::io::copy passes every byte through copy’s own transfer buffer, and std::io::Stdin keeps a process-lifetime internal buffer. Nothing here can reach either. To close the transfer buffer too, read into a Fixed<[u8; N]> chunk and write_all each chunk through this impl.
  • The std feature, which gates this impl. Without it the recommended path for unknown-length secrets is simply absent, and the growable alternatives are not.

What it eliminates is the unbounded chain of unwiped reallocations — the residue that scales with input size, leaving an intermediate copy on the heap for every growth. That is the defect; the buffers above are bounded and do not accumulate.

Source§

fn write(&mut self, buf: &[u8]) -> Result<usize>

Writes a buffer into this writer, returning how many bytes were written. Read more
Source§

fn flush(&mut self) -> Result<()>

Flushes this output stream, ensuring that all intermediately buffered contents reach their destination. Read more
1.36.0 · Source§

fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> Result<usize, Error>

Like write, except that it writes from a slice of buffers. Read more
Source§

fn is_write_vectored(&self) -> bool

🔬This is a nightly-only experimental API. (can_vector)
Determines if this Writer has an efficient write_vectored implementation. Read more
1.0.0 · Source§

fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>

Attempts to write an entire buffer into this writer. Read more
Source§

fn write_all_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> Result<(), Error>

🔬This is a nightly-only experimental API. (write_all_vectored)
Attempts to write multiple buffers into this writer. Read more
1.0.0 · Source§

fn write_fmt(&mut self, args: Arguments<'_>) -> Result<(), Error>

Writes a formatted string into this writer, returning any error encountered. Read more
1.0.0 · Source§

fn by_ref(&mut self) -> &mut Self
where Self: Sized,

Creates a “by reference” adapter for this instance of Write. Read more
Source§

impl<T: ?Sized + Zeroize> Zeroize for Dynamic<T>

Zeroizes the inner value (including Vec/String spare capacity).

Warning: does not run under panic = "abort".

Source§

fn zeroize(&mut self)

Zero out this object from memory using Rust intrinsics which ensure the zeroization operation is not “optimized away” by the compiler.
Source§

impl<T: ?Sized + Zeroize> ZeroizeOnDrop for Dynamic<T>

Marker confirming that Dynamic<T> always zeroizes on drop.

Auto Trait Implementations§

§

impl<T> Freeze for Dynamic<T>
where Box<T>: Freeze, T: ?Sized,

§

impl<T> RefUnwindSafe for Dynamic<T>
where Box<T>: RefUnwindSafe, T: ?Sized,

§

impl<T> Send for Dynamic<T>
where Box<T>: Send, T: ?Sized,

§

impl<T> Sync for Dynamic<T>
where Box<T>: Sync, T: ?Sized,

§

impl<T> Unpin for Dynamic<T>
where Box<T>: Unpin, T: ?Sized,

§

impl<T> UnsafeUnpin for Dynamic<T>
where Box<T>: UnsafeUnpin, T: ?Sized,

§

impl<T> UnwindSafe for Dynamic<T>
where Box<T>: UnwindSafe, T: ?Sized,

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> RevealScoped for T
where T: RevealSecret + ?Sized,

Source§

type Inner = <T as RevealSecret>::Inner

Available on crate feature sealed only.
The secret’s type as the closure sees it.
Source§

fn with_secret<F, R>(&self, f: F) -> R
where F: FnOnce(&<T as RevealScoped>::Inner) -> R,

Available on crate feature sealed only.
Calls f with a reference to the secret that cannot outlive the call.
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.