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 let bytes: [u8; 4] = hmac[offset..offset + 4]
183 .try_into()
184 .map_err(|_| TotpError::HmacError)?;
185
186 let otp = u32::from_be_bytes(bytes);
187 Ok((otp & 0x7fff_ffff) % (self.digits as u32))
193 }
194
195 pub fn do_totp_duration_from_epoch(&self, time: &Duration) -> Result<u32, TotpError> {
196 let secs = time.as_secs();
197 let counter = secs / self.step;
199 self.digest(counter)
200 }
201
202 pub fn do_totp(&self, time: &SystemTime) -> Result<u32, TotpError> {
203 let dur = time
204 .duration_since(SystemTime::UNIX_EPOCH)
205 .map_err(|_| TotpError::TimeError)?;
206 self.do_totp_duration_from_epoch(&dur)
207 }
208
209 pub fn verify(&self, chal: u32, time: Duration) -> bool {
210 let secs = time.as_secs();
211 let counter = secs / self.step;
212 self.digest(counter).map(|v1| v1 == chal).unwrap_or(false)
214 || self
215 .digest(counter - 1)
216 .map(|v2| v2 == chal)
217 .unwrap_or(false)
218 }
219
220 pub fn to_proto(&self, accountname: &str, issuer: &str) -> ProtoTotp {
221 ProtoTotp {
222 accountname: accountname.to_string(),
223 issuer: issuer.to_string(),
224 secret: self.secret.clone(),
225 step: self.step,
226 algo: match self.algo {
227 TotpAlgo::Sha1 => ProtoTotpAlgo::Sha1,
228 TotpAlgo::Sha256 => ProtoTotpAlgo::Sha256,
229 TotpAlgo::Sha512 => ProtoTotpAlgo::Sha512,
230 },
231 digits: self.digits.into(),
232 }
233 }
234
235 pub fn is_legacy_algo(&self) -> bool {
236 matches!(&self.algo, TotpAlgo::Sha1)
237 }
238
239 pub fn downgrade_to_legacy(self) -> Self {
240 Totp {
241 secret: self.secret,
242 step: self.step,
243 algo: TotpAlgo::Sha1,
244 digits: self.digits,
245 }
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use std::time::Duration;
252
253 use crate::credential::totp::{Totp, TotpAlgo, TotpDigits, TotpError, TOTP_DEFAULT_STEP};
254
255 #[test]
256 fn hotp_basic() {
257 let otp_sha1 = Totp::new(vec![0], 30, TotpAlgo::Sha1, TotpDigits::Six);
258 assert_eq!(otp_sha1.digest(0), Ok(328482));
259 let otp_sha256 = Totp::new(vec![0], 30, TotpAlgo::Sha256, TotpDigits::Six);
260 assert_eq!(otp_sha256.digest(0), Ok(356306));
261 let otp_sha512 = Totp::new(vec![0], 30, TotpAlgo::Sha512, TotpDigits::Six);
262 assert_eq!(otp_sha512.digest(0), Ok(674061));
263 }
264
265 fn do_test(
266 key: &[u8],
267 algo: TotpAlgo,
268 secs: u64,
269 step: u64,
270 digits: TotpDigits,
271 expect: &Result<u32, TotpError>,
272 ) {
273 let otp = Totp::new(key.to_vec(), step, algo, digits);
274 let d = Duration::from_secs(secs);
275 let r = otp.do_totp_duration_from_epoch(&d);
276 debug!(
277 "key: {:?}, algo: {:?}, time: {:?}, step: {:?}, expect: {:?} == {:?}",
278 key, algo, secs, step, expect, r
279 );
280 assert_eq!(&r, expect);
281 }
282
283 #[test]
284 fn totp_sha1_vectors() {
285 do_test(
286 &[0x00, 0x00, 0x00, 0x00],
287 TotpAlgo::Sha1,
288 1585368920,
289 TOTP_DEFAULT_STEP,
290 TotpDigits::Six,
291 &Ok(728926),
292 );
293 do_test(
294 &[0x00, 0x00, 0x00, 0x00],
295 TotpAlgo::Sha1,
296 1585368920,
297 TOTP_DEFAULT_STEP,
298 TotpDigits::Eight,
299 &Ok(74728926),
300 );
301 do_test(
302 &[0x00, 0xaa, 0xbb, 0xcc],
303 TotpAlgo::Sha1,
304 1585369498,
305 TOTP_DEFAULT_STEP,
306 TotpDigits::Six,
307 &Ok(985074),
308 );
309 }
310
311 #[test]
312 fn totp_sha256_vectors() {
313 do_test(
314 &[0x00, 0x00, 0x00, 0x00],
315 TotpAlgo::Sha256,
316 1585369682,
317 TOTP_DEFAULT_STEP,
318 TotpDigits::Six,
319 &Ok(795483),
320 );
321 do_test(
322 &[0x00, 0x00, 0x00, 0x00],
323 TotpAlgo::Sha256,
324 1585369682,
325 TOTP_DEFAULT_STEP,
326 TotpDigits::Eight,
327 &Ok(11795483),
328 );
329 do_test(
330 &[0x00, 0xaa, 0xbb, 0xcc],
331 TotpAlgo::Sha256,
332 1585369689,
333 TOTP_DEFAULT_STEP,
334 TotpDigits::Six,
335 &Ok(728402),
336 );
337 }
338
339 #[test]
340 fn totp_sha512_vectors() {
341 do_test(
342 &[0x00, 0x00, 0x00, 0x00],
343 TotpAlgo::Sha512,
344 1585369775,
345 TOTP_DEFAULT_STEP,
346 TotpDigits::Six,
347 &Ok(587735),
348 );
349 do_test(
350 &[0x00, 0x00, 0x00, 0x00],
351 TotpAlgo::Sha512,
352 1585369775,
353 TOTP_DEFAULT_STEP,
354 TotpDigits::Eight,
355 &Ok(14587735),
356 );
357 do_test(
358 &[0x00, 0xaa, 0xbb, 0xcc],
359 TotpAlgo::Sha512,
360 1585369780,
361 TOTP_DEFAULT_STEP,
362 TotpDigits::Six,
363 &Ok(952181),
364 );
365 }
366
367 #[test]
368 fn totp_allow_one_previous() {
369 let key = vec![0x00, 0xaa, 0xbb, 0xcc];
370 let secs = 1585369780;
371 let otp = Totp::new(key, TOTP_DEFAULT_STEP, TotpAlgo::Sha512, TotpDigits::Six);
372 let d = Duration::from_secs(secs);
373 assert!(otp.verify(952181, d));
375 assert!(otp.verify(685469, d));
377 assert!(!otp.verify(217213, d));
379 assert!(!otp.verify(972806, d));
381 }
382}