Skip to content

Commit 5e55819

Browse files
authored
Fix integer overflow during OTP code calculation (#443)
Digits value must be in this range -> 0..=9
2 parents 4810a69 + 9d65302 commit 5e55819

File tree

9 files changed

+79
-58
lines changed

9 files changed

+79
-58
lines changed

src/arguments/add.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use clap::Args;
1+
use clap::{value_parser, Args};
22
use color_eyre::eyre::{eyre, ErrReport};
33

44
use zeroize::Zeroize;
@@ -39,7 +39,8 @@ pub struct AddArgs {
3939
short,
4040
long,
4141
default_value_t = 6,
42-
default_value_if("type", "STEAM", "5")
42+
default_value_if("type", "STEAM", "5"),
43+
value_parser=value_parser!(u64).range(0..=9)
4344
)]
4445
pub digits: u64,
4546

src/arguments/edit.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use clap::Args;
1+
use clap::{value_parser, Args};
22
use color_eyre::eyre::eyre;
33

44
use crate::otp::{otp_algorithm::OTPAlgorithm, otp_element::OTPDatabase};
@@ -24,7 +24,7 @@ pub struct EditArgs {
2424
pub algorithm: Option<OTPAlgorithm>,
2525

2626
/// Code digits
27-
#[arg(short, long)]
27+
#[arg(short, long, value_parser=value_parser!(u64).range(0..=9))]
2828
pub digits: Option<u64>,
2929

3030
/// Code period

src/arguments/list.rs

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,11 @@ impl<'a> TryFrom<&'a OTPElement> for JsonOtpList<'a> {
5858

5959
impl SubcommandExecutor for ListArgs {
6060
fn run_command(self, otp_database: OTPDatabase) -> color_eyre::Result<OTPDatabase> {
61-
let elements_iterator = otp_database.elements.iter().enumerate();
62-
6361
if self.format.unwrap_or_default().json {
64-
let json_elements = elements_iterator
65-
.map(|(_, element)| element.try_into())
62+
let json_elements = otp_database
63+
.elements
64+
.iter()
65+
.map(|element| element.try_into())
6666
.collect::<Result<Vec<JsonOtpList>>>()?;
6767

6868
let stringified = serde_json::to_string_pretty(&json_elements)
@@ -73,19 +73,23 @@ impl SubcommandExecutor for ListArgs {
7373
"{0: <6} {1: <30} {2: <70} {3: <10}",
7474
"Index", "Issuer", "Label", "OTP"
7575
);
76-
elements_iterator.for_each(|(index, e)| {
77-
println!(
78-
"{0: <6} {1: <30} {2: <70} {3: <10}",
79-
index,
80-
if e.issuer.is_empty() {
81-
"<No issuer>"
82-
} else {
83-
e.issuer.as_str()
84-
},
85-
&e.label,
86-
e.get_otp_code().unwrap_or("ERROR".to_string())
87-
)
88-
});
76+
otp_database
77+
.elements
78+
.iter()
79+
.enumerate()
80+
.for_each(|(index, e)| {
81+
println!(
82+
"{0: <6} {1: <30} {2: <70} {3: <10}",
83+
index,
84+
if e.issuer.is_empty() {
85+
"<No issuer>"
86+
} else {
87+
e.issuer.as_str()
88+
},
89+
&e.label,
90+
e.get_otp_code().unwrap_or("ERROR".to_string())
91+
)
92+
});
8993
}
9094

9195
Ok(otp_database)

src/interface/event.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::sync::mpsc;
22
use std::thread;
33
use std::time::{Duration, Instant};
44

5-
use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, KeyEventKind, MouseEvent};
5+
use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, KeyEventKind};
66

77
use crate::interface::app::AppResult;
88

@@ -14,15 +14,15 @@ pub enum Event {
1414
/// Key press.
1515
Key(KeyEvent),
1616
/// Mouse click/scroll.
17-
Mouse(MouseEvent),
17+
Mouse(()),
1818
/// Terminal resize.
19-
Resize(u16, u16),
19+
Resize((), ()),
2020
/// Focus gained
2121
FocusGained(),
2222
/// Focus lost
2323
FocusLost(),
2424
/// Paste text
25-
Paste(String),
25+
Paste(()),
2626
}
2727

2828
/// Terminal event handler.
@@ -59,11 +59,11 @@ impl EventHandler {
5959
Ok(())
6060
}
6161
}
62-
CrosstermEvent::Mouse(e) => sender.send(Event::Mouse(e)),
63-
CrosstermEvent::Resize(w, h) => sender.send(Event::Resize(w, h)),
62+
CrosstermEvent::Mouse(_e) => sender.send(Event::Mouse(())),
63+
CrosstermEvent::Resize(_w, _h) => sender.send(Event::Resize((), ())),
6464
CrosstermEvent::FocusGained => sender.send(Event::FocusGained()),
6565
CrosstermEvent::FocusLost => sender.send(Event::FocusLost()),
66-
CrosstermEvent::Paste(e) => sender.send(Event::Paste(e)),
66+
CrosstermEvent::Paste(_e) => sender.send(Event::Paste(())),
6767
}
6868
.expect("failed to send terminal event")
6969
}

