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 /// Consume the wrapper and return the inner `Box<T>`.
84 ///
85 /// Note: If `zeroize` is enabled, prefer dropping the `Dynamic` to ensure wiping.
86 #[inline(always)]
87 pub fn into_inner(self) -> Box<T> {
88 self.0
89 }
90
91 /// Convert to a non-cloneable variant.
92 ///
93 /// Prevents accidental cloning of the secret.
94 ///
95 /// # Example
96 ///
97 /// ```
98 /// use secure_gate::{Dynamic, DynamicNoClone};
99 /// let secret = Dynamic::<String>::new("no copy".to_string());
100 /// let no_clone: DynamicNoClone<String> = secret.no_clone();
101 /// assert_eq!(no_clone.expose_secret(), "no copy");
102 /// ```
103 #[inline(always)]
104 pub fn no_clone(self) -> crate::DynamicNoClone<T> {
105 crate::DynamicNoClone::new(self.0)
106 }
107}
108
109impl<T: ?Sized> core::fmt::Debug for Dynamic<T> {
110 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
111 f.write_str("[REDACTED]")
112 }
113}
114
115// Clone impls — gated correctly
116#[cfg(not(feature = "zeroize"))]
117impl<T: Clone> Clone for Dynamic<T> {
118 #[inline(always)]
119 fn clone(&self) -> Self {
120 Dynamic(self.0.clone())
121 }
122}
123
124#[cfg(feature = "zeroize")]
125impl<T: Clone + zeroize::Zeroize> Clone for Dynamic<T> {
126 #[inline(always)]
127 fn clone(&self) -> Self {
128 Dynamic(self.0.clone())
129 }
130}
131
132// === Ergonomic helpers for common heap types ===
133impl Dynamic<String> {
134 pub fn finish_mut(&mut self) -> &mut String {
135 let s = &mut *self.0;
136 s.shrink_to_fit();
137 s
138 }
139
140 #[inline(always)]
141 pub const fn len(&self) -> usize {
142 self.0.len()
143 }
144
145 #[inline(always)]
146 pub const fn is_empty(&self) -> bool {
147 self.0.is_empty()
148 }
149}
150
151impl<T> Dynamic<Vec<T>> {
152 pub fn finish_mut(&mut self) -> &mut Vec<T> {
153 let v = &mut *self.0;
154 v.shrink_to_fit();
155 v
156 }
157
158 #[inline(always)]
159 pub const fn len(&self) -> usize {
160 self.0.len()
161 }
162
163 #[inline(always)]
164 pub const fn is_empty(&self) -> bool {
165 self.0.is_empty()
166 }
167}
168
169// === Convenient From impls ===
170impl<T> From<T> for Dynamic<T> {
171 #[inline(always)]
172 fn from(value: T) -> Self {
173 Self(Box::new(value))
174 }
175}
176
177impl<T: ?Sized> From<Box<T>> for Dynamic<T> {
178 #[inline(always)]
179 fn from(boxed: Box<T>) -> Self {
180 Self(boxed)
181 }
182}
183
184impl From<&str> for Dynamic<String> {
185 #[inline(always)]
186 fn from(s: &str) -> Self {
187 Self(Box::new(s.to_string()))
188 }
189}
190
191// Constant-time equality — only available with `conversions` feature
192#[cfg(feature = "conversions")]
193impl<T> Dynamic<T>
194where
195 T: ?Sized + AsRef<[u8]>,
196{
197 #[inline]
198 pub fn ct_eq(&self, other: &Self) -> bool {
199 use crate::conversions::SecureConversionsExt;
200 self.expose_secret()
201 .as_ref()
202 .ct_eq(other.expose_secret().as_ref())
203 }
204}
205
206// Zeroize integration
207#[cfg(feature = "zeroize")]
208impl<T: ?Sized + zeroize::Zeroize> zeroize::Zeroize for Dynamic<T> {
209 fn zeroize(&mut self) {
210 self.0.zeroize();
211 }
212}
213
214#[cfg(feature = "zeroize")]
215impl<T: ?Sized + zeroize::Zeroize> zeroize::ZeroizeOnDrop for Dynamic<T> {}