kanidmd_lib/credential/
mod.rs

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