Skip to content

Commit a2a9683

Browse files
committed
Otpauth URI parser
Still WIP but seems to work. Moved digits & period to TOTP / HOTP structs. TOTP tests currently fails because I haven't wrote the new constructors. Doc is missing / inaccurate in some parts.
1 parent 3bc8ad6 commit a2a9683

File tree

8 files changed

+386
-64
lines changed

8 files changed

+386
-64
lines changed

Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ description = "An easy-to-use library for HOTP and TOTP authentication"
44
repository = "https://github.com/tmthecoder/xotp"
55
readme = "README.md"
66
license = "MIT"
7-
keywords = ["otp", "hotp", "totp"]
7+
keywords = ["otp", "hotp", "totp", "otpauth"]
88
categories = ["cryptography"]
99
authors = ["Tejas Mehta <[email protected]>"]
10-
version = "0.1.0"
10+
version = "0.2.0"
1111
edition = "2021"
1212

1313
[dependencies]
1414
hmac = "0.12.0"
1515
sha-1 = "0.10.0"
1616
sha2 = "0.10.1"
17-
base32 = "0.4.0"
17+
base32 = "0.4.0"
18+
url = "2.2.2"

src/hotp.rs

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

3-
use crate::util::{get_code, hash_generic, MacDigest};
4-
use base32::Alphabet;
3+
use crate::util::{base32_decode, get_code, hash_generic, MacDigest};
54

65
/// A HOTP Generator
76
///
@@ -24,27 +23,29 @@ pub struct HOTP {
2423
/// Often given as a Base32 key, which can be conveniently initialize using
2524
/// the [`HOTP::from_base32`] initializers
2625
secret: Vec<u8>,
26+
27+
digits: u32,
2728
}
2829

