1use crate::db::Entry;
2use base32;
3use std::time::{Duration, SystemTime, SystemTimeError, UNIX_EPOCH};
4use totp_lite::{Sha1, Sha256, Sha512, totp_custom};
5use url::Url;
6use zeroize::{Zeroize, ZeroizeOnDrop};
7
8const DEFAULT_PERIOD: u64 = 30;
9const DEFAULT_DIGITS: u32 = 8;
10
11#[derive(Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
13pub enum TOTPAlgorithm {
14 Sha1,
15 Sha256,
16 Sha512,
17}
18
19impl std::str::FromStr for TOTPAlgorithm {
20 type Err = TOTPError;
21
22 fn from_str(s: &str) -> Result<Self, Self::Err> {
23 match s.to_uppercase().as_str() {
24 "SHA1" => Ok(TOTPAlgorithm::Sha1),
25 "SHA256" => Ok(TOTPAlgorithm::Sha256),
26 "SHA512" => Ok(TOTPAlgorithm::Sha512),
27 _ => Err(TOTPError::BadAlgorithm(s.to_string())),
28 }
29 }
30}
31
32impl std::fmt::Display for TOTPAlgorithm {
33 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
34 match self {
35 TOTPAlgorithm::Sha1 => write!(f, "SHA1"),
36 TOTPAlgorithm::Sha256 => write!(f, "SHA256"),
37 TOTPAlgorithm::Sha512 => write!(f, "SHA512"),
38 }
39 }
40}
41
42#[derive(Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
44pub struct TOTP {
45 pub label: String,
46 secret: Vec<u8>,
47 pub issuer: Option<String>,
48 pub period: u64,
49 pub digits: u32,
50 pub algorithm: TOTPAlgorithm,
51}
52
53pub struct OTPCode {
55 pub code: String,
56 pub valid_for: Duration,
57 pub period: Duration,
58}
59
60impl std::fmt::Display for OTPCode {
61 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
62 write!(
63 f,
64 "Code: {}, valid for: {}/{}s",
65 self.code,
66 self.valid_for.as_secs(),
67 self.period.as_secs(),
68 )
69 }
70}
71
72#[derive(Debug, thiserror::Error)]
74pub enum TOTPError {
75 #[error(transparent)]
76 UrlFormat(#[from] url::ParseError),
77
78 #[error(transparent)]
79 IntFormat(#[from] std::num::ParseIntError),
80
81 #[error("Missing TOTP field: {}", _0)]
82 MissingField(&'static str),
83
84 #[error(transparent)]
85 Time(#[from] SystemTimeError),
86
87 #[error("Base32 decoding error")]
88 Base32,
89
90 #[error("No OTP record found")]
91 NoRecord,
92
93 #[error("Bad URL scheme: '{}'", _0)]
94 BadScheme(String),
95
96 #[error("Bad hash algorithm: '{}'", _0)]
97 BadAlgorithm(String),
98}
99
100impl std::str::FromStr for TOTP {
101 type Err = TOTPError;
102
103 fn from_str(s: &str) -> Result<Self, Self::Err> {
104 let parsed = Url::parse(s)?;
105
106 if parsed.scheme() != "otpauth" {
107 return Err(TOTPError::BadScheme(parsed.scheme().to_string()));
108 }
109 let query_pairs = parsed.query_pairs();
110
111 let label: String = parsed.path().trim_start_matches('/').to_string();
112 let mut secret: Option<String> = None;
113 let mut issuer: Option<String> = None;
114 let mut period: u64 = DEFAULT_PERIOD;
115 let mut digits: u32 = DEFAULT_DIGITS;
116 let mut algorithm: TOTPAlgorithm = TOTPAlgorithm::Sha1;
117
118 for pair in query_pairs {
119 let (k, v) = pair;
120 match k.as_ref() {
121 "secret" => secret = Some(v.to_string()),
122 "issuer" => issuer = Some(v.to_string()),
123 "period" => period = v.parse()?,
124 "digits" => digits = v.parse()?,
125 "algorithm" => algorithm = v.parse()?,
126 _ => {}
127 }
128 }
129
130 let secret = secret.ok_or(TOTPError::MissingField("secret"))?;
131
132 let secret = base32::decode(base32::Alphabet::Rfc4648 { padding: true }, &secret).ok_or(TOTPError::Base32)?;
133
134 Ok(TOTP {
135 label,
136 secret,
137 issuer,
138 period,
139 digits,
140 algorithm,
141 })
142 }
143}
144
145impl std::fmt::Display for TOTP {
146 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
147 write!(
148 f,
149 "otpauth://totp/{}?secret={}&period={}&digits={}&issuer={}&algorithm={:?}",
150 self.label,
151 base32::encode(base32::Alphabet::Rfc4648 { padding: true }, &self.secret),
152 self.period,
153 self.digits,
154 self.issuer.as_deref().unwrap_or(""),
155 self.algorithm
156 )
157 }
158}
159
160impl TOTP {
161 pub fn value_at(&self, time: u64) -> OTPCode {
163 let code = match self.algorithm {
164 TOTPAlgorithm::Sha1 => totp_custom::<Sha1>(self.period, self.digits, &self.secret, time),
165 TOTPAlgorithm::Sha256 => totp_custom::<Sha256>(self.period, self.digits, &self.secret, time),
166 TOTPAlgorithm::Sha512 => totp_custom::<Sha512>(self.period, self.digits, &self.secret, time),
167 };
168
169 let valid_for = Duration::from_secs(self.period - (time % self.period));
170
171 OTPCode {
172 code,
173 valid_for,
174 period: Duration::from_secs(self.period),
175 }
176 }
177
178 pub fn value_now(&self) -> Result<OTPCode, SystemTimeError> {
180 let time: u64 = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
181 Ok(self.value_at(time))
182 }
183
184 pub fn get_secret(&self) -> String {
185 base32::encode(base32::Alphabet::Rfc4648 { padding: true }, &self.secret)
186 }
187}
188
189impl<'a> Entry {
190 pub fn get_otp(&'a self) -> Result<TOTP, TOTPError> {
192 self.get_raw_otp_value().ok_or(TOTPError::NoRecord)?.parse()
193 }
194
195 pub fn set_otp(&mut self, otp: Option<&TOTP>) {
196 self.set_raw_otp_value(otp.map(|o| o.to_string()).as_deref());
197 }
198
199 pub fn set_raw_otp_value(&mut self, value: Option<&str>) {
201 self.set_protected_field_pair("otp", value);
202 }
203
204 pub fn get_raw_otp_value(&'a self) -> Option<&'a str> {
206 self.get("otp")
207 }
208}
209
210#[cfg(test)]
211mod kdbx4_otp_tests {
212 use super::{TOTP, TOTPAlgorithm, TOTPError};
213 use crate::{
214 db::{Database, Entry, Group, Node, with_node},
215 key::DatabaseKey,
216 };
217 use std::{fs::File, path::Path};
218
219 #[test]
220 fn kdbx4_entry() -> Result<(), Box<dyn std::error::Error>> {
221 let path = Path::new("tests/resources/test_db_kdbx4_with_totp_entry.kdbx");
223 let key = DatabaseKey::new().with_password("test");
224 let db = Database::open(&mut File::open(path)?, key)?;
225
226 let otp_str = "otpauth://totp/KeePassXC:none?secret=JBSWY3DPEHPK3PXP&period=30&digits=6&issuer=KeePassXC";
227
228 let entry = Group::get(&db.root, &["this entry has totp"]).unwrap();
230 with_node::<Entry, _, _>(&entry, |e| {
231 assert_eq!(e.get_title(), Some("this entry has totp"));
232 assert_eq!(e.get_raw_otp_value(), Some(otp_str));
233 })
234 .unwrap();
235
236 Ok(())
237 }
238
239 #[test]
240 fn totp_default() -> Result<(), TOTPError> {
241 let otp_str = "otpauth://totp/KeePassXC:none?secret=JBSWY3DPEHPK3PXP&period=30&digits=6&issuer=KeePassXC";
242
243 let expected = TOTP {
244 label: "KeePassXC:none".to_string(),
245 secret: b"Hello!\xDE\xAD\xBE\xEF".to_vec(),
246 issuer: Some("KeePassXC".to_string()),
247 period: 30,
248 digits: 6,
249 algorithm: TOTPAlgorithm::Sha1,
250 };
251
252 assert_eq!(otp_str.parse::<TOTP>()?, expected);
253
254 Ok(())
255 }
256
257 #[test]
258 fn totp_get_secret() -> Result<(), TOTPError> {
259 let otp_str = "otpauth://totp/KeePassXC:none?secret=JBSWY3DPEHPK3PXP&period=30&digits=6&issuer=KeePassXC";
260
261 let otp = otp_str.parse::<TOTP>()?;
262
263 assert_eq!(otp.get_secret(), "JBSWY3DPEHPK3PXP".to_string());
264
265 Ok(())
266 }
267
268 #[test]
269 fn totp_sha512() -> Result<(), TOTPError> {
270 let otp_str = "otpauth://totp/sha512%20totp:none?secret=GEZDGNBVGY%3D%3D%3D%3D%3D%3D&period=30&digits=6&issuer=sha512%20totp&algorithm=SHA512";
271
272 let expected = TOTP {
273 label: "sha512%20totp:none".to_string(),
274 secret: b"123456".to_vec(),
275 issuer: Some("sha512 totp".to_string()),
276 period: 30,
277 digits: 6,
278 algorithm: TOTPAlgorithm::Sha512,
279 };
280
281 assert_eq!(otp_str.parse::<TOTP>()?, expected);
282
283 Ok(())
284 }
285
286 #[test]
287 fn totp_value() {
288 let totp = TOTP {
289 label: "KeePassXC:none".to_string(),
290 secret: b"Hello!\xDE\xAD\xBE\xEF".to_vec(),
291 issuer: Some("KeePassXC".to_string()),
292 period: 30,
293 digits: 6,
294 algorithm: TOTPAlgorithm::Sha1,
295 };
296
297 assert_eq!(totp.value_at(1234).code, "806863")
298 }
299
300 #[test]
301 fn totp_bad() {
302 assert!(matches!("not a totp string".parse::<TOTP>(), Err(TOTPError::UrlFormat(_))));
303
304 assert!(matches!(
305 "http://totp/sha512%20totp:none?secret=GEZDGNBVGY%3D%3D%3D%3D%3D%3D&period=30&digits=6&issuer=sha512%20totp&algorithm=SHA512"
306 .parse::<TOTP>(),
307 Err(TOTPError::BadScheme(_))
308 ));
309
310 assert!(matches!(
311 "otpauth://totp/sha512%20totp:none?secret=GEZDGNBVGY%3D%3D%3D%3D%3D%3D&period=30&digits=6&issuer=sha512%20totp&algorithm=SHA123".parse::<TOTP>(),
312 Err(TOTPError::BadAlgorithm(_))
313 ));
314
315 assert!(matches!(
316 "otpauth://missing_fields".parse::<TOTP>(),
317 Err(TOTPError::MissingField("secret"))
318 ));
319 }
320
321 #[test]
322 fn totp_minimal() -> Result<(), TOTPError> {
323 let otp_str = "otpauth://totp/KeePassXC:none?secret=JBSWY3DPEHPK3PXP&period=30&digits=6";
324
325 let expected = TOTP {
326 label: "KeePassXC:none".to_string(),
327 secret: b"Hello!\xDE\xAD\xBE\xEF".to_vec(),
328 issuer: None,
329 period: 30,
330 digits: 6,
331 algorithm: TOTPAlgorithm::Sha1,
332 };
333
334 assert_eq!(otp_str.parse::<TOTP>()?, expected);
335
336 Ok(())
337 }
338}