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