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("slice length mismatch"))
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/// Error for slice length mismatches in TryFrom impls.
178#[derive(Debug)]
179pub struct FromSliceError(pub &'static str);
180
181// Opt-in Clone — only for types marked CloneableSecretMarker (default no-clone)
182#[cfg(feature = "zeroize")]
183impl<T: crate::CloneableSecretMarker> Clone for Fixed<T> {
184 #[inline(always)]
185 fn clone(&self) -> Self {
186 Self(self.0.clone())
187 }
188}
189
190// Constant-time equality — only available with `ct-eq` feature
191#[cfg(feature = "ct-eq")]
192impl<const N: usize> Fixed<[u8; N]> {
193 /// Constant-time equality comparison.
194 ///
195 /// This is the **only safe way** to compare two fixed-size secrets.
196 /// Available only when the `ct-eq` feature is enabled.
197 ///
198 /// # Example
199 ///
200 /// ```
201 /// # #[cfg(feature = "ct-eq")]
202 /// # {
203 /// use secure_gate::Fixed;
204 /// let a = Fixed::new([1u8; 32]);
205 /// let b = Fixed::new([1u8; 32]);
206 /// assert!(a.ct_eq(&b));
207 /// # }
208 /// ```
209 #[inline]
210 pub fn ct_eq(&self, other: &Self) -> bool {
211 use crate::ct_eq::ConstantTimeEq;
212 self.expose_secret().ct_eq(other.expose_secret())
213 }
214}
215
216// Random generation — only available with `rand` feature
217#[cfg(feature = "rand")]
218impl<const N: usize> Fixed<[u8; N]> {
219 /// Generate fresh random bytes using the OS RNG.
220 ///
221 /// This is a convenience method that generates random bytes directly
222 /// without going through `FixedRng`. Equivalent to:
223 /// `FixedRng::<N>::generate().into_inner()`
224 ///
225 /// # Example
226 ///
227 /// ```
228 /// # #[cfg(feature = "rand")]
229 /// # {
230 /// use secure_gate::Fixed;
231 /// let key: Fixed<[u8; 32]> = Fixed::generate_random();
232 /// # }
233 /// ```
234 #[inline]
235 pub fn generate_random() -> Self {
236 crate::random::FixedRng::<N>::generate().into_inner()
237 }
238
239 /// Try to generate random bytes for Fixed.
240 ///
241 /// Returns an error if the RNG fails.
242 ///
243 /// # Example
244 ///
245 /// ```
246 /// # #[cfg(feature = "rand")]
247 /// # {
248 /// use secure_gate::Fixed;
249 /// let key: Result<Fixed<[u8; 32]>, rand::rand_core::OsError> = Fixed::try_generate_random();
250 /// assert!(key.is_ok());
251 /// # }
252 /// ```
253 #[inline]
254 pub fn try_generate_random() -> Result<Self, OsError> {
255 crate::random::FixedRng::<N>::try_generate()
256 .map(|rng: crate::random::FixedRng<N>| rng.into_inner())
257 }
258}
259
260// Zeroize integration
261#[cfg(feature = "zeroize")]
262impl<T: zeroize::Zeroize> zeroize::Zeroize for Fixed<T> {
263 fn zeroize(&mut self) {
264 self.0.zeroize();
265 }
266}
267
268#[cfg(feature = "zeroize")]
269impl<T: zeroize::Zeroize> zeroize::ZeroizeOnDrop for Fixed<T> {}