2930
/// All initializer implementations for the [`HOTP`] struct.
30-
3131
impl HOTP {
3232
/// Creates a new HOTP instance with a byte-array representation
3333
/// of the secret
3434
///
3535
/// Since only SHA1 was specified in the reference implementation and
3636
/// RFC specification, there's no need to initialize with a digest object
37-
pub fn new(secret: &[u8]) -> Self {
37+
pub fn new(secret: &[u8], digits: u32) -> Self {
3838
HOTP {
3939
secret: secret.to_vec(),
40+
digits,
4041
}
4142
}
4243

4344
/// Creates a new HOTP instance from a utf8-encoded string secret
4445
///
4546
/// Internally calls [`HOTP::new`] with the string's byte representation
4647
pub fn from_utf8(secret: &str) -> Self {
47-
HOTP::new(secret.as_bytes())
48+
HOTP::new(secret.as_bytes(), 6)
4849
}
4950

5051
/// Creates a new HOTP instance from a base32-encoded string secret
@@ -55,28 +56,32 @@ impl HOTP {
5556
/// This method panics if the provided string is not correctly base32
5657
/// encoded.
5758
pub fn from_base32(secret: &str) -> Self {
58-
let decoded = base32::decode(Alphabet::RFC4648 { padding: false }, secret)
59-
.expect("Failed to decode base32 string");
60-
HOTP::new(&decoded)
59+
let decoded = base32_decode(secret).expect("Failed to decode base32 string");
60+
HOTP::new(&decoded, 6)
6161
}
6262
}
6363

64-
/// All otp generation methods for the [`HOTP`] struct.
64+
impl HOTP {
65+
pub fn get_digits(&self) -> u32 {
66+
self.digits
67+
}
68+
}
6569

70+
/// All otp generation methods for the [`HOTP`] struct.
6671
impl HOTP {
6772
/// Generates and returns the HOTP value
6873
///
6974
/// Uses the given counter value with the specified digit count
7075
///
7176
/// # Panics
7277
/// This method panics if the hash's secret is incorrectly given.
73-
pub fn get_otp(&self, counter: u64, digits: u32) -> u32 {
78+
pub fn get_otp(&self, counter: u64) -> u32 {
7479
let hash = hash_generic(&counter.to_be_bytes(), &self.secret, &MacDigest::SHA1);
7580
let offset = (hash[hash.len() - 1] & 0xf) as usize;
7681
let bytes: [u8; 4] = hash[offset..offset + 4]
7782
.try_into()
7883
.expect("Failed byte get");
7984

80-
get_code(bytes, digits)
85+
get_code(bytes, self.digits)
8186
}
8287
}

src/lib.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@
1515
//! let counter = 0;
1616
//! // Get a HOTP instance with a '&str' secret
1717
//! let hotp_str = HOTP::from_utf8(secret);
18-
//! // Get an otp with the given counter and digit count
19-
//! let otp_from_str = hotp_str.get_otp(counter, 6);
18+
//! // Get an otp with the given counter
19+
//! let otp_from_str = hotp_str.get_otp(counter);
2020
//! println!("The otp from hotp_str: {}", otp_from_str);
2121
//!
2222
//! // Alternatively, get a HOTP instance with a '&[u8]' secret
23-
//! let hotp_bytes = HOTP::new(secret.as_bytes());
24-
//! // Get an otp with the given counter and digit count
25-
//! let otp_from_bytes = hotp_bytes.get_otp(counter, 6);
23+
//! let hotp_bytes = HOTP::new(secret.as_bytes(), 6);
24+
//! // Get an otp with the given counter
25+
//! let otp_from_bytes = hotp_bytes.get_otp(counter);
2626
//! println!("The otp from hotp_bytes: {}", otp_from_bytes);
2727
//! }
2828
//! ```
@@ -43,21 +43,21 @@
4343
//! // Get a TOTP instance with an '&str' secret and default SHA1 Digest
4444
//! let totp_sha1_str = TOTP::from_utf8(secret);
4545
//! // Get an otp with the given counter and elapsed seconds
46-
//! let otp_sha1 = totp_sha1_str.get_otp(elapsed_seconds, 8);
46+
//! let otp_sha1 = totp_sha1_str.get_otp(elapsed_seconds);
4747
//! println!("The otp from totp_sha1_str: {}", otp_sha1);
4848
//!
4949
//! // Alternatively get a TOTP instance with an '&[u8]' secret
5050
//! // and different digest (Sha256 or Sha512)
51-
//! let totp_sha256_bytes = TOTP::new_with_digest(
51+
//! let totp_sha256_bytes = TOTP::new(
5252
//! secret.as_bytes(),
53-
//! MacDigest::SHA256
53+
//! MacDigest::SHA256, // SHA256 algorithm
54+
//! 8, // 8 digits
55+
//! 60 // 60 seconds interval
5456
//! );
5557
//! // Get an otp with the given counter, time and other custom params
56-
//! let otp_sha256 = totp_sha256_bytes.get_otp_with_custom(
58+
//! let otp_sha256 = totp_sha256_bytes.get_otp_with_custom_time_start(
5759
//! elapsed_seconds,
58-
//! 30, // A 60-second time step
5960
//! 0, // Start time at unix epoch
60-
//! 6 // 8-digit code
6161
//! );
6262
//! println!("The otp from totp_sha256_bytes: {}", otp_sha256);
6363
//! }

src/totp.rs

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
use crate::util::{get_code, hash_generic, MacDigest};
2-
use base32::Alphabet;
1+
use crate::util::{base32_decode, get_code, hash_generic, MacDigest};
32

43
/// A TOTP generator
54
///
@@ -30,28 +29,38 @@ pub struct TOTP {
3029
/// Unless an initializer ending in 'with_digest' is used, this value
3130
/// defaults to [`MacDigest::SHA1`]
3231
mac_digest: MacDigest,
32+
33+
digits: u32,
34+
35+
period: u64,
3336
}
3437

3538
/// All initializer implementations for the [`TOTP`] struct
3639
impl TOTP {
40+
pub fn new(secret: &[u8], mac_digest: MacDigest, digits: u32, period: u64) -> Self {
41+
TOTP {
42+
secret: secret.to_vec(),
43+
mac_digest,
44+
digits,
45+
period,
46+
}
47+
}
48+
3749
/// Creates a new TOTP instance with a byte-array representation
3850
/// of the secret
3951
///
4052
/// Defaults to using [`MacDigest::SHA1`] as the digest for HMAC
4153
/// operations.
42-
pub fn new(secret: &[u8]) -> Self {
43-
TOTP::new_with_digest(secret, MacDigest::SHA1)
54+
pub fn from_secret(secret: &[u8]) -> Self {
55+
TOTP::from_secret_with_digest(secret, MacDigest::SHA1)
4456
}
4557

4658
/// Creates a new TOTP instance with a byte-array representation
4759
/// of the secret and a specific digest for HMAC operations
4860
///
4961
/// Allows for non-SHA1 algorithms to be used with TOTP generation
50-
pub fn new_with_digest(secret: &[u8], mac_digest: MacDigest) -> Self {
51-
TOTP {
52-
secret: secret.to_vec(),
53-
mac_digest,
54-
}
62+
pub fn from_secret_with_digest(secret: &[u8], mac_digest: MacDigest) -> Self {
63+
TOTP::new(secret, mac_digest, 6, 30)
5564
}
5665

5766
/// Creates a new TOTP instance from a utf8-encoded string secret
@@ -67,7 +76,7 @@ impl TOTP {
6776
/// Like [`TOTP::new_with_digest`], this method allows a digest to be specified
6877
/// instead of the default SHA1 being used.
6978
pub fn from_utf8_with_digest(secret: &str, mac_digest: MacDigest) -> Self {
70-
TOTP::new_with_digest(secret.as_bytes(), mac_digest)
79+
TOTP::from_secret_with_digest(secret.as_bytes(), mac_digest)
7180
}
7281

7382
/// Creates a new TOTP instance from a base32-encoded string secret
@@ -90,9 +99,22 @@ impl TOTP {
9099
/// # Panics
91100
/// This method panics if the provided string is not correctly base32 encoded.
92101
pub fn from_base32_with_digest(secret: &str, mac_digest: MacDigest) -> Self {
93-
let decoded = base32::decode(Alphabet::RFC4648 { padding: false }, secret)
94-
.expect("Failed to decode base32 string");
95-
TOTP::new_with_digest(&decoded, mac_digest)
102+
let decoded = base32_decode(secret).expect("Failed to decode base32 string");
103+
TOTP::from_secret_with_digest(&decoded, mac_digest)
104+
}
105+
}
106+
107+
impl TOTP {
108+
pub fn get_digest(&self) -> MacDigest {
109+
self.mac_digest
110+
}
111+
112+
pub fn get_digits(&self) -> u32 {
113+
self.digits
114+
}
115+
116+
pub fn get_period(&self) -> u64 {
117+
self.period
96118
}
97119
}
98120

@@ -111,8 +133,8 @@ impl TOTP {
111133
/// # Panics
112134
/// This method panics if the called [`TOTP::get_otp_with_custom`] method
113135
/// does, which would happen if the hash's secret is incorrectly given.
114-
pub fn get_otp(&self, time: u64, digits: u32) -> u32 {
115-
self.get_otp_with_custom(time, 30, 0, digits)
136+
pub fn get_otp(&self, time: u64) -> u32 {
137+
self.get_otp_with_custom_time_start(time, 0)
116138
}
117139

118140
/// Generates and returns the TOTP value for the time with a provided step,
@@ -125,21 +147,15 @@ impl TOTP {
125147
///
126148
/// # Panics
127149
/// This method panics if the hash's secret is incorrectly given.
128-
pub fn get_otp_with_custom(
129-
&self,
130-
time: u64,
131-
time_step: u64,
132-
time_start: u64,
133-
digits: u32,
134-
) -> u32 {
135-
let time_count = (time - time_start) / time_step;
150+
pub fn get_otp_with_custom_time_start(&self, time: u64, time_start: u64) -> u32 {
151+
let time_count = (time - time_start) / self.period;
136152

137153
let hash = hash_generic(&time_count.to_be_bytes(), &self.secret, &self.mac_digest);
138154
let offset = (hash[hash.len() - 1] & 0xf) as usize;
139155
let bytes: [u8; 4] = hash[offset..offset + 4]
140156
.try_into()
141157
.expect("Failed byte get");
142158

143-
get_code(bytes, digits)
159+
get_code(bytes, self.digits)
144160
}
145161
}

0 commit comments

Comments
 (0)