secure_gate/fixed.rs
1// ==========================================================================
2// src/fixed.rs
3// ==========================================================================
4
5use core::fmt;
6
7/// Stack-allocated secure secret wrapper.
8///
9/// This is a zero-cost wrapper for fixed-size secrets like byte arrays or primitives.
10/// The inner field is private, forcing all access through explicit methods.
11///
12/// Security invariants:
13/// - No `Deref` or `AsRef` — prevents silent access or borrowing.
14/// - No implicit `Copy` — even for `[u8; N]`, duplication must be explicit via `.clone()`.
15/// - `Debug` is always redacted.
16///
17/// # Examples
18///
19/// Basic usage:
20/// ```
21/// use secure_gate::Fixed;
22/// let secret = Fixed::new(42u32);
23/// assert_eq!(*secret.expose_secret(), 42);
24/// ```
25///
26/// For byte arrays (most common):
27/// ```
28/// use secure_gate::{Fixed, fixed_alias};
29/// fixed_alias!(pub Aes256Key, 32); // Visibility required
30/// let key_bytes = [0x42u8; 32];
31/// let key: Aes256Key = Fixed::from(key_bytes);
32/// assert_eq!(key.len(), 32);
33/// assert_eq!(key.expose_secret()[0], 0x42);
34/// ```
35///
36/// With `zeroize` feature (automatic wipe on drop):
37/// ```
38/// # #[cfg(feature = "zeroize")]
39/// # {
40/// use secure_gate::Fixed;
41/// let mut secret = Fixed::new([1u8, 2, 3]);
42/// drop(secret); // memory wiped automatically
43/// # }
44/// ```
45pub struct Fixed<T>(T); // ← field is PRIVATE
46
47impl<T> Fixed<T> {
48 /// Wrap a value in a `Fixed` secret.
49 ///
50 /// This is zero-cost and const-friendly.
51 ///
52 /// # Example
53 ///
54 /// ```
55 /// use secure_gate::Fixed;
56 /// const SECRET: Fixed<u32> = Fixed::new(42);
57 /// ```
58 #[inline(always)]
59 pub const fn new(value: T) -> Self {
60 Fixed(value)
61 }
62
63 /// Expose the inner value for read-only access.
64 ///
65 /// This is the **only** way to read the secret — loud and auditable.
66 ///
67 /// # Example
68 ///
69 /// ```
70 /// use secure_gate::Fixed;
71 /// let secret = Fixed::new("hunter2");
72 /// assert_eq!(secret.expose_secret(), &"hunter2");
73 /// ```
74 #[inline(always)]
75 pub const fn expose_secret(&self) -> &T {
76 &self.0
77 }
78
79 /// Expose the inner value for mutable access.
80 ///
81 /// This is the **only** way to mutate the secret — loud and auditable.
82 ///
83 /// # Example
84 ///
85 /// ```
86 /// use secure_gate::Fixed;
87 /// let mut secret = Fixed::new([1u8, 2, 3]);
88 /// secret.expose_secret_mut()[0] = 42;
89 /// assert_eq!(secret.expose_secret()[0], 42);
90 /// ```
91 #[inline(always)]
92 pub fn expose_secret_mut(&mut self) -> &mut T {
93 &mut self.0
94 }
95
96
97 /// Convert to a non-cloneable variant.
98 ///
99 /// This prevents accidental cloning of the secret.
100 ///
101 /// # Example
102 ///
103 /// ```
104 /// use secure_gate::Fixed;
105 /// let secret = Fixed::new([1u8; 32]);
106 /// let no_clone = secret.no_clone();
107 /// // no_clone cannot be cloned
108 /// ```
109 #[inline(always)]
110 pub fn no_clone(self) -> crate::FixedNoClone<T> {
111 crate::FixedNoClone::new(self.0)
112 }
113}
114
115// Explicit zeroization — only available with `zeroize` feature
116#[cfg(feature = "zeroize")]
117impl<T: zeroize::Zeroize> Fixed<T> {
118 /// Explicitly zeroize the secret immediately.
119 ///
120 /// This is useful when you want to wipe memory before the value goes out of scope,
121 /// or when you want to make the zeroization intent explicit in the code.
122 ///
123 /// # Example
124 ///
125 /// ```
126 /// # #[cfg(feature = "zeroize")]
127 /// # {
128 /// use secure_gate::Fixed;
129 /// let mut key = Fixed::new([42u8; 32]);
130 /// // ... use key ...
131 /// key.zeroize_now(); // Explicit wipe - makes intent clear
132 /// # }
133 /// ```
134 #[inline]
135 pub fn zeroize_now(&mut self) {
136 self.0.zeroize();
137 }
138}
139
140// === Byte-array specific helpers ===
141
142impl<const N: usize> Fixed<[u8; N]> {
143 /// Returns the fixed length in bytes.
144 ///
145 /// This is safe public metadata — does not expose the secret.
146 #[inline(always)]
147 pub const fn len(&self) -> usize {
148 N
149 }
150
151 /// Returns `true` if the fixed secret is empty (zero-length).
152 ///
153 /// This is safe public metadata — does not expose the secret.
154 #[inline(always)]
155 pub const fn is_empty(&self) -> bool {
156 N == 0
157 }
158
159 /// Create from a byte slice of exactly `N` bytes.
160 ///
161 /// Panics if the slice length does not match `N`.
162 ///
163 /// # Example
164 ///
165 /// ```
166 /// use secure_gate::Fixed;
167 /// let bytes: &[u8] = &[1, 2, 3];
168 /// let secret = Fixed::<[u8; 3]>::from_slice(bytes);
169 /// assert_eq!(secret.expose_secret(), &[1, 2, 3]);
170 /// ```
171 #[inline]
172 pub fn from_slice(bytes: &[u8]) -> Self {
173 assert_eq!(bytes.len(), N, "slice length mismatch");
174 let mut arr = [0u8; N];
175 arr.copy_from_slice(&bytes[..N]);
176 Self::new(arr)
177 }
178}
179
180impl<const N: usize> From<[u8; N]> for Fixed<[u8; N]> {
181 /// Wrap a raw byte array in a `Fixed` secret.
182 ///
183 /// Zero-cost conversion.
184 ///
185 /// # Example
186 ///
187 /// ```
188 /// use secure_gate::Fixed;
189 /// let key: Fixed<[u8; 4]> = [1, 2, 3, 4].into();
190 /// ```
191 #[inline(always)]
192 fn from(arr: [u8; N]) -> Self {
193 Self::new(arr)
194 }
195}
196
197// Debug is always redacted
198impl<T> fmt::Debug for Fixed<T> {
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 f.write_str("[REDACTED]")
201 }
202}
203
204// Explicit Clone only — no implicit Copy
205impl<T: Clone> Clone for Fixed<T> {
206 #[inline(always)]
207 fn clone(&self) -> Self {
208 Self(self.0.clone())
209 }
210}
211
212// REMOVED: Copy impl for Fixed<[u8; N]>
213// Implicit copying of secrets is a footgun — duplication must be intentional.
214
215// Constant-time equality — only available with `conversions` feature
216#[cfg(feature = "conversions")]
217impl<const N: usize> Fixed<[u8; N]> {
218 /// Constant-time equality comparison.
219 ///
220 /// This is the **only safe way** to compare two fixed-size secrets.
221 /// Available only when the `conversions` feature is enabled.
222 ///
223 /// # Example
224 ///
225 /// ```
226 /// # #[cfg(feature = "conversions")]
227 /// # {
228 /// use secure_gate::Fixed;
229 /// let a = Fixed::new([1u8; 32]);
230 /// let b = Fixed::new([1u8; 32]);
231 /// assert!(a.ct_eq(&b));
232 /// # }
233 /// ```
234 #[inline]
235 pub fn ct_eq(&self, other: &Self) -> bool {
236 use crate::conversions::SecureConversionsExt;
237 self.expose_secret().ct_eq(other.expose_secret())
238 }
239
240 /// Create a `Fixed` secret from a hex string.
241 ///
242 /// Returns `Err` if the hex string is invalid or doesn't match the expected length.
243 /// Available only when the `conversions` feature is enabled.
244 ///
245 /// # Example
246 ///
247 /// ```
248 /// # #[cfg(feature = "conversions")]
249 /// # {
250 /// use secure_gate::Fixed;
251 /// let key = Fixed::<[u8; 4]>::from_hex("deadbeef")?;
252 /// assert_eq!(key.expose_secret(), &[0xde, 0xad, 0xbe, 0xef]);
253 /// # }
254 /// # Ok::<(), &'static str>(())
255 /// ```
256 pub fn from_hex(hex: &str) -> Result<Self, &'static str> {
257 let mut bytes = hex::decode(hex)
258 .map_err(|_| "invalid hex string")?;
259
260 if bytes.len() != N {
261 #[cfg(feature = "zeroize")]
262 zeroize::Zeroize::zeroize(&mut bytes);
263 return Err("hex string length mismatch");
264 }
265
266 let mut arr = [0u8; N];
267 arr.copy_from_slice(&bytes);
268 #[cfg(feature = "zeroize")]
269 zeroize::Zeroize::zeroize(&mut bytes); // Zeroize temporary Vec after copy
270 Ok(Self::new(arr))
271 }
272
273 /// Create a `Fixed` secret from a base64url string (no padding).
274 ///
275 /// Returns `Err` if the base64url string is invalid or doesn't match the expected length.
276 /// Available only when the `conversions` feature is enabled.
277 ///
278 /// # Example
279 ///
280 /// ```
281 /// # #[cfg(feature = "conversions")]
282 /// # {
283 /// use secure_gate::Fixed;
284 /// use base64::engine::general_purpose::URL_SAFE_NO_PAD;
285 /// use base64::Engine;
286 /// let b64 = URL_SAFE_NO_PAD.encode([0xde, 0xad, 0xbe, 0xef]);
287 /// let key = Fixed::<[u8; 4]>::from_base64url(&b64)?;
288 /// assert_eq!(key.expose_secret(), &[0xde, 0xad, 0xbe, 0xef]);
289 /// # }
290 /// # Ok::<(), &'static str>(())
291 /// ```
292 pub fn from_base64url(b64: &str) -> Result<Self, &'static str> {
293 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
294 use base64::Engine;
295
296 let mut bytes = URL_SAFE_NO_PAD.decode(b64)
297 .map_err(|_| "invalid base64url string")?;
298
299 if bytes.len() != N {
300 #[cfg(feature = "zeroize")]
301 zeroize::Zeroize::zeroize(&mut bytes);
302 return Err("base64url string length mismatch");
303 }
304
305 let mut arr = [0u8; N];
306 arr.copy_from_slice(&bytes);
307 #[cfg(feature = "zeroize")]
308 zeroize::Zeroize::zeroize(&mut bytes); // Zeroize temporary Vec after copy
309 Ok(Self::new(arr))
310 }
311}
312
313// Random generation — only available with `rand` feature
314#[cfg(feature = "rand")]
315impl<const N: usize> Fixed<[u8; N]> {
316 /// Generate fresh random bytes using the OS RNG.
317 ///
318 /// This is a convenience method that generates random bytes directly
319 /// without going through `FixedRng`. Equivalent to:
320 /// `FixedRng::<N>::generate().into_inner()`
321 ///
322 /// # Example
323 ///
324 /// ```
325 /// # #[cfg(feature = "rand")]
326 /// # {
327 /// use secure_gate::Fixed;
328 /// let key: Fixed<[u8; 32]> = Fixed::generate_random();
329 /// # }
330 /// ```
331 #[inline]
332 pub fn generate_random() -> Self {
333 crate::rng::FixedRng::<N>::generate().into_inner()
334 }
335}
336
337// Zeroize integration
338#[cfg(feature = "zeroize")]
339impl<T: zeroize::Zeroize> zeroize::Zeroize for Fixed<T> {
340 fn zeroize(&mut self) {
341 self.0.zeroize();
342 }
343}
344
345#[cfg(feature = "zeroize")]
346impl<T: zeroize::Zeroize> zeroize::ZeroizeOnDrop for Fixed<T> {}