1use std::convert::TryFrom;
2
3use hashbrown::{HashMap as Map, HashSet};
4use kanidm_proto::internal::{CredentialDetail, CredentialDetailType, OperationError};
5use time::OffsetDateTime;
6use uuid::Uuid;
7use webauthn_rs::prelude::{AuthenticationResult, Passkey, SecurityKey};
8use webauthn_rs_core::proto::{Credential as WebauthnCredential, CredentialV3};
9
10use crate::be::dbvalue::{DbBackupCodeV1, DbCred};
11
12pub mod apppwd;
13pub mod softlock;
14pub mod totp;
15
16use self::totp::TOTP_DEFAULT_STEP;
17
18use kanidm_lib_crypto::CryptoPolicy;
19
20use crate::credential::softlock::CredSoftLockPolicy;
21use crate::credential::totp::Totp;
22
23pub use kanidm_lib_crypto::Password;
35
36#[derive(Clone, Debug, PartialEq, Eq)]
37pub struct BackupCodes {
38 code_set: HashSet<String>,
39}
40
41impl TryFrom<DbBackupCodeV1> for BackupCodes {
42 type Error = ();
43
44 fn try_from(value: DbBackupCodeV1) -> Result<Self, Self::Error> {
45 Ok(BackupCodes {
46 code_set: value.code_set,
47 })
48 }
49}
50
51impl BackupCodes {
52 pub fn new(code_set: HashSet<String>) -> Self {
53 BackupCodes { code_set }
54 }
55
56 pub fn verify(&self, code_chal: &str) -> bool {
57 self.code_set.contains(code_chal)
58 }
59
60 pub fn remove(&mut self, code_chal: &str) -> bool {
61 self.code_set.remove(code_chal)
62 }
63
64 pub fn to_dbbackupcodev1(&self) -> DbBackupCodeV1 {
65 DbBackupCodeV1 {
66 code_set: self.code_set.clone(),
67 }
68 }
69}
70
71#[derive(Clone, Debug, PartialEq)]
72pub struct Credential {
84 pub(crate) type_: CredentialType,
86 pub(crate) uuid: Uuid,
89 timestamp: OffsetDateTime,
92}
93
94#[derive(Clone, Debug, PartialEq)]
95pub enum CredentialType {
100 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 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 let timestamp = value.last_changed_timestamp();
147
148 match value {
150 DbCred::V2Password {
151 password: db_password,
152 uuid,
153 ..
154 }
155 | DbCred::Pw {
156 password: Some(db_password),
157 webauthn: _,
158 totp: _,
159 backup_code: _,
160 claims: _,
161 uuid,
162 } => {
163 let v_password = Password::try_from(db_password)?;
164 let type_ = CredentialType::Password(v_password);
165 if type_.is_valid() {
166 Ok(Credential {
167 type_,
168 uuid,
169 timestamp,
170 })
171 } else {
172 Err(())
173 }
174 }
175 DbCred::V2GenPassword {
176 password: db_password,
177 uuid,
178 ..
179 }
180 | DbCred::GPw {
181 password: Some(db_password),
182 webauthn: _,
183 totp: _,
184 backup_code: _,
185 claims: _,
186 uuid,
187 } => {
188 let v_password = Password::try_from(db_password)?;
189 let type_ = CredentialType::GeneratedPassword(v_password);
190 if type_.is_valid() {
191 Ok(Credential {
192 type_,
193 uuid,
194 timestamp,
195 })
196 } else {
197 Err(())
198 }
199 }
200 DbCred::PwMfa {
201 password: Some(db_password),
202 webauthn: maybe_db_webauthn,
203 totp,
204 backup_code,
205 claims: _,
206 uuid,
207 } => {
208 let v_password = Password::try_from(db_password)?;
209
210 let v_totp = match totp {
211 Some(dbt) => {
212 let l = "totp".to_string();
213 let t = Totp::try_from(dbt)?;
214 Map::from([(l, t)])
215 }
216 None => Map::default(),
217 };
218
219 let v_webauthn = match maybe_db_webauthn {
220 Some(db_webauthn) => db_webauthn
221 .into_iter()
222 .map(|wc| {
223 (
224 wc.label,
225 SecurityKey::from(WebauthnCredential::from(CredentialV3 {
226 cred_id: wc.id,
227 cred: wc.cred,
228 counter: wc.counter,
229 verified: wc.verified,
230 registration_policy: wc.registration_policy,
231 })),
232 )
233 })
234 .collect(),
235 None => Default::default(),
236 };
237
238 let v_backup_code = match backup_code {
239 Some(dbb) => Some(BackupCodes::try_from(dbb)?),
240 None => None,
241 };
242
243 let type_ =
244 CredentialType::PasswordMfa(v_password, v_totp, v_webauthn, v_backup_code);
245
246 if type_.is_valid() {
247 Ok(Credential {
248 type_,
249 uuid,
250 timestamp,
251 })
252 } else {
253 Err(())
254 }
255 }
256 DbCred::Wn {
257 password: _,
258 webauthn: Some(db_webauthn),
259 totp: _,
260 backup_code: _,
261 claims: _,
262 uuid,
263 } => {
264 let v_webauthn = db_webauthn
265 .into_iter()
266 .map(|wc| {
267 (
268 wc.label,
269 Passkey::from(WebauthnCredential::from(CredentialV3 {
270 cred_id: wc.id,
271 cred: wc.cred,
272 counter: wc.counter,
273 verified: wc.verified,
274 registration_policy: wc.registration_policy,
275 })),
276 )
277 })
278 .collect();
279
280 let type_ = CredentialType::Webauthn(v_webauthn);
281
282 if type_.is_valid() {
283 Ok(Credential {
284 type_,
285 uuid,
286 timestamp,
287 })
288 } else {
289 Err(())
290 }
291 }
292 DbCred::TmpWn {
293 webauthn: db_webauthn,
294 uuid,
295 } => {
296 let v_webauthn = db_webauthn.into_iter().collect();
297 let type_ = CredentialType::Webauthn(v_webauthn);
298
299 if type_.is_valid() {
300 Ok(Credential {
301 type_,
302 uuid,
303 timestamp,
304 })
305 } else {
306 Err(())
307 }
308 }
309 DbCred::V2PasswordMfa {
310 password: db_password,
311 totp: maybe_db_totp,
312 backup_code,
313 webauthn: db_webauthn,
314 uuid,
315 } => {
316 let v_password = Password::try_from(db_password)?;
317
318 let v_totp = match maybe_db_totp {
319 Some(dbt) => {
320 let l = "totp".to_string();
321 let t = Totp::try_from(dbt)?;
322 Map::from([(l, t)])
323 }
324 None => Map::default(),
325 };
326
327 let v_backup_code = match backup_code {
328 Some(dbb) => Some(BackupCodes::try_from(dbb)?),
329 None => None,
330 };
331
332 let v_webauthn = db_webauthn.into_iter().collect();
333
334 let type_ =
335 CredentialType::PasswordMfa(v_password, v_totp, v_webauthn, v_backup_code);
336
337 if type_.is_valid() {
338 Ok(Credential {
339 type_,
340 uuid,
341 timestamp,
342 })
343 } else {
344 Err(())
345 }
346 }
347 DbCred::V3PasswordMfa {
348 password: db_password,
349 totp: db_totp,
350 backup_code,
351 webauthn: db_webauthn,
352 uuid,
353 ..
354 } => {
355 let v_password = Password::try_from(db_password)?;
356
357 let v_totp = db_totp
358 .into_iter()
359 .map(|(l, dbt)| Totp::try_from(dbt).map(|t| (l, t)))
360 .collect::<Result<Map<_, _>, _>>()?;
361
362 let v_backup_code = match backup_code {
363 Some(dbb) => Some(BackupCodes::try_from(dbb)?),
364 None => None,
365 };
366
367 let v_webauthn = db_webauthn.into_iter().collect();
368
369 let type_ =
370 CredentialType::PasswordMfa(v_password, v_totp, v_webauthn, v_backup_code);
371
372 if type_.is_valid() {
373 Ok(Credential {
374 type_,
375 uuid,
376 timestamp,
377 })
378 } else {
379 Err(())
380 }
381 }
382 credential => {
383 error!("Database content may be corrupt - invalid credential state");
384 debug!(%credential);
385 debug!(?credential);
386 Err(())
387 }
388 }
389 }
390}
391
392impl Credential {
393 pub fn timestamp(&self) -> OffsetDateTime {
394 self.timestamp
395 }
396
397 pub fn new_password_only(
399 policy: &CryptoPolicy,
400 cleartext: &str,
401 timestamp: OffsetDateTime,
402 ) -> Result<Self, OperationError> {
403 Password::new(policy, cleartext)
404 .map_err(|e| {
405 error!(crypto_err = ?e);
406 OperationError::CryptographyError
407 })
408 .map(|password| Self::new_from_password(password, timestamp))
409 }
410
411 pub fn new_generatedpassword_only(
413 policy: &CryptoPolicy,
414 cleartext: &str,
415 timestamp: OffsetDateTime,
416 ) -> Result<Self, OperationError> {
417 Password::new(policy, cleartext)
418 .map_err(|e| {
419 error!(crypto_err = ?e);
420 OperationError::CryptographyError
421 })
422 .map(|password| Self::new_from_generatedpassword(password, timestamp))
423 }
424
425 pub fn set_password(
428 &self,
429 policy: &CryptoPolicy,
430 cleartext: &str,
431 timestamp: OffsetDateTime,
432 ) -> Result<Self, OperationError> {
433 Password::new(policy, cleartext)
434 .map_err(|e| {
435 error!(crypto_err = ?e);
436 OperationError::CryptographyError
437 })
438 .map(|pw| self.update_password(pw, timestamp))
439 }
440
441 pub fn upgrade_password(
442 &self,
443 policy: &CryptoPolicy,
444 cleartext: &str,
445 ) -> Result<Option<Self>, OperationError> {
446 let valid = self.password_ref().and_then(|pw| {
447 pw.verify(cleartext).map_err(|e| {
448 error!(crypto_err = ?e);
449 OperationError::CryptographyError
450 })
451 })?;
452
453 if valid {
454 let pw = Password::new(policy, cleartext).map_err(|e| {
455 error!(crypto_err = ?e);
456 OperationError::CryptographyError
457 })?;
458
459 let mut cred = self.update_password(pw, self.timestamp);
463 cred.uuid = self.uuid;
464
465 Ok(Some(cred))
466 } else {
467 Ok(None)
469 }
470 }
471
472 #[cfg(test)] pub fn append_securitykey(
477 &self,
478 label: String,
479 cred: SecurityKey,
480 ) -> Result<Self, OperationError> {
481 let type_ = match &self.type_ {
482 CredentialType::Password(pw) | CredentialType::GeneratedPassword(pw) => {
483 let mut wan = Map::new();
484 wan.insert(label, cred);
485 CredentialType::PasswordMfa(pw.clone(), Map::default(), wan, None)
486 }
487 CredentialType::PasswordMfa(pw, totp, map, backup_code) => {
488 let mut nmap = map.clone();
489 if nmap.insert(label.clone(), cred).is_some() {
490 return Err(OperationError::InvalidAttribute(format!(
491 "Webauthn label '{label:?}' already exists"
492 )));
493 }
494 CredentialType::PasswordMfa(pw.clone(), totp.clone(), nmap, backup_code.clone())
495 }
496 CredentialType::Webauthn(map) => CredentialType::Webauthn(map.clone()),
498 };
499
500 Ok(Credential {
502 type_,
503 uuid: Uuid::new_v4(),
505 timestamp: self.timestamp,
507 })
508 }
509
510 pub fn remove_securitykey(
512 &self,
513 label: &str,
514 timestamp: OffsetDateTime,
515 ) -> Result<Self, OperationError> {
516 let type_ = match &self.type_ {
517 CredentialType::Password(_)
518 | CredentialType::GeneratedPassword(_)
519 | CredentialType::Webauthn(_) => {
520 return Err(OperationError::InvalidAttribute(
521 "SecurityKey is not present on this credential".to_string(),
522 ));
523 }
524 CredentialType::PasswordMfa(pw, totp, map, backup_code) => {
525 let mut nmap = map.clone();
526 if nmap.remove(label).is_none() {
527 return Err(OperationError::InvalidAttribute(format!(
528 "Removing Webauthn token with label '{label:?}': does not exist"
529 )));
530 }
531 if nmap.is_empty() {
532 if !totp.is_empty() {
533 CredentialType::PasswordMfa(
534 pw.clone(),
535 totp.clone(),
536 nmap,
537 backup_code.clone(),
538 )
539 } else {
540 CredentialType::Password(pw.clone())
542 }
543 } else {
544 CredentialType::PasswordMfa(pw.clone(), totp.clone(), nmap, backup_code.clone())
545 }
546 }
547 };
548
549 Ok(Credential {
551 type_,
552 uuid: Uuid::new_v4(),
554 timestamp,
556 })
557 }
558
559 #[allow(clippy::ptr_arg)]
560 pub fn update_webauthn_properties(
563 &self,
564 auth_result: &AuthenticationResult,
565 ) -> Result<Option<Self>, OperationError> {
566 let type_ = match &self.type_ {
567 CredentialType::Password(_pw) | CredentialType::GeneratedPassword(_pw) => {
568 return Ok(None);
573 }
574 CredentialType::Webauthn(map) => {
575 let mut nmap = map.clone();
576 nmap.values_mut().for_each(|pk| {
577 pk.update_credential(auth_result);
578 });
579 CredentialType::Webauthn(nmap)
580 }
581 CredentialType::PasswordMfa(pw, totp, map, backup_code) => {
582 let mut nmap = map.clone();
583 nmap.values_mut().for_each(|sk| {
584 sk.update_credential(auth_result);
585 });
586 CredentialType::PasswordMfa(pw.clone(), totp.clone(), nmap, backup_code.clone())
587 }
588 };
589
590 Ok(Some(Credential {
591 type_,
592 uuid: Uuid::new_v4(),
594 timestamp: self.timestamp,
595 }))
596 }
597
598 pub(crate) fn has_securitykey(&self) -> bool {
599 match &self.type_ {
600 CredentialType::PasswordMfa(_, _, map, _) => !map.is_empty(),
601 _ => false,
602 }
603 }
604
605 pub fn securitykey_ref(&self) -> Result<&Map<String, SecurityKey>, OperationError> {
607 match &self.type_ {
608 CredentialType::Webauthn(_)
609 | CredentialType::Password(_)
610 | CredentialType::GeneratedPassword(_) => Err(OperationError::InvalidAccountState(
611 "non-webauthn cred type?".to_string(),
612 )),
613 CredentialType::PasswordMfa(_, _, map, _) => Ok(map),
614 }
615 }
616
617 pub fn passkey_ref(&self) -> Result<&Map<String, Passkey>, OperationError> {
618 match &self.type_ {
619 CredentialType::PasswordMfa(_, _, _, _)
620 | CredentialType::Password(_)
621 | CredentialType::GeneratedPassword(_) => Err(OperationError::InvalidAccountState(
622 "non-webauthn cred type?".to_string(),
623 )),
624 CredentialType::Webauthn(map) => Ok(map),
625 }
626 }
627
628 pub fn password_ref(&self) -> Result<&Password, OperationError> {
630 match &self.type_ {
631 CredentialType::Password(pw)
632 | CredentialType::GeneratedPassword(pw)
633 | CredentialType::PasswordMfa(pw, _, _, _) => Ok(pw),
634 CredentialType::Webauthn(_) => Err(OperationError::InvalidAccountState(
635 "non-password cred type?".to_string(),
636 )),
637 }
638 }
639
640 pub fn is_mfa(&self) -> bool {
641 match &self.type_ {
642 CredentialType::Password(_) | CredentialType::GeneratedPassword(_) => false,
643 CredentialType::PasswordMfa(..) | CredentialType::Webauthn(_) => true,
644 }
645 }
646
647 #[cfg(test)]
648 pub fn verify_password(&self, cleartext: &str) -> Result<bool, OperationError> {
649 self.password_ref().and_then(|pw| {
650 pw.verify(cleartext).map_err(|e| {
651 error!(crypto_err = ?e);
652 OperationError::CryptographyError
653 })
654 })
655 }
656
657 pub fn to_db_valuev1(&self) -> DbCred {
659 let uuid = self.uuid;
660 match &self.type_ {
661 CredentialType::Password(pw) => DbCred::V2Password {
662 password: pw.to_dbpasswordv1(),
663 uuid,
664 timestamp: self.timestamp,
665 },
666 CredentialType::GeneratedPassword(pw) => DbCred::V2GenPassword {
667 password: pw.to_dbpasswordv1(),
668 uuid,
669 timestamp: self.timestamp,
670 },
671 CredentialType::PasswordMfa(pw, totp, map, backup_code) => DbCred::V3PasswordMfa {
672 password: pw.to_dbpasswordv1(),
673 totp: totp
674 .iter()
675 .map(|(l, t)| (l.clone(), t.to_dbtotpv1()))
676 .collect(),
677 backup_code: backup_code.as_ref().map(|b| b.to_dbbackupcodev1()),
678 webauthn: map.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
679 uuid,
680 timestamp: self.timestamp,
681 },
682 CredentialType::Webauthn(map) => DbCred::TmpWn {
683 webauthn: map.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
684 uuid,
685 },
686 }
687 }
688
689 pub(crate) fn update_password(&self, pw: Password, timestamp: OffsetDateTime) -> Self {
690 let type_ = match &self.type_ {
691 CredentialType::Password(_) | CredentialType::GeneratedPassword(_) => {
692 CredentialType::Password(pw)
693 }
694 CredentialType::PasswordMfa(_, totp, wan, backup_code) => {
695 CredentialType::PasswordMfa(pw, totp.clone(), wan.clone(), backup_code.clone())
696 }
697 CredentialType::Webauthn(wan) => CredentialType::Webauthn(wan.clone()),
699 };
700 Credential {
701 type_,
702 uuid: Uuid::new_v4(),
704 timestamp,
706 }
707 }
708
709 pub(crate) fn append_totp(&self, label: String, totp: Totp, timestamp: OffsetDateTime) -> Self {
711 let type_ = match &self.type_ {
712 CredentialType::Password(pw) | CredentialType::GeneratedPassword(pw) => {
713 CredentialType::PasswordMfa(
714 pw.clone(),
715 Map::from([(label, totp)]),
716 Map::new(),
717 None,
718 )
719 }
720 CredentialType::PasswordMfa(pw, totps, wan, backup_code) => {
721 let mut totps = totps.clone();
722 let replaced = totps.insert(label, totp).is_none();
723 debug_assert!(replaced);
724
725 CredentialType::PasswordMfa(pw.clone(), totps, wan.clone(), backup_code.clone())
726 }
727 CredentialType::Webauthn(wan) => {
728 debug_assert!(false);
729 CredentialType::Webauthn(wan.clone())
730 }
731 };
732 Credential {
733 type_,
734 uuid: Uuid::new_v4(),
736 timestamp,
738 }
739 }
740
741 pub(crate) fn remove_totp(&self, label: &str, timestamp: OffsetDateTime) -> Self {
742 let type_ = match &self.type_ {
743 CredentialType::PasswordMfa(pw, totp, wan, backup_code) => {
744 let mut totp = totp.clone();
745 let removed = totp.remove(label).is_some();
746 debug_assert!(removed);
747
748 if wan.is_empty() && totp.is_empty() {
749 CredentialType::Password(pw.clone())
751 } else {
752 CredentialType::PasswordMfa(pw.clone(), totp, wan.clone(), backup_code.clone())
753 }
754 }
755 _ => self.type_.clone(),
756 };
757 Credential {
758 type_,
759 uuid: Uuid::new_v4(),
761 timestamp,
763 }
764 }
765
766 pub(crate) fn has_totp_by_name(&self, label: &str) -> bool {
767 match &self.type_ {
768 CredentialType::PasswordMfa(_, totp, _, _) => totp.contains_key(label),
769 _ => false,
770 }
771 }
772
773 pub(crate) fn new_from_generatedpassword(pw: Password, timestamp: OffsetDateTime) -> Self {
774 Credential {
775 type_: CredentialType::GeneratedPassword(pw),
776 uuid: Uuid::new_v4(),
777 timestamp,
778 }
779 }
780
781 pub(crate) fn new_from_password(pw: Password, timestamp: OffsetDateTime) -> Self {
782 Credential {
783 type_: CredentialType::Password(pw),
784 uuid: Uuid::new_v4(),
785 timestamp,
786 }
787 }
788
789 pub(crate) fn softlock_policy(&self) -> CredSoftLockPolicy {
790 match &self.type_ {
791 CredentialType::Password(_pw) | CredentialType::GeneratedPassword(_pw) => {
792 CredSoftLockPolicy::Password
793 }
794 CredentialType::PasswordMfa(_pw, totp, wan, _) => {
795 if !totp.is_empty() {
797 let min_step = totp
799 .iter()
800 .map(|(_, t)| t.step)
801 .min()
802 .unwrap_or(TOTP_DEFAULT_STEP);
803 CredSoftLockPolicy::Totp(min_step)
804 } else if !wan.is_empty() {
805 CredSoftLockPolicy::Webauthn
806 } else {
807 CredSoftLockPolicy::Password
808 }
809 }
810 CredentialType::Webauthn(_wan) => CredSoftLockPolicy::Webauthn,
811 }
812 }
813
814 pub(crate) fn update_backup_code(
815 &self,
816 backup_codes: BackupCodes,
817 timestamp: OffsetDateTime,
818 ) -> Result<Self, OperationError> {
819 match &self.type_ {
820 CredentialType::PasswordMfa(pw, totp, wan, _) => Ok(Credential {
821 type_: CredentialType::PasswordMfa(
822 pw.clone(),
823 totp.clone(),
824 wan.clone(),
825 Some(backup_codes),
826 ),
827 uuid: Uuid::new_v4(),
829 timestamp,
831 }),
832 _ => Err(OperationError::InvalidAccountState(
833 "Non-MFA credential type".to_string(),
834 )),
835 }
836 }
837
838 pub(crate) fn invalidate_backup_code(
839 self,
840 code_to_remove: &str,
841 ) -> Result<Self, OperationError> {
842 match self.type_ {
843 CredentialType::PasswordMfa(pw, totp, wan, opt_backup_codes) => {
844 match opt_backup_codes {
845 Some(mut backup_codes) => {
846 backup_codes.remove(code_to_remove);
847 Ok(Credential {
848 type_: CredentialType::PasswordMfa(pw, totp, wan, Some(backup_codes)),
849 uuid: self.uuid,
852 timestamp: self.timestamp,
853 })
854 }
855 _ => Err(OperationError::InvalidAccountState(
856 "backup code does not exist".to_string(),
857 )),
858 }
859 }
860 _ => Err(OperationError::InvalidAccountState(
861 "Non-MFA credential type".to_string(),
862 )),
863 }
864 }
865
866 pub(crate) fn remove_backup_code(
867 &self,
868 timestamp: OffsetDateTime,
869 ) -> Result<Self, OperationError> {
870 match &self.type_ {
871 CredentialType::PasswordMfa(pw, totp, wan, _) => Ok(Credential {
872 type_: CredentialType::PasswordMfa(pw.clone(), totp.clone(), wan.clone(), None),
873 uuid: Uuid::new_v4(),
875 timestamp,
877 }),
878 _ => Err(OperationError::InvalidAccountState(
879 "Non-MFA credential type".to_string(),
880 )),
881 }
882 }
883}
884
885impl CredentialType {
886 fn is_valid(&self) -> bool {
887 match self {
888 CredentialType::Password(_) | CredentialType::GeneratedPassword(_) => true,
889 CredentialType::PasswordMfa(_, m_totp, webauthn, _) => {
890 !m_totp.is_empty() || !webauthn.is_empty() }
892 CredentialType::Webauthn(webauthn) => !webauthn.is_empty(),
893 }
894 }
895}
896
897#[cfg(test)]
898mod tests {
899 use std::time::Duration;
900
901 use crate::credential::totp::{Totp, TOTP_DEFAULT_STEP};
902 use crate::credential::Credential;
903 use kanidm_lib_crypto::{CryptoPolicy, Password};
904 use time::OffsetDateTime;
905
906 #[test]
907 fn test_credential_timestamp_updated_on_totp_append() {
908 let pw = Password::new(&CryptoPolicy::minimum(), "test_password")
909 .expect("Failed to create password");
910 let original_cred = Credential::new_from_password(pw, OffsetDateTime::UNIX_EPOCH);
911 let original_timestamp = original_cred.timestamp;
912
913 let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
914 let updated_cred = original_cred.append_totp(
915 "test_totp".to_string(),
916 totp,
917 OffsetDateTime::UNIX_EPOCH + Duration::from_millis(10),
918 );
919
920 assert!(updated_cred.timestamp > original_timestamp);
922 assert_ne!(original_cred.uuid, updated_cred.uuid);
923 }
924
925 #[test]
926 fn test_credential_timestamp_updated_on_totp_remove() {
927 let pw = Password::new(&CryptoPolicy::minimum(), "test_password")
928 .expect("Failed to create password");
929 let cred = Credential::new_from_password(pw, OffsetDateTime::UNIX_EPOCH);
930
931 let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
932 let cred_with_totp = cred.append_totp(
933 "test_totp".to_string(),
934 totp,
935 OffsetDateTime::UNIX_EPOCH + Duration::from_millis(10),
936 );
937 let timestamp_after_append = cred_with_totp.timestamp;
938
939 let cred_removed = cred_with_totp.remove_totp(
940 "test_totp",
941 OffsetDateTime::UNIX_EPOCH + Duration::from_millis(20),
942 );
943
944 assert!(cred_removed.timestamp > timestamp_after_append);
946 assert_ne!(cred_with_totp.uuid, cred_removed.uuid);
947 }
948
949 #[test]
950 fn test_credential_timestamp_updated_on_password_change() {
951 let original_cred = Credential::new_password_only(
952 &CryptoPolicy::minimum(),
953 "original_password",
954 OffsetDateTime::UNIX_EPOCH,
955 )
956 .expect("Failed to create credential");
957 let original_timestamp = original_cred.timestamp;
958
959 let updated_cred = original_cred
960 .set_password(
961 &CryptoPolicy::minimum(),
962 "new_password",
963 OffsetDateTime::UNIX_EPOCH + Duration::from_millis(10),
964 )
965 .expect("Failed to update password");
966
967 assert!(updated_cred.timestamp > original_timestamp);
969 assert_ne!(original_cred.uuid, updated_cred.uuid);
970 }
971
972 #[test]
973 fn test_credential_timestamp_preserved_on_password_upgrade() {
974 let original_cred = Credential::new_password_only(
975 &CryptoPolicy::minimum(),
976 "test_password",
977 OffsetDateTime::UNIX_EPOCH,
978 )
979 .expect("Failed to create credential");
980 let original_timestamp = original_cred.timestamp;
981 let original_uuid = original_cred.uuid;
982
983 let maybe_upgraded = original_cred
986 .upgrade_password(&CryptoPolicy::minimum(), "test_password")
987 .expect("Failed to upgrade password");
988
989 if let Some(upgraded_cred) = maybe_upgraded {
990 assert_eq!(original_uuid, upgraded_cred.uuid);
992 assert!(upgraded_cred.timestamp >= original_timestamp);
994 }
995 }
996
997 #[test]
998 fn test_credential_timestamp_on_mfa_operations() {
999 use crate::credential::BackupCodes;
1000 use hashbrown::HashSet;
1001
1002 let pw = Password::new(&CryptoPolicy::minimum(), "test_password")
1003 .expect("Failed to create password");
1004 let cred = Credential::new_from_password(pw, OffsetDateTime::UNIX_EPOCH);
1005
1006 let totp = Totp::generate_secure(TOTP_DEFAULT_STEP);
1008 let mfa_cred = cred.append_totp(
1009 "test_totp".to_string(),
1010 totp,
1011 OffsetDateTime::UNIX_EPOCH + Duration::from_millis(10),
1012 );
1013 let mfa_timestamp = mfa_cred.timestamp;
1014
1015 let backup_codes =
1017 BackupCodes::new(HashSet::from(["code1".to_string(), "code2".to_string()]));
1018 let cred_with_backup = mfa_cred
1019 .update_backup_code(
1020 backup_codes,
1021 OffsetDateTime::UNIX_EPOCH + Duration::from_millis(20),
1022 )
1023 .expect("Failed to add backup codes");
1024
1025 assert!(cred_with_backup.timestamp > mfa_timestamp);
1027 assert_ne!(mfa_cred.uuid, cred_with_backup.uuid);
1028
1029 let cred_removed_backup = cred_with_backup
1031 .remove_backup_code(OffsetDateTime::UNIX_EPOCH + Duration::from_millis(30))
1032 .expect("Failed to remove backup codes");
1033
1034 assert!(cred_removed_backup.timestamp > cred_with_backup.timestamp);
1036 assert_ne!(cred_with_backup.uuid, cred_removed_backup.uuid);
1037 }
1038}