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
183        // This is based on "dynamic truncation" where the offset into
184        // the hmac is dynamic based on the last byte of the hmac output.
185        // Since the array is u8, and we & with 0x0F, the value of offset
186        // must be in the range 0 to 15. All hmac outputs are 20 bytes
187        // or greater, so 15 + 4 == 19 as the upper bound will always
188        // be within the bounds of the hmac array.
189        // As a result, this is safe to slice.
190        #[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        // Treat as a u31, this masks the first bit.
197        // then modulo based on the number of digits requested.
198        // * For 6 digits modulo 1_000_000
199        // * For 8 digits modulo 100_000_000
200        // Based on this 9 is max digits.
201        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        // do the window calculation
207        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        // Any error becomes a failure.
222        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        // Step
383        assert!(otp.verify(952181, d));
384        // Step - 1
385        assert!(otp.verify(685469, d));
386        // This is step - 2
387        assert!(!otp.verify(217213, d));
388        // This is step + 1
389        assert!(!otp.verify(972806, d));
390    }
391}