Skip to main content

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::CloneableSecret> 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::SerializableSecret,
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> {}