Skip to content

Commit 44852e5

Browse files
authored
Merge pull request #5 from tmthecoder/add-otp-struct
Add a shared `OTPResult` struct
2 parents 1eec5c1 + 6b519b4 commit 44852e5

File tree

7 files changed

+101
-16
lines changed

7 files changed

+101
-16
lines changed

src/hotp.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Implementation of the HOTP standard according to RFC4226 by Tejas Mehta
22

3+
use crate::otp_result::OTPResult;
34
use crate::util::{base32_decode, get_code, hash_generic, MacDigest};
45

56
/// A HOTP Generator
@@ -101,13 +102,14 @@ impl HOTP {
101102
///
102103
/// # Panics
103104
/// This method panics if the hash's secret is incorrectly given.
104-
pub fn get_otp(&self, counter: u64) -> u32 {
105+
pub fn get_otp(&self, counter: u64) -> OTPResult {
105106
let hash = hash_generic(&counter.to_be_bytes(), &self.secret, &MacDigest::SHA1);
106107
let offset = (hash[hash.len() - 1] & 0xf) as usize;
107108
let bytes: [u8; 4] = hash[offset..offset + 4]
108109
.try_into()
109110
.expect("Failed byte get");
110111

111-
get_code(bytes, self.digits)
112+
let code = get_code(bytes, self.digits);
113+
OTPResult::new(self.digits, code)
112114
}
113115
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,4 @@
8080
pub mod hotp;
8181
pub mod totp;
8282
pub mod util;
83+
pub mod otp_result;

src/otp_result.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use std::fmt;
2+
use std::fmt::Formatter;
3+
4+
/// A convenience struct to hold the result of a [`HOTP`] or [`TOTP`]
5+
/// generation.
6+
///
7+
/// Contains the amount of digits the OTP should be, and the actual OTP,
8+
/// which will be equal to or less than the digit count. Currently houses
9+
/// a convenience [`OTPResult::as_string`] which returns a zero-padded string
10+
/// that has a length of [`OTPResult::digits`]. Additionally, the numerical
11+
/// representation of the code can be got with [`OTPResult::as_u32`].
12+
///
13+
/// Returned as a result of either [`HOTP::get_otp`], [`TOTP::get_otp`]
14+
/// or [`TOTP::get_otp_with_custom_time_start`].
15+
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
16+
pub struct OTPResult {
17+
digits: u32,
18+
code: u32,
19+
}
20+
21+
/// Constructors for the [`OTPResult`] struct.
22+
impl OTPResult {
23+
/// Creates a new instance with the provided digit count and OTP code.
24+
pub fn new(digits: u32, code: u32 ) -> Self {
25+
OTPResult { digits, code }
26+
}
27+
}
28+
29+
/// Getters for the [`OTPResult`] struct.
30+
impl OTPResult {
31+
/// Gets the digit count given to the struct on creation.
32+
///
33+
/// Also the count used to determine how long the formatted string will be.
34+
pub fn get_digits(&self) -> u32 { self.digits }
35+
}
36+
37+
/// Convenience code getters for the [`OTPResult`] struct
38+
impl OTPResult {
39+
/// Returns the OTP as a formatted string of length [`OTPResult.digits`].
40+
///
41+
/// If [`OTPResult::code`] is less than [`OTPResult::digits`] long, leading zeroes
42+
/// will be added to the string.
43+
pub fn as_string(&self) -> String {
44+
format!("{:01$}", self.code as usize, self.digits as usize)
45+
}
46+
47+
48+
/// Returns the OTP as it's original numerical representation
49+
///
50+
/// This number may not be [`OTPResult::digits`] long.
51+
pub fn as_u32(&self) -> u32 {
52+
self.code
53+
}
54+
}
55+
56+
/// A Display implementation for the [`OTPResult`] struct
57+
///
58+
/// Returns the String-formatted code, which is zero-padded
59+
/// to be [`OTPResult::digits`] long.
60+
impl fmt::Display for OTPResult {
61+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
62+
write!(f, "{}", self.as_string())
63+
}
64+
}

src/totp.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::otp_result::OTPResult;
12
use crate::util::{base32_decode, get_code, hash_generic, MacDigest};
23

34
/// A TOTP generator
@@ -130,7 +131,7 @@ impl TOTP {
130131
}
131132
}
132133

