secure_gate/traits/fixed_storage.rs
1//! Marker trait asserting that a secret's storage owns no heap allocation.
2//!
3//! [`Fixed<T>`](crate::Fixed) is documented as having no reallocation surface, which is
4//! what lets `SECURITY.md` exempt it from the heap-residue weakness that
5//! [`Dynamic<T>`](crate::Dynamic) carries. That exemption was a claim about the *shape*
6//! people were expected to use, not something the type system checked: `Fixed<T>` is
7//! bounded only by `Zeroize`, so `Fixed<Vec<u8>>` and `Fixed<String>` compiled, and
8//! `fixed_newtype!(pub Name, generic Vec<u8>)` was a documented path straight to them.
9//! Measured, they leak exactly as much as the `Dynamic` equivalent — and worse, because
10//! `Dynamic<Vec<u8>>` has a safe-growth `std::io::Write` impl and `Fixed<Vec<u8>>` has
11//! nothing.
12//!
13//! [`FixedStorage`] is what makes the exemption true. [`Fixed::new`](crate::Fixed::new)
14//! requires it, so the compiler now rejects an inner type that owns a heap allocation.
15//!
16//! # The contract
17//!
18//! Implementing `FixedStorage` for a type asserts that **the type owns no heap
19//! allocation**. Nothing else, and note that this is stricter than "cannot be resized".
20//!
21//! The first version of this trait asked only for a fixed capacity, and blessed `Box<[T]>`
22//! on the reasoning that a boxed slice has its length fixed at construction. That was
23//! wrong, and measurably so: the residue never needed a capacity change. Whole-value
24//! replacement abandons the allocation just as well, and the wrapper wipes whatever it
25//! holds at drop, not what it used to hold. On a `Fixed<Box<[u8]>>` carrying a 1024-byte
26//! secret, `with_secret_mut(|slot| *slot = other_boxed_slice)` released the original block
27//! with **1024 of 1024** bytes intact, and so did the same assignment through
28//! `expose_secret_mut`. The inline contrast, `Fixed<[u8; 1024]>` assigned the same way,
29//! freed nothing at all — there is no allocation to abandon. So `Box<[T]>` is no longer
30//! implemented, and the predicate is heap ownership rather than resizability.
31//!
32//! The test to apply to your own type: *does `Self` own a heap allocation?* If yes, do not
33//! implement this — reach for [`Dynamic`](crate::Dynamic), which documents the residue.
34//!
35//! # This is an assertion, not an enforcement
36//!
37//! Like [`CloneableSecret`](crate::CloneableSecret), the compiler checks that you wrote
38//! the impl, not that the claim is true. A type with a `Vec` field that implements
39//! `FixedStorage` anyway will compile and will leak, which is measured and deliberate:
40//! the alternative is a closed set of blessed types, and that would break the one thing
41//! the `generic` arm of [`fixed_newtype!`](crate::fixed_newtype) exists for — holding a
42//! custom secret on a target with no allocator.
43//!
44//! What the marker buys is that the claim is now *written down at a greppable line* in
45//! the crate making it, rather than being an unstated assumption. That is the same trade
46//! the other opt-in markers in this crate make.
47//!
48//! # What is implemented for you
49//!
50//! - the integer, floating-point, `bool`, `char` and `()` primitives;
51//! - the twelve `NonZero` integers;
52//! - `[T; N]` for any `N`, when `T: FixedStorage` — which covers `[u8; 32]` and the
53//! non-byte arrays the `generic` arm exists for, such as `[i16; 256]`;
54//! - tuples up to ten elements of `FixedStorage` types, matching `zeroize`'s own ceiling;
55//! - `Option<T>` when `T: FixedStorage` — a discriminant adds no heap storage;
56//! - `Wrapping<T>` and `MaybeUninit<T>` when `T: FixedStorage` — both are `T`'s own storage;
57//! - `zeroize::Zeroizing<T>` when `T: FixedStorage`;
58//! - `Fixed<T>` itself when `T: FixedStorage`, so the wrapper nests.
59//!
60//! # Why that list has to be this long
61//!
62//! The bound is an allow-list, which means it fails closed: a type is refused both when it
63//! owns a heap allocation and when nothing has asserted that it does not. For a type you
64//! define, the second case costs one line. For a type you do **not** own it costs nothing,
65//! because you cannot pay it — `impl FixedStorage for NonZeroU32` in your crate is E0117,
66//! the orphan rule, and no amount of effort downstream changes that.
67//!
68//! So every heap-free type reachable through `zeroize`'s own `Zeroize` impls has to be
69//! blessed here or it is simply unavailable as a `Fixed` inner type, with the caller's only
70//! escape being to change their own type. That is the reason for the `NonZero`, `Wrapping`,
71//! `MaybeUninit` and `Zeroizing` impls above, and for tuples reaching ten rather than four:
72//! each was a shape that compiled before this bound existed and that no downstream crate
73//! could have restored. Ten is not arbitrary — `zeroize` implements `Zeroize` for tuples up
74//! to ten elements and no further, so an eleventh element fails the `Zeroize` bound before
75//! this one is ever consulted.
76//!
77//! The deny-list alternative — accept everything except a known set of heap owners — was
78//! rejected deliberately. It has no false rejections, but it fails **open**: a custom type
79//! wrapping a `Vec` would be accepted in silence, which is the exact weakness this trait
80//! exists to close.
81//!
82//! Nothing heap-owning is implemented, deliberately. `Box<[T]>` was and is not: see the
83//! contract above for the measurement that removed it. `Box<[u8; N]>` never arises anyway,
84//! because `zeroize` does not implement `Zeroize` for a `Box` of a sized type. For a
85//! heap-only secret of fixed size, the supported shape is `Dynamic<[u8; N]>`.
86//!
87//! A custom inner type needs one line:
88//!
89//! ```rust
90//! use secure_gate::{FixedStorage, Fixed, RevealSecret, SentinelValue};
91//! use zeroize::Zeroize;
92//!
93//! struct Poly([i16; 256]);
94//!
95//! impl Zeroize for Poly {
96//! fn zeroize(&mut self) {
97//! self.0.zeroize();
98//! }
99//! }
100//! impl SentinelValue for Poly {
101//! fn sentinel_value() -> Self {
102//! Poly([0i16; 256])
103//! }
104//! }
105//! // Asserted here: `Poly` owns no heap allocation.
106//! impl FixedStorage for Poly {}
107//!
108//! let p = Fixed::new(Poly([7i16; 256]));
109//! assert_eq!(p.with_secret(|poly| poly.0[0]), 7);
110//! ```
111//!
112//! # If the bound rejects your type
113//!
114//! The error is the bare unsatisfied-bound form, `E0277` naming the inner type and the
115//! unsatisfied `FixedStorage` bound, followed by the list of implementors. `main` improves it
116//! with
117//! `#[diagnostic::on_unimplemented]`, which needs Rust 1.78 and so cannot be used on this
118//! line's MSRV 1.70. The guidance that attribute carries is therefore written out here:
119//!
120//! - If the inner type owns a heap allocation, reach for [`Dynamic`](crate::Dynamic), which
121//! documents the residue and offers one safe growth path.
122//! - `Dynamic` needs this crate's `alloc` feature. If you are seeing this error with `alloc`
123//! off while still able to name `Vec` or `String` — which happens when `zeroize`'s own
124//! `alloc` feature is enabled elsewhere in the dependency graph — then enabling this crate's
125//! `alloc` is the fix. In that configuration `Fixed<Vec<u8>>` is nameable while
126//! `secure_gate::Dynamic` does not exist at all, which is the one corner where the two
127//! wrappers do not simply substitute for each other.
128//! - If the type genuinely owns no heap allocation, it is only missing the assertion. For a
129//! type you define, write an empty `impl FixedStorage` block for it. That is an assertion
130//! the compiler cannot check, so only write it if it is true.
131//! - If it is a type you do **not** own, you cannot write that impl at all: the orphan rule
132//! rejects it with `E0117`. Wrap it in a newtype of your own and implement the marker for
133//! that, or ask for an impl upstream. The heap-free types `zeroize` supports are blessed
134//! here already, for exactly this reason — see "Why that list has to be this long" above.
135//!
136//! # Naming the type still compiles
137//!
138//! The bound is on [`Fixed::new`](crate::Fixed::new), not on the struct, which keeps the
139//! property the zero-size guard already documents: `type Bad = Fixed<Vec<u8>>;` is a
140//! legal type expression and no guard in the type could make it otherwise. What the bound
141//! removes is every *value* of it.
142
143/// Marker trait asserting that a type owns no heap allocation.
144///
145/// Required by [`Fixed::new`](crate::Fixed::new). See the
146/// [module documentation](self) for the contract, for what is already implemented,
147/// and for why this is an assertion rather than an enforcement.
148///
149/// No methods — its only purpose is to gate construction of a
150/// [`Fixed<T>`](crate::Fixed).
151pub trait FixedStorage {}
152
153macro_rules! __sg_fixed_storage_prims {
154 ($($t:ty),* $(,)?) => {
155 $(
156 impl FixedStorage for $t {}
157 )*
158 };
159}
160
161__sg_fixed_storage_prims!(
162 u8,
163 u16,
164 u32,
165 u64,
166 u128,
167 usize,
168 i8,
169 i16,
170 i32,
171 i64,
172 i128,
173 isize,
174 bool,
175 char,
176 f32,
177 f64,
178 ()
179);
180
181// The twelve `NonZero` integers `zeroize` implements `Zeroize` for. Each is a single integer
182// with a niche, so there is no allocation to abandon.
183//
184// These are foreign types, so a downstream crate cannot assert this itself: `impl
185// FixedStorage for NonZeroU32` is E0117. That is why the impls live here -- without them,
186// `Fixed<NonZeroU32>` would be refused with no fix available to the caller.
187__sg_fixed_storage_prims!(
188 core::num::NonZeroU8,
189 core::num::NonZeroU16,
190 core::num::NonZeroU32,
191 core::num::NonZeroU64,
192 core::num::NonZeroU128,
193 core::num::NonZeroUsize,
194 core::num::NonZeroI8,
195 core::num::NonZeroI16,
196 core::num::NonZeroI32,
197 core::num::NonZeroI64,
198 core::num::NonZeroI128,
199 core::num::NonZeroIsize,
200);
201
202/// An array never reallocates, whatever its length, so it is `FixedStorage` exactly when
203/// its element type is. This is the impl that covers `[u8; N]` and the non-byte arrays the
204/// `generic` arm of [`fixed_newtype!`](crate::fixed_newtype) exists for.
205impl<T: FixedStorage, const N: usize> FixedStorage for [T; N] {}
206
207/// `Wrapping` is a transparent arithmetic wrapper: same storage as its payload.
208impl<T: FixedStorage> FixedStorage for core::num::Wrapping<T> {}
209
210/// `MaybeUninit<T>` is `T`'s storage, initialised or not. No allocation either way.
211impl<T: FixedStorage> FixedStorage for core::mem::MaybeUninit<T> {}
212
213/// `zeroize::Zeroizing<T>` holds `T` inline and wipes it on drop, so it qualifies exactly
214/// when `T` does.
215///
216/// This one matters out of proportion to its size. `Zeroizing` is the idiomatic "wipe this
217/// on drop" wrapper from this crate's only non-optional dependency, so
218/// `Fixed<Zeroizing<[u8; 32]>>` is a shape people reach for -- and because both the trait and
219/// the type are foreign to the caller, the orphan rule leaves them no way to assert it. It
220/// has to be here or it is unavailable.
221impl<T: FixedStorage + zeroize::Zeroize> FixedStorage for zeroize::Zeroizing<T> {}
222
223/// A `Fixed<T>` holds `T` inline, so the wrapper nests: `Fixed<Fixed<[u8; 32]>>` is legal
224/// exactly when the payload qualifies. Without this impl the crate's own wrapper could not
225/// be used as its own inner type.
226impl<T: zeroize::Zeroize + FixedStorage> FixedStorage for crate::Fixed<T> {}
227
228macro_rules! __sg_fixed_storage_tuples {
229 ($(($($name:ident),+);)*) => {
230 $(
231 impl<$($name: FixedStorage),+> FixedStorage for ($($name,)+) {}
232 )*
233 };
234}
235
236__sg_fixed_storage_tuples! {
237 (A);
238 (A, B);
239 (A, B, C);
240 (A, B, C, D);
241 (A, B, C, D, E);
242 (A, B, C, D, E, F);
243 (A, B, C, D, E, F, G);
244 (A, B, C, D, E, F, G, H);
245 (A, B, C, D, E, F, G, H, I);
246 (A, B, C, D, E, F, G, H, I, J);
247}
248
249/// An `Option` adds a discriminant and no heap storage, so it qualifies exactly when its
250/// payload does. `Option<[u8; 32]>` is a legitimate fixed-size secret; `Option<Vec<u8>>`
251/// and `Option<Box<[u8]>>` are both rejected, which is the point.
252impl<T: FixedStorage> FixedStorage for Option<T> {}