Skip to content

Commit 75242c3

Browse files
committed
fix: use builder pattern to build and validate OTPElement
1 parent 6295dd3 commit 75242c3

File tree

2 files changed

+72
-25
lines changed

2 files changed

+72
-25
lines changed

src/arguments/add.rs

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
use clap::{value_parser, Args};
2-
use color_eyre::eyre::{eyre, ErrReport};
2+
use color_eyre::eyre::{eyre, ErrReport, Result};
33

44
use zeroize::Zeroize;
55

66
use crate::otp::{
77
from_otp_uri::FromOtpUri,
88
otp_algorithm::OTPAlgorithm,
9-
otp_element::{OTPDatabase, OTPElement},
9+
otp_element::{OTPDatabase, OTPElement, OTPElementBuilder},
1010
otp_type::OTPType,
1111
};
1212

@@ -72,9 +72,6 @@ impl SubcommandExecutor for AddArgs {
7272
} else {
7373
get_from_args(self)?
7474
};
75-
if !otp_element.valid_secret() {
76-
return Err(eyre!("Invalid secret."));
77-
}
7875

7976
database.add_element(otp_element);
8077
Ok(database)
@@ -83,19 +80,19 @@ impl SubcommandExecutor for AddArgs {
8380

8481
fn get_from_args(matches: AddArgs) -> color_eyre::Result<OTPElement> {
8582
let secret = rpassword::prompt_password("Insert the secret: ").map_err(ErrReport::from)?;
86-
Ok(map_args_to_code(secret, matches))
83+
map_args_to_code(secret, matches).map_err(ErrReport::from)
8784
}
8885

89-
fn map_args_to_code(secret: String, matches: AddArgs) -> OTPElement {
90-
OTPElement {
91-
secret,
92-
issuer: matches.issuer,
93-
label: matches.label.unwrap(),
94-
digits: matches.digits,
95-
type_: matches.otp_type,
96-
algorithm: matches.algorithm,
97-
period: matches.period,
98-
counter: matches.counter,
99-
pin: matches.pin,
100-
}
86+
fn map_args_to_code(secret: String, matches: AddArgs) -> Result<OTPElement> {
87+
OTPElementBuilder::default()
88+
.secret(secret)
89+
.issuer(matches.issuer)
90+
.label(matches.label.unwrap())
91+
.digits(matches.digits)
92+
.type_(matches.otp_type)
93+
.algorithm(matches.algorithm)
94+
.period(matches.period)
95+
.counter(matches.counter)
96+
.pin(matches.pin)
97+
.build()
10198
}

src/otp/otp_element.rs

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use color_eyre::eyre::ErrReport;
1+
use color_eyre::eyre::{eyre, ErrReport};
2+
use derive_builder::Builder;
23
use std::{fs::File, io::Write, vec};
34

45
use crate::crypto::cryptography::{argon_derive_key, encrypt_string_with_key, gen_salt};
@@ -126,17 +127,30 @@ impl OTPDatabase {
126127
}
127128
}
128129

129-
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, Hash, Zeroize, ZeroizeOnDrop)]
130+
#[derive(
131+
Serialize, Deserialize, Builder, Clone, PartialEq, Eq, Debug, Hash, Zeroize, ZeroizeOnDrop,
132+
)]
133+
#[builder(
134+
setter(into),
135+
build_fn(validate = "Self::validate", error = "ErrReport")
136+
)]
130137
pub struct OTPElement {
138+
#[builder(setter(custom))]
131139
pub secret: String,
132140
pub issuer: String,
133141
pub label: String,
142+
#[builder(default = "6")]
134143
pub digits: u64,
135144
#[serde(rename = "type")]
145+
#[builder(default)]
136146
pub type_: OTPType,
147+
#[builder(default)]
137148
pub algorithm: OTPAlgorithm,
149+
#[builder(default = "30")]
138150
pub period: u64,
151+
#[builder(setter(into), default)]
139152
pub counter: Option<u64>,
153+
#[builder(setter(into), default)]
140154
pub pin: Option<String>,
141155
}
142156

@@ -212,11 +226,34 @@ impl OTPElement {
212226
let s = (value % exponential).to_string();
213227
Ok("0".repeat(self.digits as usize - s.chars().count()) + s.as_str())
214228
}
229+
}
215230

216-
pub fn valid_secret(&self) -> bool {
217-
match self.type_ {
218-
OTPType::Motp => hex::decode(&self.secret).is_ok(),
219-
_ => BASE32_NOPAD.decode(self.secret.as_bytes()).is_ok(),
231+
impl OTPElementBuilder {
232+
/// Makes the secret insertion case insensitive
233+
pub fn secret<VALUE: Into<String>>(&mut self, value: VALUE) -> &mut Self {
234+
self.secret = Some(value.into().to_uppercase());
235+
self
236+
}
237+
238+
/// Check if the OTPElement is valid
239+
fn validate(&self) -> Result<(), ErrReport> {
240+
if self.secret.is_none() {
241+
return Err(eyre!("Secret must be set",));
242+
}
243+
244+
if self.secret.as_ref().unwrap().is_empty() {
245+
return Err(eyre!("Secret must not be empty",));
246+
}
247+
248+
// Validate secret encoding
249+
match self.type_.unwrap_or_default() {
250+
OTPType::Motp => hex::decode(&self.secret.as_ref().unwrap())
251+
.map(|_| {})
252+
.map_err(ErrReport::from),
253+
_ => BASE32_NOPAD
254+
.decode(self.secret.as_ref().unwrap().as_bytes())
255+
.map(|_| {})
256+
.map_err(ErrReport::from),
220257
}
221258
}
222259
}
@@ -230,8 +267,8 @@ fn get_label(issuer: &str, label: &str) -> String {
230267
#[cfg(test)]
231268
mod test {
232269
use crate::otp::otp_element::OTPAlgorithm::Sha1;
233-
use crate::otp::otp_element::OTPElement;
234270
use crate::otp::otp_element::OTPType::Totp;
271+
use crate::otp::otp_element::{OTPElement, OTPElementBuilder};
235272

236273
use crate::otp::from_otp_uri::FromOtpUri;
237274
use crate::otp::otp_error::OtpError;
@@ -315,4 +352,17 @@ mod test {
315352
// Assert
316353
assert_eq!(Err(OtpError::InvalidDigits), result)
317354
}
355+
356+
#[test]
357+
fn test_lowercase_secret() {
358+
// Arrange / Act
359+
let result = OTPElementBuilder::default()
360+
.secret("aa")
361+
.label("label")
362+
.issuer("")
363+
.build();
364+
365+
// Assert
366+
assert_eq!("AA", result.unwrap().secret);
367+
}
318368
}

0 commit comments

Comments
 (0)