1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
use hmac::{Hmac, KeyInit, Mac};
use serde::Deserialize;
use sha1::Sha1;
type HmacSha1 = Hmac<Sha1>;
/// TOTP algorithm parameters.
///
/// Deserializes from YAML/TOML config. Defaults follow RFC 6238:
/// 6 digits, 30-second step, ±1-step verification window.
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct TotpConfig {
/// Number of OTP digits (default: 6).
pub digits: u32,
/// Time step duration in seconds (default: 30).
pub step_secs: u64,
/// Number of adjacent time steps to accept on each side of the current
/// step during verification (default: 1).
pub window: u32,
}
impl Default for TotpConfig {
fn default() -> Self {
Self {
digits: 6,
step_secs: 30,
window: 1,
}
}
}
/// TOTP authenticator compatible with RFC 6238 and authenticator apps
/// (Google Authenticator, Authy, etc.).
///
/// Requires feature `"auth"`.
pub struct Totp {
secret: Vec<u8>,
config: TotpConfig,
}
impl Totp {
/// Creates a new `Totp` from a raw secret byte slice and configuration.
pub fn new(secret: Vec<u8>, config: &TotpConfig) -> Self {
Self {
secret,
config: config.clone(),
}
}
/// Generates a cryptographically random 20-byte secret and returns it
/// as a base32-encoded string suitable for QR code provisioning URIs.
pub fn generate_secret() -> String {
let mut bytes = [0u8; 20];
rand::fill(&mut bytes);
crate::encoding::base32::encode(&bytes)
}
/// Creates a `Totp` from a base32-encoded secret string.
///
/// # Errors
///
/// Returns `Error::bad_request` if the string is not valid base32.
pub fn from_base32(encoded: &str, config: &TotpConfig) -> crate::Result<Self> {
let bytes = crate::encoding::base32::decode(encoded)
.map_err(|_| crate::Error::bad_request("invalid base32 secret"))?;
Ok(Self::new(bytes, config))
}
/// Generates the current TOTP code using the system clock.
///
/// # Panics
///
/// Panics if the system clock is before the Unix epoch.
pub fn generate(&self) -> String {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system clock before epoch")
.as_secs();
self.generate_at(now)
}
/// Generates a TOTP code for the given Unix timestamp in seconds.
pub fn generate_at(&self, timestamp: u64) -> String {
let counter = timestamp / self.config.step_secs;
let code = hotp(&self.secret, counter, self.config.digits);
format!("{:0>width$}", code, width = self.config.digits as usize)
}
/// Verifies `code` against the current time, accepting codes within the
/// configured window of adjacent time steps.
///
/// Comparison is constant-time to prevent timing attacks.
///
/// # Panics
///
/// Panics if the system clock is before the Unix epoch.
pub fn verify(&self, code: &str) -> bool {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system clock before epoch")
.as_secs();
self.verify_at(code, now)
}
/// Verifies `code` against the given Unix `timestamp` in seconds,
/// accepting codes within the configured window of adjacent time steps.
///
/// Comparison is constant-time to prevent timing attacks.
pub fn verify_at(&self, code: &str, timestamp: u64) -> bool {
let current_step = timestamp / self.config.step_secs;
let window = self.config.window as u64;
let start = current_step.saturating_sub(window);
let end = current_step + window;
use subtle::ConstantTimeEq;
let mut found = subtle::Choice::from(0);
for step in start..=end {
let expected = hotp(&self.secret, step, self.config.digits);
let expected_str =
format!("{:0>width$}", expected, width = self.config.digits as usize);
found |= code.as_bytes().ct_eq(expected_str.as_bytes());
}
found.into()
}
/// Returns an `otpauth://totp/` URI for QR code generation.
///
/// The URI encodes the issuer, account name, base32 secret, digit count,
/// and time period. Authenticator apps scan this URI to provision the key.
pub fn otpauth_uri(&self, issuer: &str, account: &str) -> String {
let secret_b32 = crate::encoding::base32::encode(&self.secret);
let encoded_account = urlencoding_encode(account);
let encoded_issuer = urlencoding_encode(issuer);
format!(
"otpauth://totp/{encoded_issuer}:{encoded_account}?secret={secret_b32}&issuer={encoded_issuer}&digits={}&period={}",
self.config.digits, self.config.step_secs
)
}
}
fn hotp(secret: &[u8], counter: u64, digits: u32) -> u32 {
let mut mac = HmacSha1::new_from_slice(secret).expect("HMAC can take key of any size");
mac.update(&counter.to_be_bytes());
let result = mac.finalize().into_bytes();
let offset = (result[19] & 0x0f) as usize;
let code = u32::from_be_bytes([
result[offset] & 0x7f,
result[offset + 1],
result[offset + 2],
result[offset + 3],
]);
code % 10u32.pow(digits)
}
fn urlencoding_encode(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for b in s.bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
result.push(b as char);
}
_ => {
result.push_str(&format!("%{b:02X}"));
}
}
}
result
}