pub struct Dynamic<T: ?Sized + Zeroize> { /* private fields */ }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>>
| Constructor | Feature | Notes |
|---|---|---|
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-hex | Constant-time hex decoding |
try_from_base32(s) | encoding-base32 | Constant-time Base32 decoding |
try_from_base64url(s) | encoding-base64 | Constant-time Base64url decoding |
try_from_bech32(s, hrp) | encoding-bech32 | HRP-validated Bech32 |
try_from_bech32_unchecked(s) | encoding-bech32 | Bech32 without HRP check |
try_from_bech32m(s, hrp) | encoding-bech32 | HRP-validated Bech32m |
try_from_bech32m_unchecked(s) | encoding-bech32 | Bech32m without HRP check |
from_random(len) | rand | System RNG |
from_rng(len, rng) | rand | Custom RNG |
§See also
RevealSecret/RevealSecretMut— the 3-tier access traits.Fixed<T>— stack-allocated alternative.
Implementations§
Source§impl<T: ?Sized + Zeroize> Dynamic<T>
impl<T: ?Sized + Zeroize> Dynamic<T>
Sourcepub fn new<U>(value: U) -> Self
pub fn new<U>(value: U) -> Self
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>>
impl Dynamic<Vec<u8>>
Sourcepub fn try_from_hex(s: &str) -> Result<Self, HexError>
Available on crate feature encoding-hex only.
pub fn try_from_hex(s: &str) -> Result<Self, HexError>
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>>
impl Dynamic<Vec<u8>>
Sourcepub fn try_from_base32(s: &str) -> Result<Self, Base32Error>
Available on crate feature encoding-base32 only.
pub fn try_from_base32(s: &str) -> Result<Self, Base32Error>
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>>
impl Dynamic<Vec<u8>>
Sourcepub fn try_from_base64url(s: &str) -> Result<Self, Base64Error>
Available on crate feature encoding-base64 only.
pub fn try_from_base64url(s: &str) -> Result<Self, Base64Error>
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>>
impl Dynamic<Vec<u8>>
Sourcepub fn try_from_bech32(s: &str, expected_hrp: &str) -> Result<Self, Bech32Error>
Available on crate feature encoding-bech32 only.
pub fn try_from_bech32(s: &str, expected_hrp: &str) -> Result<Self, Bech32Error>
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.
Sourcepub fn try_from_bech32_sized<const C: usize>(
s: &str,
expected_hrp: &str,
) -> Result<Self, Bech32Error>
Available on crate feature encoding-bech32 only.
pub fn try_from_bech32_sized<const C: usize>( s: &str, expected_hrp: &str, ) -> Result<Self, Bech32Error>
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.
Sourcepub fn try_from_bech32_unchecked(s: &str) -> Result<Self, Bech32Error>
Available on crate feature encoding-bech32 only.
pub fn try_from_bech32_unchecked(s: &str) -> Result<Self, Bech32Error>
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.
Sourcepub fn try_from_bech32_unchecked_sized<const C: usize>(
s: &str,
) -> Result<Self, Bech32Error>
Available on crate feature encoding-bech32 only.
pub fn try_from_bech32_unchecked_sized<const C: usize>( s: &str, ) -> Result<Self, Bech32Error>
encoding-bech32 only.Like try_from_bech32_unchecked, accepting
strings up to C characters.
Source§impl Dynamic<Vec<u8>>
impl Dynamic<Vec<u8>>
Sourcepub fn try_from_bech32m(
s: &str,
expected_hrp: &str,
) -> Result<Self, Bech32Error>
Available on crate feature encoding-bech32 only.
pub fn try_from_bech32m( s: &str, expected_hrp: &str, ) -> Result<Self, Bech32Error>
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.
Sourcepub fn try_from_bech32m_sized<const C: usize>(
s: &str,
expected_hrp: &str,
) -> Result<Self, Bech32Error>
Available on crate feature encoding-bech32 only.
pub fn try_from_bech32m_sized<const C: usize>( s: &str, expected_hrp: &str, ) -> Result<Self, Bech32Error>
encoding-bech32 only.Like try_from_bech32m, accepting strings up to C
characters. See Bech32mSized for what C above
BECH32_CODE_LENGTH costs.
Sourcepub fn try_from_bech32m_unchecked(s: &str) -> Result<Self, Bech32Error>
Available on crate feature encoding-bech32 only.
pub fn try_from_bech32m_unchecked(s: &str) -> Result<Self, Bech32Error>
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.
Sourcepub fn try_from_bech32m_unchecked_sized<const C: usize>(
s: &str,
) -> Result<Self, Bech32Error>
Available on crate feature encoding-bech32 only.
pub fn try_from_bech32m_unchecked_sized<const C: usize>( s: &str, ) -> Result<Self, Bech32Error>
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>>.
impl Dynamic<Vec<u8>>
Construction helpers and random generation for Dynamic<Vec<u8>>.
Sourcepub fn new_with<F>(len: usize, f: F) -> Self
pub fn new_with<F>(len: usize, f: F) -> Self
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
try_new_with— when the fill can fail.SlotWriter— append to the slot without writing offsets by hand.
Sourcepub fn try_new_with<F, E>(len: usize, f: F) -> Result<Self, E>
pub fn try_new_with<F, E>(len: usize, f: F) -> Result<Self, 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
lencame 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>>
impl Dynamic<Vec<u8>>
Sourcepub fn from_random(len: usize) -> Self
Available on crate feature rand only.
pub fn from_random(len: usize) -> Self
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);Sourcepub fn from_rng<R: TryRng + TryCryptoRng>(
len: usize,
rng: &mut R,
) -> Result<Self, R::Error>
Available on crate feature rand only.
pub fn from_rng<R: TryRng + TryCryptoRng>( len: usize, rng: &mut R, ) -> Result<Self, R::Error>
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>>
impl Dynamic<Vec<u8>>
Sourcepub fn as_reader(&self) -> DynamicReader<'_> ⓘ
Available on crate feature std only.
pub fn as_reader(&self) -> DynamicReader<'_> ⓘ
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>>
impl Dynamic<Vec<u8>>
Sourcepub 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.
pub fn deserialize_with_limit<'de, D>(
deserializer: D,
limit: usize,
) -> Result<Self, D::Error>where
D: Deserializer<'de>,
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>
impl Dynamic<String>
Sourcepub 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.
pub fn deserialize_with_limit<'de, D>(
deserializer: D,
limit: usize,
) -> Result<Self, D::Error>where
D: Deserializer<'de>,
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.
impl<T: Zeroize + CloneableSecret> Clone for Dynamic<T>
cloneable only.Opt-in cloning — requires cloneable feature and CloneableSecret
marker. Each clone is independently zeroized on drop, but cloning increases exposure surface.
Source§impl<T> ConstantTimeEq for Dynamic<T>
Available on crate feature ct-eq only.Constant-time equality for Dynamic<T> — routes through expose_secret().
impl<T> ConstantTimeEq for Dynamic<T>
ct-eq only.Constant-time equality for Dynamic<T> — routes through expose_secret().
== is deliberately not implemented. Always use ct_eq.
Source§impl<T: ?Sized + Zeroize> Debug for Dynamic<T>
Always prints [REDACTED] — secrets never appear in debug output.
impl<T: ?Sized + Zeroize> Debug for Dynamic<T>
Always prints [REDACTED] — secrets never appear in debug output.
Source§impl<'de> Deserialize<'de> for Dynamic<Vec<u8>>
Available on crate feature serde-deserialize only.
impl<'de> Deserialize<'de> for Dynamic<Vec<u8>>
serde-deserialize only.Source§fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>where
D: Deserializer<'de>,
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>where
D: Deserializer<'de>,
Source§impl<'de> Deserialize<'de> for Dynamic<String>
Available on crate feature serde-deserialize only.
impl<'de> Deserialize<'de> for Dynamic<String>
serde-deserialize only.Source§fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>where
D: Deserializer<'de>,
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>where
D: Deserializer<'de>,
Source§impl<T: ?Sized + Zeroize> Drop for Dynamic<T>
Unconditionally zeroizes the inner value when the wrapper is dropped.
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§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.
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§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.
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§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.
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§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.
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§impl<T: ?Sized + Zeroize> RevealSecret for Dynamic<T>
impl<T: ?Sized + Zeroize> RevealSecret for Dynamic<T>
Source§fn into_inner(self) -> T
fn into_inner(self) -> T
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§fn with_secret<F, R>(&self, f: F) -> R
fn with_secret<F, R>(&self, f: F) -> R
Source§fn expose_secret(&self) -> &T
fn expose_secret(&self) -> &T
Source§impl SecretLen for Dynamic<String>
impl SecretLen for Dynamic<String>
Source§impl<T: Zeroize> SecretLen for Dynamic<Vec<T>>
impl<T: Zeroize> SecretLen for Dynamic<Vec<T>>
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.
impl<T: Zeroize + SerializableSecret> Serialize for Dynamic<T>
serde-serialize only.Opt-in serialization — requires serde-serialize feature and
SerializableSecret marker. Serialization exposes the
full secret — audit every impl.
Source§impl ToBase32 for Dynamic<Vec<u8>>
Available on crate feature encoding-base32 only.Base32 encoding for Dynamic<Vec<u8>>; delegates via with_secret.
impl ToBase32 for Dynamic<Vec<u8>>
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
fn to_base32(&self) -> EncodedSecret
Source§impl ToBase64Url for Dynamic<Vec<u8>>
Available on crate feature encoding-base64 only.Base64url encoding for Dynamic<Vec<u8>>; delegates via with_secret.
impl ToBase64Url for Dynamic<Vec<u8>>
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
fn to_base64url(&self) -> EncodedSecret
Source§impl ToBech32 for Dynamic<Vec<u8>>
Available on crate feature encoding-bech32 only.Bech32 encoding for Dynamic<Vec<u8>>; delegates via with_secret.
impl ToBech32 for Dynamic<Vec<u8>>
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>
fn try_to_bech32( &self, hrp: &str, case: Case, ) -> Result<EncodedSecret, Bech32Error>
Source§fn try_to_bech32_sized<const C: usize>(
&self,
hrp: &str,
case: Case,
) -> Result<EncodedSecret, Bech32Error>
fn try_to_bech32_sized<const C: usize>( &self, hrp: &str, case: Case, ) -> Result<EncodedSecret, Bech32Error>
Source§impl ToBech32m for Dynamic<Vec<u8>>
Available on crate feature encoding-bech32 only.Bech32m encoding for Dynamic<Vec<u8>>; delegates via with_secret.
impl ToBech32m for Dynamic<Vec<u8>>
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>
fn try_to_bech32m( &self, hrp: &str, case: Case, ) -> Result<EncodedSecret, Bech32Error>
Source§fn try_to_bech32m_sized<const C: usize>(
&self,
hrp: &str,
case: Case,
) -> Result<EncodedSecret, Bech32Error>
fn try_to_bech32m_sized<const C: usize>( &self, hrp: &str, case: Case, ) -> Result<EncodedSecret, Bech32Error>
Source§impl ToHex for Dynamic<Vec<u8>>
Available on crate feature encoding-hex only.Hex encoding for Dynamic<Vec<u8>>; delegates via with_secret.
impl ToHex for Dynamic<Vec<u8>>
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
fn to_hex(&self) -> EncodedSecret
Source§fn to_hex_upper(&self) -> EncodedSecret
fn to_hex_upper(&self) -> EncodedSecret
Source§impl Write for Dynamic<Vec<u8>>
Available on crate feature std only.Streams bytes directly into the protected buffer via RevealSecretMut.
impl Write for Dynamic<Vec<u8>>
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
Vecthemselves, throughwith_secret_mutorexpose_secret_mut, holds&mut Vec<u8>directly and their reallocation isVec’s, not this one’s. See the heap-reallocation residue section inSECURITY.md. - Whatever fed the bytes in. Driving this with
std::io::copypasses every byte throughcopy’s own transfer buffer, andstd::io::Stdinkeeps a process-lifetime internal buffer. Nothing here can reach either. To close the transfer buffer too, read into aFixed<[u8; N]>chunk andwrite_alleach chunk through this impl. - The
stdfeature, 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>
fn write(&mut self, buf: &[u8]) -> Result<usize>
Source§fn flush(&mut self) -> Result<()>
fn flush(&mut self) -> Result<()>
Source§fn is_write_vectored(&self) -> bool
fn is_write_vectored(&self) -> bool
can_vector)1.0.0 · Source§fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>
fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>
Source§fn write_all_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> Result<(), Error>
fn write_all_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> Result<(), Error>
write_all_vectored)Source§impl<T: ?Sized + Zeroize> Zeroize for Dynamic<T>
Zeroizes the inner value (including Vec/String spare capacity).
impl<T: ?Sized + Zeroize> Zeroize for Dynamic<T>
Zeroizes the inner value (including Vec/String spare capacity).
Warning: does not run under panic = "abort".
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>
impl<T> RefUnwindSafe for Dynamic<T>
impl<T> Send for Dynamic<T>
impl<T> Sync for Dynamic<T>
impl<T> Unpin for Dynamic<T>
impl<T> UnsafeUnpin for Dynamic<T>
impl<T> UnwindSafe for Dynamic<T>
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> DeserializeOwned for Twhere
T: for<'de> Deserialize<'de>,
Source§impl<T> RevealScoped for Twhere
T: RevealSecret + ?Sized,
impl<T> RevealScoped for Twhere
T: RevealSecret + ?Sized,
Source§type Inner = <T as RevealSecret>::Inner
type Inner = <T as RevealSecret>::Inner
sealed only.Source§fn with_secret<F, R>(&self, f: F) -> R
fn with_secret<F, R>(&self, f: F) -> R
sealed only.f with a reference to the secret that cannot outlive the call.