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
22pub 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)]
71pub struct Credential {
83 pub(crate) type_: CredentialType,
85 pub(crate) uuid: Uuid,
88 }
91
92#[derive(Clone, Debug, PartialEq)]
93pub enum CredentialType {
98 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 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 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 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 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 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 let mut cred = self.update_password(pw);
419 cred.uuid = self.uuid;
420
421 Ok(Some(cred))
422 } else {
423 Ok(None)
425 }
426 }
427
428 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 CredentialType::Webauthn(map) => CredentialType::Webauthn(map.clone()),
453 };
454
455 Ok(Credential {
457 type_,
458 uuid: Uuid::new_v4(),
460 })
461 }
462
463 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 CredentialType::Password(pw.clone())
491 }
492 } else {
493 CredentialType::PasswordMfa(pw.clone(), totp.clone(), nmap, backup_code.clone())
494 }
495 }
496 };
497
498 Ok(Credential {
500 type_,
501 uuid: Uuid::new_v4(),
503 })
504 }
505
506 #[allow(clippy::ptr_arg)]
507 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 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 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 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 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 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 CredentialType::Webauthn(wan) => CredentialType::Webauthn(wan.clone()),
642 };
643 Credential {
644 type_,
645 uuid: Uuid::new_v4(),
647 }
648 }
649
650 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 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 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 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 if !totp.is_empty() {
732 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 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 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 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() }
818 CredentialType::Webauthn(webauthn) => !webauthn.is_empty(),
819 }
820 }
821}