secure_gate/fixed.rs
1/// Fixed-size stack-allocated secure secret wrapper.
2///
3/// This is a zero-cost wrapper for fixed-size secrets like byte arrays or primitives.
4/// The inner field is private, forcing all access through explicit methods.
5///
6/// Security invariants:
7/// - No `Deref` or `AsRef` — prevents silent access or borrowing.
8/// - No implicit `Copy` — even for `[u8; N]`, duplication must be explicit via `.clone()`.
9/// - `Debug` is always redacted.
10///
11/// # Examples
12///
13/// Basic usage:
14/// ```
15/// use secure_gate::{Fixed, ExposeSecret};
16/// let secret = Fixed::new([42u8; 1]);
17/// assert_eq!(secret.expose_secret()[0], 42);
18/// ```
19///
20/// For byte arrays (most common):
21/// ```
22/// use secure_gate::{fixed_alias, Fixed, ExposeSecret};
23/// fixed_alias!(Aes256Key, 32);
24/// let key_bytes = [0x42u8; 32];
25/// let key: Aes256Key = Fixed::from(key_bytes);
26/// assert_eq!(key.len(), 32);
27/// assert_eq!(key.expose_secret()[0], 0x42);
28/// ```
29///
30/// With `zeroize` feature (automatic wipe on drop):
31/// ```
32/// # #[cfg(feature = "zeroize")]
33/// # {
34/// use secure_gate::Fixed;
35/// let mut secret = Fixed::new([1u8, 2, 3]);
36/// drop(secret); // stack memory wiped automatically
37/// # }
38/// ```
39#[cfg(feature = "rand")]
40use rand::{rngs::OsRng, TryRngCore};
41
42#[cfg(feature = "encoding-base64")]
43use crate::traits::decoding::base64_url::FromBase64UrlStr;
44#[cfg(feature = "encoding-bech32")]
45use crate::traits::decoding::bech32::FromBech32Str;
46#[cfg(feature = "encoding-bech32m")]
47use crate::traits::decoding::bech32m::FromBech32mStr;
48#[cfg(feature = "encoding-hex")]
49use crate::traits::decoding::hex::FromHexStr;
50pub struct Fixed<T> {
51 inner: T,
52}
53
54impl<T> Fixed<T> {
55 /// Wrap a value in a `Fixed` secret.
56 ///
57 /// This is zero-cost and const-friendly.
58 ///
59 /// # Example
60 ///
61 /// ```
62 /// use secure_gate::Fixed;
63 /// const SECRET: Fixed<u32> = Fixed::new(42);
64 /// ```
65 #[inline(always)]
66 pub const fn new(value: T) -> Self {
67 Fixed { inner: value }
68 }
69}
70
71/// # Byte-array specific helpers
72impl<const N: usize> Fixed<[u8; N]> {}
73
74impl<const N: usize> From<[u8; N]> for Fixed<[u8; N]> {
75 /// Wrap a raw byte array in a `Fixed` secret.
76 ///
77 /// Zero-cost conversion.
78 ///
79 /// # Example
80 ///
81 /// ```
82 /// use secure_gate::Fixed;
83 /// let key: Fixed<[u8; 4]> = [1, 2, 3, 4].into();
84 /// ```
85 #[inline(always)]
86 fn from(arr: [u8; N]) -> Self {
87 Self::new(arr)
88 }
89}
90
91// Fallible conversion from byte slice.
92impl<const N: usize> core::convert::TryFrom<&[u8]> for Fixed<[u8; N]> {
93 type Error = crate::error::FromSliceError;
94
95 /// Attempt to create a `Fixed` from a byte slice.
96 /// In debug builds, panics with detailed information on length mismatch to aid development.
97 /// In release builds, returns an error on length mismatch to prevent information leaks.
98 ///
99 /// # Example
100 ///
101 /// ```
102 /// use secure_gate::Fixed;
103 /// let slice: &[u8] = &[1u8, 2, 3, 4];
104 /// let key: Result<Fixed<[u8; 4]>, _> = slice.try_into();
105 /// assert!(key.is_ok());
106 /// ```
107 fn try_from(slice: &[u8]) -> Result<Self, Self::Error> {
108 if slice.len() != N {
109 #[cfg(debug_assertions)]
110 panic!(
111 "Fixed<{}> from_slice: expected exactly {} bytes, got {}",
112 N,
113 N,
114 slice.len()
115 );
116 #[cfg(not(debug_assertions))]
117 return Err(crate::error::FromSliceError::LengthMismatch);
118 }
119 let mut arr = [0u8; N];
120 arr.copy_from_slice(slice);
121 Ok(Self::new(arr))
122 }
123}
124
125impl<const N: usize, T> crate::ExposeSecret for Fixed<[T; N]> {
126 type Inner = [T; N];
127
128 #[inline(always)]
129 fn with_secret<F, R>(&self, f: F) -> R
130 where
131 F: FnOnce(&[T; N]) -> R,
132 {
133 f(&self.inner)
134 }
135
136 #[inline(always)]
137 fn expose_secret(&self) -> &[T; N] {
138 &self.inner
139 }
140
141 #[inline(always)]
142 fn len(&self) -> usize {
143 N * core::mem::size_of::<T>()
144 }
145}
146
147impl<const N: usize, T> crate::ExposeSecretMut for Fixed<[T; N]> {
148 #[inline(always)]
149 fn with_secret_mut<F, R>(&mut self, f: F) -> R
150 where
151 F: FnOnce(&mut [T; N]) -> R,
152 {
153 f(&mut self.inner)
154 }
155
156 #[inline(always)]
157 fn expose_secret_mut(&mut self) -> &mut [T; N] {
158 &mut self.inner
159 }
160}
161
162// Random generation — only available with `rand` feature.
163#[cfg(feature = "rand")]
164impl<const N: usize> Fixed<[u8; N]> {
165 /// Generate a secure random instance (panics on failure).
166 ///
167 /// Fill with fresh random bytes using the System RNG.
168 /// Panics on RNG failure for fail-fast crypto code. Guarantees secure entropy
169 /// from system sources.
170 ///
171 /// # Example
172 ///
173 /// ```
174 /// # #[cfg(feature = "rand")]
175 /// # {
176 /// use secure_gate::{Fixed, ExposeSecret};
177 /// let random: Fixed<[u8; 32]> = Fixed::from_random();
178 /// assert_eq!(random.len(), 32);
179 /// # }
180 /// ```
181 #[inline]
182 pub fn from_random() -> Self {
183 let mut bytes = [0u8; N];
184 OsRng
185 .try_fill_bytes(&mut bytes)
186 .expect("OsRng failure is a program error");
187 Self::from(bytes)
188 }
189}
190
191// Decoding constructors — only available with encoding features.
192#[cfg(feature = "encoding-hex")]
193impl<const N: usize> Fixed<[u8; N]> {
194 /// Decode a hex string into a Fixed secret.
195 ///
196 /// The decoded bytes must exactly match the array length `N`.
197 ///
198 /// # Example
199 ///
200 /// ```
201 /// # #[cfg(feature = "encoding-hex")]
202 /// use secure_gate::{Fixed, ExposeSecret};
203 /// let hex_string = "424344"; // 3 bytes
204 /// let secret: Fixed<[u8; 3]> = Fixed::try_from_hex(hex_string).unwrap();
205 /// assert_eq!(secret.expose_secret()[0], 0x42);
206 /// ```
207 pub fn try_from_hex(s: &str) -> Result<Self, crate::error::HexError> {
208 let bytes: Vec<u8> = s.try_from_hex()?;
209 if bytes.len() != N {
210 return Err(crate::error::HexError::InvalidLength {
211 expected: N,
212 got: bytes.len(),
213 });
214 }
215 let mut arr = [0u8; N];
216 arr.copy_from_slice(&bytes);
217 Ok(Self::new(arr))
218 }
219}
220
221#[cfg(feature = "encoding-base64")]
222impl<const N: usize> Fixed<[u8; N]> {
223 /// Decode a base64url string into a Fixed secret.
224 ///
225 /// The decoded bytes must exactly match the array length `N`.
226 ///
227 /// # Example
228 ///
229 /// ```
230 /// # #[cfg(feature = "encoding-base64")]
231 /// use secure_gate::{Fixed, ExposeSecret};
232 /// let b64_string = "QkNE"; // 3 bytes
233 /// let secret: Fixed<[u8; 3]> = Fixed::try_from_base64url(b64_string).unwrap();
234 /// assert_eq!(secret.expose_secret()[0], 0x42);
235 /// ```
236 pub fn try_from_base64url(s: &str) -> Result<Self, crate::error::Base64Error> {
237 let bytes: Vec<u8> = s.try_from_base64url()?;
238 if bytes.len() != N {
239 return Err(crate::error::Base64Error::InvalidLength {
240 expected: N,
241 got: bytes.len(),
242 });
243 }
244 let mut arr = [0u8; N];
245 arr.copy_from_slice(&bytes);
246 Ok(Self::new(arr))
247 }
248}
249
250#[cfg(feature = "encoding-bech32")]
251impl<const N: usize> Fixed<[u8; N]> {
252 /// Decode a bech32 string into a Fixed secret, discarding the HRP.
253 ///
254 /// The decoded bytes must exactly match the array length `N`.
255 ///
256 /// # Example
257 ///
258 /// ```
259 /// # #[cfg(feature = "encoding-bech32")]
260 /// use secure_gate::{Fixed, ExposeSecret, ToBech32};
261 /// let original = Fixed::new([1, 2, 3, 4]);
262 /// let bech32_string = original.with_secret(|s| s.to_bech32("test"));
263 /// let decoded = Fixed::<[u8; 4]>::try_from_bech32(&bech32_string).unwrap();
264 /// // HRP "test" is discarded
265 /// ```
266 pub fn try_from_bech32(s: &str) -> Result<Self, crate::error::Bech32Error> {
267 let (_hrp, bytes): (_, Vec<u8>) = s.try_from_bech32()?;
268 if bytes.len() != N {
269 return Err(crate::error::Bech32Error::InvalidLength {
270 expected: N,
271 got: bytes.len(),
272 });
273 }
274 let mut arr = [0u8; N];
275 arr.copy_from_slice(&bytes);
276 Ok(Self::new(arr))
277 }
278}
279
280#[cfg(feature = "encoding-bech32m")]
281impl<const N: usize> Fixed<[u8; N]> {
282 /// Decode a bech32m string into a Fixed secret, discarding the HRP.
283 ///
284 /// The decoded bytes must exactly match the array length `N`.
285 ///
286 /// # Example
287 ///
288 /// ```
289 /// # #[cfg(feature = "encoding-bech32m")]
290 /// use secure_gate::Fixed;
291 /// // Note: Bech32m strings must be valid Bech32m format
292 /// let bech32m_string = "abc1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw"; // 32 bytes
293 /// let secret: Result<Fixed<[u8; 32]>, _> = Fixed::try_from_bech32m(bech32m_string);
294 /// // Returns Result<Fixed<[u8; 32]>, Bech32Error>
295 /// ```
296 pub fn try_from_bech32m(s: &str) -> Result<Self, crate::error::Bech32Error> {
297 let (_hrp, bytes): (_, Vec<u8>) = s.try_from_bech32m()?;
298 if bytes.len() != N {
299 return Err(crate::error::Bech32Error::InvalidLength {
300 expected: N,
301 got: bytes.len(),
302 });
303 }
304 let mut arr = [0u8; N];
305 arr.copy_from_slice(&bytes);
306 Ok(Self::new(arr))
307 }
308}
309
310#[cfg(feature = "ct-eq")]
311impl<T> crate::ConstantTimeEq for Fixed<T>
312where
313 T: crate::ConstantTimeEq,
314{
315 fn ct_eq(&self, other: &Self) -> bool {
316 self.inner.ct_eq(&other.inner)
317 }
318}
319
320// Constant-time equality
321#[cfg(feature = "ct-eq")]
322impl<const N: usize> Fixed<[u8; N]> {
323 /// Constant-time equality comparison.
324 ///
325 /// This is the **only safe way** to compare two fixed-size secrets.
326 /// Available only when the `ct-eq` feature is enabled.
327 ///
328 /// # Example
329 ///
330 /// ```
331 /// # #[cfg(feature = "ct-eq")]
332 /// # {
333 /// use secure_gate::Fixed;
334 /// let a = Fixed::new([1u8; 32]);
335 /// let b = Fixed::new([1u8; 32]);
336 /// assert!(a.ct_eq(&b));
337 /// # }
338 /// ```
339 #[inline]
340 pub fn ct_eq(&self, other: &Self) -> bool {
341 use crate::traits::ConstantTimeEq;
342 self.inner.ct_eq(&other.inner)
343 }
344}
345
346#[cfg(feature = "ct-eq-hash")]
347impl<T> crate::ConstantTimeEqExt for Fixed<T>
348where
349 T: AsRef<[u8]> + crate::ConstantTimeEq,
350{
351 fn len(&self) -> usize {
352 self.inner.as_ref().len()
353 }
354
355 fn ct_eq_hash(&self, other: &Self) -> bool {
356 crate::traits::ct_eq_hash_bytes(self.inner.as_ref(), other.inner.as_ref())
357 }
358}
359
360// Redacted Debug implementation
361impl<T> core::fmt::Debug for Fixed<T> {
362 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
363 f.write_str("[REDACTED]")
364 }
365}
366
367#[cfg(feature = "cloneable")]
368impl<T: crate::CloneableType> Clone for Fixed<T> {
369 fn clone(&self) -> Self {
370 Self::new(self.inner.clone())
371 }
372}
373
374#[cfg(feature = "serde-serialize")]
375impl<T> serde::Serialize for Fixed<T>
376where
377 T: crate::SerializableType,
378{
379 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
380 where
381 S: serde::Serializer,
382 {
383 self.inner.serialize(serializer)
384 }
385}
386
387/// Custom serde deserialization for byte arrays (direct to sequence).
388#[cfg(feature = "serde-deserialize")]
389impl<'de, const N: usize> serde::Deserialize<'de> for Fixed<[u8; N]> {
390 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
391 where
392 D: serde::Deserializer<'de>,
393 {
394 use serde::de::Visitor;
395 use std::fmt;
396
397 struct FixedVisitor<const M: usize>;
398
399 impl<'de, const M: usize> Visitor<'de> for FixedVisitor<M> {
400 type Value = Fixed<[u8; M]>;
401
402 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
403 write!(formatter, "a byte array of length {}", M)
404 }
405
406 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
407 where
408 A: serde::de::SeqAccess<'de>,
409 {
410 let mut vec = alloc::vec::Vec::with_capacity(M);
411 while let Some(value) = seq.next_element()? {
412 vec.push(value);
413 }
414 if vec.len() != M {
415 return Err(serde::de::Error::invalid_length(
416 vec.len(),
417 &M.to_string().as_str(),
418 ));
419 }
420 let mut arr = [0u8; M];
421 arr.copy_from_slice(&vec);
422 Ok(Fixed::new(arr))
423 }
424 }
425
426 deserializer.deserialize_seq(FixedVisitor::<N>)
427 }
428}
429
430// Zeroize integration
431#[cfg(feature = "zeroize")]
432impl<T: zeroize::Zeroize> zeroize::Zeroize for Fixed<T> {
433 fn zeroize(&mut self) {
434 self.inner.zeroize();
435 }
436}
437
438/// Zeroize on drop integration
439#[cfg(feature = "zeroize")]
440impl<T: zeroize::Zeroize> zeroize::ZeroizeOnDrop for Fixed<T> {}