Skip to content

Commit

Permalink
zeroize_derive: add #[zeroize(skip)] attribute (#654)
Browse files Browse the repository at this point in the history
  • Loading branch information
tarcieri committed Nov 5, 2021
1 parent 6ee51e9 commit 808dd24
Show file tree
Hide file tree
Showing 2 changed files with 255 additions and 1 deletion.
152 changes: 151 additions & 1 deletion zeroize/derive/src/lib.rs
Expand Up @@ -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
);

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -262,6 +304,42 @@ mod tests {
}
}

#[test]
fn zeroize_with_skip() {
test_derive! {
derive_zeroize_without_drop {
struct Z {
a: String,
b: Vec<u8>,
#[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!(
Expand Down Expand Up @@ -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<u8>,
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<u8>,
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<u8>,
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<u8>,
c: [u8; 3],
},
}
));
}

fn parse_zeroize_test(unparsed: &str) -> TokenStream {
derive_zeroize(Structure::new(
&parse_str(unparsed).expect("Failed to parse test input"),
Expand Down
104 changes: 104 additions & 0 deletions zeroize/tests/zeroize_derive.rs
Expand Up @@ -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<u8>,
#[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<u8>,
bytearray: [u8; 3],
number: usize,
boolean: bool,
},
Variant4 {
string: String,
vec: Vec<u8>,
#[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
));
}
}

0 comments on commit 808dd24

Please sign in to comment.