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//! # Naming the type still compiles
113//!
114//! The bound is on [`Fixed::new`](crate::Fixed::new), not on the struct, which keeps the
115//! property the zero-size guard already documents: `type Bad = Fixed<Vec<u8>>;` is a
116//! legal type expression and no guard in the type could make it otherwise. What the bound
117//! removes is every *value* of it.
118
119/// Marker trait asserting that a type owns no heap allocation.
120///
121/// Required by [`Fixed::new`](crate::Fixed::new). See the
122/// [module documentation](self) for the contract, for what is already implemented,
123/// and for why this is an assertion rather than an enforcement.
124///
125/// No methods — its only purpose is to gate construction of a
126/// [`Fixed<T>`](crate::Fixed).
127#[diagnostic::on_unimplemented(
128 message = "`{Self}` is not known to be free of heap allocations, so it cannot be the inner type of a `Fixed`",
129 label = "`Fixed` holds its secret inline; `{Self}` does not implement `FixedStorage`",
130 note = "`Fixed<T>` exists so a secret can be held with no allocator at all, and `FixedStorage` is what keeps that true: a heap allocation can be abandoned unwiped, either by a reallocation or by replacing the whole value. The bound is an allow-list, so there are two reasons to land here — `{Self}` owns a heap allocation, or nothing has asserted that it does not.",
131 note = "If `{Self}` does own one — a `Vec`, a `String`, a `Box<[T]>`, a collection, or a struct holding any of those — then this is the bound doing its job: reach for `Dynamic<T>`, which documents the residue and offers one safe growth path. `Dynamic` needs this crate's `alloc` feature, so if you are seeing this with `alloc` off while still able to name `Vec`, enabling it is the fix.",
132 note = "If `{Self}` owns no heap allocation, it is only missing the assertion. For a type you define, add an empty `impl FixedStorage` block for it. For a type you do not own, the orphan rule makes that impossible and rejects the impl with E0117 — wrap it in a newtype of your own and implement the marker for that, or ask for an impl upstream.",
133 note = "Already covered: the primitives, `[T; N]`, tuples up to ten elements, `Option<T>`, the twelve `NonZero` integers, `Wrapping<T>`, `MaybeUninit<T>`, `zeroize::Zeroizing<T>`, and `Fixed<T>` itself — each when its payload qualifies."
134)]
135pub trait FixedStorage {}
136
137macro_rules! __sg_fixed_storage_prims {
138 ($($t:ty),* $(,)?) => {
139 $(
140 impl FixedStorage for $t {}
141 )*
142 };
143}
144
145__sg_fixed_storage_prims!(
146 u8,
147 u16,
148 u32,
149 u64,
150 u128,
151 usize,
152 i8,
153 i16,
154 i32,
155 i64,
156 i128,
157 isize,
158 bool,
159 char,
160 f32,
161 f64,
162 ()
163);
164
165// The twelve `NonZero` integers `zeroize` implements `Zeroize` for. Each is a single integer
166// with a niche, so there is no allocation to abandon.
167//
168// These are foreign types, so a downstream crate cannot assert this itself: `impl
169// FixedStorage for NonZeroU32` is E0117. That is why the impls live here — without them,
170// `Fixed<NonZeroU32>` would be refused with no fix available to the caller.
171__sg_fixed_storage_prims!(
172 core::num::NonZeroU8,
173 core::num::NonZeroU16,
174 core::num::NonZeroU32,
175 core::num::NonZeroU64,
176 core::num::NonZeroU128,
177 core::num::NonZeroUsize,
178 core::num::NonZeroI8,
179 core::num::NonZeroI16,
180 core::num::NonZeroI32,
181 core::num::NonZeroI64,
182 core::num::NonZeroI128,
183 core::num::NonZeroIsize,
184);
185
186/// An array never reallocates, whatever its length, so it is `FixedStorage` exactly when
187/// its element type is. This is the impl that covers `[u8; N]` and the non-byte arrays the
188/// `generic` arm of [`fixed_newtype!`](crate::fixed_newtype) exists for.
189impl<T: FixedStorage, const N: usize> FixedStorage for [T; N] {}
190
191/// `Wrapping` is a transparent arithmetic wrapper: same storage as its payload.
192impl<T: FixedStorage> FixedStorage for core::num::Wrapping<T> {}
193
194/// `MaybeUninit<T>` is `T`'s storage, initialised or not. No allocation either way.
195impl<T: FixedStorage> FixedStorage for core::mem::MaybeUninit<T> {}
196
197/// `zeroize::Zeroizing<T>` holds `T` inline and wipes it on drop, so it qualifies exactly
198/// when `T` does.
199///
200/// This one matters out of proportion to its size. `Zeroizing` is the idiomatic "wipe this
201/// on drop" wrapper from this crate's only non-optional dependency, so
202/// `Fixed<Zeroizing<[u8; 32]>>` is a shape people reach for — and because both the trait and
203/// the type are foreign to the caller, the orphan rule leaves them no way to assert it. It
204/// has to be here or it is unavailable.
205impl<T: FixedStorage + zeroize::Zeroize> FixedStorage for zeroize::Zeroizing<T> {}
206
207/// A `Fixed<T>` holds `T` inline, so the wrapper nests: `Fixed<Fixed<[u8; 32]>>` is legal
208/// exactly when the payload qualifies. Without this impl the crate's own wrapper could not
209/// be used as its own inner type.
210impl<T: zeroize::Zeroize + FixedStorage> FixedStorage for crate::Fixed<T> {}
211
212macro_rules! __sg_fixed_storage_tuples {
213 ($(($($name:ident),+);)*) => {
214 $(
215 impl<$($name: FixedStorage),+> FixedStorage for ($($name,)+) {}
216 )*
217 };
218}
219
220__sg_fixed_storage_tuples! {
221 (A);
222 (A, B);
223 (A, B, C);
224 (A, B, C, D);
225 (A, B, C, D, E);
226 (A, B, C, D, E, F);
227 (A, B, C, D, E, F, G);
228 (A, B, C, D, E, F, G, H);
229 (A, B, C, D, E, F, G, H, I);
230 (A, B, C, D, E, F, G, H, I, J);
231}
232
233/// An `Option` adds a discriminant and no heap storage, so it qualifies exactly when its
234/// payload does. `Option<[u8; 32]>` is a legitimate fixed-size secret; `Option<Vec<u8>>`
235/// and `Option<Box<[u8]>>` are both rejected, which is the point.
236impl<T: FixedStorage> FixedStorage for Option<T> {}