kanidmd_lib/credential/
totp.rs

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
12// Update to match advice that totp hmac key should be the same
13// number of bytes as the output.
14const 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/// <https://tools.ietf.org/html/rfc6238> which relies on <https://tools.ietf.org/html/rfc4226>
90#[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        // Default.
108        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    // Create a new token with secure key and algo.
147    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        // Now take the hmac and encode it as hotp expects.
177        // https://tools.ietf.org/html/rfc4226#page-7
178        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        // Treat as a u31, this masks the first bit.
188        // then modulo based on the number of digits requested.
189        // * For 6 digits modulo 1_000_000
190        // * For 8 digits modulo 100_000_000
191        // Based on this 9 is max digits.
192        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        // do the window calculation
198        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        // Any error becomes a failure.
213        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        // Step
374        assert!(otp.verify(952181, d));
375        // Step - 1
376        assert!(otp.verify(685469, d));
377        // This is step - 2
378        assert!(!otp.verify(217213, d));
379        // This is step + 1
380        assert!(!otp.verify(972806, d));
381    }
382}