Skip to content

Commit dd2627d

Browse files
authored
Merge pull request #54 from nepalez/master
Support `ZeroizeOnDrop` for `BazeIban`
2 parents 5a6efec + e5feadd commit dd2627d

7 files changed

Lines changed: 130 additions & 43 deletions

File tree

iban_validate/Cargo.toml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ path = "src/lib.rs"
2121

2222
[features]
2323
default = []
24+
zeroize = ["dep:zeroize", "dep:zeroize_derive"]
2425

2526
# Enables all features when building documentation
2627
[package.metadata.docs.rs]
27-
features = ["serde"]
28+
features = ["serde", "zeroize"]
2829

2930
[dependencies.serde]
3031
version = "1"
@@ -36,6 +37,16 @@ features = ["derive"]
3637
version = "0.7"
3738
default-features = false
3839

40+
[dependencies.zeroize]
41+
version = "1"
42+
optional = true
43+
default-features = false
44+
45+
[dependencies.zeroize_derive]
46+
version = "1"
47+
optional = true
48+
default-features = false
49+
3950
[dev-dependencies]
4051
proptest = "1"
4152
static_assertions = "1"

iban_validate/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ fn main() -> Result<(), ParseIbanError> {
3737
- A flexible API that is useful even when the country is not in the Swift registry (using [`BaseIban`]). Instead of using panic, the crate provides typed errors with what went wrong.
3838
- All functionality can be used in a `no_std` environment.
3939
- Optional serialization and deserialization via [`serde`](https://crates.io/crates/serde).
40+
- Optional memory zeroization when [`BaseIban`] is dropped via [`zeroize`](https://crates.io/crates/zeroize).
4041
- CI tested results via the Swift provided and custom test cases, as well as proptest.
4142
- `#![forbid(unsafe_code)]`, making sure all code is written in safe Rust.
4243

@@ -55,6 +56,7 @@ iban_validate = "5"
5556
The following features can be used to configure the crate:
5657

5758
- _serde_: Enable `serde` support for [`Iban`] and [`BaseIban`].
59+
- _zeroize_: Support `ZeroizeOnDrop` for the [`BaseIban`].
5860

5961
## Contributing
6062

iban_validate/src/base_iban.rs

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use core::str::FromStr;
77
use core::{convert::TryFrom, error::Error};
88
#[cfg(feature = "serde")]
99
use serde::{Deserialize, Deserializer, Serialize, Serializer};
10+
#[cfg(feature = "zeroize")]
11+
use zeroize_derive::ZeroizeOnDrop;
1012

1113
/// The size of a group of characters in the paper format.
1214
const PAPER_GROUP_SIZE: usize = 4;
@@ -85,7 +87,19 @@ const MAX_IBAN_LEN: usize = 34;
8587
/// assert_eq!(&format!("{}", iban), "RO66 BACX 0000 0012 3456 7890");
8688
/// # Ok::<(), ParseBaseIbanError>(())
8789
/// ```
88-
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
90+
///
91+
/// ## Zeroization
92+
/// If the `zeroize` feature is enabled, then both `BaseIban` itself
93+
/// and temporary objects used during parsing will be zeroed
94+
/// when they go out of scope. This can help to
95+
/// prevent sensitive data from lingering in memory.
96+
///
97+
/// NOTICE that zeroization is NOT compatible to `Copy` trait,
98+
/// that's why the `BaseIban` does not implement `Copy`
99+
/// when the "zeroize" feature is enabled.
100+
#[derive(Clone, Eq, PartialEq, Hash)]
101+
#[cfg_attr(not(feature = "zeroize"), derive(Copy))]
102+
#[cfg_attr(feature = "zeroize", derive(ZeroizeOnDrop))]
89103
pub struct BaseIban {
90104
/// The string representing the IBAN. The string contains only uppercase
91105
/// ASCII and digits and no whitespace. It starts with two letters followed
@@ -261,13 +275,16 @@ impl BaseIban {
261275
/// Parse a standardized IBAN string from an iterator. We iterate through
262276
/// bytes, not characters. When a character is not ASCII, the IBAN is
263277
/// automatically invalid.
264-
fn try_form_string_from_electronic<T>(
265-
mut chars: T,
266-
) -> Result<ArrayString<MAX_IBAN_LEN>, ParseBaseIbanError>
278+
///
279+
/// SECURITY: If the `zeroized` feature is turned on, then all temporary
280+
/// objects are zeroized in the memory.
281+
fn try_form_string_from_electronic<T>(mut chars: T) -> Result<Self, ParseBaseIbanError>
267282
where
268283
T: Iterator<Item = u8>,
269284
{
270-
let mut address_no_spaces = ArrayString::<MAX_IBAN_LEN>::new();
285+
let mut output = Self {
286+
s: ArrayString::<MAX_IBAN_LEN>::new(),
287+
};
271288

272289
// First expect exactly two uppercase letters and append them to the
273290
// string.
@@ -276,7 +293,8 @@ impl BaseIban {
276293
.next()
277294
.filter(u8::is_ascii_uppercase)
278295
.ok_or(ParseBaseIbanError::InvalidFormat)?;
279-
address_no_spaces
296+
output
297+
.s
280298
.try_push(c as char)
281299
.map_err(|_| ParseBaseIbanError::InvalidFormat)?;
282300
}
@@ -287,7 +305,8 @@ impl BaseIban {
287305
.next()
288306
.filter(u8::is_ascii_digit)
289307
.ok_or(ParseBaseIbanError::InvalidFormat)?;
290-
address_no_spaces
308+
output
309+
.s
291310
.try_push(c as char)
292311
.map_err(|_| ParseBaseIbanError::InvalidFormat)?;
293312
}
@@ -298,21 +317,20 @@ impl BaseIban {
298317
// destination string.
299318
for c in chars {
300319
if c.is_ascii_alphanumeric() {
301-
address_no_spaces
320+
output
321+
.s
302322
.try_push(c.to_ascii_uppercase() as char)
303323
.map_err(|_| ParseBaseIbanError::InvalidFormat)?;
304324
} else {
305325
return Err(ParseBaseIbanError::InvalidFormat);
306326
}
307327
}
308328

309-
Ok(address_no_spaces)
329+
Ok(output)
310330
}
311331

312332
/// Parse a pretty print 'paper' IBAN from a `str`.
313-
fn try_form_string_from_pretty_print(
314-
s: &str,
315-
) -> Result<ArrayString<MAX_IBAN_LEN>, ParseBaseIbanError> {
333+
fn try_form_string_from_pretty_print(s: &str) -> Result<Self, ParseBaseIbanError> {
316334
// The pretty print format consists of a number of groups of four
317335
// characters, separated by a space.
318336

@@ -357,17 +375,14 @@ impl FromStr for BaseIban {
357375
/// invalid, an [`ParseBaseIbanError`](crate::ParseBaseIbanError) will be
358376
/// returned.
359377
fn from_str(address: &str) -> Result<Self, Self::Err> {
360-
let address_no_spaces =
361-
BaseIban::try_form_string_from_electronic(address.as_bytes().iter().copied())
362-
.or_else(|_| BaseIban::try_form_string_from_pretty_print(address))?;
378+
let output = BaseIban::try_form_string_from_electronic(address.as_bytes().iter().copied())
379+
.or_else(|_| BaseIban::try_form_string_from_pretty_print(address))?;
363380

364-
if !BaseIban::validate_checksum(&address_no_spaces) {
365-
return Err(ParseBaseIbanError::InvalidChecksum);
381+
if BaseIban::validate_checksum(&output.s) {
382+
Ok(output)
383+
} else {
384+
Err(ParseBaseIbanError::InvalidChecksum)
366385
}
367-
368-
Ok(BaseIban {
369-
s: address_no_spaces,
370-
})
371386
}
372387
}
373388

iban_validate/src/lib.rs

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,16 @@ impl Display for Iban {
235235
/// # Ok::<(), ParseIbanError>(())
236236
/// ```
237237
/// [`parse()`]: https://doc.rust-lang.org/std/primitive.str.html#method.parse
238-
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
238+
///
239+
/// ## Zeroization
240+
/// If the `zeroize` feature is enabled, then a `BaseIban`
241+
/// wrapped by the `Iban` will be zeroed upon a drop.
242+
///
243+
/// NOTICE that zeroization is NOT compatible to `Copy` trait,
244+
/// that's why the `Iban` does not implement `Copy`
245+
/// when the "zeroize" feature is enabled.
246+
#[derive(Clone, Eq, PartialEq, Hash)]
247+
#[cfg_attr(not(feature = "zeroize"), derive(Copy))]
239248
pub struct Iban {
240249
/// The inner IBAN, which has been checked.
241250
base_iban: BaseIban,
@@ -257,13 +266,22 @@ pub struct Iban {
257266
/// // The following IBAN doesn't follow the country format
258267
/// let base_iban: BaseIban = "AL84212110090000AB023569874".parse()?;
259268
/// assert_eq!(
260-
/// Iban::try_from(base_iban),
269+
/// Iban::try_from(base_iban.clone()),
261270
/// Err(ParseIbanError::InvalidBban(base_iban))
262271
/// );
263272
/// # Ok::<(), ParseBaseIbanError>(())
264273
/// ```
265-
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
274+
///
275+
/// ## Zeroization
276+
/// If the `zeroize` feature is enabled, then the `BaseIban`
277+
/// wrapped by the `ParseIbanError` will be zeroed upon a drop.
278+
///
279+
/// NOTICE that zeroization is NOT compatible to `Copy` trait,
280+
/// that's why the `ParseIbanError` does not implement `Copy`
281+
/// when the "zeroize" feature is enabled.
282+
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
266283
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
284+
#[cfg_attr(not(feature = "zeroize"), derive(Copy))]
267285
pub enum ParseIbanError {
268286
/// This variant indicates that the basic IBAN structure was not followed.
269287
InvalidBaseIban {
@@ -357,15 +375,24 @@ impl TryFrom<BaseIban> for Iban {
357375
/// access to some basic functionality nonetheless.
358376
fn try_from(base_iban: BaseIban) -> Result<Iban, ParseIbanError> {
359377
use countries::Matchable;
360-
generated::country_pattern(base_iban.country_code())
361-
.ok_or(ParseIbanError::UnknownCountry(base_iban))
362-
.and_then(|matcher: &[(usize, _)]| {
363-
if matcher.match_str(base_iban.bban_unchecked()) {
364-
Ok(Iban { base_iban })
365-
} else {
366-
Err(ParseIbanError::InvalidBban(base_iban))
367-
}
368-
})
378+
379+
#[cfg(not(feature = "zeroize"))]
380+
let pattern = generated::country_pattern(base_iban.country_code())
381+
.ok_or(ParseIbanError::UnknownCountry(base_iban));
382+
383+
#[cfg(feature = "zeroize")]
384+
let binding = base_iban.clone();
385+
#[cfg(feature = "zeroize")]
386+
let pattern = generated::country_pattern(binding.country_code())
387+
.ok_or(ParseIbanError::UnknownCountry(base_iban.clone()));
388+
389+
pattern.and_then(|matcher: &[(usize, _)]| {
390+
if matcher.match_str(base_iban.bban_unchecked()) {
391+
Ok(Iban { base_iban })
392+
} else {
393+
Err(ParseIbanError::InvalidBban(base_iban))
394+
}
395+
})
369396
}
370397
}
371398

iban_validate/tests/as_ref.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ fn test_as_ref() -> Result<(), Box<dyn Error>> {
1313
base_iban.to_string()
1414
}
1515

16-
let s = pretty_format(iban);
16+
let s = pretty_format(&iban);
1717
assert_eq!(s.as_str(), "KW81 CBKU 0000 0000 0000 1234 5601 01");
1818
assert_eq!(iban.to_string(), s);
1919

iban_validate/tests/impls.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ use core::hash::Hash;
55
use core::str::FromStr;
66
use iban::{BaseIban, Iban, ParseBaseIbanError, ParseIbanError};
77
use static_assertions::assert_impl_all;
8+
#[cfg(feature = "zeroize")]
9+
use zeroize::ZeroizeOnDrop;
810

911
assert_impl_all!(
10-
BaseIban: Copy,
11-
Clone,
12+
BaseIban: Clone,
1213
Eq,
1314
PartialEq,
1415
Hash,
@@ -23,8 +24,7 @@ assert_impl_all!(
2324
AsMut<BaseIban>
2425
);
2526
assert_impl_all!(
26-
Iban: Copy,
27-
Clone,
27+
Iban: Clone,
2828
Eq,
2929
PartialEq,
3030
Hash,
@@ -43,8 +43,7 @@ assert_impl_all!(
4343
AsMut<Iban>
4444
);
4545
assert_impl_all!(
46-
ParseBaseIbanError: Copy,
47-
Clone,
46+
ParseBaseIbanError: Clone,
4847
Eq,
4948
PartialEq,
5049
Hash,
@@ -57,8 +56,7 @@ assert_impl_all!(
5756
AsMut<ParseBaseIbanError>
5857
);
5958
assert_impl_all!(
60-
ParseIbanError: Copy,
61-
Clone,
59+
ParseIbanError: Clone,
6260
Eq,
6361
PartialEq,
6462
Hash,
@@ -74,6 +72,17 @@ assert_impl_all!(
7472
assert_impl_all!(ParseBaseIbanError: core::error::Error);
7573
assert_impl_all!(ParseIbanError: core::error::Error);
7674

75+
#[cfg(not(feature = "zeroize"))]
76+
assert_impl_all!(BaseIban: Copy);
77+
#[cfg(not(feature = "zeroize"))]
78+
assert_impl_all!(Iban: Copy);
79+
#[cfg(not(feature = "zeroize"))]
80+
assert_impl_all!(ParseBaseIbanError: Copy);
81+
#[cfg(not(feature = "zeroize"))]
82+
assert_impl_all!(ParseIbanError: Copy);
83+
#[cfg(feature = "zeroize")]
84+
assert_impl_all!(BaseIban: ZeroizeOnDrop);
85+
7786
#[cfg(feature = "serde")]
7887
mod impls_serde {
7988
use super::{assert_impl_all, BaseIban, Iban};

iban_validate/tests/zeroize.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#![cfg(feature = "zeroize")]
2+
3+
use iban::{BaseIban, IbanLike};
4+
use zeroize::Zeroize;
5+
6+
#[test]
7+
fn zeroize() {
8+
let mut base_iban = "DE44500105175407324931"
9+
.parse::<BaseIban>()
10+
.expect("valid IBAN");
11+
12+
assert_eq!(
13+
base_iban.electronic_str(),
14+
String::from("DE44500105175407324931")
15+
);
16+
17+
base_iban.zeroize();
18+
19+
assert_eq!(
20+
base_iban.electronic_str(),
21+
String::from("\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0")
22+
);
23+
}

0 commit comments

Comments
 (0)