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> {}