kanidmd_lib/credential/
mod.rs

1use std::convert::TryFrom;
2
3use hashbrown::{HashMap as Map, HashSet};
4use kanidm_proto::internal::{CredentialDetail, CredentialDetailType, OperationError};
5use uuid::Uuid;
6use webauthn_rs::prelude::{AuthenticationResult, Passkey, SecurityKey};
7use webauthn_rs_core::proto::{Credential as WebauthnCredential, CredentialV3};
8
9use crate::be::dbvalue::{DbBackupCodeV1, DbCred};
10
11pub mod apppwd;
12pub mod softlock;
13pub mod totp;
14
15use self::totp::TOTP_DEFAULT_STEP;
16
17use kanidm_lib_crypto::CryptoPolicy;
18
19use crate::credential::softlock::CredSoftLockPolicy;
20use crate::credential::totp::Totp;
21
22// These are in order of "relative" strength.
23/*
24#[derive(Clone, Debug)]
25pub enum Policy {
26    PasswordOnly,
27    WebauthnOnly,
28    GeneratedPassword,
29    PasswordAndWebauthn,
30}
31*/
32
33pub use kanidm_lib_crypto::Password;
34
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct BackupCodes {
37    code_set: HashSet<String>,
38}
39
40impl TryFrom<DbBackupCodeV1> for BackupCodes {
41    type Error = ();
42
43    fn try_from(value: DbBackupCodeV1) -> Result<Self, Self::Error> {
44        Ok(BackupCodes {
45            code_set: value.code_set,
46        })
47    }
48}
49
50impl BackupCodes {
51    pub fn new(code_set: HashSet<String>) -> Self {
52        BackupCodes { code_set }
53    }
54
55    pub fn verify(&self, code_chal: &str) -> bool {
56        self.code_set.contains(code_chal)
57    }
58
59    pub fn remove(&mut self, code_chal: &str) -> bool {
60        self.code_set.remove(code_chal)
61    }
62
63    pub fn to_dbbackupcodev1(&self) -> DbBackupCodeV1 {
64        DbBackupCodeV1 {
65            code_set: self.code_set.clone(),
66        }
67    }
68}
69
70#[derive(Clone, Debug, PartialEq)]
71/// This is how we store credentials in the server. An account can have many credentials, and
72/// a credential can have many factors. Only successful auth to a credential as a whole unit
73/// will succeed. For example:
74/// A: Credential { password: aaa }
75/// B: Credential { password: bbb, otp: ... }
76/// In this case, if we selected credential B, and then provided password "aaa" we would deny
77/// the auth as the password of B was incorrect. Additionally, while A only needs the "password",
78/// B requires both the password and otp to be valid.
79///
80/// In this way, each Credential provides its own password requirements and policy, and requires
81/// some metadata to support this such as it's source and strength etc.
82pub struct Credential {
83    // policy: Policy,
84    pub(crate) type_: CredentialType,
85    // Uuid of Credential, used by auth session to lock this specific credential
86    // if required.
87    pub(crate) uuid: Uuid,
88    // TODO #59: Add auth policy IE validUntil, lock state ...
89    // locked: bool
90}
91
92#[derive(Clone, Debug, PartialEq)]
93/// The type of credential that is stored. Each of these represents a full set of 'what is required'
94/// to complete an authentication session. The reason to have these typed like this is so we can
95/// apply policy later to what classes or levels of credentials can be used. We use these types
96/// to also know what type of auth session handler to initiate.
97pub enum CredentialType {
98    // Anonymous,
99    Password(Password),
100    GeneratedPassword(Password),
101    PasswordMfa(
102        Password,
103        Map<String, Totp>,
104        Map<String, SecurityKey>,
105        Option<BackupCodes>,
106    ),
107    Webauthn(Map<String, Passkey>),
108}
109
110impl From<&Credential> for CredentialDetail {
111    fn from(value: &Credential) -> Self {
112        CredentialDetail {
113            uuid: value.uuid,
114            type_: match &value.type_ {
115                CredentialType::Password(_) => CredentialDetailType::Password,
116                CredentialType::GeneratedPassword(_) => CredentialDetailType::GeneratedPassword,
117                CredentialType::Webauthn(wan) => {
118                    let labels: Vec<_> = wan.keys().cloned().collect();
119                    CredentialDetailType::Passkey(labels)
120                }
121                CredentialType::PasswordMfa(_, totp, wan, backup_code) => {
122                    // Don't sort - we need these in order to match to what the user
123                    // sees so they can remove by index.
124                    let wan_labels: Vec<_> = wan.keys().cloned().collect();
125                    let totp_labels: Vec<_> = totp.keys().cloned().collect();
126
127                    CredentialDetailType::PasswordMfa(
128                        totp_labels,
129                        wan_labels,
130                        backup_code.as_ref().map(|c| c.code_set.len()).unwrap_or(0),
131                    )
132                }
133            },
134        }
135    }
136}
137
138impl TryFrom<DbCred> for Credential {
139    type Error = ();
140
141    fn try_from(value: DbCred) -> Result<Self, Self::Error> {
142        // Work out what the policy is?
143        match value {
144            DbCred::V2Password {
145                password: db_password,
146                uuid,
147            }
148            | DbCred::Pw {
149                password: Some(db_password),
150                webauthn: _,
151                totp: _,
152                backup_code: _,
153                claims: _,
154                uuid,
155            } => {
156                let v_password = Password::try_from(db_password)?;
157                let type_ = CredentialType::Password(v_password);
158                if type_.is_valid() {
159                    Ok(Credential { type_, uuid })
160                } else {
161                    Err(())
162                }
163            }
164            DbCred::V2GenPassword {
165                password: db_password,
166                uuid,
167            }
168            | DbCred::GPw {
169                password: Some(db_password),
170                webauthn: _,
171                totp: _,
172                backup_code: _,
173                claims: _,
174                uuid,
175            } => {
176                let v_password = Password::try_from(db_password)?;
177                let type_ = CredentialType::GeneratedPassword(v_password);
178                if type_.is_valid() {
179                    Ok(Credential { type_, uuid })
180                } else {
181                    Err(())
182                }
183            }
184            DbCred::PwMfa {
185                password: Some(db_password),
186                webauthn: maybe_db_webauthn,
187                totp,
188                backup_code,
189                claims: _,
190                uuid,
191            } => {
192                let v_password = Password::try_from(db_password)?;
193
194                let v_totp = match totp {
195                    Some(dbt) => {
196                        let l = "totp".to_string();
197                        let t = Totp::try_from(dbt)?;
198                        Map::from([(l, t)])
199                    }
200                    None => Map::default(),
201                };
202
203                let v_webauthn = match maybe_db_webauthn {
204                    Some(db_webauthn) => db_webauthn
205                        .into_iter()
206                        .map(|wc| {
207                            (
208                                wc.label,
209                                SecurityKey::from(WebauthnCredential::from(CredentialV3 {
210                                    cred_id: wc.id,
211                                    cred: wc.cred,
212                                    counter: wc.counter,
213                                    verified: wc.verified,
214                                    registration_policy: wc.registration_policy,
215                                })),
216                            )
217                        })
218                        .collect(),
219                    None => Default::default(),
220                };
221
222                let v_backup_code = match backup_code {
223                    Some(dbb) => Some(BackupCodes::try_from(dbb)?),
224                    None => None,
225                };
226
227                let type_ =
228                    CredentialType::PasswordMfa(v_password, v_totp, v_webauthn, v_backup_code);
229
230                if type_.is_valid() {
231                    Ok(Credential { type_, uuid })
232                } else {
233                    Err(())
234                }
235            }
236            DbCred::Wn {
237                password: _,
238                webauthn: Some(db_webauthn),
239                totp: _,
240                backup_code: _,
241                claims: _,
242                uuid,
243            } => {
244                let v_webauthn = db_webauthn
245                    .into_iter()
246                    .map(|wc| {
247                        (
248                            wc.label,
249                            Passkey::from(WebauthnCredential::from(CredentialV3 {
250                                cred_id: wc.id,
251                                cred: wc.cred,
252                                counter: wc.counter,
253                                verified: wc.verified,
254                                registration_policy: wc.registration_policy,
255                            })),
256                        )
257                    })
258                    .collect();
259
260                let type_ = CredentialType::Webauthn(v_webauthn);
261
262                if type_.is_valid() {
263                    Ok(Credential { type_, uuid })
264                } else {
265                    Err(())
266                }
267            }
268            DbCred::TmpWn {
269                webauthn: db_webauthn,
270                uuid,
271            } => {
272                let v_webauthn = db_webauthn.into_iter().collect();
273                let type_ = CredentialType::Webauthn(v_webauthn);
274
275                if type_.is_valid() {
276                    Ok(Credential { type_, uuid })
277                } else {
278                    Err(())
279                }
280            }
281            DbCred::V2PasswordMfa {
282                password: db_password,
283                totp: maybe_db_totp,
284                backup_code,
285                webauthn: db_webauthn,
286                uuid,
287            } => {
288                let v_password = Password::try_from(db_password)?;
289
290                let v_totp = match maybe_db_totp {
291                    Some(dbt) => {
292                        let l = "totp".to_string();
293                        let t = Totp::try_from(dbt)?;
294                        Map::from([(l, t)])
295                    }
296                    None => Map::default(),
297                };
298
299                let v_backup_code = match backup_code {
300                    Some(dbb) => Some(BackupCodes::try_from(dbb)?),
301                    None => None,
302                };
303
304                let v_webauthn = db_webauthn.into_iter().collect();
305
306                let type_ =
307                    CredentialType::PasswordMfa(v_password, v_totp, v_webauthn, v_backup_code);
308
309                if type_.is_valid() {
310                    Ok(Credential { type_, uuid })
311                } else {
312                    Err(())
313                }
314            }
315            DbCred::V3PasswordMfa {
316                password: db_password,
317                totp: db_totp,
318                backup_code,
319                webauthn: db_webauthn,
320                uuid,
321            } => {
322                let v_password = Password::try_from(db_password)?;
323
324                let v_totp = db_totp
325                    .into_iter()
326                    .map(|(l, dbt)| Totp::try_from(dbt).map(|t| (l, t)))
327                    .collect::<Result<Map<_, _>, _>>()?;
328
329                let v_backup_code = match backup_code {
330                    Some(dbb) => Some(BackupCodes::try_from(dbb)?),
331                    None => None,
332                };
333
334                let v_webauthn = db_webauthn.into_iter().collect();
335
336                let type_ =
337                    CredentialType::PasswordMfa(v_password, v_totp, v_webauthn, v_backup_code);
338
339                if type_.is_valid() {
340                    Ok(Credential { type_, uuid })
341                } else {
342                    Err(())
343                }
344            }
345            credential => {
346                error!("Database content may be corrupt - invalid credential state");
347                debug!(%credential);
348                debug!(?credential);
349                Err(())
350            }
351        }
352    }
353}
354
355impl Credential {
356    /// Create a new credential that contains a CredentialType::Password
357    pub fn new_password_only(
358        policy: &CryptoPolicy,
359        cleartext: &str,
360    ) -> Result<Self, OperationError> {
361        Password::new(policy, cleartext)
362            .map_err(|e| {
363                error!(crypto_err = ?e);
364                e.into()
365            })
366            .map(Self::new_from_password)
367    }
368
369    /// Create a new credential that contains a CredentialType::GeneratedPassword
370    pub fn new_generatedpassword_only(
371        policy: &CryptoPolicy,
372        cleartext: &str,
373    ) -> Result<Self, OperationError> {
374        Password::new(policy, cleartext)
375            .map_err(|e| {
376                error!(crypto_err = ?e);
377                e.into()
378            })
379            .map(Self::new_from_generatedpassword)
380    }
381
382    /// Update the state of the Password on this credential, if a password is present. If possible
383    /// this will convert the credential to a PasswordMFA in some cases, or fail in others.
384    pub fn set_password(
385        &self,
386        policy: &CryptoPolicy,
387        cleartext: &str,
388    ) -> Result<Self, OperationError> {
389        Password::new(policy, cleartext)
390            .map_err(|e| {
391                error!(crypto_err = ?e);
392                e.into()
393            })
394            .map(|pw| self.update_password(pw))
395    }
396
397    pub fn upgrade_password(
398        &self,
399        policy: &CryptoPolicy,
400        cleartext: &str,
401    ) -> Result<Option<Self>, OperationError> {
402        let valid = self.password_ref().and_then(|pw| {
403            pw.verify(cleartext).map_err(|e| {
404                error!(crypto_err = ?e);
405                e.into()
406            })
407        })?;
408
409        if valid {
410            let pw = Password::new(policy, cleartext).map_err(|e| {
411                error!(crypto_err = ?e);
412                e.into()
413            })?;
414
415            // Note, during update_password we normally rotate the uuid, here we
416            // set it back to our current value. This is because we are just
417            // updating the hash value, not actually changing the password itself.
418            let mut cred = self.update_password(pw);
419            cred.uuid = self.uuid;
420
421            Ok(Some(cred))
422        } else {
423            // No updates needed, password has changed.
424            Ok(None)
425        }
426    }
427
428    /// Extend this credential with another alternate webauthn credential. This is especially
429    /// useful for `PasswordMfa` where you can have many webauthn credentials and a password
430    /// generally so that one is a backup.
431    pub fn append_securitykey(
432        &self,
433        label: String,
434        cred: SecurityKey,
435    ) -> Result<Self, OperationError> {
436        let type_ = match &self.type_ {
437            CredentialType::Password(pw) | CredentialType::GeneratedPassword(pw) => {
438                let mut wan = Map::new();
439                wan.insert(label, cred);
440                CredentialType::PasswordMfa(pw.clone(), Map::default(), wan, None)
441            }
442            CredentialType::PasswordMfa(pw, totp, map, backup_code) => {
443                let mut nmap = map.clone();
444                if nmap.insert(label.clone(), cred).is_some() {
445                    return Err(OperationError::InvalidAttribute(format!(
446                        "Webauthn label '{label:?}' already exists"
447                    )));
448                }
449                CredentialType::PasswordMfa(pw.clone(), totp.clone(), nmap, backup_code.clone())
450            }
451            // Ignore
452            CredentialType::Webauthn(map) => CredentialType::Webauthn(map.clone()),
453        };
454
455        // Check stuff
456        Ok(Credential {
457            type_,
458            // Rotate the credential id on any change to invalidate sessions.
459            uuid: Uuid::new_v4(),
460        })
461    }
462
463    /// Remove a webauthn token identified by `label` from this Credential.
464    pub fn remove_securitykey(&self, label: &str) -> Result<Self, OperationError> {
465        let type_ = match &self.type_ {
466            CredentialType::Password(_)
467            | CredentialType::GeneratedPassword(_)
468            | CredentialType::Webauthn(_) => {
469                return Err(OperationError::InvalidAttribute(
470                    "SecurityKey is not present on this credential".to_string(),
471                ));
472            }
473            CredentialType::PasswordMfa(pw, totp, map, backup_code) => {
474                let mut nmap = map.clone();
475                if nmap.remove(label).is_none() {
476                    return Err(OperationError::InvalidAttribute(format!(
477                        "Removing Webauthn token with label '{label:?}': does not exist"
478                    )));
479                }
480                if nmap.is_empty() {
481                    if !totp.is_empty() {
482                        CredentialType::PasswordMfa(
483                            pw.clone(),
484                            totp.clone(),
485                            nmap,
486                            backup_code.clone(),
487                        )
488                    } else {
489                        // Note: No need to keep backup code if it is no longer MFA
490                        CredentialType::Password(pw.clone())
491                    }
492                } else {
493                    CredentialType::PasswordMfa(pw.clone(), totp.clone(), nmap, backup_code.clone())
494                }
495            }
496        };
497
498        // Check stuff
499        Ok(Credential {
500            type_,
501            // Rotate the credential id on any change to invalidate sessions.
502            uuid: Uuid::new_v4(),
503        })
504    }
505
506    #[allow(clippy::ptr_arg)]
507    /// After a successful authentication with Webauthn, we need to advance the credentials
508    /// counter value to prevent certain classes of replay attacks.
509    pub fn update_webauthn_properties(
510        &self,
511        auth_result: &AuthenticationResult,
512    ) -> Result<Option<Self>, OperationError> {
513        let type_ = match &self.type_ {
514            CredentialType::Password(_pw) | CredentialType::GeneratedPassword(_pw) => {
515                // Should not be possible!
516                // -- this does occur when we have mixed pw/passkey
517                // and we need to do an update, so we just mask this no Ok(None).
518                // return Err(OperationError::InvalidState);
519                return Ok(None);
520            }
521            CredentialType::Webauthn(map) => {
522                let mut nmap = map.clone();
523                nmap.values_mut().for_each(|pk| {
524                    pk.update_credential(auth_result);
525                });
526                CredentialType::Webauthn(nmap)
527            }
528            CredentialType::PasswordMfa(pw, totp, map, backup_code) => {
529                let mut nmap = map.clone();
530                nmap.values_mut().for_each(|sk| {
531                    sk.update_credential(auth_result);
532                });
533                CredentialType::PasswordMfa(pw.clone(), totp.clone(), nmap, backup_code.clone())
534            }
535        };
536
537        Ok(Some(Credential {
538            type_,
539            // Rotate the credential id on any change to invalidate sessions.
540            uuid: Uuid::new_v4(),
541        }))
542    }
543
544    pub(crate) fn has_securitykey(&self) -> bool {
545        match &self.type_ {
546            CredentialType::PasswordMfa(_, _, map, _) => !map.is_empty(),
547            _ => false,
548        }
549    }
550
551    /// Get a reference to the contained webuthn credentials, if any.
552    pub fn securitykey_ref(&self) -> Result<&Map<String, SecurityKey>, OperationError> {
553        match &self.type_ {
554            CredentialType::Webauthn(_)
555            | CredentialType::Password(_)
556            | CredentialType::GeneratedPassword(_) => Err(OperationError::InvalidAccountState(
557                "non-webauthn cred type?".to_string(),
558            )),
559            CredentialType::PasswordMfa(_, _, map, _) => Ok(map),
560        }
561    }
562
563    pub fn passkey_ref(&self) -> Result<&Map<String, Passkey>, OperationError> {
564        match &self.type_ {
565            CredentialType::PasswordMfa(_, _, _, _)
566            | CredentialType::Password(_)
567            | CredentialType::GeneratedPassword(_) => Err(OperationError::InvalidAccountState(
568                "non-webauthn cred type?".to_string(),
569            )),
570            CredentialType::Webauthn(map) => Ok(map),
571        }
572    }
573
574    /// Get a reference to the contained password, if any.
575    pub fn password_ref(&self) -> Result<&Password, OperationError> {
576        match &self.type_ {
577            CredentialType::Password(pw)
578            | CredentialType::GeneratedPassword(pw)
579            | CredentialType::PasswordMfa(pw, _, _, _) => Ok(pw),
580            CredentialType::Webauthn(_) => Err(OperationError::InvalidAccountState(
581                "non-password cred type?".to_string(),
582            )),
583        }
584    }
585
586    pub fn is_mfa(&self) -> bool {
587        match &self.type_ {
588            CredentialType::Password(_) | CredentialType::GeneratedPassword(_) => false,
589            CredentialType::PasswordMfa(..) | CredentialType::Webauthn(_) => true,
590        }
591    }
592
593    #[cfg(test)]
594    pub fn verify_password(&self, cleartext: &str) -> Result<bool, OperationError> {
595        self.password_ref().and_then(|pw| {
596            pw.verify(cleartext).map_err(|e| {
597                error!(crypto_err = ?e);
598                e.into()
599            })
600        })
601    }
602
603    /// Extract this credential into it's Serialisable Database form, ready for persistence.
604    pub fn to_db_valuev1(&self) -> DbCred {
605        let uuid = self.uuid;
606        match &self.type_ {
607            CredentialType::Password(pw) => DbCred::V2Password {
608                password: pw.to_dbpasswordv1(),
609                uuid,
610            },
611            CredentialType::GeneratedPassword(pw) => DbCred::V2GenPassword {
612                password: pw.to_dbpasswordv1(),
613                uuid,
614            },
615            CredentialType::PasswordMfa(pw, totp, map, backup_code) => DbCred::V3PasswordMfa {
616                password: pw.to_dbpasswordv1(),
617                totp: totp
618                    .iter()
619                    .map(|(l, t)| (l.clone(), t.to_dbtotpv1()))
620                    .collect(),
621                backup_code: backup_code.as_ref().map(|b| b.to_dbbackupcodev1()),
622                webauthn: map.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
623                uuid,
624            },
625            CredentialType::Webauthn(map) => DbCred::TmpWn {
626                webauthn: map.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
627                uuid,
628            },
629        }
630    }
631
632    pub(crate) fn update_password(&self, pw: Password) -> Self {
633        let type_ = match &self.type_ {
634            CredentialType::Password(_) | CredentialType::GeneratedPassword(_) => {
635                CredentialType::Password(pw)
636            }
637            CredentialType::PasswordMfa(_, totp, wan, backup_code) => {
638                CredentialType::PasswordMfa(pw, totp.clone(), wan.clone(), backup_code.clone())
639            }
640            // Ignore
641            CredentialType::Webauthn(wan) => CredentialType::Webauthn(wan.clone()),
642        };
643        Credential {
644            type_,
645            // Rotate the credential id on any change to invalidate sessions.
646            uuid: Uuid::new_v4(),
647        }
648    }
649
650    // We don't make totp accessible from outside the crate for now.
651    pub(crate) fn append_totp(&self, label: String, totp: Totp) -> Self {
652        let type_ = match &self.type_ {
653            CredentialType::Password(pw) | CredentialType::GeneratedPassword(pw) => {
654                CredentialType::PasswordMfa(
655                    pw.clone(),
656                    Map::from([(label, totp)]),
657                    Map::new(),
658                    None,
659                )
660            }
661            CredentialType::PasswordMfa(pw, totps, wan, backup_code) => {
662                let mut totps = totps.clone();
663                let replaced = totps.insert(label, totp).is_none();
664                debug_assert!(replaced);
665
666                CredentialType::PasswordMfa(pw.clone(), totps, wan.clone(), backup_code.clone())
667            }
668            CredentialType::Webauthn(wan) => {
669                debug_assert!(false);
670                CredentialType::Webauthn(wan.clone())
671            }
672        };
673        Credential {
674            type_,
675            // Rotate the credential id on any change to invalidate sessions.
676            uuid: Uuid::new_v4(),
677        }
678    }
679
680    pub(crate) fn remove_totp(&self, label: &str) -> Self {
681        let type_ = match &self.type_ {
682            CredentialType::PasswordMfa(pw, totp, wan, backup_code) => {
683                let mut totp = totp.clone();
684                let removed = totp.remove(label).is_some();
685                debug_assert!(removed);
686
687                if wan.is_empty() && totp.is_empty() {
688                    // Note: No need to keep backup code if it is no longer MFA
689                    CredentialType::Password(pw.clone())
690                } else {
691                    CredentialType::PasswordMfa(pw.clone(), totp, wan.clone(), backup_code.clone())
692                }
693            }
694            _ => self.type_.clone(),
695        };
696        Credential {
697            type_,
698            // Rotate the credential id on any change to invalidate sessions.
699            uuid: Uuid::new_v4(),
700        }
701    }
702
703    pub(crate) fn has_totp_by_name(&self, label: &str) -> bool {
704        match &self.type_ {
705            CredentialType::PasswordMfa(_, totp, _, _) => totp.contains_key(label),
706            _ => false,
707        }
708    }
709
710    pub(crate) fn new_from_generatedpassword(pw: Password) -> Self {
711        Credential {
712            type_: CredentialType::GeneratedPassword(pw),
713            uuid: Uuid::new_v4(),
714        }
715    }
716
717    pub(crate) fn new_from_password(pw: Password) -> Self {
718        Credential {
719            type_: CredentialType::Password(pw),
720            uuid: Uuid::new_v4(),
721        }
722    }
723
724    pub(crate) fn softlock_policy(&self) -> CredSoftLockPolicy {
725        match &self.type_ {
726            CredentialType::Password(_pw) | CredentialType::GeneratedPassword(_pw) => {
727                CredSoftLockPolicy::Password
728            }
729            CredentialType::PasswordMfa(_pw, totp, wan, _) => {
730                // For backup code, use totp/wan policy (whatever is available)
731                if !totp.is_empty() {
732                    // What's the min step?
733                    let min_step = totp
734                        .iter()
735                        .map(|(_, t)| t.step)
736                        .min()
737                        .unwrap_or(TOTP_DEFAULT_STEP);
738                    CredSoftLockPolicy::Totp(min_step)
739                } else if !wan.is_empty() {
740                    CredSoftLockPolicy::Webauthn
741                } else {
742                    CredSoftLockPolicy::Password
743                }
744            }
745            CredentialType::Webauthn(_wan) => CredSoftLockPolicy::Webauthn,
746        }
747    }
748
749    pub(crate) fn update_backup_code(
750        &self,
751        backup_codes: BackupCodes,
752    ) -> Result<Self, OperationError> {
753        match &self.type_ {
754            CredentialType::PasswordMfa(pw, totp, wan, _) => Ok(Credential {
755                type_: CredentialType::PasswordMfa(
756                    pw.clone(),
757                    totp.clone(),
758                    wan.clone(),
759                    Some(backup_codes),
760                ),
761                // Rotate the credential id on any change to invalidate sessions.
762                uuid: Uuid::new_v4(),
763            }),
764            _ => Err(OperationError::InvalidAccountState(
765                "Non-MFA credential type".to_string(),
766            )),
767        }
768    }
769
770    pub(crate) fn invalidate_backup_code(
771        self,
772        code_to_remove: &str,
773    ) -> Result<Self, OperationError> {
774        match self.type_ {
775            CredentialType::PasswordMfa(pw, totp, wan, opt_backup_codes) => {
776                match opt_backup_codes {
777                    Some(mut backup_codes) => {
778                        backup_codes.remove(code_to_remove);
779                        Ok(Credential {
780                            type_: CredentialType::PasswordMfa(pw, totp, wan, Some(backup_codes)),
781                            // Don't rotate uuid here since this is a consumption of a backup
782                            // code.
783                            uuid: self.uuid,
784                        })
785                    }
786                    _ => Err(OperationError::InvalidAccountState(
787                        "backup code does not exist".to_string(),
788                    )),
789                }
790            }
791            _ => Err(OperationError::InvalidAccountState(
792                "Non-MFA credential type".to_string(),
793            )),
794        }
795    }
796
797    pub(crate) fn remove_backup_code(&self) -> Result<Self, OperationError> {
798        match &self.type_ {
799            CredentialType::PasswordMfa(pw, totp, wan, _) => Ok(Credential {
800                type_: CredentialType::PasswordMfa(pw.clone(), totp.clone(), wan.clone(), None),
801                // Rotate the credential id on any change to invalidate sessions.
802                uuid: Uuid::new_v4(),
803            }),
804            _ => Err(OperationError::InvalidAccountState(
805                "Non-MFA credential type".to_string(),
806            )),
807        }
808    }
809}
810
811impl CredentialType {
812    fn is_valid(&self) -> bool {
813        match self {
814            CredentialType::Password(_) | CredentialType::GeneratedPassword(_) => true,
815            CredentialType::PasswordMfa(_, m_totp, webauthn, _) => {
816                !m_totp.is_empty() || !webauthn.is_empty() // ignore backup code (it should only be a complement for totp/webauth)
817            }
818            CredentialType::Webauthn(webauthn) => !webauthn.is_empty(),
819        }
820    }
821}