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