Skip to content

Commit 36e85c8

Browse files
authored
Case insensitive secrets + improved secret error messages (#453)
This PR permits case insensitive Base32 or hex secrets (in case of Motp codes). I have also improved error messages in case of a bad secret value.
2 parents 68a36a0 + 68444e4 commit 36e85c8

File tree

6 files changed

+229
-26
lines changed

6 files changed

+229
-26
lines changed

Cargo.lock

Lines changed: 86 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,4 @@ crossterm = "0.27.0"
5353
url = "2.5.2"
5454
color-eyre = "0.6.3"
5555
enum_dispatch = "0.3.13"
56+
derive_builder = "0.20.0"

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::{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_algorithm.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ pub enum OTPAlgorithm {
1313
Md5,
1414
}
1515

16+
impl Default for OTPAlgorithm {
17+
fn default() -> Self {
18+
Self::Sha1
19+
}
20+
}
21+
1622
impl fmt::Display for OTPAlgorithm {
1723
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1824
let to_string = match self {

src/otp/otp_element.rs

Lines changed: 115 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(setter(custom), 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,50 @@ 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+
/// Makes the secret insertion case insensitive
239+
pub fn type_<VALUE: Into<OTPType>>(&mut self, value: VALUE) -> &mut Self {
240+
let otp_type: OTPType = value.into();
241+
242+
if otp_type == OTPType::Motp {
243+
// Motp codes must be lowercase
244+
self.secret = self.secret.as_ref().map(|s| s.to_lowercase())
245+
} else {
246+
// Base32 codes must be uppercase
247+
self.secret = self.secret.as_ref().map(|s| s.to_uppercase())
248+
}
249+
250+
self.type_ = Some(otp_type);
251+
self
252+
}
253+
254+
/// Check if the OTPElement is valid
255+
fn validate(&self) -> Result<(), ErrReport> {
256+
if self.secret.is_none() {
257+
return Err(eyre!("Secret must be set",));
258+
}
259+
260+
if self.secret.as_ref().unwrap().is_empty() {
261+
return Err(eyre!("Secret must not be empty",));
262+
}
263+
264+
// Validate secret encoding
265+
match self.type_.unwrap_or_default() {
266+
OTPType::Motp => hex::decode(self.secret.as_ref().unwrap())
267+
.map(|_| {})
268+
.map_err(|e| eyre!("Invalid hex secret: {e}")),
269+
_ => BASE32_NOPAD
270+
.decode(self.secret.as_ref().unwrap().as_bytes())
271+
.map(|_| {})
272+
.map_err(|e| eyre!("Invalid BASE32 secret: {e}")),
220273
}
221274
}
222275
}
@@ -230,11 +283,12 @@ fn get_label(issuer: &str, label: &str) -> String {
230283
#[cfg(test)]
231284
mod test {
232285
use crate::otp::otp_element::OTPAlgorithm::Sha1;
233-
use crate::otp::otp_element::OTPElement;
234286
use crate::otp::otp_element::OTPType::Totp;
287+
use crate::otp::otp_element::{OTPElement, OTPElementBuilder};
235288

236289
use crate::otp::from_otp_uri::FromOtpUri;
237290
use crate::otp::otp_error::OtpError;
291+
use crate::otp::otp_type::OTPType;
238292

239293
#[test]
240294
fn test_serialization_otp_uri_full_element() {
@@ -315,4 +369,58 @@ mod test {
315369
// Assert
316370
assert_eq!(Err(OtpError::InvalidDigits), result)
317371
}
372+
373+
#[test]
374+
fn test_lowercase_secret() {
375+
// Arrange / Act
376+
let result = OTPElementBuilder::default()
377+
.secret("aa")
378+
.label("label")
379+
.issuer("")
380+
.build();
381+
382+
// Assert
383+
assert_eq!("AA", result.unwrap().secret);
384+
}
385+
386+
#[test]
387+
fn test_invalid_secret_base32() {
388+
let result = OTPElementBuilder::default()
389+
.secret("aaa")
390+
.label("label")
391+
.issuer("")
392+
.build();
393+
394+
assert_eq!(
395+
"Invalid BASE32 secret: invalid length at 2",
396+
result.unwrap_err().to_string()
397+
)
398+
}
399+
400+
#[test]
401+
fn valid_hex_secret() {
402+
let result = OTPElementBuilder::default()
403+
.secret("aAAf")
404+
.label("label")
405+
.issuer("")
406+
.type_(OTPType::Motp)
407+
.build();
408+
409+
assert_eq!("aaaf", result.unwrap().secret)
410+
}
411+
412+
#[test]
413+
fn invalid_secret_hex() {
414+
let result = OTPElementBuilder::default()
415+
.secret("aaa")
416+
.label("label")
417+
.issuer("")
418+
.type_(OTPType::Motp)
419+
.build();
420+
421+
assert_eq!(
422+
"Invalid hex secret: Odd number of digits",
423+
result.unwrap_err().to_string()
424+
)
425+
}
318426
}

src/otp/otp_type.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ pub enum OTPType {
2424
Motp,
2525
}
2626

27+
impl Default for OTPType {
28+
fn default() -> Self {
29+
Self::Totp
30+
}
31+
}
32+
2733
impl fmt::Display for OTPType {
2834
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2935
let to_string = match self {

0 commit comments

Comments
 (0)