133-
// All getters
134+
/// All getters for the [`TOTP`] struct
134135
impl TOTP {
135136
/// Gets the algorithm used for code generation.
136137
pub fn get_digest(&self) -> MacDigest {
@@ -148,7 +149,7 @@ impl TOTP {
148149
}
149150
}
150151

151-
// All otp generation methods for the [`TOTP`] struct.
152+
/// All otp generation methods for the [`TOTP`] struct.
152153
impl TOTP {
153154
/// Generates and returns the TOTP value for the specified time.
154155
///
@@ -158,7 +159,7 @@ impl TOTP {
158159
/// # Panics
159160
/// This method panics if the [`TOTP::get_otp_with_custom_time_start`]
160161
/// method does, which happens if the hash's secret is incorrectly given.
161-
pub fn get_otp(&self, time: u64) -> u32 {
162+
pub fn get_otp(&self, time: u64) -> OTPResult {
162163
self.get_otp_with_custom_time_start(time, 0)
163164
}
164165

@@ -171,7 +172,7 @@ impl TOTP {
171172
///
172173
/// # Panics
173174
/// This method panics if the hash's secret is incorrectly given.
174-
pub fn get_otp_with_custom_time_start(&self, time: u64, time_start: u64) -> u32 {
175+
pub fn get_otp_with_custom_time_start(&self, time: u64, time_start: u64) -> OTPResult {
175176
let time_count = (time - time_start) / self.period;
176177

177178
let hash = hash_generic(&time_count.to_be_bytes(), &self.secret, &self.mac_digest);
@@ -180,6 +181,8 @@ impl TOTP {
180181
.try_into()
181182
.expect("Failed byte get");
182183

183-
get_code(bytes, self.digits)
184+
185+
let code = get_code(bytes, self.digits);
186+
OTPResult::new(self.digits, code)
184187
}
185188
}

tests/hotp.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,21 @@ static SECRET_BASE32: &str = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ";
88
/// the Secret Key as a byte array
99
fn run_rfc_test_bytes(count: u64) -> u32 {
1010
let hotp = HOTP::new(SECRET_BYTES, 6);
11-
hotp.get_otp(count)
11+
hotp.get_otp(count).as_u32()
1212
}
1313

1414
/// Generic test method to get the HOTP code with
1515
/// the Secret Key as a string literal
1616
fn run_rfc_test_utf8(count: u64) -> u32 {
1717
let hotp = HOTP::default_from_utf8(SECRET_UTF8);
18-
hotp.get_otp(count)
18+
hotp.get_otp(count).as_u32()
1919
}
2020

2121
/// Generic test method to get the HOTP code with
2222
/// the Secret Key as a base32-encoded string
2323
fn run_rfc_test_base32(count: u64) -> u32 {
2424
let hotp = HOTP::default_from_base32(SECRET_BASE32);
25-
hotp.get_otp(count)
25+
hotp.get_otp(count).as_u32()
2626
}
2727

2828
// All RFC4226 Test Cases (All SHA1)

tests/otp_result.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use xotp::otp_result::OTPResult;
2+
3+
// Tests whether a code with less than 6 digits adds on leading zeroes
4+
#[test]
5+
fn test_padding_needed() {
6+
let result = OTPResult::new(6, 1234);
7+
assert_eq!("001234", result.as_string())
8+
}
9+
10+
// Tests whether the formatter will leave the code string as-is
11+
#[test]
12+
fn test_padding_not_needed() {
13+
let result = OTPResult::new(6, 123456);
14+
assert_eq!("123456", result.as_string())
15+
}

tests/totp.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ static SECRET_BASE32_SHA512: &str = "GEZDGNBVGY3TQOJQGEZ\
2525
/// the SHA1 Secret Key as a byte array
2626
fn run_rfc_test_bytes(time: u64) -> u32 {
2727
let totp = TOTP::new(SECRET_BYTES_SHA1, MacDigest::SHA1, 8, 30);
28-
totp.get_otp(time)
28+
totp.get_otp(time).as_u32()
2929
}
3030

3131
/// Generic test method to get the TOTP code with
@@ -37,14 +37,14 @@ fn run_rfc_test_bytes_with_digest(time: u64, digest: MacDigest) -> u32 {
3737
SECRET_BYTES_SHA512
3838
};
3939
let totp = TOTP::new(secret, digest, 8, 30);
40-
totp.get_otp(time)
40+
totp.get_otp(time).as_u32()
4141
}
4242

4343
/// Generic test method to get the TOTP code with
4444
/// the SHA1 Secret Key as a string literal
4545
fn run_rfc_test_utf8(time: u64) -> u32 {
4646
let totp = TOTP::new_from_utf8(SECRET_UTF8_SHA1, MacDigest::SHA1, 8, 30);
47-
totp.get_otp(time)
47+
totp.get_otp(time).as_u32()
4848
}
4949

5050
/// Generic test method to get the TOTP code with
@@ -56,14 +56,14 @@ fn run_rfc_test_utf8_with_digest(time: u64, digest: MacDigest) -> u32 {
5656
SECRET_UTF8_SHA512
5757
};
5858
let totp = TOTP::new_from_utf8(secret, digest, 8, 30);
59-
totp.get_otp(time)
59+
totp.get_otp(time).as_u32()
6060
}
6161

6262
/// Generic test method to get the TOTP code with
6363
/// the SHA1 Secret Key as a base32-encoded string
6464
fn run_rfc_test_base32(time: u64) -> u32 {
6565
let totp = TOTP::new_from_base32(SECRET_BASE32_SHA1, MacDigest::SHA1, 8, 30);
66-
totp.get_otp(time)
66+
totp.get_otp(time).as_u32()
6767
}
6868

6969
/// Generic test method to get the TOTP code with
@@ -75,7 +75,7 @@ fn run_rfc_test_base32_with_digest(time: u64, digest: MacDigest) -> u32 {
7575
SECRET_BASE32_SHA512
7676
};
7777
let totp = TOTP::new_from_base32(secret, digest, 8, 30);
78-
totp.get_otp(time)
78+
totp.get_otp(time).as_u32()
7979
}
8080

8181
// All SHA-1 Tests for TOTP from rfc6238

0 commit comments

Comments
 (0)