From 808dd24caf31c8ded50943d77c01447b472aff00 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Thu, 4 Nov 2021 20:12:47 -0600 Subject: [PATCH] zeroize_derive: add `#[zeroize(skip)]` attribute (#654) Original PR: https://github.com/iqlusioninc/crates/pull/900 --- zeroize/derive/src/lib.rs | 152 +++++++++++++++++++++++++++++++- zeroize/tests/zeroize_derive.rs | 104 ++++++++++++++++++++++ 2 files changed, 255 insertions(+), 1 deletion(-) diff --git a/zeroize/derive/src/lib.rs b/zeroize/derive/src/lib.rs index 8bd25283..6c61f2f2 100644 --- a/zeroize/derive/src/lib.rs +++ b/zeroize/derive/src/lib.rs @@ -18,6 +18,8 @@ decl_derive!( /// /// - `#[zeroize(drop)]`: derives the `Drop` trait, calling `zeroize()` /// when this item is dropped. + /// - `#[zeroize(skip)]`: skips this field or variant when calling + /// `zeroize()`. derive_zeroize ); @@ -131,17 +133,57 @@ impl ZeroizeAttrs { }; self.drop = true; + } else if meta.path().is_ident("skip") { + if variant.is_none() && binding.is_none() { + panic!(concat!( + "The #[zeroize(skip)] attribute is not allowed on a `struct` or `enum`. ", + "Use it on a field or variant instead.", + )) + } } else { panic!("unknown #[zeroize] attribute type: {:?}", meta.path()); } } } +fn filter_skip(attrs: &[Attribute], start: bool) -> bool { + let mut result = start; + + for attr in attrs.iter().filter_map(|attr| attr.parse_meta().ok()) { + if let Meta::List(list) = attr { + if list.path.is_ident(ZEROIZE_ATTR) { + for nested in list.nested { + if let NestedMeta::Meta(Meta::Path(path)) = nested { + if path.is_ident("skip") { + assert!(result, "duplicate #[zeroize] skip flags"); + result = false; + } + } + } + } + } + } + + result +} + /// Custom derive for `Zeroize` (without `Drop`) fn derive_zeroize_without_drop(mut s: synstructure::Structure<'_>) -> TokenStream { s.bind_with(|_| BindStyle::RefMut); - let zeroizers = s.each(|bi| quote! { #bi.zeroize(); }); + let zeroizers = s + .filter_variants(|vi| { + let result = filter_skip(vi.ast().attrs, true); + + // check for duplicate `#[zeroize(skip)]` attributes in nested variants + for field in vi.ast().fields { + filter_skip(&field.attrs, result); + } + + result + }) + .filter(|bi| filter_skip(&bi.ast().attrs, true)) + .each(|bi| quote! { #bi.zeroize(); }); s.bound_impl( quote!(zeroize::Zeroize), @@ -262,6 +304,42 @@ mod tests { } } + #[test] + fn zeroize_with_skip() { + test_derive! { + derive_zeroize_without_drop { + struct Z { + a: String, + b: Vec, + #[zeroize(skip)] + c: [u8; 3], + } + } + expands to { + #[allow(non_upper_case_globals)] + #[doc(hidden)] + const _DERIVE_zeroize_Zeroize_FOR_Z: () = { + extern crate zeroize; + impl zeroize::Zeroize for Z { + fn zeroize(&mut self) { + match self { + Z { + a: ref mut __binding_0, + b: ref mut __binding_1, + .. + } => { + { __binding_0.zeroize(); } + { __binding_1.zeroize(); } + } + } + } + } + }; + } + no_build // tests the code compiles are in the `zeroize` crate + } + } + #[test] fn zeroize_on_struct() { parse_zeroize_test(stringify!( @@ -386,6 +464,78 @@ mod tests { )); } + #[test] + #[should_panic( + expected = "The #[zeroize(skip)] attribute is not allowed on a `struct` or `enum`. Use it on a field or variant instead." + )] + fn zeroize_skip_on_struct() { + parse_zeroize_test(stringify!( + #[zeroize(skip)] + struct Z { + a: String, + b: Vec, + c: [u8; 3], + } + )); + } + + #[test] + #[should_panic( + expected = "The #[zeroize(skip)] attribute is not allowed on a `struct` or `enum`. Use it on a field or variant instead." + )] + fn zeroize_skip_on_enum() { + parse_zeroize_test(stringify!( + #[zeroize(skip)] + enum Z { + Variant1, + Variant2, + } + )); + } + + #[test] + #[should_panic(expected = "duplicate #[zeroize] skip flags")] + fn zeroize_duplicate_skip() { + parse_zeroize_test(stringify!( + struct Z { + a: String, + #[zeroize(skip)] + #[zeroize(skip)] + b: Vec, + c: [u8; 3], + } + )); + } + + #[test] + #[should_panic(expected = "duplicate #[zeroize] skip flags")] + fn zeroize_duplicate_skip_list() { + parse_zeroize_test(stringify!( + struct Z { + a: String, + #[zeroize(skip, skip)] + b: Vec, + c: [u8; 3], + } + )); + } + + #[test] + #[should_panic(expected = "duplicate #[zeroize] skip flags")] + fn zeroize_duplicate_skip_enum() { + parse_zeroize_test(stringify!( + enum Z { + #[zeroize(skip)] + Variant { + a: String, + #[zeroize(skip)] + b: Vec, + c: [u8; 3], + }, + } + )); + } + fn parse_zeroize_test(unparsed: &str) -> TokenStream { derive_zeroize(Structure::new( &parse_str(unparsed).expect("Failed to parse test input"), diff --git a/zeroize/tests/zeroize_derive.rs b/zeroize/tests/zeroize_derive.rs index a9f1a18f..24f537c3 100644 --- a/zeroize/tests/zeroize_derive.rs +++ b/zeroize/tests/zeroize_derive.rs @@ -104,4 +104,108 @@ mod custom_derive_tests { impl Drop for ZeroizeNoDropEnum { fn drop(&mut self) {} } + + #[test] + fn derive_struct_skip() { + #[derive(Zeroize)] + #[zeroize(drop)] + struct Z { + string: String, + vec: Vec, + #[zeroize(skip)] + bytearray: [u8; 3], + number: usize, + boolean: bool, + } + + let mut value = Z { + string: String::from("Hello, world!"), + vec: vec![1, 2, 3], + bytearray: [4, 5, 6], + number: 42, + boolean: true, + }; + + value.zeroize(); + + assert!(value.string.is_empty()); + assert!(value.vec.is_empty()); + assert_eq!(&value.bytearray, &[4, 5, 6]); + assert_eq!(value.number, 0); + assert!(!value.boolean); + } + + #[test] + fn derive_enum_skip() { + #[derive(Zeroize)] + #[zeroize(drop)] + enum Z { + #[allow(dead_code)] + Variant1, + #[zeroize(skip)] + Variant2([u8; 3]), + #[zeroize(skip)] + Variant3 { + string: String, + vec: Vec, + bytearray: [u8; 3], + number: usize, + boolean: bool, + }, + Variant4 { + string: String, + vec: Vec, + #[zeroize(skip)] + bytearray: [u8; 3], + number: usize, + boolean: bool, + }, + } + + let mut value = Z::Variant2([4, 5, 6]); + + value.zeroize(); + + assert!(matches!(&value, Z::Variant2([4, 5, 6]))); + + let mut value = Z::Variant3 { + string: String::from("Hello, world!"), + vec: vec![1, 2, 3], + bytearray: [4, 5, 6], + number: 42, + boolean: true, + }; + + value.zeroize(); + + assert!(matches!( + &value, + Z::Variant3 { string, vec, bytearray, number, boolean } + if string == "Hello, world!" && + vec == &[1, 2, 3] && + bytearray == &[4, 5, 6] && + *number == 42 && + *boolean + )); + + let mut value = Z::Variant4 { + string: String::from("Hello, world!"), + vec: vec![1, 2, 3], + bytearray: [4, 5, 6], + number: 42, + boolean: true, + }; + + value.zeroize(); + + assert!(matches!( + &value, + Z::Variant4 { string, vec, bytearray, number, boolean } + if string.is_empty() && + vec.is_empty() && + bytearray == &[4, 5, 6] && + *number == 0 && + !boolean + )); + } }