secure_gate/
fixed.rs

1// ==========================================================================
2// src/fixed.rs
3// ==========================================================================
4
5use core::convert::TryFrom;
6use core::fmt;
7
8#[cfg(feature = "rand")]
9use rand::rand_core::OsError;
10
11/// Stack-allocated secure secret wrapper.
12///
13/// This is a zero-cost wrapper for fixed-size secrets like byte arrays or primitives.
14/// The inner field is private, forcing all access through explicit methods.
15///
16/// Security invariants:
17/// - No `Deref` or `AsRef` — prevents silent access or borrowing.
18/// - No implicit `Copy` — even for `[u8; N]`, duplication must be explicit via `.clone()`.
19/// - `Debug` is always redacted.
20///
21/// # Examples
22///
23/// Basic usage:
24/// ```
25/// use secure_gate::Fixed;
26/// let secret = Fixed::new(42u32);
27/// assert_eq!(*secret.expose_secret(), 42);
28/// ```
29///
30/// For byte arrays (most common):
31/// ```
32/// use secure_gate::{Fixed, fixed_alias};
33/// fixed_alias!(Aes256Key, 32);
34/// let key_bytes = [0x42u8; 32];
35/// let key: Aes256Key = Fixed::from(key_bytes);
36/// assert_eq!(key.len(), 32);
37/// assert_eq!(key.expose_secret()[0], 0x42);
38/// ```
39///
40/// With `zeroize` feature (automatic wipe on drop):
41/// ```
42/// # #[cfg(feature = "zeroize")]
43/// # {
44/// use secure_gate::Fixed;
45/// let mut secret = Fixed::new([1u8, 2, 3]);
46/// drop(secret); // memory wiped automatically
47/// # }
48/// ```
49pub struct Fixed<T>(T); // ← field is PRIVATE
50
51impl<T> Fixed<T> {
52    /// Wrap a value in a `Fixed` secret.
53    ///
54    /// This is zero-cost and const-friendly.
55    ///
56    /// # Example
57    ///
58    /// ```
59    /// use secure_gate::Fixed;
60    /// const SECRET: Fixed<u32> = Fixed::new(42);
61    /// ```
62    #[inline(always)]
63    pub const fn new(value: T) -> Self {
64        Fixed(value)
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    ///
71    /// # Example
72    ///
73    /// ```
74    /// use secure_gate::Fixed;
75    /// let secret = Fixed::new("hunter2");
76    /// assert_eq!(secret.expose_secret(), &"hunter2");
77    /// ```
78    #[inline(always)]
79    pub const fn expose_secret(&self) -> &T {
80        &self.0
81    }
82
83    /// Expose the inner value for mutable access.
84    ///
85    /// This is the **only** way to mutate the secret — loud and auditable.
86    ///
87    /// # Example
88    ///
89    /// ```
90    /// use secure_gate::Fixed;
91    /// let mut secret = Fixed::new([1u8, 2, 3]);
92    /// secret.expose_secret_mut()[0] = 42;
93    /// assert_eq!(secret.expose_secret()[0], 42);
94    /// ```
95    #[inline(always)]
96    pub fn expose_secret_mut(&mut self) -> &mut T {
97        &mut self.0
98    }
99}
100
101// === Byte-array specific helpers ===
102
103impl<const N: usize> Fixed<[u8; N]> {
104    /// Returns the fixed length in bytes.
105    ///
106    /// This is safe public metadata — does not expose the secret.
107    #[inline(always)]
108    pub const fn len(&self) -> usize {
109        N
110    }
111
112    /// Returns `true` if the fixed secret is empty (zero-length).
113    ///
114    /// This is safe public metadata — does not expose the secret.
115    #[inline(always)]
116    pub const fn is_empty(&self) -> bool {
117        N == 0
118    }
119
120    /// Create from a byte slice of exactly `N` bytes.
121    ///
122    /// Panics if the slice length does not match `N`.
123    /// For fallible construction, use `TryFrom<&[u8]>` instead.
124    ///
125    /// # Example
126    ///
127    /// ```
128    /// use secure_gate::Fixed;
129    /// let bytes: &[u8] = &[1, 2, 3];
130    /// let secret = Fixed::<[u8; 3]>::from_slice(bytes);
131    /// assert_eq!(secret.expose_secret(), &[1, 2, 3]);
132    /// ```
133    #[inline]
134    pub fn from_slice(bytes: &[u8]) -> Self {
135        Self::try_from(bytes).expect("slice length mismatch")
136    }
137}
138
139impl<const N: usize> core::convert::TryFrom<&[u8]> for Fixed<[u8; N]> {
140    type Error = FromSliceError;
141
142    fn try_from(slice: &[u8]) -> Result<Self, Self::Error> {
143        if slice.len() != N {
144            Err(FromSliceError::new(slice.len(), N))
145        } else {
146            let mut arr = [0u8; N];
147            arr.copy_from_slice(slice);
148            Ok(Self::new(arr))
149        }
150    }
151}
152
153impl<const N: usize> From<[u8; N]> for Fixed<[u8; N]> {
154    /// Wrap a raw byte array in a `Fixed` secret.
155    ///
156    /// Zero-cost conversion.
157    ///
158    /// # Example
159    ///
160    /// ```
161    /// use secure_gate::Fixed;
162    /// let key: Fixed<[u8; 4]> = [1, 2, 3, 4].into();
163    /// ```
164    #[inline(always)]
165    fn from(arr: [u8; N]) -> Self {
166        Self::new(arr)
167    }
168}
169
170// Debug is always redacted
171impl<T> fmt::Debug for Fixed<T> {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        f.write_str("[REDACTED]")
174    }
175}
176
177// Regular equality — fallback when ct-eq feature is disabled
178#[cfg(not(feature = "ct-eq"))]
179impl<T: PartialEq> PartialEq for Fixed<T> {
180    fn eq(&self, other: &Self) -> bool {
181        self.expose_secret() == other.expose_secret()
182    }
183}
184
185#[cfg(not(feature = "ct-eq"))]
186impl<T: Eq> Eq for Fixed<T> {}
187
188/// Error for slice length mismatches in TryFrom impls.
189#[derive(Debug)]
190pub struct FromSliceError {
191    pub actual_len: usize,
192    pub expected_len: usize,
193}
194
195impl FromSliceError {
196    /// Create a new FromSliceError with the actual and expected lengths.
197    pub fn new(actual_len: usize, expected_len: usize) -> Self {
198        Self {
199            actual_len,
200            expected_len,
201        }
202    }
203}
204
205impl core::fmt::Display for FromSliceError {
206    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
207        write!(
208            f,
209            "slice length mismatch: expected {} bytes, got {} bytes",
210            self.expected_len, self.actual_len
211        )
212    }
213}
214
215impl core::error::Error for FromSliceError {}
216
217// Opt-in Clone — only for types marked CloneableSecretMarker (default no-clone)
218#[cfg(feature = "zeroize")]
219impl<T: crate::CloneableSecretMarker> Clone for Fixed<T> {
220    #[inline(always)]
221    fn clone(&self) -> Self {
222        Self(self.0.clone())
223    }
224}
225
226// Constant-time equality — only available with `ct-eq` feature
227#[cfg(feature = "ct-eq")]
228impl<const N: usize> Fixed<[u8; N]> {
229    /// Constant-time equality comparison.
230    ///
231    /// This is the **only safe way** to compare two fixed-size secrets.
232    /// Available only when the `ct-eq` feature is enabled.
233    ///
234    /// # Example
235    ///
236    /// ```
237    /// # #[cfg(feature = "ct-eq")]
238    /// # {
239    /// use secure_gate::Fixed;
240    /// let a = Fixed::new([1u8; 32]);
241    /// let b = Fixed::new([1u8; 32]);
242    /// assert!(a.ct_eq(&b));
243    /// # }
244    /// ```
245    #[inline]
246    pub fn ct_eq(&self, other: &Self) -> bool {
247        use crate::ct_eq::ConstantTimeEq;
248        self.expose_secret().ct_eq(other.expose_secret())
249    }
250}
251
252// Random generation — only available with `rand` feature
253#[cfg(feature = "rand")]
254impl<const N: usize> Fixed<[u8; N]> {
255    /// Generate fresh random bytes using the OS RNG.
256    ///
257    /// This is a convenience method that generates random bytes directly
258    /// without going through `FixedRng`. Equivalent to:
259    /// `FixedRng::<N>::generate().into_inner()`
260    ///
261    /// # Example
262    ///
263    /// ```
264    /// # #[cfg(feature = "rand")]
265    /// # {
266    /// use secure_gate::Fixed;
267    /// let key: Fixed<[u8; 32]> = Fixed::generate_random();
268    /// # }
269    /// ```
270    #[inline]
271    pub fn generate_random() -> Self {
272        crate::random::FixedRng::<N>::generate().into_inner()
273    }
274
275    /// Try to generate random bytes for Fixed.
276    ///
277    /// Returns an error if the RNG fails.
278    ///
279    /// # Example
280    ///
281    /// ```
282    /// # #[cfg(feature = "rand")]
283    /// # {
284    /// use secure_gate::Fixed;
285    /// let key: Result<Fixed<[u8; 32]>, rand::rand_core::OsError> = Fixed::try_generate_random();
286    /// assert!(key.is_ok());
287    /// # }
288    /// ```
289    #[inline]
290    pub fn try_generate_random() -> Result<Self, OsError> {
291        crate::random::FixedRng::<N>::try_generate()
292            .map(|rng: crate::random::FixedRng<N>| rng.into_inner())
293    }
294}
295
296// Zeroize integration
297#[cfg(feature = "zeroize")]
298impl<T: zeroize::Zeroize> zeroize::Zeroize for Fixed<T> {
299    fn zeroize(&mut self) {
300        self.0.zeroize();
301    }
302}
303
304#[cfg(feature = "zeroize")]
305impl<T: zeroize::Zeroize> zeroize::ZeroizeOnDrop for Fixed<T> {}