secure_gate/
fixed.rs

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