src/otp/algorithms/hotp_maker.rs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,18 +86,16 @@ mod tests {
8686
#[test]
8787
fn test_hotp() {
8888
assert_eq!(
89-
format_code(generate_hotp::<Sha1>("BASE32SECRET3232", 0).unwrap(), 6),
90-
"260182"
91-
);
92-
assert_eq!(
93-
format_code(generate_hotp::<Sha1>("BASE32SECRET3232", 1).unwrap(), 6),
94-
"055283"
89+
455260182,
90+
generate_hotp::<Sha1>("BASE32SECRET3232", 0).unwrap()
9591
);
9692
}
9793

98-
fn format_code(value: u32, digits: u32) -> String {
99-
// Get the formatted code
100-
let s = (value % 10_u32.pow(digits)).to_string();
101-
"0".repeat(digits as usize - s.chars().count()) + s.as_str()
94+
#[test]
95+
fn test_hotp_2() {
96+
assert_eq!(
97+
1617055283,
98+
generate_hotp::<Sha1>("BASE32SECRET3232", 1).unwrap()
99+
);
102100
}
103101
}

src/otp/algorithms/steam_otp_maker.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@ use super::totp_maker::totp;
88
const STEAM_ALPHABET: &str = "23456789BCDFGHJKMNPQRTVWXY";
99

1010
pub fn steam(secret: &str, algorithm: OTPAlgorithm, digits: usize) -> Result<String, OtpError> {
11-
match totp(secret, algorithm) {
12-
Ok(v) => Ok(to_steam_string(v as usize, digits)),
13-
Err(e) => Err(e),
14-
}
11+
totp(secret, algorithm).map(|v| to_steam_string(v as usize, digits))
1512
}
1613

1714
fn to_steam_string(mut code: usize, digits: usize) -> String {

src/otp/algorithms/totp_maker.rs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,8 @@ mod tests {
3030
#[test]
3131
fn test_totp() {
3232
assert_eq!(
33-
format_code(
34-
generate_totp("BASE32SECRET3232", OTPAlgorithm::Sha1, 0, 30, 0).unwrap(),
35-
6
36-
),
37-
"260182"
33+
455260182,
34+
generate_totp("BASE32SECRET3232", OTPAlgorithm::Sha1, 0, 30, 0).unwrap()
3835
);
3936
}
40-
41-
fn format_code(value: u32, digits: u32) -> String {
42-
// Get the formatted code
43-
let s = (value % 10_u32.pow(digits)).to_string();
44-
"0".repeat(digits as usize - s.chars().count()) + s.as_str()
45-
}
4637
}

src/otp/otp_element.rs

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,13 +171,13 @@ impl OTPElement {
171171
OTPType::Totp => {
172172
let code = totp(&self.secret, self.algorithm)?;
173173

174-
Ok(self.format_code(code))
174+
Ok(self.format_code(code)?)
175175
}
176176
OTPType::Hotp => match self.counter {
177177
Some(counter) => {
178178
let code = hotp(&self.secret, self.algorithm, counter)?;
179179

180-
Ok(self.format_code(code))
180+
Ok(self.format_code(code)?)
181181
}
182182
None => Err(OtpError::MissingCounter),
183183
},
@@ -204,10 +204,13 @@ impl OTPElement {
204204
}
205205
}
206206

207-
pub fn format_code(&self, value: u32) -> String {
207+
fn format_code(&self, value: u32) -> Result<String, OtpError> {
208208
// Get the formatted code
209-
let s = (value % 10_u32.pow(self.digits as u32)).to_string();
210-
"0".repeat(self.digits as usize - s.chars().count()) + s.as_str()
209+
let exponential = 10_u32
210+
.checked_pow(self.digits as u32)
211+
.ok_or(OtpError::InvalidDigits)?;
212+
let s = (value % exponential).to_string();
213+
Ok("0".repeat(self.digits as usize - s.chars().count()) + s.as_str())
211214
}
212215

213216
pub fn valid_secret(&self) -> bool {
@@ -231,6 +234,7 @@ mod test {
231234
use crate::otp::otp_element::OTPType::Totp;
232235

233236
use crate::otp::from_otp_uri::FromOtpUri;
237+
use crate::otp::otp_error::OtpError;
234238

235239
#[test]
236240
fn test_serialization_otp_uri_full_element() {
@@ -287,4 +291,28 @@ mod test {
287291
let otp_uri = "otpauth://totp/2Ponies%40Github%20No.1?secret=JBSWY3DPEHPK3PXP&algorithm=SHA1&digits=6&period=30&lock=false&issuer=test";
288292
assert_eq!(true, OTPElement::from_otp_uri(otp_uri).is_ok())
289293
}
294+
295+
#[test]
296+
fn test_invalid_digits_should_not_overflow() {
297+
// Arrange
298+
let invalid_digits_value = 10;
299+
300+
let element = OTPElement {
301+
secret: "xr5gh44x7bprcqgrdtulafeevt5rxqlbh5wvked22re43dh2d4mapv5g".to_uppercase(),
302+
issuer: String::from("IssuerText"),
303+
label: String::from("LabelText"),
304+
digits: invalid_digits_value,
305+
type_: Totp,
306+
algorithm: Sha1,
307+
period: 30,
308+
counter: None,
309+
pin: None,
310+
};
311+
312+
// Act
313+
let result = element.get_otp_code();
314+
315+
// Assert
316+
assert_eq!(Err(OtpError::InvalidDigits), result)
317+
}
290318
}

src/otp/otp_error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub enum OtpError {
99
MissingCounter, // Missing counter for HOTP codes
1010
InvalidOffset, // Invalid offset
1111
InvalidDigest, // Invalid digest
12+
InvalidDigits, // Invalid Digits value (too high or low)
1213
}
1314

1415
impl Display for OtpError {
@@ -22,6 +23,7 @@ impl Display for OtpError {
2223
OtpError::InvalidDigest => f.write_str("Invalid digest"),
2324
OtpError::InvalidOffset => f.write_str("Invalid offset"),
2425
OtpError::ShortSecret => f.write_str("Secret length less than 16 bytes"),
26+
OtpError::InvalidDigits => f.write_str("Digits value too high or low"),
2527
}
2628
}
2729
}

0 commit comments

Comments
 (0)