1#![deny(unsafe_code)]
2#![deny(unsafe_op_in_unsafe_fn)]
3
4use proc_macro::TokenStream;
5use proc_macro2::{Span, TokenStream as TokenStream2};
6use quote::quote;
7use syn::{
8 parse_macro_input, parse_quote, spanned::Spanned, Attribute, Data, DataStruct, DeriveInput,
9 Error, Fields, Generics, LitStr, Path, Result, WherePredicate,
10};
11
12#[proc_macro_derive(SecureSanitize, attributes(sanitization))]
30pub fn derive_secure_sanitize(input: TokenStream) -> TokenStream {
31 let input = parse_macro_input!(input as DeriveInput);
32 expand_secure_sanitize(&input)
33 .unwrap_or_else(Error::into_compile_error)
34 .into()
35}
36
37#[proc_macro_derive(SecureSanitizeOnDrop, attributes(sanitization))]
70pub fn derive_secure_sanitize_on_drop(input: TokenStream) -> TokenStream {
71 let input = parse_macro_input!(input as DeriveInput);
72 expand_secure_sanitize_on_drop(&input)
73 .unwrap_or_else(Error::into_compile_error)
74 .into()
75}
76
77#[proc_macro_derive(ConstantTimeEq, attributes(sanitization))]
89pub fn derive_constant_time_eq(input: TokenStream) -> TokenStream {
90 let input = parse_macro_input!(input as DeriveInput);
91 expand_constant_time_eq(&input)
92 .unwrap_or_else(Error::into_compile_error)
93 .into()
94}
95
96#[proc_macro_derive(ConditionallySelectable, attributes(sanitization))]
106pub fn derive_conditionally_selectable(input: TokenStream) -> TokenStream {
107 let input = parse_macro_input!(input as DeriveInput);
108 expand_conditionally_selectable(&input)
109 .unwrap_or_else(Error::into_compile_error)
110 .into()
111}
112
113#[derive(Default)]
114struct ContainerOptions {
115 crate_path: Option<Path>,
116 bound_override: Option<Vec<WherePredicate>>,
117}
118
119#[derive(Default)]
120struct FieldOptions {
121 skip: bool,
122 skip_span: Option<Span>,
123 reason: Option<LitStr>,
124 bound_override: Option<Vec<WherePredicate>>,
125}
126
127fn expand_secure_sanitize(input: &DeriveInput) -> Result<TokenStream2> {
128 let options = parse_container_options(&input.attrs)?;
129 validate_container_option_scope(input, &options)?;
130 let crate_path = crate_path(&options);
131 let body = match &input.data {
132 Data::Struct(data) => expand_struct_body(data, &crate_path)?,
133 Data::Enum(_) => {
134 return Err(Error::new_spanned(
135 input,
136 "SecureSanitize cannot be derived for enums because inactive variant bytes are unreachable; use a struct-based state machine or a reviewed manual implementation that sanitizes before every transition",
137 ))
138 }
139 Data::Union(_) => {
140 return Err(Error::new_spanned(
141 input,
142 "SecureSanitize cannot be derived for unions; implement it manually using documented unsafe code for the active field",
143 ))
144 }
145 };
146 let generics = add_sanitize_bounds(input.generics.clone(), &input.data, &crate_path, &options)?;
147 let name = &input.ident;
148 let (impl_generics, type_generics, where_clause) = generics.split_for_impl();
149
150 Ok(quote! {
151 impl #impl_generics #crate_path::SecureSanitize for #name #type_generics #where_clause {
152 #[inline]
153 fn secure_sanitize(&mut self) {
154 #body
155 }
156 }
157
158 impl #impl_generics #crate_path::DropSafeSanitize for #name #type_generics #where_clause {}
159 })
160}
161
162fn expand_secure_sanitize_on_drop(input: &DeriveInput) -> Result<TokenStream2> {
163 let options = parse_container_options(&input.attrs)?;
164 validate_container_option_scope(input, &options)?;
165 validate_field_options(&input.data)?;
166 let crate_path = crate_path(&options);
167
168 match &input.data {
169 Data::Struct(_) => {}
170 Data::Enum(_) => {
171 return Err(Error::new_spanned(
172 input,
173 "SecureSanitizeOnDrop cannot be derived for enums because final-drop sanitization cannot clear inactive bytes retained by earlier variant transitions; use a stable-layout struct state machine or a reviewed manual Drop implementation with secure_replace before every transition",
174 ))
175 }
176 Data::Union(_) => {
177 return Err(Error::new_spanned(
178 input,
179 "SecureSanitizeOnDrop cannot be derived for unions",
180 ))
181 }
182 }
183
184 let name = &input.ident;
185 let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
186
187 Ok(quote! {
188 impl #impl_generics Drop for #name #type_generics #where_clause {
189 #[inline]
190 fn drop(&mut self) {
191 fn require_drop_contract<T: ?Sized + #crate_path::DropSafeSanitize + ::core::marker::Unpin>() {}
192 require_drop_contract::<Self>();
193 #crate_path::SecureSanitize::secure_sanitize(self);
194 }
195 }
196 })
197}
198
199fn expand_constant_time_eq(input: &DeriveInput) -> Result<TokenStream2> {
200 let options = parse_container_options(&input.attrs)?;
201 validate_container_option_scope(input, &options)?;
202 let crate_path = crate_path(&options);
203 let body = match &input.data {
204 Data::Struct(data) => expand_ct_eq_struct_body(data, &crate_path)?,
205 Data::Enum(_) => {
206 return Err(Error::new_spanned(
207 input,
208 "ConstantTimeEq cannot be derived for enums; compare explicit struct wrappers or implement the active-variant semantics manually",
209 ))
210 }
211 Data::Union(_) => {
212 return Err(Error::new_spanned(
213 input,
214 "ConstantTimeEq cannot be derived for unions; implement it manually using documented unsafe code for the active field",
215 ))
216 }
217 };
218 let trait_path: TokenStream2 = quote!(#crate_path::ct::ConstantTimeEq);
219 let generics = add_trait_bounds(
220 input.generics.clone(),
221 &input.data,
222 &trait_path,
223 &options,
224 SkipPolicy::Allow,
225 )?;
226 let name = &input.ident;
227 let (impl_generics, type_generics, where_clause) = generics.split_for_impl();
228
229 Ok(quote! {
230 impl #impl_generics #crate_path::ct::ConstantTimeEq for #name #type_generics #where_clause {
231 #[inline]
232 fn ct_eq(&self, other: &Self) -> #crate_path::ct::Choice {
233 #body
234 }
235 }
236 })
237}
238
239fn expand_conditionally_selectable(input: &DeriveInput) -> Result<TokenStream2> {
240 let options = parse_container_options(&input.attrs)?;
241 validate_container_option_scope(input, &options)?;
242 let crate_path = crate_path(&options);
243 let body = match &input.data {
244 Data::Struct(data) => expand_ct_select_struct_body(data, &crate_path)?,
245 Data::Enum(_) => {
246 return Err(Error::new_spanned(
247 input,
248 "ConditionallySelectable cannot be derived for enums; select explicit struct wrappers or implement the active-variant semantics manually",
249 ))
250 }
251 Data::Union(_) => {
252 return Err(Error::new_spanned(
253 input,
254 "ConditionallySelectable cannot be derived for unions; implement it manually using documented unsafe code for the active field",
255 ))
256 }
257 };
258 let trait_path: TokenStream2 = quote!(#crate_path::ct::ConditionallySelectable);
259 let generics = add_trait_bounds(
260 input.generics.clone(),
261 &input.data,
262 &trait_path,
263 &options,
264 SkipPolicy::Reject,
265 )?;
266 let name = &input.ident;
267 let (impl_generics, type_generics, where_clause) = generics.split_for_impl();
268
269 Ok(quote! {
270 impl #impl_generics #crate_path::ct::ConditionallySelectable for #name #type_generics #where_clause {
271 #[inline]
272 fn conditional_select(
273 left: &Self,
274 right: &Self,
275 choice: #crate_path::ct::Choice,
276 ) -> Self {
277 #body
278 }
279 }
280 })
281}
282
283fn crate_path(options: &ContainerOptions) -> Path {
284 options
285 .crate_path
286 .clone()
287 .unwrap_or_else(|| parse_quote!(::sanitization))
288}
289
290fn validate_container_option_scope(
291 _input: &DeriveInput,
292 _options: &ContainerOptions,
293) -> Result<()> {
294 Ok(())
295}
296
297fn validate_field_options(data: &Data) -> Result<()> {
298 for field in sanitized_fields(data)? {
299 parse_field_options(&field.attrs)?;
300 }
301 Ok(())
302}
303
304fn add_sanitize_bounds(
305 mut generics: Generics,
306 data: &Data,
307 crate_path: &Path,
308 options: &ContainerOptions,
309) -> Result<Generics> {
310 let where_clause = generics.make_where_clause();
311
312 if let Some(bounds) = &options.bound_override {
313 where_clause.predicates.extend(bounds.iter().cloned());
314 return Ok(generics);
315 }
316
317 for field in sanitized_fields(data)? {
318 let field_options = parse_field_options(&field.attrs)?;
319 if field_options.skip {
320 continue;
321 }
322
323 if let Some(bounds) = field_options.bound_override {
324 where_clause.predicates.extend(bounds);
325 } else {
326 let ty = &field.ty;
327 where_clause
328 .predicates
329 .push(parse_quote!(#ty: #crate_path::SecureSanitize));
330 }
331 }
332
333 Ok(generics)
334}
335
336#[derive(Clone, Copy)]
337enum SkipPolicy {
338 Allow,
339 Reject,
340}
341
342fn add_trait_bounds(
343 mut generics: Generics,
344 data: &Data,
345 trait_path: &TokenStream2,
346 options: &ContainerOptions,
347 skip_policy: SkipPolicy,
348) -> Result<Generics> {
349 let where_clause = generics.make_where_clause();
350
351 if let Some(bounds) = &options.bound_override {
352 where_clause.predicates.extend(bounds.iter().cloned());
353 return Ok(generics);
354 }
355
356 for field in sanitized_fields(data)? {
357 let field_options = parse_field_options(&field.attrs)?;
358 if field_options.skip {
359 if matches!(skip_policy, SkipPolicy::Reject) {
360 return Err(Error::new_spanned(
361 field,
362 "#[sanitization(skip)] is not supported for this derive because every output field must be constructed",
363 ));
364 }
365 continue;
366 }
367
368 if let Some(bounds) = field_options.bound_override {
369 where_clause.predicates.extend(bounds);
370 } else {
371 let ty = &field.ty;
372 where_clause.predicates.push(parse_quote!(#ty: #trait_path));
373 }
374 }
375
376 Ok(generics)
377}
378
379fn sanitized_fields(data: &Data) -> Result<Vec<&syn::Field>> {
380 let mut fields = Vec::new();
381 match data {
382 Data::Struct(data) => fields.extend(data.fields.iter()),
383 Data::Enum(data) => {
384 for variant in &data.variants {
385 fields.extend(variant.fields.iter());
386 }
387 }
388 Data::Union(_) => {}
389 }
390 Ok(fields)
391}
392
393fn expand_struct_body(data: &DataStruct, crate_path: &Path) -> Result<TokenStream2> {
394 let calls = field_calls_for_struct(&data.fields, crate_path)?;
395 Ok(quote!(#(#calls)*))
396}
397
398fn expand_ct_eq_struct_body(data: &DataStruct, crate_path: &Path) -> Result<TokenStream2> {
399 let mut calls = Vec::new();
400
401 for (index, field) in data.fields.iter().enumerate() {
402 if parse_field_options(&field.attrs)?.skip {
403 continue;
404 }
405
406 let (left, right) = match &field.ident {
407 Some(ident) => (quote!(&self.#ident), quote!(&other.#ident)),
408 None => {
409 let index = syn::Index::from(index);
410 (quote!(&self.#index), quote!(&other.#index))
411 }
412 };
413 calls.push(quote! {
414 result = result & #crate_path::ct::ConstantTimeEq::ct_eq(#left, #right);
415 });
416 }
417
418 Ok(quote! {
419 let mut result = #crate_path::ct::Choice::TRUE;
420 #(#calls)*
421 result
422 })
423}
424
425fn expand_ct_select_struct_body(data: &DataStruct, crate_path: &Path) -> Result<TokenStream2> {
426 match &data.fields {
427 Fields::Named(fields) => {
428 let mut selected = Vec::new();
429 for field in &fields.named {
430 if parse_field_options(&field.attrs)?.skip {
431 return Err(Error::new_spanned(
432 field,
433 "#[sanitization(skip)] is not supported for ConditionallySelectable derives",
434 ));
435 }
436 let ident = field.ident.as_ref().expect("named field");
437 selected.push(quote! {
438 #ident: #crate_path::ct::ConditionallySelectable::conditional_select(
439 &left.#ident,
440 &right.#ident,
441 choice,
442 )
443 });
444 }
445 Ok(quote!(Self { #(#selected),* }))
446 }
447 Fields::Unnamed(fields) => {
448 let mut selected = Vec::new();
449 for (index, field) in fields.unnamed.iter().enumerate() {
450 if parse_field_options(&field.attrs)?.skip {
451 return Err(Error::new_spanned(
452 field,
453 "#[sanitization(skip)] is not supported for ConditionallySelectable derives",
454 ));
455 }
456 let index = syn::Index::from(index);
457 selected.push(quote! {
458 #crate_path::ct::ConditionallySelectable::conditional_select(
459 &left.#index,
460 &right.#index,
461 choice,
462 )
463 });
464 }
465 Ok(quote!(Self(#(#selected),*)))
466 }
467 Fields::Unit => Ok(quote!(Self)),
468 }
469}
470
471fn field_calls_for_struct(fields: &Fields, crate_path: &Path) -> Result<Vec<TokenStream2>> {
472 let mut calls = Vec::new();
473
474 for (index, field) in fields.iter().enumerate() {
475 if parse_field_options(&field.attrs)?.skip {
476 continue;
477 }
478
479 let access = match &field.ident {
480 Some(ident) => quote!(&mut self.#ident),
481 None => {
482 let index = syn::Index::from(index);
483 quote!(&mut self.#index)
484 }
485 };
486 calls.push(quote!(#crate_path::SecureSanitize::secure_sanitize(#access);));
487 }
488
489 Ok(calls)
490}
491
492fn parse_container_options(attrs: &[Attribute]) -> Result<ContainerOptions> {
493 let mut options = ContainerOptions::default();
494
495 for attr in attrs
496 .iter()
497 .filter(|attr| attr.path().is_ident("sanitization"))
498 {
499 attr.parse_nested_meta(|meta| {
500 if meta.path.is_ident("crate") {
501 if options.crate_path.is_some() {
502 return Err(meta.error("duplicate sanitization crate option"));
503 }
504 let value = meta.value()?;
505 let literal: LitStr = value.parse()?;
506 options.crate_path = Some(literal.parse()?);
507 Ok(())
508 } else if meta.path.is_ident("bound") {
509 if options.bound_override.is_some() {
510 return Err(meta.error("duplicate sanitization bound option"));
511 }
512 let value = meta.value()?;
513 let literal: LitStr = value.parse()?;
514 options.bound_override = Some(parse_bounds(&literal)?);
515 Ok(())
516 } else {
517 Err(meta.error("unsupported sanitization container attribute"))
518 }
519 })?;
520 }
521
522 Ok(options)
523}
524
525fn parse_field_options(attrs: &[Attribute]) -> Result<FieldOptions> {
526 let mut options = FieldOptions::default();
527
528 for attr in attrs
529 .iter()
530 .filter(|attr| attr.path().is_ident("sanitization"))
531 {
532 attr.parse_nested_meta(|meta| {
533 if meta.path.is_ident("skip") {
534 if options.skip {
535 return Err(meta.error("duplicate sanitization skip option"));
536 }
537 options.skip = true;
538 options.skip_span = Some(meta.path.span());
539 Ok(())
540 } else if meta.path.is_ident("reason") {
541 if options.reason.is_some() {
542 return Err(meta.error("duplicate sanitization reason option"));
543 }
544 let value = meta.value()?;
545 let literal: LitStr = value.parse()?;
546 if literal.value().trim().is_empty() {
547 return Err(meta.error("sanitization skip reason must not be empty"));
548 }
549 options.reason = Some(literal);
550 Ok(())
551 } else if meta.path.is_ident("bound") {
552 if options.bound_override.is_some() {
553 return Err(meta.error("duplicate sanitization bound option"));
554 }
555 let value = meta.value()?;
556 let literal: LitStr = value.parse()?;
557 options.bound_override = Some(parse_bounds(&literal)?);
558 Ok(())
559 } else {
560 Err(meta.error("unsupported sanitization field attribute"))
561 }
562 })?;
563 }
564
565 if options.skip && options.reason.is_none() {
566 return Err(Error::new(
567 options.skip_span.unwrap_or_else(Span::call_site),
568 "#[sanitization(skip)] requires a non-empty reason, for example #[sanitization(skip, reason = \"public metadata\")]",
569 ));
570 }
571 if !options.skip {
572 if let Some(reason) = options.reason {
573 return Err(Error::new_spanned(
574 reason,
575 "sanitization reason is only valid together with skip",
576 ));
577 }
578 }
579
580 Ok(options)
581}
582
583fn parse_bounds(literal: &LitStr) -> Result<Vec<WherePredicate>> {
584 let text = literal.value();
585 if text.trim().is_empty() {
586 return Ok(Vec::new());
587 }
588
589 let where_clause: syn::WhereClause = syn::parse_str(&format!("where {text}"))
590 .map_err(|error| Error::new(literal.span(), error))?;
591 Ok(where_clause.predicates.into_iter().collect())
592}