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