secure_gate/dynamic.rs
1// ==========================================================================
2// src/dynamic.rs
3// ==========================================================================
4
5extern crate alloc;
6
7use alloc::boxed::Box;
8
9/// Heap-allocated secure secret wrapper.
10///
11/// This is a thin wrapper around `Box<T>` with enforced explicit exposure.
12/// Suitable for dynamic-sized secrets like `String` or `Vec<u8>`.
13///
14/// Security invariants:
15/// - No `Deref` or `AsRef` — prevents silent access.
16/// - `Debug` is always redacted.
17/// - With `zeroize`, wipes the entire allocation on drop (including spare capacity).
18///
19/// # Examples
20///
21/// Basic usage:
22/// ```
23/// use secure_gate::Dynamic;
24/// let secret: Dynamic<String> = "hunter2".into();
25/// assert_eq!(secret.expose_secret(), "hunter2");
26/// ```
27///
28/// Mutable access:
29/// ```
30/// use secure_gate::Dynamic;
31/// let mut secret = Dynamic::<String>::new("pass".to_string());
32/// secret.expose_secret_mut().push('!');
33/// assert_eq!(secret.expose_secret(), "pass!");
34/// ```
35///
36/// With `zeroize` (automatic wipe):
37/// ```
38/// # #[cfg(feature = "zeroize")]
39/// # {
40/// use secure_gate::Dynamic;
41/// let secret = Dynamic::<Vec<u8>>::new(vec![1u8; 32]);
42/// drop(secret); // heap wiped automatically
43/// # }
44/// ```
45pub struct Dynamic<T: ?Sized>(Box<T>);
46
47impl<T: ?Sized> Dynamic<T> {
48 /// Wrap an already-boxed value.
49 ///
50 /// Zero-cost — just wraps the `Box`.
51 #[inline(always)]
52 pub fn new_boxed(value: Box<T>) -> Self {
53 Dynamic(value)
54 }
55
56 /// Wrap a value by boxing it.
57 ///
58 /// Uses `Into<Box<T>>` for flexibility.
59 #[inline(always)]
60 pub fn new<U>(value: U) -> Self
61 where
62 U: Into<Box<T>>,
63 {
64 Dynamic(value.into())
65 }
66
67 /// Expose the inner value for read-only access.
68 ///
69 /// This is the **only** way to read the secret — loud and auditable.
70 #[inline(always)]
71 pub const fn expose_secret(&self) -> &T {
72 &self.0
73 }
74
75 /// Expose the inner value for mutable access.
76 ///
77 /// This is the **only** way to mutate the secret — loud and auditable.
78 #[inline(always)]
79 pub fn expose_secret_mut(&mut self) -> &mut T {
80 &mut self.0
81 }
82
83
84 /// Convert to a non-cloneable variant.
85 ///
86 /// Prevents accidental cloning of the secret.
87 ///
88 /// # Example
89 ///
90 /// ```
91 /// use secure_gate::{Dynamic, DynamicNoClone};
92 /// let secret = Dynamic::<String>::new("no copy".to_string());
93 /// let no_clone: DynamicNoClone<String> = secret.no_clone();
94 /// assert_eq!(no_clone.expose_secret(), "no copy");
95 /// ```
96 #[inline(always)]
97 pub fn no_clone(self) -> crate::DynamicNoClone<T> {
98 crate::DynamicNoClone::new(self.0)
99 }
100}
101
102// Explicit zeroization — only available with `zeroize` feature
103#[cfg(feature = "zeroize")]
104impl<T: ?Sized + zeroize::Zeroize> Dynamic<T> {
105 /// Explicitly zeroize the secret immediately.
106 ///
107 /// This is useful when you want to wipe memory before the value goes out of scope,
108 /// or when you want to make the zeroization intent explicit in the code.
109 ///
110 /// # Example
111 ///
112 /// ```
113 /// # #[cfg(feature = "zeroize")]
114 /// # {
115 /// use secure_gate::Dynamic;
116 /// let mut password = Dynamic::<String>::new("secret".to_string());
117 /// // ... use password ...
118 /// password.zeroize_now(); // Explicit wipe - makes intent clear
119 /// # }
120 /// ```
121 #[inline]
122 pub fn zeroize_now(&mut self) {
123 self.0.zeroize();
124 }
125}
126
127impl<T: ?Sized> core::fmt::Debug for Dynamic<T> {
128 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
129 f.write_str("[REDACTED]")
130 }
131}
132
133// Clone impls — gated correctly
134#[cfg(not(feature = "zeroize"))]
135impl<T: Clone> Clone for Dynamic<T> {
136 #[inline(always)]
137 fn clone(&self) -> Self {
138 Dynamic(self.0.clone())
139 }
140}
141
142#[cfg(feature = "zeroize")]
143impl<T: Clone + zeroize::Zeroize> Clone for Dynamic<T> {
144 #[inline(always)]
145 fn clone(&self) -> Self {
146 Dynamic(self.0.clone())
147 }
148}
149
150// === Ergonomic helpers for common heap types ===
151impl Dynamic<String> {
152 #[inline(always)]
153 pub const fn len(&self) -> usize {
154 self.0.len()
155 }
156
157 #[inline(always)]
158 pub const fn is_empty(&self) -> bool {
159 self.0.is_empty()
160 }
161}
162
163impl<T> Dynamic<Vec<T>> {
164 #[inline(always)]
165 pub const fn len(&self) -> usize {
166 self.0.len()
167 }
168
169 #[inline(always)]
170 pub const fn is_empty(&self) -> bool {
171 self.0.is_empty()
172 }
173}
174
175// === Convenient From impls ===
176impl<T> From<T> for Dynamic<T> {
177 #[inline(always)]
178 fn from(value: T) -> Self {
179 Self(Box::new(value))
180 }
181}
182
183impl<T: ?Sized> From<Box<T>> for Dynamic<T> {
184 #[inline(always)]
185 fn from(boxed: Box<T>) -> Self {
186 Self(boxed)
187 }
188}
189
190impl From<&str> for Dynamic<String> {
191 #[inline(always)]
192 fn from(s: &str) -> Self {
193 Self(Box::new(s.to_string()))
194 }
195}
196
197// Constant-time equality — only available with `conversions` feature
198#[cfg(feature = "conversions")]
199impl<T> Dynamic<T>
200where
201 T: ?Sized + AsRef<[u8]>,
202{
203 #[inline]
204 pub fn ct_eq(&self, other: &Self) -> bool {
205 use crate::conversions::SecureConversionsExt;
206 self.expose_secret()
207 .as_ref()
208 .ct_eq(other.expose_secret().as_ref())
209 }
210}
211
212// Random generation — only available with `rand` feature
213#[cfg(feature = "rand")]
214impl Dynamic<Vec<u8>> {
215 /// Generate fresh random bytes of the specified length using the OS RNG.
216 ///
217 /// This is a convenience method that generates random bytes directly
218 /// without going through `DynamicRng`. Equivalent to:
219 /// `DynamicRng::generate(len).into_inner()`
220 ///
221 /// # Example
222 ///
223 /// ```
224 /// # #[cfg(feature = "rand")]
225 /// # {
226 /// use secure_gate::Dynamic;
227 /// let random: Dynamic<Vec<u8>> = Dynamic::generate_random(64);
228 /// assert_eq!(random.len(), 64);
229 /// # }
230 /// ```
231 #[inline]
232 pub fn generate_random(len: usize) -> Self {
233 crate::rng::DynamicRng::generate(len).into_inner()
234 }
235}
236
237// Zeroize integration
238#[cfg(feature = "zeroize")]
239impl<T: ?Sized + zeroize::Zeroize> zeroize::Zeroize for Dynamic<T> {
240 fn zeroize(&mut self) {
241 self.0.zeroize();
242 }
243}
244
245#[cfg(feature = "zeroize")]
246impl<T: ?Sized + zeroize::Zeroize> zeroize::ZeroizeOnDrop for Dynamic<T> {}