secure_gate/fixed.rs
1//! Stack-allocated wrapper for fixed-size secrets.
2//!
3//! [`Fixed<T>`] is a zero-cost wrapper that enforces explicit, auditable access to
4//! sensitive data stored inline on the stack. It is the primary secret type for
5//! fixed-length material such as cryptographic keys, nonces, and seeds.
6//!
7//! # Security invariants
8//!
9//! - **No `Deref`, `AsRef`, or `Copy`** — the inner value cannot leak through
10//! implicit conversions.
11//! - **`Debug` always prints `[REDACTED]`** — secrets never appear in logs or
12//! panic messages.
13//! - **Unconditional zeroization on drop** — the inner `T` is overwritten with
14//! zeroes when the wrapper is dropped, even on error paths.
15//! - **Opt-in `Clone`** — requires `T: CloneableSecret` and the `cloneable` feature.
16//! - **Opt-in `Serialize`** — requires the `SerializableSecret` marker and the
17//! `serde-serialize`/`serde-deserialize` features.
18//! - **Avoid move-by-value for long-lived secrets.** Each move of a `Fixed<T>`
19//! bitwise-copies the bytes to a new location and leaves the original stack
20//! slot uncleared until a later frame overwrites it. Pass `&Fixed<T>` /
21//! `&mut Fixed<T>` by reference, keep the wrapper short-scope, or use
22//! [`Dynamic<T>`](crate::Dynamic) for long-lived material (heap-only — no
23//! stack residue surface). See `SECURITY.md` § "Inherent Rust Limitations".
24//!
25//! # Construction
26//!
27//! | Constructor | Notes |
28//! |---|---|
29//! | [`Fixed::new(value)`](Fixed::new) | Ergonomic default; `const fn`. |
30//! | [`Fixed::new_with(f)`](Fixed::new_with) | Scoped — preferred for stack-residue minimization. |
31//! | [`Fixed::try_new_with(f)`](Fixed::try_new_with) | Scoped, for a fill that can fail; wipes on `Err`. |
32//!
33//! Prefer [`new_with`](Fixed::new_with) in high-assurance code: it writes directly
34//! into the wrapper's storage, avoiding the intermediate stack copy that `new` may
35//! produce.
36//!
37//! # 3-tier access model
38//!
39//! ```rust
40//! use secure_gate::{Fixed, RevealSecret, RevealSecretMut};
41//!
42//! let mut secret = Fixed::new([1u8, 2, 3, 4]);
43//!
44//! // Tier 1 — scoped (preferred): borrow is confined to the closure.
45//! let sum = secret.with_secret(|arr| arr.iter().sum::<u8>());
46//! assert_eq!(sum, 10);
47//!
48//! // Tier 2 — direct: returns a reference. Use as an escape hatch.
49//! let first: u8 = secret.expose_secret()[0];
50//! assert_eq!(first, 1);
51//!
52//! // Tier 1 mutable — scoped mutation (preferred over Tier 2 mutable).
53//! secret.with_secret_mut(|arr| arr[0] = 0xFF);
54//!
55//! // Tier 3 — owned: consumes the wrapper for final use.
56//! let owned = secret.into_inner();
57//! ```
58//!
59//! # Warning: no `static` secrets
60//!
61//! `Drop` does not run on `static` items. Placing a `Fixed` in a `static` or
62//! `lazy_static!` will **skip zeroization**. Always use stack or heap allocation.
63//!
64//! Also ensure your profile sets `panic = "unwind"` — `panic = "abort"` skips
65//! destructors and therefore skips zeroization.
66//!
67//! # Import path
68//!
69//! All public items are re-exported at the crate root. Use:
70//!
71//! ```rust
72//! use secure_gate::Fixed;
73//! ```
74//!
75//! Not `secure_gate::fixed::Fixed`.
76//!
77//! # See also
78//!
79//! - [`Dynamic<T>`](crate::Dynamic) — heap-allocated alternative for variable-length
80//! secrets (passwords, API keys, ciphertexts). Requires the `alloc` feature.
81//!
82//! # Examples
83//!
84//! ```rust
85//! use secure_gate::{Fixed, RevealSecret};
86//!
87//! let secret = Fixed::new([1u8, 2, 3, 4]);
88//! let sum = secret.with_secret(|arr| arr.iter().sum::<u8>());
89//! assert_eq!(sum, 10);
90//! ```
91
92use crate::RevealSecret;
93use crate::RevealSecretMut;
94
95#[cfg(all(feature = "encoding-base32", feature = "alloc"))]
96use crate::traits::encoding::base32::ToBase32;
97#[cfg(all(feature = "encoding-base64", feature = "alloc"))]
98use crate::traits::encoding::base64_url::ToBase64Url;
99#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
100use crate::traits::encoding::bech32::ToBech32;
101#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
102use crate::traits::encoding::bech32m::ToBech32m;
103#[cfg(all(feature = "encoding-hex", feature = "alloc"))]
104use crate::traits::encoding::hex::ToHex;
105
106#[cfg(feature = "rand")]
107use rand::{TryCryptoRng, TryRng, rngs::SysRng};
108use zeroize::Zeroize;
109
110/// Drains a validated Bech32/Bech32m payload into a stack buffer.
111///
112/// Shared by the four `try_from_bech32*` constructors. Copies exactly `N`
113/// payload bytes into a `Zeroizing<[u8; N]>`; keeps counting (without storing)
114/// past `N` so the length-mismatch error reports the exact decoded length.
115/// The iteration count is bounded by the checksum-validated input string, so
116/// oversized inputs cost at most one bounded pass. Works without `alloc`.
117#[cfg(feature = "encoding-bech32")]
118fn drain_bech32_payload<const N: usize>(
119 checked: &bech32::primitives::decode::CheckedHrpstring<'_>,
120) -> Result<zeroize::Zeroizing<[u8; N]>, crate::error::Bech32Error> {
121 let mut buf = zeroize::Zeroizing::new([0u8; N]);
122 let mut count = 0usize;
123 for byte in checked.byte_iter() {
124 if count < N {
125 buf[count] = byte;
126 }
127 count += 1;
128 }
129 if count != N {
130 return Err(crate::error::Bech32Error::InvalidLength {
131 expected: N,
132 got: count,
133 });
134 }
135 Ok(buf)
136 // On the error path, buf is zeroized on drop.
137}
138
139/// Zero-cost stack-allocated wrapper for fixed-size secrets.
140///
141/// `Fixed<T>` stores a `T: Zeroize` value inline and unconditionally zeroizes it
142/// on drop. There is no `Deref`, `AsRef`, or `Copy` — every access is explicit
143/// through [`RevealSecret`] or [`RevealSecretMut`].
144///
145/// **Choose `Fixed` for short-lived secrets passed by reference.** Inline storage means a
146/// `Fixed` can live on the stack, and moving one by value copies its bytes and leaves the
147/// old slot unwiped: drop zeroizes only where the value ends up. Build it in place with
148/// [`new_with`](Self::new_with), pass `&Fixed` rather than `Fixed`, and for a secret held a
149/// long time prefer [`Dynamic`](crate::Dynamic) (one heap address) or, with the `sealed`
150/// feature, `SealedFixed`. `SECURITY.md` §1 has the details.
151///
152/// # Examples
153///
154/// ```rust
155/// use secure_gate::{Fixed, RevealSecret};
156///
157/// // Create a secret key.
158/// let key = Fixed::new([0xABu8; 32]);
159///
160/// // Scoped access — the borrow cannot escape the closure.
161/// let first = key.with_secret(|k| k[0]);
162/// assert_eq!(first, 0xAB);
163///
164/// // Debug is always redacted.
165/// assert_eq!(format!("{:?}", key), "[REDACTED]");
166/// ```
167///
168/// # Constructors
169///
170/// | Constructor | Feature | Notes |
171/// |---|---|---|
172/// | [`new(value)`](Self::new) | — | `const fn`, ergonomic default |
173/// | [`new_with(f)`](Self::new_with) | — | Scoped; preferred for stack-residue minimization |
174/// | [`try_new_with(f)`](Self::try_new_with) | — | Scoped, fallible fill; zeroizes the partial write on `Err` |
175/// | [`From<[u8; N]>`](#impl-From<%5Bu8;+N%5D>-for-Fixed<%5Bu8;+N%5D>) | — | Equivalent to `new` |
176/// | [`TryFrom<&[u8]>`](#impl-TryFrom<%26%5Bu8%5D>-for-Fixed<%5Bu8;+N%5D>) | — | Length-checked slice conversion |
177/// | [`try_from_hex`](Self::try_from_hex) | `encoding-hex` | Constant-time hex decoding |
178/// | [`try_from_base32`](Self::try_from_base32) | `encoding-base32` | Constant-time Base32 decoding |
179/// | [`try_from_base64url`](Self::try_from_base64url) | `encoding-base64` | Constant-time Base64url decoding |
180/// | [`try_from_bech32`](Self::try_from_bech32) | `encoding-bech32` | HRP-validated Bech32 decoding |
181/// | [`try_from_bech32_unchecked`](Self::try_from_bech32_unchecked) | `encoding-bech32` | Bech32 without HRP check |
182/// | [`try_from_bech32m`](Self::try_from_bech32m) | `encoding-bech32` | HRP-validated Bech32m decoding |
183/// | [`try_from_bech32m_unchecked`](Self::try_from_bech32m_unchecked) | `encoding-bech32` | Bech32m without HRP check |
184/// | [`from_random()`](Self::from_random) | `rand` | System RNG |
185/// | [`from_rng(rng)`](Self::from_rng) | `rand` | Custom RNG |
186///
187/// # What `T` may be
188///
189/// [`Fixed::new`] requires [`FixedStorage`](crate::FixedStorage) on the inner type: a
190/// type that owns no heap allocation. This is what makes the documented
191/// claim that `Fixed` has no reallocation surface true rather than aspirational. It used
192/// to be bounded only by `Zeroize`, so `Fixed<Vec<u8>>`, `Fixed<String>` and even
193/// `Fixed<[Vec<u8>; 2]>` compiled and abandoned an unwiped buffer holding the whole
194/// secret on any capacity change. Arrays, the primitives, tuples, `Option` and boxed
195/// slices satisfy the marker already; a custom inner type writes one line. For a
196/// growable payload use [`Dynamic`](crate::Dynamic), which documents the residue and
197/// offers one safe growth path.
198///
199/// The bound is on the constructor rather than on the struct, so naming
200/// `Fixed<Vec<u8>>` still compiles and no value of it can exist — the same shape as the
201/// zero-size guard below.
202///
203/// # Zero-size
204///
205/// A value of zero size has nothing to protect, so a `Fixed` cannot be built around
206/// one. [`new`](Self::new) and [`new_with`](Self::new_with) each assert a non-zero size
207/// in a `const`, and every other constructor in the table above funnels through those
208/// two, so `Fixed<[u8; 0]>` — and any other zero-sized inner type — is a compile error
209/// however it is reached. The assertion is evaluated at monomorphization rather than at
210/// the declaration, so it fires at the first concrete zero-sized construction, including
211/// one reached through generic code; generic code that is never instantiated with a
212/// zero-sized `T` is unaffected, and naming such a type without constructing one still
213/// compiles.
214///
215/// The diagnostic names the offending type in the failing constant's path — for example
216/// `Fixed::<[u8; 0]>::NON_ZERO_SIZED` — and a `while instantiating` note repeats the span of
217/// the `new` call the monomorphization reached — the error's own span is the assertion
218/// here, not in your code. The note does not name the instantiation that caused it: for a generic `fn build<const N: usize>()`, calling `build::<0>()` reports
219/// against the `Fixed::new` line inside `build`, and the `::<0>` call site appears nowhere.
220///
221/// **Two limits on when it fires, both measured.** It is raised during codegen, so
222/// `cargo check` does not report it. And it only fires for a *codegen root*: a non-generic
223/// `#[inline]` function in a library is not one, and every method the newtype macros
224/// generate carries an inline attribute, so a library whose only zero-sized constructions sit
225/// behind `#[inline]` or generic code builds, tests and publishes clean — the error then
226/// surfaces in each downstream crate that instantiates it, blaming this crate and the
227/// dependency's macro invocation rather than the consumer's call.
228///
229/// Root-ness is a property of the compiler and the profile, not of the attribute alone, so
230/// removing `#[inline]` does not reliably restore the error. Measured: on 1.85 a library
231/// exporting a plain non-generic `pub fn empty() -> Empty { Empty::new([]) }` fails
232/// `cargo build` and `cargo test` but passes `cargo build --release`, because the
233/// cross-crate-inlining heuristic added in 1.75 drops a small function from the exported
234/// root set in an optimized build; on 1.70 the same crate fails in both profiles. Treat
235/// instantiating the type in a test or a binary as the only reliable check.
236///
237/// Stated precisely: no *value* of a zero-sized `Fixed` can exist at runtime, because
238/// nothing can construct one, and a binary or test that tries fails to compile. The guard
239/// does not stop a library from exporting an unusable zero-sized API. Neither limit is a
240/// choice — a condition on a generic parameter has nothing to evaluate until that parameter
241/// is known, and nothing on stable Rust moves it earlier.
242///
243/// [`Dynamic`](crate::Dynamic) has no compile-time counterpart, for a reason about its
244/// payload rather than its own size: whether a `Vec` or `String` is empty is a runtime
245/// property, so there is nothing for a compile-time check to decide. A statically
246/// zero-sized inner type is the gap that leaves — `Dynamic<Zst>` constructs where
247/// `Fixed<Zst>` does not.
248///
249/// # RustCrypto integration
250///
251/// RustCrypto's `BlockEncrypt`/`BlockDecrypt` take `&mut GenericArray<u8, U16>`, and
252/// `GenericArray::from_mut_slice` **borrows** its argument instead of copying it. That
253/// is what lets a block cipher run *inside* the wrapper, so the plaintext never
254/// materializes in memory nothing is obliged to wipe:
255///
256/// ```rust
257/// use aes::cipher::{generic_array::GenericArray, BlockDecrypt, KeyInit};
258/// use aes::Aes128;
259/// use secure_gate::{Fixed, RevealSecretMut};
260///
261/// let cipher = Aes128::new(GenericArray::from_slice(&[0x42u8; 16]));
262/// let mut block: Fixed<[u8; 16]> = Fixed::new([0u8; 16]);
263///
264/// // `from_mut_slice` reborrows the wrapper's own storage, so zeroize-on-drop still
265/// // covers every path out of this scope — early returns and panics included.
266/// block.with_secret_mut(|b| cipher.decrypt_block(GenericArray::from_mut_slice(b)));
267/// ```
268///
269/// The shape type inference nudges you toward is the footgun. It compiles, it is the
270/// easy thing to reach for, and it defeats the wrapper:
271///
272/// ```rust
273/// # use aes::cipher::{generic_array::GenericArray, BlockDecrypt, KeyInit};
274/// # use aes::Aes128;
275/// # use secure_gate::{Fixed, RevealSecret};
276/// # let cipher = Aes128::new(GenericArray::from_slice(&[0x42u8; 16]));
277/// # let block: Fixed<[u8; 16]> = Fixed::new([0u8; 16]);
278/// // Copies the secret OUT. `copy` is an ordinary stack value: no zeroization on
279/// // drop, and it holds plaintext-equivalent bytes for the rest of the scope.
280/// let mut copy = block.with_secret(|b| aes::Block::from(*b));
281/// cipher.decrypt_block(&mut copy);
282/// ```
283///
284/// The same reasoning applies to [`Dynamic`](crate::Dynamic)`<Vec<u8>>` through
285/// [`with_secret_mut`](crate::RevealSecretMut::with_secret_mut), and to the
286/// slice-at-a-time APIs (`decrypt_blocks`, `apply_keystream`), which take
287/// `&mut [Block]` and `&mut [u8]` and so borrow just as well.
288///
289/// # See also
290///
291/// - [`RevealSecret`] / [`RevealSecretMut`] — the 3-tier access traits.
292/// - [`new_with`](Self::new_with) — scoped constructor preferred over [`new`](Self::new).
293///
294/// # Note
295///
296/// `const fn new` compiles in `static` position, but **must not** be used there
297/// because `Drop` does not run on statics, which means zeroization is skipped.
298#[must_use = "Fixed<T> holds secret material; dropping it on the floor usually indicates a bug — bind with `let _name = ...` or chain a method call"]
299pub struct Fixed<T: zeroize::Zeroize> {
300 inner: T,
301}
302
303impl<T: zeroize::Zeroize> Fixed<T> {
304 // Post-monomorphization guard: a zero-sized `T` has nothing to protect. Fires at the
305 // first concrete construction, never at the declaration. An associated `const` rather
306 // than an inline `const {}` block so the same hunk builds on the 0.8 line's MSRV 1.70.
307 const NON_ZERO_SIZED: () = assert!(
308 ::core::mem::size_of::<T>() > 0,
309 "secure-gate: Fixed<T> cannot hold a zero-sized value; there is nothing to protect"
310 );
311
312 /// Creates a new [`Fixed<T>`] by wrapping a value.
313 ///
314 /// This is a `const fn`, so it can be evaluated at compile time. However,
315 /// **do not** use it to initialize `static` items — `Drop` does not run on
316 /// statics, so zeroization would be skipped.
317 ///
318 /// Prefer [`new_with`](Fixed::new_with) when `T: `[`SentinelValue`](crate::SentinelValue)
319 /// and minimizing stack residue matters: `new` may leave an intermediate copy
320 /// of `value` on the caller's stack frame.
321 ///
322 /// A zero-sized `T` is rejected at compile time; see [Zero-size](#zero-size).
323 ///
324 /// # Examples
325 ///
326 /// ```rust
327 /// use secure_gate::{Fixed, RevealSecret, SecretLen};
328 ///
329 /// let secret = Fixed::new([0u8; 32]);
330 /// assert_eq!(secret.len(), 32);
331 /// ```
332 #[inline(always)]
333 pub const fn new(value: T) -> Self
334 where
335 T: crate::FixedStorage,
336 {
337 // Binding the unit-valued const is what forces it to be evaluated; clippy reads
338 // that as a pointless binding. Kept explicit because the 1.70 lint (the 0.8
339 // line's MSRV) fires on every spelling that still triggers the evaluation.
340 #[allow(clippy::let_unit_value)]
341 let () = Self::NON_ZERO_SIZED;
342 Fixed { inner: value }
343 }
344
345 /// Writes directly into the wrapper's storage via a user-supplied closure,
346 /// eliminating the intermediate stack copy that [`new`](Self::new) may produce.
347 ///
348 /// The slot is initialized to [`T::sentinel_value()`](crate::SentinelValue::sentinel_value)
349 /// before the closure runs. Prefer this over [`new(value)`](Self::new) when
350 /// minimizing stack residue matters (long-lived keys, high-assurance environments).
351 ///
352 /// # The slot's starting content is a contract, not an accident
353 ///
354 /// For `T = [E; N]` with `E: Default` — every array this crate ever hands you —
355 /// [`SentinelValue`](crate::SentinelValue)'s own contract fixes that starting value
356 /// at `[E::default(); N]`, all-zero for the byte and integer arrays this type is
357 /// built around. That is a promise `SentinelValue` makes on purpose (see its
358 /// "Provided implementations" contract), not a side effect of how the sentinel
359 /// happens to be built today, so a caller may depend on it exactly as it depends on
360 /// any other documented behavior.
361 ///
362 /// Concretely: a closure that writes fewer than `N` elements — a
363 /// [`SlotWriter`](crate::SlotWriter) doing RFC 9180-style concatenation that falls
364 /// short of the slot, a key derivation shorter than its buffer — leaves the
365 /// untouched tail at zero rather than at whatever bytes happened to be on the
366 /// stack. Code that treats that tail as zero-padding, instead of re-zeroing it
367 /// itself before returning, is relying on this guarantee; the
368 /// [`SlotWriter`](crate::SlotWriter) documentation's "40-bit key, zero-padded to
369 /// 16" example is exactly that reliance, asserted in a doctest.
370 ///
371 /// Requires [`FixedStorage`](crate::FixedStorage) and
372 /// [`SentinelValue`](crate::SentinelValue) on `T`. Arrays of a `Default`
373 /// element already have both; a custom inner type needs the same pair
374 /// `into_inner` already asks for.
375 ///
376 /// # `T` must be inferable at the call site
377 ///
378 /// This constructor is generic over `T`, so the closure's parameter type is
379 /// not known until `T` is. A return type alone does not resolve it in time:
380 /// `fn k() -> Fixed<[u8; 32]> { Fixed::new_with(|a| a.fill(1)) }` fails with
381 /// **E0282**, `type annotations needed for &mut _`, because the closure body
382 /// is checked before the expected return type reaches `T`. Name the type in
383 /// either position — both compile, pick whichever reads better:
384 ///
385 /// ```rust
386 /// use secure_gate::Fixed;
387 ///
388 /// // On the type:
389 /// let a = Fixed::<[u8; 32]>::new_with(|x| x.fill(1));
390 /// // Or on the closure parameter:
391 /// let b = Fixed::new_with(|x: &mut [u8; 32]| x.fill(1));
392 /// # let _ = (a, b);
393 /// ```
394 ///
395 /// Before 0.9.0-rc.10 this method was inherent to `Fixed<[u8; N]>`, where the
396 /// parameter was always a byte array and inference had nothing to resolve. A
397 /// newtype built by [`fixed_newtype!`](crate::fixed_newtype) is unaffected in
398 /// both arms: its generated `new_with` has a concrete parameter type.
399 ///
400 /// # Security rationale
401 ///
402 /// With [`Fixed::new(value)`](Self::new), the caller first builds `value` on
403 /// its own stack frame, then moves it into the wrapper. The compiler *may*
404 /// elide the copy, but this is not guaranteed — leaving a plaintext residue
405 /// on the stack. `new_with` avoids this by giving the closure a mutable
406 /// reference to the wrapper's *own* storage, so the secret is never placed
407 /// anywhere else.
408 ///
409 /// # Examples
410 ///
411 /// ```rust
412 /// use secure_gate::{Fixed, RevealSecret};
413 ///
414 /// // Fill from a closure — no intermediate stack copy.
415 /// let secret = Fixed::<[u8; 4]>::new_with(|arr| arr.fill(0xAB));
416 /// assert_eq!(secret.expose_secret(), &[0xAB; 4]);
417 ///
418 /// // Copy from an existing slice.
419 /// let src = [1u8, 2, 3, 4];
420 /// let secret = Fixed::<[u8; 4]>::new_with(|arr| arr.copy_from_slice(&src));
421 /// ```
422 ///
423 /// A non-byte array uses the same constructor:
424 ///
425 /// ```rust
426 /// use secure_gate::{Fixed, RevealSecret};
427 ///
428 /// let poly = Fixed::<[i16; 4]>::new_with(|c| c.fill(7));
429 /// assert_eq!(poly.expose_secret(), &[7i16; 4]);
430 /// ```
431 ///
432 /// # See also
433 ///
434 /// - [`try_new_with`](Self::try_new_with) — the same thing when the fill can
435 /// fail, rather than routing the error out through a captured local.
436 /// - [`Dynamic::new_with`](crate::Dynamic::new_with) — the heap-allocated
437 /// equivalent (requires `alloc`).
438 ///
439 /// If the secret will outlive the current function, prefer
440 /// [`Dynamic<T>`](crate::Dynamic) over moving `Fixed<T>` around — each
441 /// move-by-value leaves residue in the previous stack slot.
442 #[inline(always)]
443 pub fn new_with<F>(f: F) -> Self
444 where
445 T: crate::FixedStorage + crate::SentinelValue,
446 F: FnOnce(&mut T),
447 {
448 // Binding the unit-valued const is what forces it to be evaluated; clippy reads
449 // that as a pointless binding. Kept explicit because the 1.70 lint (the 0.8
450 // line's MSRV) fires on every spelling that still triggers the evaluation.
451 #[allow(clippy::let_unit_value)]
452 let () = Self::NON_ZERO_SIZED;
453 let mut this = Self {
454 inner: <T as crate::SentinelValue>::sentinel_value(),
455 };
456 f(&mut this.inner);
457 this
458 }
459
460 /// Writes directly into the wrapper's storage via a closure that may fail.
461 ///
462 /// The fallible sibling of [`new_with`](Self::new_with), and identical to it in
463 /// every respect but the closure's return type. Reach for it whenever filling the
464 /// secret can fail — reading from a file, a socket, a device — so that writing in
465 /// place and handling the error stay one expression.
466 ///
467 /// On `Err` the partially written slot is **zeroized before the error is
468 /// returned**: the wrapper exists for the whole of the closure's run, so
469 /// [`Drop`](Fixed#impl-Drop-for-Fixed<T>) wipes whatever the closure managed to
470 /// write. A failed fill leaves nothing behind, in memory or in the return value.
471 ///
472 /// On `Ok`, the slot the closure saw started at the same value
473 /// [`new_with`](Self::new_with)'s does —
474 /// [`T::sentinel_value()`](crate::SentinelValue::sentinel_value), which for `T =
475 /// [E; N]` with `E: Default` is `[E::default(); N]`, all-zero for the byte and
476 /// integer arrays this type is built around. That starting value is a contract of
477 /// [`SentinelValue`](crate::SentinelValue)'s, documented on `new_with` above, not an
478 /// incidental one — so a closure here that only partially fills the slot before
479 /// returning `Ok` leaves the same reliable zero tail behind, on the same guarantee.
480 ///
481 /// Requires [`FixedStorage`](crate::FixedStorage) and
482 /// [`SentinelValue`](crate::SentinelValue) on `T`, exactly as `new_with` does.
483 ///
484 /// # Why `try_`, given the rest of this type
485 ///
486 /// Elsewhere here `try_` marks a constructor that *parses* untrusted input —
487 /// [`try_from_hex`](Self::try_from_hex), [`try_from_base32`](Self::try_from_base32),
488 /// `TryFrom<&[u8]>` — while [`from_rng`](Self::from_rng) is fallible and keeps
489 /// `from_`. This method widens the prefix to its ordinary Rust meaning, the one
490 /// `try_reserve` and `try_lock` carry: **`try_` marks a constructor that can fail**,
491 /// whether because it validates input or because the caller's fill can.
492 ///
493 /// # `T` must be inferable at the call site
494 ///
495 /// Generic over `T`, so the closure's parameter type is not known until `T` is —
496 /// the same constraint [`new_with`](Self::new_with) documents, with the same
497 /// **E0282** if nothing names it. A return type alone does not resolve it. Name
498 /// the type in either position:
499 ///
500 /// ```rust
501 /// use secure_gate::Fixed;
502 /// # #[derive(Debug)] struct ShortRead;
503 ///
504 /// // On the type:
505 /// let a = Fixed::<[u8; 32]>::try_new_with(|x| { x.fill(1); Ok::<(), ShortRead>(()) })?;
506 /// // Or on the closure parameter:
507 /// let b = Fixed::try_new_with(|x: &mut [u8; 32]| { x.fill(1); Ok::<(), ShortRead>(()) })?;
508 /// # let _ = (a, b);
509 /// # Ok::<(), ShortRead>(())
510 /// ```
511 ///
512 /// # Errors
513 ///
514 /// Returns whatever the closure returns. The error type is the caller's; this
515 /// method neither inspects nor wraps it.
516 ///
517 /// # Examples
518 ///
519 /// Filling from a reader, which is the case this exists for — the bytes land in
520 /// the wrapper's own storage and never occupy a caller-owned buffer:
521 ///
522 /// ```rust
523 /// # #[cfg(feature = "std")] {
524 /// use secure_gate::{Fixed, RevealSecret};
525 /// use std::io::Read;
526 ///
527 /// fn read_key<R: Read>(r: &mut R) -> std::io::Result<Fixed<[u8; 32]>> {
528 /// Fixed::<[u8; 32]>::try_new_with(|slot| r.read_exact(slot))
529 /// }
530 ///
531 /// let key = read_key(&mut [7u8; 32].as_slice()).unwrap();
532 /// assert_eq!(key.expose_secret(), &[7u8; 32]);
533 ///
534 /// // A short read fails, and yields no secret at all.
535 /// assert!(read_key(&mut [7u8; 8].as_slice()).is_err());
536 /// # }
537 /// ```
538 ///
539 /// # See also
540 ///
541 /// - [`new_with`](Self::new_with) — the infallible form; prefer it when the fill
542 /// cannot fail.
543 #[inline(always)]
544 pub fn try_new_with<F, E>(f: F) -> Result<Self, E>
545 where
546 T: crate::FixedStorage + crate::SentinelValue,
547 F: FnOnce(&mut T) -> Result<(), E>,
548 {
549 // Binding the unit-valued const is what forces it to be evaluated; clippy reads
550 // that as a pointless binding. Kept explicit because the 1.70 lint (the 0.8
551 // line's MSRV) fires on every spelling that still triggers the evaluation.
552 #[allow(clippy::let_unit_value)]
553 let () = Self::NON_ZERO_SIZED;
554 let mut this = Self {
555 inner: <T as crate::SentinelValue>::sentinel_value(),
556 };
557 // On the error path `this` is dropped here, so `Fixed::drop` zeroizes whatever
558 // the closure wrote before it gave up. That is the whole reason the wrapper is
559 // built before the fill rather than after it.
560 f(&mut this.inner)?;
561 Ok(this)
562 }
563}
564
565/// Converts a byte array into a [`Fixed`] wrapper (equivalent to [`Fixed::new`]).
566///
567/// # Examples
568///
569/// ```rust
570/// use secure_gate::Fixed;
571///
572/// let secret: Fixed<[u8; 4]> = [1u8, 2, 3, 4].into();
573/// ```
574impl<const N: usize> From<[u8; N]> for Fixed<[u8; N]> {
575 #[inline(always)]
576 fn from(arr: [u8; N]) -> Self {
577 Self::new(arr)
578 }
579}
580
581/// Converts a byte slice into `Fixed<[u8; N]>`, failing if the length does not
582/// match `N`.
583///
584/// Internally uses [`Fixed::new_with`] so the secret is written directly into
585/// the wrapper's storage.
586///
587/// # Errors
588///
589/// Returns [`FromSliceError::InvalidLength`](crate::error::FromSliceError) when
590/// `slice.len() != N`.
591///
592/// # Examples
593///
594/// ```rust
595/// use secure_gate::{Fixed, RevealSecret};
596///
597/// // Success — exact length.
598/// let data = [0xFFu8; 4];
599/// let secret = Fixed::<[u8; 4]>::try_from(data.as_slice()).unwrap();
600/// assert_eq!(secret.expose_secret()[0], 0xFF);
601///
602/// // Failure — wrong length.
603/// let short = [0u8; 2];
604/// assert!(Fixed::<[u8; 4]>::try_from(short.as_slice()).is_err());
605/// ```
606impl<const N: usize> core::convert::TryFrom<&[u8]> for Fixed<[u8; N]> {
607 type Error = crate::error::FromSliceError;
608
609 fn try_from(slice: &[u8]) -> Result<Self, Self::Error> {
610 if slice.len() != N {
611 return Err(crate::error::FromSliceError::InvalidLength {
612 expected: N,
613 got: slice.len(),
614 });
615 }
616 Ok(Self::new_with(|arr| arr.copy_from_slice(slice)))
617 }
618}
619
620/// Hex encoding and decoding for `Fixed<[u8; N]>`.
621///
622/// Encoding uses a constant-time backend (`base16ct`). Decoding works with or without
623/// the `alloc` feature — on no-alloc targets the bytes are decoded directly into a
624/// `Zeroizing<[u8; N]>` stack buffer.
625#[cfg(feature = "encoding-hex")]
626impl<const N: usize> Fixed<[u8; N]> {
627 /// Decodes a hex string (lowercase, uppercase, or mixed) into `Fixed<[u8; N]>`.
628 ///
629 /// Uses a constant-time backend (`base16ct`) for both paths.
630 ///
631 /// - **With `alloc`**: decodes into a `Zeroizing<Vec<u8>>` then copies onto the stack.
632 /// The temporary heap buffer is zeroed on drop even if an error occurs.
633 /// - **Without `alloc`**: decodes directly into a `Zeroizing<[u8; N]>` stack buffer.
634 /// No heap allocation occurs.
635 ///
636 /// # Errors
637 ///
638 /// - [`HexError::InvalidHex`](crate::HexError::InvalidHex) — non-hex characters or odd-length input.
639 /// - [`HexError::InvalidLength`](crate::HexError::InvalidLength) — decoded byte count does not equal `N`.
640 ///
641 /// # Examples
642 ///
643 /// ```rust
644 /// # #[cfg(feature = "encoding-hex")]
645 /// # {
646 /// use secure_gate::{Fixed, RevealSecret, ToHex};
647 ///
648 /// // Round-trip: encode then decode.
649 /// let original = Fixed::new([0xDE, 0xAD, 0xBE, 0xEF]);
650 /// # #[cfg(feature = "alloc")]
651 /// # {
652 /// let hex_str = original.to_hex();
653 /// let decoded = Fixed::<[u8; 4]>::try_from_hex(&hex_str).unwrap();
654 /// assert_eq!(decoded.expose_secret(), &[0xDE, 0xAD, 0xBE, 0xEF]);
655 /// # }
656 ///
657 /// // Wrong length fails.
658 /// assert!(Fixed::<[u8; 2]>::try_from_hex("deadbeef").is_err());
659 /// # }
660 /// ```
661 pub fn try_from_hex(hex: &str) -> Result<Self, crate::error::HexError> {
662 #[cfg(feature = "alloc")]
663 {
664 use zeroize::Zeroizing;
665 let bytes = Zeroizing::new(
666 base16ct::mixed::decode_vec(hex.as_bytes())
667 .map_err(|_| crate::error::HexError::InvalidHex)?,
668 );
669 if bytes.len() != N {
670 return Err(crate::error::HexError::InvalidLength {
671 expected: N,
672 got: bytes.len(),
673 });
674 }
675 Ok(Self::new_with(|arr| arr.copy_from_slice(&bytes)))
676 }
677 #[cfg(not(feature = "alloc"))]
678 {
679 use zeroize::Zeroizing;
680 // no-alloc path: decode directly into a stack buffer; no heap allocation
681 // base16ct::mixed accepts upper, lower, and mixed-case hex
682 let mut buf = Zeroizing::new([0u8; N]);
683 let decoded = base16ct::mixed::decode(hex.as_bytes(), &mut *buf)
684 .map_err(|_| crate::error::HexError::InvalidHex)?;
685 if decoded.len() != N {
686 return Err(crate::error::HexError::InvalidLength {
687 expected: N,
688 got: decoded.len(),
689 });
690 }
691 Ok(Self::new_with(|arr| arr.copy_from_slice(decoded)))
692 // buf is zeroized on drop (both success and error paths)
693 }
694 }
695}
696
697/// Base32 encoding and decoding for `Fixed<[u8; N]>` (RFC 4648 §6, uppercase, unpadded).
698///
699/// Encoding uses a constant-time backend (`base32ct`). Decoding works with or without
700/// the `alloc` feature — on no-alloc targets the bytes are decoded directly into a
701/// `Zeroizing<[u8; N]>` stack buffer.
702#[cfg(feature = "encoding-base32")]
703impl<const N: usize> Fixed<[u8; N]> {
704 /// Decodes an uppercase, unpadded Base32 string (RFC 4648 §6 alphabet) into
705 /// `Fixed<[u8; N]>`.
706 ///
707 /// Uses a constant-time backend (`base32ct`) on both paths.
708 ///
709 /// - **With `alloc`**: decodes into a `Zeroizing<Vec<u8>>` then copies onto the stack.
710 /// - **Without `alloc`**: decodes directly into a `Zeroizing<[u8; N]>` stack buffer.
711 ///
712 /// Decoding is strict about the alphabet, case, padding and length, but lenient
713 /// about non-canonical trailing bits: unused bits in the final group are ignored
714 /// rather than rejected, so `"MZ"` and `"MY"` both decode to `[0x66]`. Decoding is
715 /// therefore not injective — two distinct strings can yield the same bytes, and
716 /// only encoder-produced strings are canonical. Do not use the decoded value to
717 /// decide that two encoded strings were equal.
718 ///
719 /// # Errors
720 ///
721 /// - [`Base32Error::InvalidBase32`](crate::Base32Error::InvalidBase32) — wrong alphabet
722 /// or case, `=` padding, or a length that is impossible for unpadded Base32. Without
723 /// `alloc` an input that decodes to *more* than `N` bytes also lands here rather than in
724 /// `InvalidLength`, because the fixed stack buffer cannot hold the overflow to measure it.
725 /// - [`Base32Error::InvalidLength`](crate::Base32Error::InvalidLength) — decoded byte
726 /// count does not equal `N`.
727 ///
728 /// # Examples
729 ///
730 /// ```rust
731 /// # #[cfg(feature = "encoding-base32")]
732 /// # {
733 /// use secure_gate::{Fixed, RevealSecret, ToBase32};
734 ///
735 /// # #[cfg(feature = "alloc")]
736 /// # {
737 /// // Round-trip.
738 /// let original = Fixed::new([0xDE, 0xAD, 0xBE, 0xEF]);
739 /// let encoded = original.to_base32();
740 /// assert_eq!(&*encoded, "32W353Y");
741 /// let decoded = Fixed::<[u8; 4]>::try_from_base32(&encoded).unwrap();
742 /// assert_eq!(decoded.expose_secret(), &[0xDE, 0xAD, 0xBE, 0xEF]);
743 /// # }
744 /// # }
745 /// ```
746 pub fn try_from_base32(s: &str) -> Result<Self, crate::error::Base32Error> {
747 #[cfg(feature = "alloc")]
748 {
749 use base32ct::{Base32UpperUnpadded, Encoding};
750 use zeroize::Zeroizing;
751 let bytes = Zeroizing::new(
752 Base32UpperUnpadded::decode_vec(s)
753 .map_err(|_| crate::error::Base32Error::InvalidBase32)?,
754 );
755 if bytes.len() != N {
756 return Err(crate::error::Base32Error::InvalidLength {
757 expected: N,
758 got: bytes.len(),
759 });
760 }
761 Ok(Self::new_with(|arr| arr.copy_from_slice(&bytes)))
762 }
763 #[cfg(not(feature = "alloc"))]
764 {
765 use base32ct::{Base32UpperUnpadded, Encoding};
766 use zeroize::Zeroizing;
767 let mut buf = Zeroizing::new([0u8; N]);
768 let decoded = Base32UpperUnpadded::decode(s, &mut *buf)
769 .map_err(|_| crate::error::Base32Error::InvalidBase32)?;
770 if decoded.len() != N {
771 return Err(crate::error::Base32Error::InvalidLength {
772 expected: N,
773 got: decoded.len(),
774 });
775 }
776 Ok(Self::new_with(|arr| arr.copy_from_slice(decoded)))
777 // buf is zeroized on drop (both success and error paths)
778 }
779 }
780}
781
782/// Base64url encoding and decoding for `Fixed<[u8; N]>`.
783///
784/// Encoding uses a constant-time backend (`base64ct`). Decoding works with or without
785/// the `alloc` feature — on no-alloc targets the bytes are decoded directly into a
786/// `Zeroizing<[u8; N]>` stack buffer.
787#[cfg(feature = "encoding-base64")]
788impl<const N: usize> Fixed<[u8; N]> {
789 /// Decodes an unpadded Base64url string (RFC 4648, URL-safe alphabet) into
790 /// `Fixed<[u8; N]>`.
791 ///
792 /// Uses a constant-time backend (`base64ct`) on both paths.
793 ///
794 /// - **With `alloc`**: decodes into a `Zeroizing<Vec<u8>>` then copies onto the stack.
795 /// - **Without `alloc`**: decodes directly into a `Zeroizing<[u8; N]>` stack buffer.
796 ///
797 /// # Errors
798 ///
799 /// - [`Base64Error::InvalidBase64`](crate::Base64Error::InvalidBase64) — non-base64 characters or invalid padding.
800 /// - [`Base64Error::InvalidLength`](crate::Base64Error::InvalidLength) — decoded byte count does not equal `N`.
801 ///
802 /// # Examples
803 ///
804 /// ```rust
805 /// # #[cfg(feature = "encoding-base64")]
806 /// # {
807 /// use secure_gate::{Fixed, RevealSecret, ToBase64Url};
808 ///
809 /// # #[cfg(feature = "alloc")]
810 /// # {
811 /// // Round-trip.
812 /// let original = Fixed::new([0xDE, 0xAD, 0xBE, 0xEF]);
813 /// let encoded = original.to_base64url();
814 /// let decoded = Fixed::<[u8; 4]>::try_from_base64url(&encoded).unwrap();
815 /// assert_eq!(decoded.expose_secret(), &[0xDE, 0xAD, 0xBE, 0xEF]);
816 /// # }
817 /// # }
818 /// ```
819 pub fn try_from_base64url(s: &str) -> Result<Self, crate::error::Base64Error> {
820 #[cfg(feature = "alloc")]
821 {
822 use base64ct::{Base64UrlUnpadded, Encoding};
823 use zeroize::Zeroizing;
824 let bytes = Zeroizing::new(
825 Base64UrlUnpadded::decode_vec(s)
826 .map_err(|_| crate::error::Base64Error::InvalidBase64)?,
827 );
828 if bytes.len() != N {
829 return Err(crate::error::Base64Error::InvalidLength {
830 expected: N,
831 got: bytes.len(),
832 });
833 }
834 Ok(Self::new_with(|arr| arr.copy_from_slice(&bytes)))
835 }
836 #[cfg(not(feature = "alloc"))]
837 {
838 use base64ct::{Base64UrlUnpadded, Encoding};
839 use zeroize::Zeroizing;
840 let mut buf = Zeroizing::new([0u8; N]);
841 let decoded = Base64UrlUnpadded::decode(s, &mut *buf)
842 .map_err(|_| crate::error::Base64Error::InvalidBase64)?;
843 if decoded.len() != N {
844 return Err(crate::error::Base64Error::InvalidLength {
845 expected: N,
846 got: decoded.len(),
847 });
848 }
849 Ok(Self::new_with(|arr| arr.copy_from_slice(decoded)))
850 // buf is zeroized on drop (both success and error paths)
851 }
852 }
853}
854
855/// Bech32 (BIP-173) encoding and decoding for `Fixed<[u8; N]>`.
856///
857/// The plain constructors accept strings up to
858/// [`BECH32_CODE_LENGTH`](crate::BECH32_CODE_LENGTH); the `_sized::<C>` constructors
859/// take the code length as a parameter for larger payloads. For Bitcoin address formats
860/// use `ToBech32m` and stay within BIP-173's 90-character convention.
861#[cfg(feature = "encoding-bech32")]
862impl<const N: usize> Fixed<[u8; N]> {
863 /// Decodes a Bech32 (BIP-173) string into `Fixed<[u8; N]>`, validating that the HRP
864 /// matches `expected_hrp` (case-insensitive).
865 ///
866 /// HRP comparison is non-constant-time — this is intentional, as the HRP is public
867 /// metadata, not secret material. Timing leaks on HRP mismatch are acceptable because
868 /// the HRP is not secret. Prefer this over
869 /// [`try_from_bech32_unchecked`](Self::try_from_bech32_unchecked) to prevent
870 /// cross-protocol confusion attacks.
871 ///
872 /// Works without `alloc` — decodes into a stack-allocated `Zeroizing<[u8; N]>` buffer.
873 pub fn try_from_bech32(s: &str, expected_hrp: &str) -> Result<Self, crate::error::Bech32Error> {
874 Self::try_from_bech32_sized::<{ crate::BECH32_CODE_LENGTH }>(s, expected_hrp)
875 }
876
877 /// Like [`try_from_bech32`](Self::try_from_bech32), accepting strings up to `C`
878 /// characters.
879 ///
880 /// `C` is the bech32 *code length* -- the cap on the whole encoded string -- and is
881 /// independent of `N`, the decoded byte count this wrapper holds. Pass the `C` the
882 /// string was encoded with, or any larger value. See
883 /// [`Bech32Sized`](crate::Bech32Sized) for what `C` above
884 /// [`BECH32_CODE_LENGTH`](crate::BECH32_CODE_LENGTH) costs.
885 ///
886 /// Works without `alloc` -- decodes into a stack-allocated `Zeroizing<[u8; N]>` buffer.
887 pub fn try_from_bech32_sized<const C: usize>(
888 s: &str,
889 expected_hrp: &str,
890 ) -> Result<Self, crate::error::Bech32Error> {
891 use crate::traits::encoding::bech32::Bech32Sized;
892 use bech32::primitives::decode::CheckedHrpstring;
893 let checked = CheckedHrpstring::new::<Bech32Sized<C>>(s)
894 .map_err(|_| crate::error::Bech32Error::OperationFailed)?;
895 // HRP check before any payload byte is materialized (case-insensitive
896 // comparison — timing leak is acceptable since HRP is public metadata)
897 if !checked.hrp().as_str().eq_ignore_ascii_case(expected_hrp) {
898 return Err(crate::error::Bech32Error::UnexpectedHrp);
899 }
900 let buf = drain_bech32_payload::<N>(&checked)?;
901 Ok(Self::new_with(|arr| arr.copy_from_slice(&*buf)))
902 // buf is zeroized on drop
903 }
904
905 /// Decodes a Bech32 (BIP-173) string into `Fixed<[u8; N]>` without validating the HRP.
906 ///
907 /// Any valid HRP is accepted as long as the checksum is valid and the payload length
908 /// equals `N`. Use [`try_from_bech32`](Self::try_from_bech32) in security-critical code
909 /// to prevent cross-protocol confusion attacks.
910 ///
911 /// Works without `alloc` — decodes into a stack-allocated `Zeroizing<[u8; N]>` buffer.
912 pub fn try_from_bech32_unchecked(s: &str) -> Result<Self, crate::error::Bech32Error> {
913 Self::try_from_bech32_unchecked_sized::<{ crate::BECH32_CODE_LENGTH }>(s)
914 }
915
916 /// Like [`try_from_bech32_unchecked`](Self::try_from_bech32_unchecked), accepting
917 /// strings up to `C` characters.
918 pub fn try_from_bech32_unchecked_sized<const C: usize>(
919 s: &str,
920 ) -> Result<Self, crate::error::Bech32Error> {
921 use crate::traits::encoding::bech32::Bech32Sized;
922 use bech32::primitives::decode::CheckedHrpstring;
923 let checked = CheckedHrpstring::new::<Bech32Sized<C>>(s)
924 .map_err(|_| crate::error::Bech32Error::OperationFailed)?;
925 let buf = drain_bech32_payload::<N>(&checked)?;
926 Ok(Self::new_with(|arr| arr.copy_from_slice(&*buf)))
927 // buf is zeroized on drop
928 }
929}
930
931/// Bech32m (BIP-350) encoding and decoding for `Fixed<[u8; N]>`.
932///
933/// The plain constructors accept strings up to
934/// [`BECH32_CODE_LENGTH`](crate::BECH32_CODE_LENGTH); the `_sized::<C>` constructors
935/// take the code length as a parameter. Bitcoin address tooling expects BIP-173's
936/// 90-character cap, which is narrower than either and is the caller's to respect.
937#[cfg(feature = "encoding-bech32")]
938impl<const N: usize> Fixed<[u8; N]> {
939 /// Decodes a Bech32m (BIP-350) string into `Fixed<[u8; N]>`, validating that the HRP
940 /// matches `expected_hrp` (case-insensitive).
941 ///
942 /// HRP comparison is non-constant-time — this is intentional, as the HRP is public
943 /// metadata, not secret material. Timing leaks on HRP mismatch are acceptable because
944 /// the HRP is not secret. Prefer this over
945 /// [`try_from_bech32m_unchecked`](Self::try_from_bech32m_unchecked) to prevent
946 /// cross-protocol confusion attacks.
947 ///
948 /// Works without `alloc` — decodes into a stack-allocated `Zeroizing<[u8; N]>` buffer.
949 pub fn try_from_bech32m(
950 s: &str,
951 expected_hrp: &str,
952 ) -> Result<Self, crate::error::Bech32Error> {
953 Self::try_from_bech32m_sized::<{ crate::BECH32_CODE_LENGTH }>(s, expected_hrp)
954 }
955
956 /// Like [`try_from_bech32m`](Self::try_from_bech32m), accepting strings up to `C`
957 /// characters.
958 ///
959 /// `C` is the bech32m *code length*, independent of `N`, the decoded byte count.
960 /// See [`Bech32mSized`](crate::Bech32mSized) for what `C` above
961 /// [`BECH32_CODE_LENGTH`](crate::BECH32_CODE_LENGTH) costs.
962 ///
963 /// Works without `alloc` -- decodes into a stack-allocated `Zeroizing<[u8; N]>` buffer.
964 pub fn try_from_bech32m_sized<const C: usize>(
965 s: &str,
966 expected_hrp: &str,
967 ) -> Result<Self, crate::error::Bech32Error> {
968 use crate::traits::encoding::bech32m::Bech32mSized;
969 use bech32::primitives::decode::CheckedHrpstring;
970 let checked = CheckedHrpstring::new::<Bech32mSized<C>>(s)
971 .map_err(|_| crate::error::Bech32Error::OperationFailed)?;
972 // HRP check before any payload byte is materialized (case-insensitive
973 // comparison — timing leak is acceptable since HRP is public metadata)
974 if !checked.hrp().as_str().eq_ignore_ascii_case(expected_hrp) {
975 return Err(crate::error::Bech32Error::UnexpectedHrp);
976 }
977 let buf = drain_bech32_payload::<N>(&checked)?;
978 Ok(Self::new_with(|arr| arr.copy_from_slice(&*buf)))
979 // buf is zeroized on drop
980 }
981
982 /// Decodes a Bech32m (BIP-350) string into `Fixed<[u8; N]>` without validating the HRP.
983 ///
984 /// Any valid HRP is accepted as long as the checksum is valid and the payload length
985 /// equals `N`. Use [`try_from_bech32m`](Self::try_from_bech32m) in security-critical
986 /// code to prevent cross-protocol confusion attacks.
987 ///
988 /// Works without `alloc` — decodes into a stack-allocated `Zeroizing<[u8; N]>` buffer.
989 pub fn try_from_bech32m_unchecked(s: &str) -> Result<Self, crate::error::Bech32Error> {
990 Self::try_from_bech32m_unchecked_sized::<{ crate::BECH32_CODE_LENGTH }>(s)
991 }
992
993 /// Like [`try_from_bech32m_unchecked`](Self::try_from_bech32m_unchecked), accepting
994 /// strings up to `C` characters.
995 pub fn try_from_bech32m_unchecked_sized<const C: usize>(
996 s: &str,
997 ) -> Result<Self, crate::error::Bech32Error> {
998 use crate::traits::encoding::bech32m::Bech32mSized;
999 use bech32::primitives::decode::CheckedHrpstring;
1000 let checked = CheckedHrpstring::new::<Bech32mSized<C>>(s)
1001 .map_err(|_| crate::error::Bech32Error::OperationFailed)?;
1002 let buf = drain_bech32_payload::<N>(&checked)?;
1003 Ok(Self::new_with(|arr| arr.copy_from_slice(&*buf)))
1004 // buf is zeroized on drop
1005 }
1006}
1007
1008/// Hex encoding for `Fixed<[u8; N]>` — same constant-time backend as the
1009/// [`ToHex`] blanket impl on the inner bytes; delegates via `with_secret`.
1010///
1011/// Bring the trait into scope to call these: `use secure_gate::ToHex;`.
1012///
1013/// ```rust
1014/// use secure_gate::{Fixed, ToHex};
1015///
1016/// let key = Fixed::new([0xABu8; 4]);
1017/// assert_eq!(&*key.to_hex(), "abababab");
1018/// assert_eq!(&*key.to_hex_upper(), "ABABABAB");
1019/// // Both return an `EncodedSecret` — the encoded form wipes itself on drop.
1020/// ```
1021#[cfg(all(feature = "encoding-hex", feature = "alloc"))]
1022impl<const N: usize> ToHex for Fixed<[u8; N]> {
1023 #[inline]
1024 fn to_hex(&self) -> crate::EncodedSecret {
1025 self.with_secret(|s| s.to_hex())
1026 }
1027
1028 #[inline]
1029 fn to_hex_upper(&self) -> crate::EncodedSecret {
1030 self.with_secret(|s| s.to_hex_upper())
1031 }
1032}
1033
1034/// Base32 encoding for `Fixed<[u8; N]>`; delegates via `with_secret`.
1035///
1036/// Bring the trait into scope to call these: `use secure_gate::ToBase32;`.
1037///
1038/// ```rust
1039/// use secure_gate::{Fixed, ToBase32};
1040///
1041/// let key = Fixed::new([0xABu8; 4]);
1042/// assert_eq!(&*key.to_base32(), "VOV2XKY");
1043/// ```
1044#[cfg(all(feature = "encoding-base32", feature = "alloc"))]
1045impl<const N: usize> ToBase32 for Fixed<[u8; N]> {
1046 #[inline]
1047 fn to_base32(&self) -> crate::EncodedSecret {
1048 self.with_secret(|s| s.to_base32())
1049 }
1050}
1051
1052/// Base64url encoding for `Fixed<[u8; N]>`; delegates via `with_secret`.
1053///
1054/// Bring the trait into scope to call these: `use secure_gate::ToBase64Url;`.
1055///
1056/// ```rust
1057/// use secure_gate::{Fixed, ToBase64Url};
1058///
1059/// let key = Fixed::new([0xABu8; 4]);
1060/// assert_eq!(&*key.to_base64url(), "q6urqw");
1061/// ```
1062#[cfg(all(feature = "encoding-base64", feature = "alloc"))]
1063impl<const N: usize> ToBase64Url for Fixed<[u8; N]> {
1064 #[inline]
1065 fn to_base64url(&self) -> crate::EncodedSecret {
1066 self.with_secret(|s| s.to_base64url())
1067 }
1068}
1069
1070/// Bech32 encoding for `Fixed<[u8; N]>`; delegates via `with_secret`.
1071///
1072/// Bring the trait into scope to call these: `use secure_gate::ToBech32;`.
1073#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
1074impl<const N: usize> ToBech32 for Fixed<[u8; N]> {
1075 #[inline]
1076 fn try_to_bech32(
1077 &self,
1078 hrp: &str,
1079 case: crate::Case,
1080 ) -> Result<crate::EncodedSecret, crate::error::Bech32Error> {
1081 self.with_secret(|s| s.try_to_bech32(hrp, case))
1082 }
1083
1084 #[inline]
1085 fn try_to_bech32_sized<const C: usize>(
1086 &self,
1087 hrp: &str,
1088 case: crate::Case,
1089 ) -> Result<crate::EncodedSecret, crate::error::Bech32Error> {
1090 self.with_secret(|s| s.try_to_bech32_sized::<C>(hrp, case))
1091 }
1092}
1093
1094/// Bech32m encoding for `Fixed<[u8; N]>`; delegates via `with_secret`.
1095///
1096/// Bring the trait into scope to call these: `use secure_gate::ToBech32m;`.
1097#[cfg(all(feature = "encoding-bech32", feature = "alloc"))]
1098impl<const N: usize> ToBech32m for Fixed<[u8; N]> {
1099 #[inline]
1100 fn try_to_bech32m(
1101 &self,
1102 hrp: &str,
1103 case: crate::Case,
1104 ) -> Result<crate::EncodedSecret, crate::error::Bech32Error> {
1105 self.with_secret(|s| s.try_to_bech32m(hrp, case))
1106 }
1107
1108 #[inline]
1109 fn try_to_bech32m_sized<const C: usize>(
1110 &self,
1111 hrp: &str,
1112 case: crate::Case,
1113 ) -> Result<crate::EncodedSecret, crate::error::Bech32Error> {
1114 self.with_secret(|s| s.try_to_bech32m_sized::<C>(hrp, case))
1115 }
1116}
1117
1118/// Explicit access to immutable [`Fixed<[T; N]>`] contents.
1119impl<T: zeroize::Zeroize> RevealSecret for Fixed<T> {
1120 type Inner = T;
1121
1122 #[inline(always)]
1123 fn with_secret<F, R>(&self, f: F) -> R
1124 where
1125 F: FnOnce(&T) -> R,
1126 {
1127 f(&self.inner)
1128 }
1129
1130 #[inline(always)]
1131 fn expose_secret(&self) -> &T {
1132 &self.inner
1133 }
1134
1135 /// Consumes `self` and transfers ownership of the plain `[T; N]`.
1136 ///
1137 /// Zero cost — no allocation. The sentinel placed in `self.inner` is
1138 /// `[T::default(); N]` via [`crate::SentinelValue`] (already zeroed for `u8`),
1139 /// so `Fixed::drop` zeroizes an already-zero array — a harmless no-op.
1140 /// Works for **any** array length `N` (not limited to 32 like `Default`).
1141 ///
1142 /// See [`RevealSecret::into_inner`] for the full contract, including the
1143 /// `SentinelValue` bound rationale. Protection ends at this call: the array you
1144 /// get back is plain — not wiped on drop, and its `Debug` is not redacted.
1145 #[inline(always)]
1146 fn into_inner(mut self) -> T
1147 where
1148 Self: Sized,
1149 Self::Inner: Sized + crate::SentinelValue + zeroize::Zeroize,
1150 {
1151 // Replace inner with the sentinel so Fixed::drop zeroizes a harmless
1152 // placeholder while the caller receives the real secret. The secret is stored
1153 // inline, so this transfers the bytes rather than handing over an allocation;
1154 // what the sentinel guarantees is that no second copy is left behind here.
1155 core::mem::replace(&mut self.inner, crate::SentinelValue::sentinel_value())
1156 }
1157}
1158
1159/// Length is only meaningful for array-shaped secrets.
1160impl<const N: usize, T: zeroize::Zeroize> crate::SecretLen for Fixed<[T; N]> {
1161 #[inline(always)]
1162 fn len(&self) -> usize {
1163 N
1164 }
1165
1166 #[inline(always)]
1167 fn byte_len(&self) -> usize {
1168 N * core::mem::size_of::<T>()
1169 }
1170}
1171
1172/// Explicit access to mutable [`Fixed<[T; N]>`] contents.
1173impl<T: zeroize::Zeroize> RevealSecretMut for Fixed<T> {
1174 #[inline(always)]
1175 fn with_secret_mut<F, R>(&mut self, f: F) -> R
1176 where
1177 F: FnOnce(&mut T) -> R,
1178 {
1179 f(&mut self.inner)
1180 }
1181
1182 #[inline(always)]
1183 fn expose_secret_mut(&mut self) -> &mut T {
1184 &mut self.inner
1185 }
1186}
1187
1188#[cfg(feature = "rand")]
1189impl<const N: usize> Fixed<[u8; N]> {
1190 /// Fills a new `[u8; N]` with cryptographically secure random bytes and wraps it.
1191 ///
1192 /// Uses the system RNG ([`SysRng`](rand::rngs::SysRng)). Requires the `rand` feature.
1193 /// Heap-free and works in `no_std` / `no_alloc` builds.
1194 ///
1195 /// # Panics
1196 ///
1197 /// Panics if the system RNG fails to provide bytes ([`TryRng::try_fill_bytes`](rand::TryRng::try_fill_bytes)
1198 /// returns `Err`). This is treated as a fatal environment error.
1199 ///
1200 /// # Examples
1201 ///
1202 /// ```rust
1203 /// # #[cfg(feature = "rand")]
1204 /// use secure_gate::{Fixed, RevealSecret, SecretLen};
1205 ///
1206 /// # #[cfg(feature = "rand")]
1207 /// # {
1208 /// let key: Fixed<[u8; 32]> = Fixed::from_random();
1209 /// assert_eq!(key.len(), 32);
1210 /// # }
1211 /// ```
1212 #[inline]
1213 pub fn from_random() -> Self {
1214 Self::new_with(|arr| {
1215 SysRng
1216 .try_fill_bytes(arr)
1217 .expect("SysRng failure is a program error");
1218 })
1219 }
1220
1221 /// Fills a new `[u8; N]` from `rng` and wraps it.
1222 ///
1223 /// Accepts any [`TryCryptoRng`](rand::TryCryptoRng) + [`TryRng`](rand::TryRng) — for example,
1224 /// a seeded `StdRng` for deterministic tests. Requires the `rand`
1225 /// feature. Heap-free.
1226 ///
1227 /// # Errors
1228 ///
1229 /// Returns `R::Error` if [`try_fill_bytes`](rand::TryRng::try_fill_bytes) fails.
1230 ///
1231 /// # Examples
1232 ///
1233 /// ```rust
1234 /// # #[cfg(feature = "rand")]
1235 /// # {
1236 /// use rand::rngs::StdRng;
1237 /// use rand::SeedableRng;
1238 /// use secure_gate::Fixed;
1239 ///
1240 /// let mut rng = StdRng::from_seed([1u8; 32]);
1241 /// let key: Fixed<[u8; 16]> = Fixed::from_rng(&mut rng).expect("rng fill");
1242 /// # }
1243 /// ```
1244 #[inline]
1245 pub fn from_rng<R: TryRng + TryCryptoRng>(rng: &mut R) -> Result<Self, R::Error> {
1246 // This body was the captured-local workaround `try_new_with` exists to retire:
1247 // `new_with` could not report failure, so the error was written to a local and
1248 // checked afterwards. Same behaviour, one expression.
1249 Self::try_new_with(|arr| rng.try_fill_bytes(arr))
1250 }
1251}
1252
1253/// Constant-time equality for `Fixed<T>` — routes through [`expose_secret()`](crate::RevealSecret::expose_secret).
1254///
1255/// `==` is **deliberately not implemented** on `Fixed`. Always use `ct_eq`.
1256///
1257/// ```rust
1258/// # #[cfg(feature = "ct-eq")]
1259/// # {
1260/// use secure_gate::{Fixed, ConstantTimeEq};
1261///
1262/// let a = Fixed::new([1u8; 4]);
1263/// let b = Fixed::new([1u8; 4]);
1264/// let c = Fixed::new([2u8; 4]);
1265/// assert!(a.ct_eq(&b));
1266/// assert!(!a.ct_eq(&c));
1267/// # }
1268/// ```
1269#[cfg(feature = "ct-eq")]
1270impl<T: zeroize::Zeroize> crate::ConstantTimeEq for Fixed<T>
1271where
1272 T: crate::ConstantTimeEq,
1273 Self: crate::RevealSecret<Inner = T>,
1274{
1275 fn ct_eq(&self, other: &Self) -> bool {
1276 self.expose_secret().ct_eq(other.expose_secret())
1277 }
1278}
1279
1280/// Always prints `[REDACTED]` — secrets never appear in debug output.
1281///
1282/// ```rust
1283/// use secure_gate::Fixed;
1284///
1285/// let key = Fixed::new([0xABu8; 32]);
1286/// assert_eq!(format!("{:?}", key), "[REDACTED]");
1287/// ```
1288impl<T: zeroize::Zeroize> core::fmt::Debug for Fixed<T> {
1289 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1290 f.write_str("[REDACTED]")
1291 }
1292}
1293
1294/// Opt-in cloning — requires `cloneable` feature and [`CloneableSecret`](crate::CloneableSecret)
1295/// marker on the inner type. Each clone is independently zeroized on drop, but cloning
1296/// increases the in-memory exposure surface. Use sparingly.
1297#[cfg(feature = "cloneable")]
1298impl<T: zeroize::Zeroize + crate::CloneableSecret + crate::FixedStorage> Clone for Fixed<T> {
1299 fn clone(&self) -> Self {
1300 Self::new(self.inner.clone())
1301 }
1302}
1303
1304/// Opt-in serialization — requires `serde-serialize` feature and
1305/// [`SerializableSecret`](crate::SerializableSecret) marker on the inner type.
1306/// Serialization exposes the full secret — audit every impl.
1307#[cfg(feature = "serde-serialize")]
1308impl<T: zeroize::Zeroize + crate::SerializableSecret> serde::Serialize for Fixed<T> {
1309 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1310 where
1311 S: serde::Serializer,
1312 {
1313 self.inner.serialize(serializer)
1314 }
1315}
1316
1317/// Deserialization uses `Zeroizing`-wrapped temporary buffers — zeroized even on rejection.
1318///
1319/// The sequence buffer is pre-allocated to exactly `N` bytes and **never grows**: an
1320/// input sequence longer than `N` is rejected before the element that would trigger a
1321/// reallocation is stored. This matters because a `Vec` reallocation frees the old
1322/// buffer — which would already hold `N` secret bytes — without zeroizing it.
1323///
1324/// The entry point is `deserialize_seq` (unchanged wire format for non-self-describing
1325/// formats such as postcard), but the visitor also accepts byte strings via
1326/// `visit_bytes` / `visit_byte_buf`, so self-describing formats that encode byte
1327/// arrays as byte strings (e.g. CBOR) round-trip too. Owned buffers handed over
1328/// through `visit_byte_buf` are zeroized after the copy.
1329#[cfg(feature = "serde-deserialize")]
1330impl<'de, const N: usize> serde::Deserialize<'de> for Fixed<[u8; N]> {
1331 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1332 where
1333 D: serde::Deserializer<'de>,
1334 {
1335 use core::fmt;
1336 use serde::de::Visitor;
1337 struct FixedVisitor<const M: usize>;
1338 impl<'de, const M: usize> Visitor<'de> for FixedVisitor<M> {
1339 type Value = Fixed<[u8; M]>;
1340 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1341 write!(formatter, "a byte array of length {}", M)
1342 }
1343 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
1344 where
1345 A: serde::de::SeqAccess<'de>,
1346 {
1347 let mut vec: zeroize::Zeroizing<alloc::vec::Vec<u8>> =
1348 zeroize::Zeroizing::new(alloc::vec::Vec::with_capacity(M));
1349 while let Some(value) = seq.next_element()? {
1350 // Reject over-length input *before* pushing past the reserved
1351 // capacity: growing would realloc and free the old buffer
1352 // (already holding M secret bytes) without zeroization.
1353 if vec.len() == M {
1354 return Err(serde::de::Error::invalid_length(M + 1, &self));
1355 }
1356 vec.push(value);
1357 }
1358 if vec.len() != M {
1359 return Err(serde::de::Error::invalid_length(vec.len(), &self));
1360 }
1361 Ok(Fixed::<[u8; M]>::new_with(|arr| arr.copy_from_slice(&vec)))
1362 }
1363 fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
1364 where
1365 E: serde::de::Error,
1366 {
1367 if v.len() != M {
1368 return Err(serde::de::Error::invalid_length(v.len(), &self));
1369 }
1370 Ok(Fixed::<[u8; M]>::new_with(|arr| arr.copy_from_slice(v)))
1371 }
1372 fn visit_byte_buf<E>(self, v: alloc::vec::Vec<u8>) -> Result<Self::Value, E>
1373 where
1374 E: serde::de::Error,
1375 {
1376 // Take ownership under Zeroizing so the deserializer-provided
1377 // buffer is wiped after the copy — the default forwarding impl
1378 // would drop it unzeroized.
1379 let v = zeroize::Zeroizing::new(v);
1380 self.visit_bytes(&v)
1381 }
1382 }
1383 deserializer.deserialize_seq(FixedVisitor::<N>)
1384 }
1385}
1386
1387/// Zeroizes the inner value. Called automatically by [`Drop`].
1388///
1389/// **Warning:** zeroization does not run for `static` items or under `panic = "abort"`.
1390impl<T: zeroize::Zeroize> zeroize::Zeroize for Fixed<T> {
1391 fn zeroize(&mut self) {
1392 self.inner.zeroize();
1393 }
1394}
1395
1396/// Unconditionally zeroizes the inner value when the wrapper is dropped.
1397///
1398/// **Warning:** `Drop` does not run for `static` items or under `panic = "abort"`.
1399impl<T: zeroize::Zeroize> Drop for Fixed<T> {
1400 fn drop(&mut self) {
1401 self.zeroize();
1402 }
1403}
1404
1405/// Marker confirming that `Fixed<T>` always zeroizes on drop.
1406impl<T: zeroize::Zeroize> zeroize::ZeroizeOnDrop for Fixed<T> {}