secure_gate/
fixed.rs

1// ==========================================================================
2// src/fixed.rs
3// ==========================================================================
4
5use core::fmt;
6
7/// Stack-allocated secure secret wrapper.
8///
9/// This is a zero-cost wrapper for fixed-size secrets like byte arrays or primitives.
10/// The inner field is private, forcing all access through explicit methods.
11///
12/// Security invariants:
13/// - No `Deref` or `AsRef` — prevents silent access or borrowing.
14/// - No implicit `Copy` — even for `[u8; N]`, duplication must be explicit via `.clone()`.
15/// - `Debug` is always redacted.
16///
17/// # Examples
18///
19/// Basic usage:
20/// ```
21/// use secure_gate::Fixed;
22/// let secret = Fixed::new(42u32);
23/// assert_eq!(*secret.expose_secret(), 42);
24/// ```
25///
26/// For byte arrays (most common):
27/// ```
28/// use secure_gate::{Fixed, fixed_alias};
29/// fixed_alias!(Aes256Key, 32);
30/// let key_bytes = [0x42u8; 32];
31/// let key: Aes256Key = Fixed::from(key_bytes);
32/// assert_eq!(key.len(), 32);
33/// assert_eq!(key.expose_secret()[0], 0x42);
34/// ```
35///
36/// With `zeroize` feature (automatic wipe on drop):
37/// ```
38/// # #[cfg(feature = "zeroize")]
39/// # {
40/// use secure_gate::Fixed;
41/// let mut secret = Fixed::new([1u8, 2, 3]);
42/// drop(secret); // memory wiped automatically
43/// # }
44/// ```
45pub struct Fixed<T>(T); // ← field is PRIVATE
46
47impl<T> Fixed<T> {
48    /// Wrap a value in a `Fixed` secret.
49    ///
50    /// This is zero-cost and const-friendly.
51    ///
52    /// # Example
53    ///
54    /// ```
55    /// use secure_gate::Fixed;
56    /// const SECRET: Fixed<u32> = Fixed::new(42);
57    /// ```
58    #[inline(always)]
59    pub const fn new(value: T) -> Self {
60        Fixed(value)
61    }
62
63    /// Expose the inner value for read-only access.
64    ///
65    /// This is the **only** way to read the secret — loud and auditable.
66    ///
67    /// # Example
68    ///
69    /// ```
70    /// use secure_gate::Fixed;
71    /// let secret = Fixed::new("hunter2");
72    /// assert_eq!(secret.expose_secret(), &"hunter2");
73    /// ```
74    #[inline(always)]
75    pub const fn expose_secret(&self) -> &T {
76        &self.0
77    }
78
79    /// Expose the inner value for mutable access.
80    ///
81    /// This is the **only** way to mutate the secret — loud and auditable.
82    ///
83    /// # Example
84    ///
85    /// ```
86    /// use secure_gate::Fixed;
87    /// let mut secret = Fixed::new([1u8, 2, 3]);
88    /// secret.expose_secret_mut()[0] = 42;
89    /// assert_eq!(secret.expose_secret()[0], 42);
90    /// ```
91    #[inline(always)]
92    pub fn expose_secret_mut(&mut self) -> &mut T {
93        &mut self.0
94    }
95
96    /// Consume the wrapper and return the inner value.
97    ///
98    /// Useful for passing secrets to functions that consume ownership.
99    ///
100    /// Note: If `zeroize` is enabled, prefer dropping the `Fixed` to ensure wiping.
101    ///
102    /// # Example
103    ///
104    /// ```
105    /// use secure_gate::Fixed;
106    /// let secret = Fixed::new(42u32);
107    /// let inner = secret.into_inner();
108    /// assert_eq!(inner, 42);
109    /// ```
110    #[inline(always)]
111    pub fn into_inner(self) -> T {
112        self.0
113    }
114
115    /// Convert to a non-cloneable variant.
116    ///
117    /// This prevents accidental cloning of the secret.
118    ///
119    /// # Example
120    ///
121    /// ```
122    /// use secure_gate::Fixed;
123    /// let secret = Fixed::new([1u8; 32]);
124    /// let no_clone = secret.no_clone();
125    /// // no_clone cannot be cloned
126    /// ```
127    #[inline(always)]
128    pub fn no_clone(self) -> crate::FixedNoClone<T> {
129        crate::FixedNoClone::new(self.0)
130    }
131}
132
133// === Byte-array specific helpers ===
134
135impl<const N: usize> Fixed<[u8; N]> {
136    /// Returns the fixed length in bytes.
137    ///
138    /// This is safe public metadata — does not expose the secret.
139    #[inline(always)]
140    pub const fn len(&self) -> usize {
141        N
142    }
143
144    /// Returns `true` if the fixed secret is empty (zero-length).
145    ///
146    /// This is safe public metadata — does not expose the secret.
147    #[inline(always)]
148    pub const fn is_empty(&self) -> bool {
149        N == 0
150    }
151
152    /// Create from a byte slice of exactly `N` bytes.
153    ///
154    /// Panics if the slice length does not match `N`.
155    ///
156    /// # Example
157    ///
158    /// ```
159    /// use secure_gate::Fixed;
160    /// let bytes: &[u8] = &[1, 2, 3];
161    /// let secret = Fixed::<[u8; 3]>::from_slice(bytes);
162    /// assert_eq!(secret.expose_secret(), &[1, 2, 3]);
163    /// ```
164    #[inline]
165    pub fn from_slice(bytes: &[u8]) -> Self {
166        assert_eq!(bytes.len(), N, "slice length mismatch");
167        let mut arr = [0u8; N];
168        arr.copy_from_slice(&bytes[..N]);
169        Self::new(arr)
170    }
171}
172
173impl<const N: usize> From<[u8; N]> for Fixed<[u8; N]> {
174    /// Wrap a raw byte array in a `Fixed` secret.
175    ///
176    /// Zero-cost conversion.
177    ///
178    /// # Example
179    ///
180    /// ```
181    /// use secure_gate::Fixed;
182    /// let key: Fixed<[u8; 4]> = [1, 2, 3, 4].into();
183    /// ```
184    #[inline(always)]
185    fn from(arr: [u8; N]) -> Self {
186        Self::new(arr)
187    }
188}
189
190// Debug is always redacted
191impl<T> fmt::Debug for Fixed<T> {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        f.write_str("[REDACTED]")
194    }
195}
196
197// Explicit Clone only — no implicit Copy
198impl<T: Clone> Clone for Fixed<T> {
199    #[inline(always)]
200    fn clone(&self) -> Self {
201        Self(self.0.clone())
202    }
203}
204
205// REMOVED: Copy impl for Fixed<[u8; N]>
206// Implicit copying of secrets is a footgun — duplication must be intentional.
207
208// Constant-time equality — only available with `conversions` feature
209#[cfg(feature = "conversions")]
210impl<const N: usize> Fixed<[u8; N]> {
211    /// Constant-time equality comparison.
212    ///
213    /// This is the **only safe way** to compare two fixed-size secrets.
214    /// Available only when the `conversions` feature is enabled.
215    ///
216    /// # Example
217    ///
218    /// ```
219    /// # #[cfg(feature = "conversions")]
220    /// # {
221    /// use secure_gate::Fixed;
222    /// let a = Fixed::new([1u8; 32]);
223    /// let b = Fixed::new([1u8; 32]);
224    /// assert!(a.ct_eq(&b));
225    /// # }
226    /// ```
227    #[inline]
228    pub fn ct_eq(&self, other: &Self) -> bool {
229        use crate::conversions::SecureConversionsExt;
230        self.expose_secret().ct_eq(other.expose_secret())
231    }
232}
233
234// Zeroize integration
235#[cfg(feature = "zeroize")]
236impl<T: zeroize::Zeroize> zeroize::Zeroize for Fixed<T> {
237    fn zeroize(&mut self) {
238        self.0.zeroize();
239    }
240}
241
242#[cfg(feature = "zeroize")]
243impl<T: zeroize::Zeroize> zeroize::ZeroizeOnDrop for Fixed<T> {}