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
24pub 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)]
73pub struct Credential {
85 pub(crate) type_: CredentialType,
87 pub(crate) uuid: Uuid,
90 }
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 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 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 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 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 let mut cred = self.update_password(pw);
421 cred.uuid = self.uuid;
422
423 Ok(Some(cred))
424 } else {
425 Ok(None)
427 }
428 }
429
430 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 CredentialType::Webauthn(map) => CredentialType::Webauthn(map.clone()),
455 };
456
457 Ok(Credential {
459 type_,
460 uuid: Uuid::new_v4(),
462 })
463 }
464
465 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 CredentialType::Password(pw.clone())
493 }
494 } else {
495 CredentialType::PasswordMfa(pw.clone(), totp.clone(), nmap, backup_code.clone())
496 }
497 }
498 };
499
500 Ok(Credential {
502 type_,
503 uuid: Uuid::new_v4(),
505 })
506 }
507
508 #[allow(clippy::ptr_arg)]
509 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 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 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 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 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 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 CredentialType::Webauthn(wan) => CredentialType::Webauthn(wan.clone()),
644 };
645 Credential {
646 type_,
647 uuid: Uuid::new_v4(),
649 }
650 }
651
652 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 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 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 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 if !totp.is_empty() {
734 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 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 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 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() }
838 CredentialType::Webauthn(webauthn) => !webauthn.is_empty(),
839 }
840 }
841}