Skip to main content

secure_gate/macros/
fixed_newtype.rs

1//! `fixed_newtype!` — nominal newtype over `Fixed<[u8; N]>`.
2//!
3//! Ships in 0.9.0. Design record:
4//! [`docs/nominal_newtypes.md`](https://github.com/Slurp9187/secure-gate/blob/main/docs/design/nominal_newtypes.md) — a repository file, not part of the
5//! published crate, so this is a link and not a path.
6
7/// Creates a distinct nominal type wrapping [`Fixed<[u8; N]>`](crate::Fixed).
8///
9/// Generates a `struct` rather than a `type` alias: two `fixed_newtype!` types
10/// of the same `N` are **not** interchangeable, where two `type` aliases of
11/// that `N` are one and the same type. Use it when distinct cryptographic
12/// roles share a shape — an encryption key and a MAC key are both
13/// `Fixed<[u8; 32]>`, and under a plain alias the compiler cannot tell them
14/// apart.
15///
16/// # Syntax
17///
18/// ```text
19/// fixed_newtype!(pub Name, N);                          // public, auto-generated doc
20/// fixed_newtype!(pub(crate) Name, N);                   // crate-visible
21/// fixed_newtype!(Name, N);                              // private
22/// fixed_newtype!(pub Name, N, "doc string");            // with custom doc
23/// fixed_newtype!(pub Name, N, derive: [FromWrapper]);   // with opt-in impls
24/// fixed_newtype!(pub Name, N, "doc", derive: [FromWrapper]);
25/// fixed_newtype!(pub Name, generic [T; N]);             // array: adds SecretLen
26/// fixed_newtype!(pub Name, generic T);                  // reduced API, opted into
27/// fixed_newtype!(pub Name, generic T, "doc string");
28/// fixed_newtype!(pub Name, generic T, derive: [WrapperAccess]);
29/// fixed_newtype!(pub Name, generic T, "doc", derive: [WrapperAccess]);
30/// ```
31///
32/// Supported `derive:` options are `Deserialize`, `FromWrapper`, `IntoWrapper`, and `WrapperAccess` (= both directions), plus `ConstantTimeEq` **on the `generic` arms only**. `Deserialize` and `ConstantTimeEq` constrain the inner type — it must implement them, which rules them out for most `generic` inner types (`[i16; 256]` has neither) — while the three wrapper-access tokens apply to any. See
33/// *Cloning and serialization* below for the two that are deliberately absent.
34///
35/// **`ConstantTimeEq` is not an option on the size-literal arm, and naming it is an
36/// error.** A `[u8; N]` payload gets [`ct_eq`](crate::ConstantTimeEq::ct_eq)
37/// automatically whenever the `ct-eq` feature is on, because the wrapper it forwards
38/// to always has it and a plain `type Name = Fixed<[u8; N]>;` therefore always did.
39/// Requiring the token here put the extra word on the spelling this crate recommends
40/// as the safer of the two, for the one comparison whose alternative is a timing
41/// leak. Since the arm now emits the impl itself, a second one from the `derive:`
42/// list is `E0119: conflicting implementations` — delete the token. It remains a
43/// real opt-in on the `generic` arms, where the inner type may or may not implement
44/// the trait.
45///
46/// # Examples
47///
48/// All three visibility forms:
49///
50/// ```rust
51/// use secure_gate::{fixed_newtype, SecretLen};
52///
53/// fixed_newtype!(pub EncKey, 32);          // public
54/// fixed_newtype!(pub(crate) HmacKey, 32);  // crate-visible
55/// fixed_newtype!(local_nonce, 12);         // private
56///
57/// let key = EncKey::new([42u8; 32]);
58/// assert_eq!(key.len(), 32);
59/// ```
60///
61/// Distinct roles do not mix — this is the point:
62///
63/// ```rust,compile_fail
64/// use secure_gate::fixed_newtype;
65///
66/// fixed_newtype!(pub EncKey, 32);
67/// fixed_newtype!(pub MacKey, 32);
68///
69/// fn seal(_enc: &EncKey, _mac: &MacKey) {}
70///
71/// let enc = EncKey::new([1u8; 32]);
72/// let mac = MacKey::new([2u8; 32]);
73/// seal(&mac, &enc); // E0308: roles swapped
74/// ```
75///
76/// Zero-size is a **compile error**, raised where the type is declared rather
77/// than where one is first built:
78///
79/// ```rust,compile_fail
80/// use secure_gate::fixed_newtype;
81/// fixed_newtype!(pub Bad, 0); // compile-time error: index out of bounds
82/// ```
83///
84/// # Inner types other than bytes
85///
86/// The size-literal forms cover `Fixed<[u8; N]>`, the shape nearly every
87/// secret takes. [`Fixed<T>`](crate::Fixed) holds more than that, and the
88/// `generic` marker opts a newtype into it. The requirement is
89/// `Zeroize + `[`SentinelValue`](crate::SentinelValue)` + `[`FixedStorage`](crate::FixedStorage), checked at the
90/// declaration — slightly narrower than `Fixed<T>` itself, which needs only
91/// `Zeroize`, because the generated [`into_inner`](crate::RevealSecret::into_inner)
92/// has to leave an inert value behind. Arrays of a `Default` element, `String`
93/// and `Vec<T>` already satisfy both; a custom inner type needs its own
94/// `impl SentinelValue`, which is why that trait is public:
95///
96/// ```rust
97/// use secure_gate::{fixed_newtype, RevealSecret};
98///
99/// fixed_newtype!(pub Poly, generic [i16; 256]);
100///
101/// let poly = Poly::new([0i16; 256]);
102/// assert_eq!(poly.with_secret(|coeffs| coeffs.len()), 256);
103/// ```
104///
105/// The case this exists for is `no_std`. With no allocator there is no
106/// [`Dynamic`](crate::Dynamic), so `Fixed` is the only wrapper, and a secret on
107/// such a target is frequently not bytes: an ML-KEM secret polynomial
108/// `[i16; 256]`, Ed25519 scalar limbs `[u64; 4]`, AES-256 expanded round keys
109/// `[u32; 60]`. An inner type missing either bound is a compile error reported
110/// from the expansion: `Zeroize` fails against `Fixed<T>`'s own bound, and
111/// `SentinelValue` against the one the generated `RevealSecret` restates.
112/// [`dynamic_newtype!`](crate::dynamic_newtype)'s `generic` arm carries the
113/// same pair, for the same reason.
114///
115/// Every `generic` form emits [`RevealSecret`](crate::RevealSecret),
116/// [`RevealSecretMut`](crate::RevealSecretMut), redacted `Debug`, `Zeroize`,
117/// `ZeroizeOnDrop`, `new`, and [`new_with`](crate::Fixed::new_with). What sits on top
118/// of that depends on how much shape the macro can read off the tokens you wrote, and
119/// there are two answers.
120///
121/// **A literal array — `generic [i16; 256]` — takes the array arm.** The element type
122/// and the length are both still tokens there, so the expansion can see the shape and
123/// emits two more things: [`SecretLen`](crate::SecretLen), and the declaration-site
124/// `N = 0` guard the size-literal arm has always carried. `len()` counts elements and
125/// `byte_len()` multiplies by `size_of::<T>()`, because that is what
126/// `Fixed<[T; N]>`'s own impl computes — 256 and 512 for a polynomial. The arm needs a
127/// *literal* length: `generic [i16; KEY_LEN]` is a different token sequence and falls
128/// through to the opaque arm, the same remainder a path-qualified spelling has.
129///
130/// **A true opaque inner type — `generic Poly`, a struct, an alias — takes the reduced
131/// arm.** `SecretLen` is absent there, and so is the declaration-site guard; a
132/// zero-sized inner value is refused by [`Fixed`](crate::Fixed) at construction
133/// instead. That is a limit on the macro's field of view rather than a verdict on the
134/// type: the base wrapper still implements `SecretLen` for every `Fixed<[T; N]>`, so
135/// the capability is one layer down and `derive: [IntoWrapper]` reaches it in one call.
136/// What the arm withholds is the newtype's *own* `len()`, not the answer.
137///
138/// **`From`, `TryFrom`, the encoders and the RNG constructors are absent from both.**
139/// The encoders have no defined byte order over `[i16]`, and byte order is not the
140/// whole of it: the canonical encoding of these secrets is domain-specific — ML-KEM
141/// serialises as bit-packed 12-bit coefficients, not a 16-bit dump — so an encoder here
142/// would round-trip inside this crate and disagree with every implementation outside
143/// it. RNG is a separate question with a separate answer: the byte-level fill is well
144/// defined, but uniform bits are not a valid ML-KEM coefficient, a reduced scalar, or a
145/// derived round key. It is tracked in #219 rather than settled here.
146///
147/// **`generic [u8; N]` is a compile error**, not a reduced byte array. It is the
148/// same payload as `fixed_newtype!(pub K, N)` with strictly less API, and
149/// [`dynamic_newtype!`](crate::dynamic_newtype) already refuses the analogous
150/// spellings (`generic Vec<u8>`, `generic String`). Write the size literal.
151///
152/// **The inner type must not be able to reallocate.** This arm used to accept
153/// `generic Vec<u8>` and `generic String`, which put a growable buffer inside the
154/// wrapper the documentation calls free of any realloc surface; measured, each
155/// abandoned an unwiped buffer holding the whole secret on any capacity change, and
156/// with no `io::Write` escape of the sort [`Dynamic<Vec<u8>>`](crate::Dynamic) has.
157/// [`Fixed::new`](crate::Fixed::new) now requires
158/// [`FixedStorage`](crate::FixedStorage), so those forms are a compile error reported
159/// at this declaration. Arrays, the primitives, tuples and `Option` are covered for you,
160/// which is every shape this arm exists for; a custom inner type adds
161/// `impl FixedStorage for MyType {}`, which asserts that it owns no heap
162/// allocation. For a growable payload reach for
163/// [`dynamic_newtype!`](crate::dynamic_newtype) instead, where the residue is
164/// documented and one safe growth path exists.
165///
166/// **`new_with` is forwarded.** [`Fixed::new_with`](crate::Fixed::new_with) is an
167/// inherent on every `T: `[`FixedStorage`](crate::FixedStorage)` + `[`SentinelValue`](crate::SentinelValue),
168/// not only `[u8; N]`: the slot starts as `T::sentinel_value()` and the closure
169/// writes into the wrapper's own storage. The generic arm emits it, so a
170/// polynomial does not have to take the secret by value the way `new` does.
171/// Prefer a size-literal newtype when the secret *is* a byte array — that arm
172/// is the full API, and `generic [u8; N]` is refused rather than quietly
173/// reduced.
174///
175/// # Cloning and serialization are not generated
176///
177/// There is no `derive: [Clone]` or `derive: [Serialize]`, by design. Neither
178/// can be forwarded from the wrapper — `Fixed<[u8; N]>: Clone` requires
179/// `[u8; N]: `[`CloneableSecret`](crate::CloneableSecret), which the orphan
180/// rule makes permanently unimplementable downstream (deliberately so). A
181/// generated impl would therefore have to route through `with_secret` and
182/// rebuild, opting the secret into cloning with no marker impl anywhere — a
183/// second door around the opt-in system, spelled in one word inside a macro
184/// expansion. Asking for either is a compile error that says so.
185///
186/// The generated type is local to **your** crate, so write the impl by hand
187/// when you mean it. The decision then lives in your code, where it is
188/// visible, greppable, and reviewable:
189///
190/// ```rust
191/// use secure_gate::{fixed_newtype, Fixed, RevealSecret};
192///
193/// fixed_newtype!(pub SessionKey, 32);
194///
195/// // Deliberate: each clone is an independent copy, zeroized on its own drop.
196/// // Cloning widens the window for memory-extraction attacks — audit each use.
197/// impl Clone for SessionKey {
198///     fn clone(&self) -> Self {
199///         Self::new(self.with_secret(|bytes| *bytes))
200///     }
201/// }
202///
203/// let key = SessionKey::new([7u8; 32]);
204/// let copy = key.clone();
205/// assert_eq!(copy.expose_secret()[0], 7);
206/// ```
207///
208/// The same applies to `Serialize`, with a higher bar: serialization exposes
209/// the full secret, so treat a hand-written impl as a security decision and
210/// see [`SerializableSecret`](crate::SerializableSecret) for the risk notes.
211///
212/// # Implementation Notes
213///
214/// The generated type is `#[repr(transparent)]` over the wrapper, so
215/// `size_of::<EncKey>() == N` and delegation is `#[inline]` throughout — the
216/// newtype costs nothing at runtime.
217///
218/// The size-literal and array expansions both emit
219/// `const _: () = { let _ = [(); N][0]; };`, a declaration-site courtesy: `N = 0`
220/// fails on the line that declares the type, which is the line worth pointing at.
221/// Whether the length arrives as the size argument or inside a literal array, it is
222/// a literal the macro can count at expansion.
223///
224/// It is not the only guard, and the two are not redundant.
225/// [`Fixed::new`](crate::Fixed::new) and
226/// [`Fixed::new_with`](crate::Fixed::new_with) carry a `const` assertion rejecting a
227/// zero-sized inner value at construction, which is what covers a plain `type` alias
228/// and the opaque `generic` arm; there the error falls at the first concrete
229/// construction instead. The macro guard checks a *length*, the wrapper's checks a
230/// *size* — so `generic [(); 4]` passes the first and is caught by the second, which
231/// is exactly the case `tests/compile-fail/fixed_zero_size.rs` pins.
232///
233/// **Do not add your own `Drop` impl.** None is needed: the wrapped
234/// [`Fixed`](crate::Fixed) still runs its own, so zeroization is unaffected.
235/// Adding one makes the inner field unmovable (**E0509**) and silently costs
236/// you [`into_inner`](crate::RevealSecret::into_inner).
237///
238/// # Security
239///
240/// Generated types inherit every [`Fixed`](crate::Fixed) guarantee: zeroize on
241/// drop, `Debug` that always prints `[REDACTED]`, and access only through
242/// [`RevealSecret`](crate::RevealSecret) /
243/// [`RevealSecretMut`](crate::RevealSecretMut). There is no `Deref` — a
244/// generated newtype is not coercible to its wrapper, which is what keeps the
245/// nominal separation total rather than by-value-only.
246///
247/// **Nominal separation guards against mistakes, not intent** — but nothing is
248/// generated that would undo it by accident. There is no `From<Wrapper>` and
249/// no `Deref`, so a base-typed value cannot flow into a newtype through
250/// `.into()`, and `&Newtype` never coerces to `&Wrapper` at a call site. By
251/// default the only way material enters or leaves is the 3-tier access API —
252/// a `with_secret` round trip — which is explicit and shows up in the audit
253/// sweep. Base-wrapper access is opt-in, per newtype, and **split by
254/// direction**: `derive: [FromWrapper]` adds `from_wrapper` (a base value
255/// *enters* this role), `derive: [IntoWrapper]` adds `as_wrapper`,
256/// `as_wrapper_mut`, and `into_wrapper` (material *leaves* this role toward
257/// the base type), and `derive: [WrapperAccess]` is shorthand for both.
258///
259/// These cross a different wall from `into_inner`. A newtype has two:
260///
261/// | Wall | Going in | Going out |
262/// |---|---|---|
263/// | Contents (the wrapper's protection) | `new`, `From<[u8; N]>` | `with_secret`, `expose_secret`, `into_inner` |
264/// | Role (the nominal label) | `from_wrapper` | `as_wrapper`, `as_wrapper_mut`, `into_wrapper` |
265///
266/// `into_inner` leaves the protection: it hands back the plain value, so the
267/// contents are in the caller's hands (tier 3 of the access model, audited).
268/// `into_wrapper` is a **label** drop; `into_inner` is a **protection** drop.
269/// Confusing those two names is the main way this model gets misread. `into_wrapper` only removes the label: the
270/// result is still a `Fixed`, still unreadable without `with_secret`. The
271/// role row exists so that dropping a label never forces opening the
272/// contents — without it, reaching base-typed code costs an `into_inner` plus
273/// a rebuild, a reveal the job never needed.
274///
275/// **Which direction is safe depends on the pool.** In a mixed tree the base
276/// type is not raw material: every plain `type` alias sharing it *is* that
277/// type, so a directional token connects this role to all of them at once.
278/// `FromWrapper` lets anything in the pool become this role — the source never
279/// opts in, because the source is just the base type — so a type that guards
280/// a boundary must never take it. `IntoWrapper` lets this role become anything
281/// in the pool, which is safe only when the role is no more sensitive than
282/// the least-sensitive alias sharing its base; on a secret role it is an
283/// explicit, greppable downgrade, not a neutral operation. The default,
284/// neither token, is sufficient more often than it looks. `IntoWrapper` also
285/// reopens whatever the base wrapper implements for that inner type, which on a
286/// `generic` newtype includes the surface the arm withheld: `as_wrapper().len()`
287/// and `.byte_len()` answer for any `Fixed<[T; N]>`. The reduced surface is a
288/// property of the label, not a barrier. This is now worth granting less often than
289/// it was — a literal array gets its own `len()` from the array arm, so reaching for
290/// the outbound token merely to measure a secret is no longer the trade it used to
291/// be. Where it is still the only route is the opaque arm. The exposure is
292/// largest during a partial migration — the regime real consumers live in —
293/// because while most aliases stay plain the base type is a universal donor.
294/// Audit these methods the way you audit `expose_secret()`.
295///
296/// # See also
297///
298/// - [`dynamic_newtype!`](crate::dynamic_newtype) — heap-allocated counterpart
299/// - a plain `type` alias — `pub type Aes256Key = Fixed<[u8; 32]>;` — when a
300///   readable name rather than role separation is the goal, and the name should
301///   stay interchangeable with its base. Two aliases over one shape are the
302///   same type, so the compiler will not keep two roles apart; that is the
303///   whole of the choice, and the crate's README argues it at length
304#[macro_export]
305macro_rules! fixed_newtype {
306    // ---- `generic` applied to a byte array: rejected ----
307    //
308    // `generic [u8; N]` is strictly dominated by the size-literal arm. It takes
309    // the payload this crate already specialises and then withholds `SecretLen`,
310    // `From`, `TryFrom`, the encoders and `from_random`. Same bytes, strictly
311    // less API, and no reason to write it. These must precede every
312    // `generic $inner:ty` arm: once a `:ty` fragment is parsed there is no
313    // backtracking. They match literal tokens only, so `generic [u8; KEY_LEN]`
314    // with an ident in the length slot is a different sequence and is NOT
315    // caught — the same remainder `dynamic_newtype!` documents for paths.
316    ($(#[$attr:meta])* $vis:vis $name:ident, generic [u8; $n:literal], $doc:literal, derive: [$($opt:ident),* $(,)?]) => {
317        $crate::__sg_fixed_generic_is_bytes!($n);
318    };
319    ($(#[$attr:meta])* $vis:vis $name:ident, generic [u8; $n:literal], $doc:literal) => {
320        $crate::__sg_fixed_generic_is_bytes!($n);
321    };
322    ($(#[$attr:meta])* $vis:vis $name:ident, generic [u8; $n:literal], derive: [$($opt:ident),* $(,)?]) => {
323        $crate::__sg_fixed_generic_is_bytes!($n);
324    };
325    ($(#[$attr:meta])* $vis:vis $name:ident, generic [u8; $n:literal]) => {
326        $crate::__sg_fixed_generic_is_bytes!($n);
327    };
328
329    // ---- `generic` applied to a non-byte array: the shaped array arm ----
330    //
331    // `[i16; 256]` is an array and the macro can see that it is, so it should not
332    // have to pretend it is an opaque struct. The no_std case the `generic` marker
333    // exists for is exactly this shape. This arm emits what is meaningful for any
334    // `[T; N]` and nothing that is not: the declaration-site `N = 0` guard the
335    // size-literal arm already carries, and `SecretLen`, whose answers
336    // `Fixed<[T; N]>` already computes correctly — `len` in elements, `byte_len`
337    // multiplied by `size_of::<T>()`.
338    //
339    // Still withheld: `From`/`TryFrom`, the encoders and the RNG constructors. Hex
340    // over `[i16]` has no defined byte order, and for the structured secrets this
341    // arm exists for the canonical encoding is bit-packed rather than a raw element
342    // dump, so an encoder here would round-trip inside this crate and disagree with
343    // every implementation outside it. RNG is a different question — the byte-level
344    // fill is well defined, but uniform bits are not a valid ML-KEM coefficient, a
345    // reduced scalar, or a derived round key — and is tracked separately in #219.
346    //
347    // One arm, not four. The pattern consumes a `:ty` fragment for the element
348    // type, so two arms both opening `generic [$t:ty; ...` cannot coexist: the
349    // same no-backtracking rule that forces the opaque arm below into the
350    // optional-group form. The `[u8; N]` rejects above still win, because they
351    // precede this arm and match `u8` as a literal token.
352    ($(#[$attr:meta])* $vis:vis $name:ident, generic [$t:ty; $n:literal]
353     $(, $doc:literal)? $(, derive: [$($opt:ident),* $(,)?])?) => {
354        const _: () = { let _ = [(); $n][0]; };
355
356        $crate::__sg_newtype_base!(
357            $(#[$attr])* $(#[doc = $doc])* $vis $name($crate::Fixed<[$t; $n]>),
358            derive: [$($($opt),*)?]
359        );
360        $crate::__sg_newtype_len!($name);
361
362        impl $name {
363            /// Wraps an array in place. `const fn`, like `Fixed::new`.
364            #[inline(always)]
365            pub const fn new(value: [$t; $n]) -> Self {
366                Self($crate::Fixed::new(value))
367            }
368            /// Scoped construction — writes directly into the wrapper's storage.
369            #[inline(always)]
370            pub fn new_with<F>(f: F) -> Self
371            where F: ::core::ops::FnOnce(&mut [$t; $n]) {
372                Self($crate::Fixed::new_with(f))
373            }
374
375            /// Scoped construction from a fill that can fail. On `Err` the partial
376            /// write is zeroized before the error is returned.
377            #[inline(always)]
378            pub fn try_new_with<F, E>(f: F) -> ::core::result::Result<Self, E>
379            where F: ::core::ops::FnOnce(&mut [$t; $n]) -> ::core::result::Result<(), E> {
380                ::core::result::Result::Ok(Self($crate::Fixed::try_new_with(f)?))
381            }
382        }
383    };
384
385    // ---- explicit generic arms: caller opts in to the reduced API ----
386    //
387    // Placed after the array arm. They match the literal token `generic` in the
388    // inner-type position, so they cannot shadow a size literal, and matching the
389    // marker before any `:literal` or `:ty` fragment is parsed keeps the ordering
390    // safe: `macro_rules!` does not backtrack once a fragment has been
391    // consumed. The doc-literal forms get their own arms, exactly as
392    // `dynamic_newtype!`'s generic form does, so that the third positional
393    // argument means the same thing on both macros and on every arm of each.
394    ($(#[$attr:meta])* $vis:vis $name:ident, generic $inner:ty
395     $(, $doc:literal)? $(, derive: [$($opt:ident),* $(,)?])?) => {
396        $crate::__sg_newtype_base!(
397            $(#[$attr])* $(#[doc = $doc])* $vis $name($crate::Fixed<$inner>),
398            derive: [$($($opt),*)?]
399        );
400
401        impl $name {
402            /// Wraps a value in place. `const fn`, like `Fixed::new`.
403            #[inline(always)]
404            pub const fn new(value: $inner) -> Self {
405                Self($crate::Fixed::new(value))
406            }
407            /// Scoped construction — writes directly into the wrapper's storage.
408            #[inline(always)]
409            pub fn new_with<F>(f: F) -> Self
410            where F: ::core::ops::FnOnce(&mut $inner) {
411                Self($crate::Fixed::new_with(f))
412            }
413
414            /// Scoped construction from a fill that can fail. On `Err` the partial
415            /// write is zeroized before the error is returned.
416            #[inline(always)]
417            pub fn try_new_with<F, E>(f: F) -> ::core::result::Result<Self, E>
418            where F: ::core::ops::FnOnce(&mut $inner) -> ::core::result::Result<(), E> {
419                ::core::result::Result::Ok(Self($crate::Fixed::try_new_with(f)?))
420            }
421        }
422    };
423
424    ($(#[$attr:meta])* $vis:vis $name:ident, $size:literal, $doc:literal, derive: [$($opt:ident),* $(,)?]) => {
425        $crate::fixed_newtype!($(#[$attr])* #[doc = $doc] $vis $name, $size, derive: [$($opt),*]);
426    };
427    ($(#[$attr:meta])* $vis:vis $name:ident, $size:literal, $doc:literal) => {
428        $crate::fixed_newtype!($(#[$attr])* #[doc = $doc] $vis $name, $size, derive: []);
429    };
430    ($(#[$attr:meta])* $vis:vis $name:ident, $size:literal) => {
431        $crate::fixed_newtype!($(#[$attr])* $vis $name, $size, derive: []);
432    };
433    ($(#[$attr:meta])* $vis:vis $name:ident, $size:literal, derive: [$($opt:ident),* $(,)?]) => {
434        const _: () = { let _ = [(); $size][0]; };
435
436        $crate::__sg_newtype_base!(
437            $(#[$attr])* $vis $name($crate::Fixed<[u8; $size]>), derive: [$($opt),*]
438        );
439        $crate::__sg_newtype_ct_eq!($name);
440        $crate::__sg_newtype_len!($name);
441
442        impl $name {
443            /// Wraps a fixed-size byte array. `const fn`, like `Fixed::new`.
444            #[inline(always)]
445            pub const fn new(value: [u8; $size]) -> Self {
446                Self($crate::Fixed::new(value))
447            }
448            /// Scoped construction — writes directly into the wrapper's storage.
449            #[inline(always)]
450            pub fn new_with<F>(f: F) -> Self
451            where F: ::core::ops::FnOnce(&mut [u8; $size]) {
452                Self($crate::Fixed::new_with(f))
453            }
454
455            /// Scoped construction from a fill that can fail. On `Err` the partial
456            /// write is zeroized before the error is returned.
457            #[inline(always)]
458            pub fn try_new_with<F, E>(f: F) -> ::core::result::Result<Self, E>
459            where F: ::core::ops::FnOnce(&mut [u8; $size]) -> ::core::result::Result<(), E> {
460                ::core::result::Result::Ok(Self($crate::Fixed::try_new_with(f)?))
461            }
462        }
463
464        impl ::core::convert::From<[u8; $size]> for $name {
465            #[inline(always)]
466            fn from(value: [u8; $size]) -> Self { Self($crate::Fixed::new(value)) }
467        }
468
469        impl ::core::convert::TryFrom<&[u8]> for $name {
470            type Error = $crate::FromSliceError;
471            #[inline]
472            fn try_from(slice: &[u8]) -> ::core::result::Result<Self, Self::Error> {
473                ::core::result::Result::Ok(Self(
474                    <$crate::Fixed<[u8; $size]> as ::core::convert::TryFrom<&[u8]>>::try_from(slice)?
475                ))
476            }
477        }
478
479        // Lets `sealed_fixed_newtype!` seal this role, and unseal back to it, by moving the
480        // `Fixed` across instead of copying the bytes out by hand.
481        $crate::__sg_if_sealed! {
482            impl $crate::__private::FixedRole for $name {
483                type Fixed = $crate::Fixed<[u8; $size]>;
484                #[inline(always)]
485                fn into_fixed(self) -> Self::Fixed { self.0 }
486                #[inline(always)]
487                fn from_fixed(fixed: Self::Fixed) -> Self { Self(fixed) }
488            }
489        }
490
491        $crate::__sg_if_rand! {
492            impl $name {
493                /// Generates the secret from the system RNG.
494                #[inline]
495                pub fn from_random() -> Self { Self($crate::Fixed::from_random()) }
496
497                /// Generates the secret from a caller-supplied CSPRNG.
498                #[inline]
499                pub fn from_rng<R>(rng: &mut R) -> ::core::result::Result<Self, R::Error>
500                where
501                    R: $crate::__private::TryRng + $crate::__private::TryCryptoRng,
502                {
503                    ::core::result::Result::Ok(Self($crate::Fixed::from_rng(rng)?))
504                }
505            }
506        }
507
508        $crate::__sg_if_hex! {
509            impl $name {
510                /// Constant-time hex decode into this secret type.
511                #[inline]
512                pub fn try_from_hex(hex: &str) -> ::core::result::Result<Self, $crate::HexError> {
513                    ::core::result::Result::Ok(Self($crate::Fixed::try_from_hex(hex)?))
514                }
515            }
516            $crate::__sg_if_alloc! {
517                impl $crate::ToHex for $name {
518                    #[inline]
519                    fn to_hex(&self) -> $crate::EncodedSecret {
520                        $crate::ToHex::to_hex(&self.0)
521                    }
522                    #[inline]
523                    fn to_hex_upper(&self) -> $crate::EncodedSecret {
524                        $crate::ToHex::to_hex_upper(&self.0)
525                    }
526                }
527            }
528        }
529
530        $crate::__sg_if_base32! {
531            impl $name {
532                /// Constant-time Base32 decode into this secret type.
533                #[inline]
534                pub fn try_from_base32(s: &str) -> ::core::result::Result<Self, $crate::Base32Error> {
535                    ::core::result::Result::Ok(Self($crate::Fixed::try_from_base32(s)?))
536                }
537            }
538            $crate::__sg_if_alloc! {
539                impl $crate::ToBase32 for $name {
540                    #[inline]
541                    fn to_base32(&self) -> $crate::EncodedSecret {
542                        $crate::ToBase32::to_base32(&self.0)
543                    }
544                }
545            }
546        }
547
548        $crate::__sg_if_base64! {
549            impl $name {
550                /// Constant-time Base64url decode into this secret type.
551                #[inline]
552                pub fn try_from_base64url(s: &str) -> ::core::result::Result<Self, $crate::Base64Error> {
553                    ::core::result::Result::Ok(Self($crate::Fixed::try_from_base64url(s)?))
554                }
555            }
556            $crate::__sg_if_alloc! {
557                impl $crate::ToBase64Url for $name {
558                    #[inline]
559                    fn to_base64url(&self) -> $crate::EncodedSecret {
560                        $crate::ToBase64Url::to_base64url(&self.0)
561                    }
562                }
563            }
564        }
565
566        $crate::__sg_if_bech32m! {
567            impl $name {
568                /// HRP-validated Bech32m decode into this secret type.
569                #[inline]
570                pub fn try_from_bech32m(s: &str, expected_hrp: &str)
571                    -> ::core::result::Result<Self, $crate::Bech32Error> {
572                    ::core::result::Result::Ok(Self($crate::Fixed::try_from_bech32m(s, expected_hrp)?))
573                }
574                /// Bech32m decode without HRP validation.
575                #[inline]
576                pub fn try_from_bech32m_unchecked(s: &str)
577                    -> ::core::result::Result<Self, $crate::Bech32Error> {
578                    ::core::result::Result::Ok(Self($crate::Fixed::try_from_bech32m_unchecked(s)?))
579                }
580                /// HRP-validated Bech32m decode accepting strings up to `C` characters.
581                #[inline]
582                pub fn try_from_bech32m_sized<const C: usize>(s: &str, expected_hrp: &str)
583                    -> ::core::result::Result<Self, $crate::Bech32Error> {
584                    ::core::result::Result::Ok(Self(
585                        $crate::Fixed::try_from_bech32m_sized::<C>(s, expected_hrp)?,
586                    ))
587                }
588                /// Bech32m decode without HRP validation, accepting strings up to `C` characters.
589                #[inline]
590                pub fn try_from_bech32m_unchecked_sized<const C: usize>(s: &str)
591                    -> ::core::result::Result<Self, $crate::Bech32Error> {
592                    ::core::result::Result::Ok(Self(
593                        $crate::Fixed::try_from_bech32m_unchecked_sized::<C>(s)?,
594                    ))
595                }
596            }
597            $crate::__sg_if_alloc! {
598                impl $crate::ToBech32m for $name {
599                    #[inline]
600                    fn try_to_bech32m(
601                        &self,
602                        hrp: &str,
603                        case: $crate::Case,
604                    ) -> ::core::result::Result<$crate::EncodedSecret, $crate::Bech32Error> {
605                        $crate::ToBech32m::try_to_bech32m(&self.0, hrp, case)
606                    }
607                    #[inline]
608                    fn try_to_bech32m_sized<const C: usize>(
609                        &self,
610                        hrp: &str,
611                        case: $crate::Case,
612                    ) -> ::core::result::Result<$crate::EncodedSecret, $crate::Bech32Error> {
613                        $crate::ToBech32m::try_to_bech32m_sized::<C>(&self.0, hrp, case)
614                    }
615                }
616            }
617        }
618
619        $crate::__sg_if_bech32! {
620            $crate::__sg_if_alloc! {
621                impl $crate::ToBech32 for $name {
622                    #[inline]
623                    fn try_to_bech32(
624                        &self,
625                        hrp: &str,
626                        case: $crate::Case,
627                    ) -> ::core::result::Result<$crate::EncodedSecret, $crate::Bech32Error> {
628                        $crate::ToBech32::try_to_bech32(&self.0, hrp, case)
629                    }
630                    #[inline]
631                    fn try_to_bech32_sized<const C: usize>(
632                        &self,
633                        hrp: &str,
634                        case: $crate::Case,
635                    ) -> ::core::result::Result<$crate::EncodedSecret, $crate::Bech32Error> {
636                        $crate::ToBech32::try_to_bech32_sized::<C>(&self.0, hrp, case)
637                    }
638                }
639            }
640            impl $name {
641                /// HRP-validated Bech32 decode into this secret type.
642                #[inline]
643                pub fn try_from_bech32(s: &str, expected_hrp: &str)
644                    -> ::core::result::Result<Self, $crate::Bech32Error> {
645                    ::core::result::Result::Ok(Self($crate::Fixed::try_from_bech32(s, expected_hrp)?))
646                }
647                /// Bech32 decode without HRP validation.
648                #[inline]
649                pub fn try_from_bech32_unchecked(s: &str)
650                    -> ::core::result::Result<Self, $crate::Bech32Error> {
651                    ::core::result::Result::Ok(Self($crate::Fixed::try_from_bech32_unchecked(s)?))
652                }
653                /// HRP-validated Bech32 decode accepting strings up to `C` characters.
654                #[inline]
655                pub fn try_from_bech32_sized<const C: usize>(s: &str, expected_hrp: &str)
656                    -> ::core::result::Result<Self, $crate::Bech32Error> {
657                    ::core::result::Result::Ok(Self(
658                        $crate::Fixed::try_from_bech32_sized::<C>(s, expected_hrp)?,
659                    ))
660                }
661                /// Bech32 decode without HRP validation, accepting strings up to `C` characters.
662                #[inline]
663                pub fn try_from_bech32_unchecked_sized<const C: usize>(s: &str)
664                    -> ::core::result::Result<Self, $crate::Bech32Error> {
665                    ::core::result::Result::Ok(Self(
666                        $crate::Fixed::try_from_bech32_unchecked_sized::<C>(s)?,
667                    ))
668                }
669            }
670        }
671    };
672
673    // ---- catch-all: neither a byte size nor the `generic` marker ----
674    //
675    // Uses `$($rest:tt)+` rather than `$inner:ty` so it can never itself
676    // hard-error on a malformed tail; the message names what was written.
677    ($(#[$attr:meta])* $vis:vis $name:ident, $($rest:tt)+) => {
678        ::core::compile_error!(::core::concat!(
679            "fixed_newtype!: no form matches `",
680            ::core::stringify!($($rest)+),
681            "`. The accepted forms are `N`, `N, \"doc\"`, `N, derive: [..]` and \
682             `N, \"doc\", derive: [..]`, where N is the length of the `[u8; N]` \
683             array being wrapped; and the same four with `generic <type>` in place \
684             of N, for an inner type that is not a byte array \
685             (`fixed_newtype!(pub K, generic [i16; 256])`). A literal array there \
686             also gets `SecretLen` and the declaration-site `N = 0` guard; any other \
687             inner type takes the reduced API (RevealSecret, RevealSecretMut, Debug, \
688             Zeroize, `new`, `new_with`) on purpose. `generic [u8; N]` is not one of \
689             those: write the size literal. \
690             If the length or the marker looks right, the problem is in what follows \
691             it: a `derive:` list needs brackets, and accepts only Deserialize, \
692             FromWrapper, IntoWrapper and WrapperAccess — plus ConstantTimeEq on the \
693             `generic` arms, where it is not automatic."
694        ));
695    };
696}
697
698/// Rejects `generic [u8; N]`, which is strictly dominated by the size-literal arm.
699#[doc(hidden)]
700#[macro_export]
701macro_rules! __sg_fixed_generic_is_bytes {
702    ($n:literal) => {
703        ::core::compile_error!(::core::concat!(
704            "fixed_newtype!: write `",
705            ::core::stringify!($n),
706            "`, not `generic [u8; ",
707            ::core::stringify!($n),
708            "]`. The size-literal arm is the full API for a byte array (`new_with`, \
709             `SecretLen`, `From`, `TryFrom`, the encoders, `from_random`). The \
710             `generic` arm emits only the shape-independent surface, so this \
711             spelling is the same payload with strictly less API. `generic` is \
712             for an inner type that is not `[u8; N]`."
713        ));
714    };
715}