secure_gate/
fixed.rs

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