Skip to main content

redoubt_zero_derive/
lib.rs

1// Copyright (c) 2025-2026 Federico Hoerth <memparanoid@gmail.com>
2// SPDX-License-Identifier: GPL-3.0-only
3// See LICENSE in the repository root for full license text.
4
5//! Procedural macros for the `redoubt_zero` crate.
6//!
7//! Provides the `#[derive(RedoubtZero)]` macro for automatic trait implementations.
8//!
9//! ## License
10//!
11//! GPL-3.0-only
12
13#![warn(missing_docs)]
14
15// Only run unit tests on architectures where insta (-> sha2 -> cpufeatures) compiles
16#[cfg(all(
17    test,
18    any(
19        target_arch = "x86_64",
20        target_arch = "x86",
21        target_arch = "aarch64",
22        target_arch = "loongarch64"
23    )
24))]
25mod tests;
26
27use proc_macro::TokenStream;
28use proc_macro_crate::{FoundCrate, crate_name};
29use proc_macro2::{Span, TokenStream as TokenStream2};
30use quote::{format_ident, quote};
31use syn::{
32    Attribute, Data, DeriveInput, Fields, Ident, Index, LitStr, Meta, Type, parse_macro_input,
33};
34
35/// Derives `FastZeroizable`, `ZeroizeMetadata`, `ZeroizationProbe`, and optionally `AssertZeroizeOnDrop` for a struct.
36///
37/// This macro automatically generates trait implementations for structs.
38///
39/// # Requirements
40///
41/// - All fields must implement `FastZeroizable` (except fields with `#[fast_zeroize(skip)]`)
42///
43/// # Optional Sentinel Field
44///
45/// - Named structs can include a field named `__sentinel: ZeroizeOnDropSentinel`
46/// - Tuple structs can include a field of type `ZeroizeOnDropSentinel`
47/// - If present, `AssertZeroizeOnDrop` will be implemented for testing drop behavior
48///
49/// # Attributes
50///
51/// - `#[fast_zeroize(drop)]`: Also generates a `Drop` implementation that calls `fast_zeroize()`
52/// - `#[fast_zeroize(skip)]`: Skip a field from zeroization (e.g., immutable references)
53///
54/// # Generated Implementations
55///
56/// Always generated:
57/// - `FastZeroizable`: Zeroizes all fields (except skipped)
58/// - `ZeroizeMetadata`: Sets `CAN_BE_BULK_ZEROIZED = false`
59/// - `ZeroizationProbe`: Checks if all fields are zeroized (except skipped and sentinel)
60///
61/// If `ZeroizeOnDropSentinel` field is present:
62/// - `AssertZeroizeOnDrop`: Provides test helpers for verifying zeroization on drop
63///
64/// With `#[fast_zeroize(drop)]`:
65/// - `Drop`: Calls `fast_zeroize()` on drop
66///
67/// # Examples
68///
69/// ## Without automatic Drop
70///
71/// ```rust
72/// use redoubt_zero_derive::RedoubtZero;
73/// use redoubt_zero_core::{ZeroizeOnDropSentinel, FastZeroizable};
74///
75/// #[derive(RedoubtZero)]
76/// struct ApiKey {
77///     key: Vec<u8>,
78///     __sentinel: ZeroizeOnDropSentinel,
79/// }
80///
81/// impl Drop for ApiKey {
82///     fn drop(&mut self) {
83///         self.fast_zeroize();
84///     }
85/// }
86/// ```
87///
88/// ## With automatic Drop
89///
90/// ```rust
91/// use redoubt_zero_derive::RedoubtZero;
92/// use redoubt_zero_core::{ZeroizeOnDropSentinel, FastZeroizable};
93///
94/// #[derive(RedoubtZero)]
95/// #[fast_zeroize(drop)]
96/// struct ApiKey {
97///     key: Vec<u8>,
98///     __sentinel: ZeroizeOnDropSentinel,
99/// }
100/// // Drop is automatically generated
101/// ```
102#[proc_macro_derive(RedoubtZero, attributes(fast_zeroize))]
103pub fn derive_redoubt_zero(input: TokenStream) -> TokenStream {
104    let input = parse_macro_input!(input as DeriveInput);
105    expand(input).unwrap_or_else(|e| e).into()
106}
107
108/// Finds the root crate path from a list of candidates.
109///
110/// Resolves the correct import path for `RedoubtZero` or `RedoubtZero-core` depending on context.
111/// Candidates can be crate names like "redoubt-zero" or paths like "redoubt::zero".
112pub(crate) fn find_root_with_candidates(candidates: &[&'static str]) -> TokenStream2 {
113    for &candidate in candidates {
114        // Check if candidate contains "::" (path syntax like "redoubt::zero")
115        if let Some((crate_part, path_part)) = candidate.split_once("::") {
116            match crate_name(crate_part) {
117                Ok(FoundCrate::Itself) => {
118                    let path: TokenStream2 = path_part.parse().unwrap_or_else(|_| quote!());
119                    return quote!(crate::#path);
120                }
121                Ok(FoundCrate::Name(name)) => {
122                    let crate_id = Ident::new(&name, Span::call_site());
123                    let path: TokenStream2 = path_part.parse().unwrap_or_else(|_| quote!());
124                    return quote!(#crate_id::#path);
125                }
126                Err(_) => continue,
127            }
128        } else {
129            match crate_name(candidate) {
130                Ok(FoundCrate::Itself) => return quote!(crate),
131                Ok(FoundCrate::Name(name)) => {
132                    let id = Ident::new(&name, Span::call_site());
133                    return quote!(#id);
134                }
135                Err(_) => continue,
136            }
137        }
138    }
139
140    let msg = "RedoubtZero: could not find redoubt-zero or redoubt-zero-core. Add redoubt-zero to Cargo.toml.";
141    let lit = LitStr::new(msg, Span::call_site());
142    quote! { compile_error!(#lit); }
143}
144
145/// Detects if a type is `ZeroizeOnDropSentinel` by checking the type path.
146///
147/// Used for tuple struct support where we identify the sentinel field by type.
148pub(crate) fn is_zeroize_on_drop_sentinel_type(ty: &Type) -> bool {
149    matches!(
150        ty,
151        Type::Path(type_path)
152        if type_path.path.segments.last()
153            .map(|seg| seg.ident == "ZeroizeOnDropSentinel")
154            .unwrap_or(false)
155    )
156}
157
158/// Detects if a type is a mutable reference (&mut T).
159///
160/// For mutable references, we should pass `self.field` directly instead of `&mut self.field`
161/// to avoid creating `&mut &mut T`.
162pub(crate) fn is_mut_reference_type(ty: &Type) -> bool {
163    if let Type::Reference(r) = ty {
164        r.mutability.is_some()
165    } else {
166        false
167    }
168}
169
170/// Detects if a type is an immutable reference (&T).
171///
172/// Immutable references cannot be zeroized since we don't have mutable access.
173pub(crate) fn is_immut_reference_type(ty: &Type) -> bool {
174    if let Type::Reference(r) = ty {
175        r.mutability.is_none()
176    } else {
177        false
178    }
179}
180
181/// Checks if a field has the `#[fast_zeroize(skip)]` attribute.
182fn has_fast_zeroize_skip(attrs: &[Attribute]) -> bool {
183    attrs.iter().any(|attr| match &attr.meta {
184        Meta::List(meta_list) => {
185            meta_list.path.is_ident("fast_zeroize") && meta_list.tokens.to_string().contains("skip")
186        }
187        _ => false,
188    })
189}
190
191/// Checks if the struct has the `#[fast_zeroize(drop)]` attribute.
192fn has_fast_zeroize_drop(attrs: &[Attribute]) -> bool {
193    attrs.iter().any(|attr| match &attr.meta {
194        Meta::List(meta_list) => {
195            meta_list.path.is_ident("fast_zeroize") && meta_list.tokens.to_string().contains("drop")
196        }
197        _ => false,
198    })
199}
200
201/// Sentinel field information.
202struct SentinelState {
203    index: usize,
204    access: TokenStream2,
205}
206
207/// Expands the DeriveInput into the necessary implementation of `RedoubtZero`.
208fn expand(input: DeriveInput) -> Result<TokenStream2, TokenStream2> {
209    let struct_name = &input.ident;
210    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
211
212    // 1) Resolve the `redoubt_zero_core` or `RedoubtZero` crate (prefer redoubt_zero_core)
213    let root = find_root_with_candidates(&["redoubt-zero-core", "redoubt-zero", "redoubt::zero"]);
214
215    // 2) Get all fields as a Vec
216    let all_fields: Vec<(usize, &syn::Field)> = match &input.data {
217        Data::Struct(data) => match &data.fields {
218            Fields::Named(named) => named.named.iter().enumerate().collect(),
219            Fields::Unnamed(unnamed) => unnamed.unnamed.iter().enumerate().collect(),
220            Fields::Unit => vec![],
221        },
222        _ => {
223            return Err(syn::Error::new_spanned(
224                &input.ident,
225                "RedoubtZero can only be derived for structs (named or tuple).",
226            )
227            .to_compile_error());
228        }
229    };
230
231    // 3) Identify the __sentinel field (optional)
232    let sentinel_ident = format_ident!("__sentinel");
233    let mut maybe_sentinel_state: Option<SentinelState> = None;
234
235    for (i, f) in &all_fields {
236        let is_sentinel = if let Some(ident) = &f.ident {
237            // Named field: check if name is __sentinel
238            if *ident == sentinel_ident {
239                maybe_sentinel_state = Some(SentinelState {
240                    index: *i,
241                    access: quote! { self.#sentinel_ident },
242                });
243                true
244            } else {
245                false
246            }
247        } else {
248            // Unnamed field: check if type is ZeroizeOnDropSentinel
249            if is_zeroize_on_drop_sentinel_type(&f.ty) {
250                let idx = Index::from(*i);
251                maybe_sentinel_state = Some(SentinelState {
252                    index: *i,
253                    access: quote! { self.#idx },
254                });
255                true
256            } else {
257                false
258            }
259        };
260
261        if is_sentinel {
262            break;
263        }
264    }
265
266    // 4) Validate and filter fields
267    // - Check for immutable references without #[fast_zeroize(skip)]
268    // - Filter out fields with #[fast_zeroize(skip)]
269    // - Filter out sentinel (if present)
270    let sentinel_idx = maybe_sentinel_state.as_ref().map(|s| s.index);
271
272    for (i, f) in &all_fields {
273        if Some(*i) == sentinel_idx {
274            continue;
275        }
276
277        if is_immut_reference_type(&f.ty) && !has_fast_zeroize_skip(&f.attrs) {
278            let field_name = if let Some(ident) = &f.ident {
279                format!("field `{}`", ident)
280            } else {
281                format!("field at index {}", i)
282            };
283
284            return Err(syn::Error::new_spanned(
285                &f.ty,
286                format!(
287                    "{} has type `&T` (immutable reference) which cannot be zeroized. \
288                     Add `#[fast_zeroize(skip)]` to exclude it from zeroization.",
289                    field_name
290                ),
291            )
292            .to_compile_error());
293        }
294    }
295
296    // 5) Generate two sets of field references:
297    //    - immut_refs_without_sentinel: for ZeroizationProbe (excludes sentinel and skipped)
298    //    - mut_refs_with_sentinel: for FastZeroizable (includes sentinel, excludes skipped)
299
300    // For ZeroizationProbe: filter out sentinel and skipped fields
301    // Special handling: if field is already &mut T, pass self.field directly (not &self.field)
302    let (immut_refs_without_sentinel, _): (Vec<TokenStream2>, Vec<TokenStream2>) = all_fields
303        .iter()
304        .filter(|(i, f)| Some(*i) != sentinel_idx && !has_fast_zeroize_skip(&f.attrs))
305        .map(|(i, f)| {
306            let is_mut_ref = is_mut_reference_type(&f.ty);
307
308            if let Some(ident) = &f.ident {
309                let immut_ref = if is_mut_ref {
310                    quote! { self.#ident }
311                } else {
312                    quote! { &self.#ident }
313                };
314                (immut_ref, quote! { &mut self.#ident })
315            } else {
316                let idx = Index::from(*i);
317                let immut_ref = if is_mut_ref {
318                    quote! { self.#idx }
319                } else {
320                    quote! { &self.#idx }
321                };
322                (immut_ref, quote! { &mut self.#idx })
323            }
324        })
325        .unzip();
326
327    // For FastZeroizable: include ALL fields except skipped (including sentinel)
328    // Special handling: if field is already &mut T, pass self.field directly (not &mut self.field)
329    let (_, mut_refs_with_sentinel): (Vec<TokenStream2>, Vec<TokenStream2>) = all_fields
330        .iter()
331        .filter(|(_, f)| !has_fast_zeroize_skip(&f.attrs))
332        .map(|(i, f)| {
333            let is_mut_ref = is_mut_reference_type(&f.ty);
334
335            if let Some(ident) = &f.ident {
336                let mut_ref = if is_mut_ref {
337                    quote! { self.#ident }
338                } else {
339                    quote! { &mut self.#ident }
340                };
341                (quote! { &self.#ident }, mut_ref)
342            } else {
343                let idx = Index::from(*i);
344                let mut_ref = if is_mut_ref {
345                    quote! { self.#idx }
346                } else {
347                    quote! { &mut self.#idx }
348                };
349                (quote! { &self.#idx }, mut_ref)
350            }
351        })
352        .unzip();
353
354    // 5) Calculate lengths
355    let len_without_sentinel = immut_refs_without_sentinel.len();
356    let len_without_sentinel_lit =
357        syn::LitInt::new(&len_without_sentinel.to_string(), Span::call_site());
358
359    let len_with_sentinel = mut_refs_with_sentinel.len();
360    let len_with_sentinel_lit = syn::LitInt::new(&len_with_sentinel.to_string(), Span::call_site());
361
362    // 6) Check if we should generate Drop implementation
363    let should_generate_drop = has_fast_zeroize_drop(&input.attrs);
364
365    // 7) Emit the trait implementations
366    let drop_impl = if should_generate_drop {
367        quote! {
368            impl #impl_generics Drop for #struct_name #ty_generics #where_clause {
369                fn drop(&mut self) {
370                    #root::FastZeroizable::fast_zeroize(self);
371                }
372            }
373        }
374    } else {
375        quote! {}
376    };
377
378    let output = quote! {
379        impl #impl_generics #root::ZeroizeMetadata for #struct_name #ty_generics #where_clause {
380            const CAN_BE_BULK_ZEROIZED: bool = false;
381        }
382
383        impl #impl_generics #root::FastZeroizable for #struct_name #ty_generics #where_clause {
384            fn fast_zeroize(&mut self) {
385                let fields: [&mut dyn #root::FastZeroizable; #len_with_sentinel_lit] = [
386                    #( #root::collections::to_fast_zeroizable_dyn_mut(#mut_refs_with_sentinel) ),*
387                ];
388                #root::collections::zeroize_collection(&mut fields.into_iter())
389            }
390        }
391
392        impl #impl_generics #root::ZeroizationProbe for #struct_name #ty_generics #where_clause {
393            fn is_zeroized(&self) -> bool {
394                let fields: [&dyn #root::ZeroizationProbe; #len_without_sentinel_lit] = [
395                    #( #root::collections::to_zeroization_probe_dyn_ref(#immut_refs_without_sentinel) ),*
396                ];
397                #root::collections::collection_zeroed(&mut fields.into_iter())
398            }
399        }
400
401        #drop_impl
402    };
403
404    // Conditionally implement AssertZeroizeOnDrop if sentinel is present
405    let assert_impl = if let Some(sentinel_state) = maybe_sentinel_state {
406        let sentinel_access = sentinel_state.access;
407        quote! {
408            impl #impl_generics #root::AssertZeroizeOnDrop for #struct_name #ty_generics #where_clause {
409                fn clone_sentinel(&self) -> #root::ZeroizeOnDropSentinel {
410                    #sentinel_access.clone()
411                }
412
413                fn assert_zeroize_on_drop(self) {
414                    #root::assert::assert_zeroize_on_drop(self);
415                }
416            }
417        }
418    } else {
419        quote! {}
420    };
421
422    let full_output = quote! {
423        #output
424        #assert_impl
425    };
426
427    Ok(full_output)
428}