1use std::convert::{TryFrom, TryInto};
2use std::time::{Duration, SystemTime};
3
4use kanidm_proto::internal::{TotpAlgo as ProtoTotpAlgo, TotpSecret as ProtoTotp};
5use openssl::hash::MessageDigest;
6use openssl::pkey::PKey;
7use openssl::sign::Signer;
8use rand::prelude::*;
9
10use crate::be::dbvalue::{DbTotpAlgoV1, DbTotpV1};
11
12const SECRET_SIZE_BYTES: usize = 32;
15pub const TOTP_DEFAULT_STEP: u64 = 30;
16
17#[derive(Debug, PartialEq, Eq)]
18pub enum TotpError {
19 OpenSSLError,
20 HmacError,
21 TimeError,
22}
23
24#[repr(u32)]
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum TotpDigits {
27 Six = 1_000_000,
28 Eight = 100_000_000,
29}
30
31impl TryFrom<u8> for TotpDigits {
32 type Error = ();
33
34 fn try_from(value: u8) -> Result<Self, Self::Error> {
35 match value {
36 6 => Ok(TotpDigits::Six),
37 8 => Ok(TotpDigits::Eight),
38 _ => Err(()),
39 }
40 }
41}
42
43#[allow(clippy::from_over_into)]
44impl Into<u8> for TotpDigits {
45 fn into(self) -> u8 {
46 match self {
47 TotpDigits::Six => 6,
48 TotpDigits::Eight => 8,
49 }
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Copy)]
54pub enum TotpAlgo {
55 Sha1,
56 Sha256,
57 Sha512,
58}
59
60impl TotpAlgo {
61 pub(crate) fn digest(self, key: &[u8], counter: u64) -> Result<Vec<u8>, TotpError> {
62 let key = PKey::hmac(key).map_err(|_e| TotpError::OpenSSLError)?;
63 let mut signer =
64 match self {
65 TotpAlgo::Sha1 => Signer::new(MessageDigest::sha1(), &key)
66 .map_err(|_e| TotpError::OpenSSLError)?,
67 TotpAlgo::Sha256 => Signer::new(MessageDigest::sha256(), &key)
68 .map_err(|_e| TotpError::OpenSSLError)?,
69 TotpAlgo::Sha512 => Signer::new(MessageDigest::sha512(), &key)
70 .map_err(|_e| TotpError::OpenSSLError)?,
71 };
72 signer
73 .update(&counter.to_be_bytes())
74 .map_err(|_e| TotpError::OpenSSLError)?;
75 let hmac = signer.sign_to_vec().map_err(|_e| TotpError::OpenSSLError)?;
76
77 let expect = match self {
78 TotpAlgo::Sha1 => 20,
79 TotpAlgo::Sha256 => 32,
80 TotpAlgo::Sha512 => 64,
81 };
82 if hmac.len() != expect {
83 return Err(TotpError::HmacError);
84 }
85 Ok(hmac)
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct Totp {
92 secret: Vec<u8>,
93 pub(crate) step: u64,
94 algo: TotpAlgo,
95 digits: TotpDigits,
96}
97
98impl TryFrom<DbTotpV1> for Totp {
99 type Error = ();
100
101 fn try_from(value: DbTotpV1) -> Result<Self, Self::Error> {
102 let algo = match value.algo {
103 DbTotpAlgoV1::S1 => TotpAlgo::Sha1,
104 DbTotpAlgoV1::S256 => TotpAlgo::Sha256,
105 DbTotpAlgoV1::S512 => TotpAlgo::Sha512,
106 };
107 let digits = TotpDigits::try_from(value.digits.unwrap_or(6))?;
109
110 Ok(Totp {
111 secret: value.key,
112 step: value.step,
113 algo,
114 digits,
115 })
116 }
117}
118
119impl TryFrom<ProtoTotp> for Totp {
120 type Error = ();
121
122 fn try_from(value: ProtoTotp) -> Result<Self, Self::Error> {
123 Ok(Totp {
124 secret: value.secret,
125 algo: match value.algo {
126 ProtoTotpAlgo::Sha1 => TotpAlgo::Sha1,
127 ProtoTotpAlgo::Sha256 => TotpAlgo::Sha256,
128 ProtoTotpAlgo::Sha512 => TotpAlgo::Sha512,
129 },
130 step: value.step,
131 digits: TotpDigits::try_from(value.digits)?,
132 })
133 }
134}
135
136impl Totp {
137 pub fn new(secret: Vec<u8>, step: u64, algo: TotpAlgo, digits: TotpDigits) -> Self {
138 Totp {
139 secret,
140 step,
141 algo,
142 digits,
143 }
144 }
145
146 pub fn generate_secure(step: u64) -> Self {
148 let mut rng = rand::rng();
149 let secret: Vec<u8> = (0..SECRET_SIZE_BYTES).map(|_| rng.random()).collect();
150 let algo = TotpAlgo::Sha256;
151 let digits = TotpDigits::Six;
152 Totp {
153 secret,
154 step,
155 algo,
156 digits,
157 }
158 }
159
160 pub(crate) fn to_dbtotpv1(&self) -> DbTotpV1 {
161 DbTotpV1 {
162 label: "totp".to_string(),
163 key: self.secret.clone(),
164 step: self.step,
165 algo: match self.algo {
166 TotpAlgo::Sha1 => DbTotpAlgoV1::S1,
167 TotpAlgo::Sha256 => DbTotpAlgoV1::S256,
168 TotpAlgo::Sha512 => DbTotpAlgoV1::S512,
169 },
170 digits: Some(self.digits.into()),
171 }
172 }
173
174 fn digest(&self, counter: u64) -> Result<u32, TotpError> {
175 let hmac = self.algo.digest(&self.secret, counter)?;
176 let offset = hmac
179 .last()
180 .map(|v| (v & 0xf) as usize)
181 .ok_or(TotpError::HmacError)?;
182
183 #[allow(clippy::indexing_slicing)]
191 let bytes: [u8; 4] = hmac[offset..offset + 4]
192 .try_into()
193 .map_err(|_| TotpError::HmacError)?;
194
195 let otp = u32::from_be_bytes(bytes);
196 Ok((otp & 0x7fff_ffff) % (self.digits as u32))
202 }
203
204 pub fn do_totp_duration_from_epoch(&self, time: &Duration) -> Result<u32, TotpError> {
205 let secs = time.as_secs();
206 let counter = secs / self.step;
208 self.digest(counter)
209 }
210
211 pub fn do_totp(&self, time: &SystemTime) -> Result<u32, TotpError> {
212 let dur = time
213 .duration_since(SystemTime::UNIX_EPOCH)
214 .map_err(|_| TotpError::TimeError)?;
215 self.do_totp_duration_from_epoch(&dur)
216 }
217
218 pub fn verify(&self, chal: u32, time: Duration) -> bool {
219 let secs = time.as_secs();
220 let counter = secs / self.step;
221 self.digest(counter).map(|v1| v1 == chal).unwrap_or(false)
223 || self
224 .digest(counter - 1)
225 .map(|v2| v2 == chal)
226 .unwrap_or(false)
227 }
228
229 pub fn to_proto(&self, accountname: &str, issuer: &str) -> ProtoTotp {
230 ProtoTotp {
231 accountname: accountname.to_string(),
232 issuer: issuer.to_string(),
233 secret: self.secret.clone(),
234 step: self.step,
235 algo: match self.algo {
236 TotpAlgo::Sha1 => ProtoTotpAlgo::Sha1,
237 TotpAlgo::Sha256 => ProtoTotpAlgo::Sha256,
238 TotpAlgo::Sha512 => ProtoTotpAlgo::Sha512,
239 },
240 digits: self.digits.into(),
241 }
242 }
243
244 pub fn is_legacy_algo(&self) -> bool {
245 matches!(&self.algo, TotpAlgo::Sha1)
246 }
247
248 pub fn downgrade_to_legacy(self) -> Self {
249 Totp {
250 secret: self.secret,
251 step: self.step,
252 algo: TotpAlgo::Sha1,
253 digits: self.digits,
254 }
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use std::time::Duration;
261
262 use crate::credential::totp::{Totp, TotpAlgo, TotpDigits, TotpError, TOTP_DEFAULT_STEP};
263
264 #[test]
265 fn hotp_basic() {
266 let otp_sha1 = Totp::new(vec![0], 30, TotpAlgo::Sha1, TotpDigits::Six);
267 assert_eq!(otp_sha1.digest(0), Ok(328482));
268 let otp_sha256 = Totp::new(vec![0], 30, TotpAlgo::Sha256, TotpDigits::Six);
269 assert_eq!(otp_sha256.digest(0), Ok(356306));
270 let otp_sha512 = Totp::new(vec![0], 30, TotpAlgo::Sha512, TotpDigits::Six);
271 assert_eq!(otp_sha512.digest(0), Ok(674061));
272 }
273
274 fn do_test(
275 key: &[u8],
276 algo: TotpAlgo,
277 secs: u64,
278 step: u64,
279 digits: TotpDigits,
280 expect: &Result<u32, TotpError>,
281 ) {
282 let otp = Totp::new(key.to_vec(), step, algo, digits);
283 let d = Duration::from_secs(secs);
284 let r = otp.do_totp_duration_from_epoch(&d);
285 debug!(
286 "key: {:?}, algo: {:?}, time: {:?}, step: {:?}, expect: {:?} == {:?}",
287 key, algo, secs, step, expect, r
288 );
289 assert_eq!(&r, expect);
290 }
291
292 #[test]
293 fn totp_sha1_vectors() {
294 do_test(
295 &[0x00, 0x00, 0x00, 0x00],
296 TotpAlgo::Sha1,
297 1585368920,
298 TOTP_DEFAULT_STEP,
299 TotpDigits::Six,
300 &Ok(728926),
301 );
302 do_test(
303 &[0x00, 0x00, 0x00, 0x00],
304 TotpAlgo::Sha1,
305 1585368920,
306 TOTP_DEFAULT_STEP,
307 TotpDigits::Eight,
308 &Ok(74728926),
309 );
310 do_test(
311 &[0x00, 0xaa, 0xbb, 0xcc],
312 TotpAlgo::Sha1,
313 1585369498,
314 TOTP_DEFAULT_STEP,
315 TotpDigits::Six,
316 &Ok(985074),
317 );
318 }
319
320 #[test]
321 fn totp_sha256_vectors() {
322 do_test(
323 &[0x00, 0x00, 0x00, 0x00],
324 TotpAlgo::Sha256,
325 1585369682,
326 TOTP_DEFAULT_STEP,
327 TotpDigits::Six,
328 &Ok(795483),
329 );
330 do_test(
331 &[0x00, 0x00, 0x00, 0x00],
332 TotpAlgo::Sha256,
333 1585369682,
334 TOTP_DEFAULT_STEP,
335 TotpDigits::Eight,
336 &Ok(11795483),
337 );
338 do_test(
339 &[0x00, 0xaa, 0xbb, 0xcc],
340 TotpAlgo::Sha256,
341 1585369689,
342 TOTP_DEFAULT_STEP,
343 TotpDigits::Six,
344 &Ok(728402),
345 );
346 }
347
348 #[test]
349 fn totp_sha512_vectors() {
350 do_test(
351 &[0x00, 0x00, 0x00, 0x00],
352 TotpAlgo::Sha512,
353 1585369775,
354 TOTP_DEFAULT_STEP,
355 TotpDigits::Six,
356 &Ok(587735),
357 );
358 do_test(
359 &[0x00, 0x00, 0x00, 0x00],
360 TotpAlgo::Sha512,
361 1585369775,
362 TOTP_DEFAULT_STEP,
363 TotpDigits::Eight,
364 &Ok(14587735),
365 );
366 do_test(
367 &[0x00, 0xaa, 0xbb, 0xcc],
368 TotpAlgo::Sha512,
369 1585369780,
370 TOTP_DEFAULT_STEP,
371 TotpDigits::Six,
372 &Ok(952181),
373 );
374 }
375
376 #[test]
377 fn totp_allow_one_previous() {
378 let key = vec![0x00, 0xaa, 0xbb, 0xcc];
379 let secs = 1585369780;
380 let otp = Totp::new(key, TOTP_DEFAULT_STEP, TotpAlgo::Sha512, TotpDigits::Six);
381 let d = Duration::from_secs(secs);
382 assert!(otp.verify(952181, d));
384 assert!(otp.verify(685469, d));
386 assert!(!otp.verify(217213, d));
388 assert!(!otp.verify(972806, d));
390 }
391}