Skip to main content

kanidm_proto/internal/
credupdate.rs

1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3use std::fmt;
4use url::Url;
5use utoipa::ToSchema;
6use uuid::Uuid;
7
8use webauthn_rs_proto::CreationChallengeResponse;
9use webauthn_rs_proto::RegisterPublicKeyCredential;
10
11pub use sshkey_attest::proto::PublicKey as SshPublicKey;
12
13#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
14#[serde(rename_all = "lowercase")]
15pub enum TotpAlgo {
16    Sha1,
17    Sha256,
18    Sha512,
19}
20
21impl fmt::Display for TotpAlgo {
22    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
23        match self {
24            TotpAlgo::Sha1 => write!(f, "SHA1"),
25            TotpAlgo::Sha256 => write!(f, "SHA256"),
26            TotpAlgo::Sha512 => write!(f, "SHA512"),
27        }
28    }
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
32pub struct TotpSecret {
33    pub accountname: String,
34    /// User-facing name of the system, issuer of the TOTP
35    pub issuer: String,
36    pub secret: Vec<u8>,
37    pub algo: TotpAlgo,
38    pub step: u64,
39    pub digits: u8,
40}
41
42impl TotpSecret {
43    /// <https://github.com/google/google-authenticator/wiki/Key-Uri-Format>
44    pub fn to_uri(&self) -> String {
45        let accountname = urlencoding::Encoded(&self.accountname);
46        let issuer = urlencoding::Encoded(&self.issuer);
47        let label = format!("{issuer}:{accountname}");
48        let algo = self.algo.to_string();
49        let secret = self.get_secret();
50        let period = self.step;
51        let digits = self.digits;
52
53        format!(
54            "otpauth://totp/{label}?secret={secret}&issuer={issuer}&algorithm={algo}&digits={digits}&period={period}"
55        )
56    }
57
58    pub fn get_secret(&self) -> String {
59        base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &self.secret)
60    }
61}
62
63/// Structure denoting the parameters for triggering a credential update intent
64/// token to be send to the account.
65#[derive(Debug, Serialize, Deserialize, ToSchema)]
66pub struct CUIntentSend {
67    pub ttl: Option<u64>,
68    pub email: Option<String>,
69}
70
71#[derive(Debug, Serialize, Deserialize, ToSchema)]
72pub struct CUIntentToken {
73    pub token: String,
74    #[serde(with = "time::serde::timestamp")]
75    pub expiry_time: time::OffsetDateTime,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
79pub struct CUSessionToken {
80    pub token: String,
81}
82
83#[derive(Clone, Serialize, Deserialize)]
84#[serde(rename_all = "lowercase")]
85pub enum CURequest {
86    PrimaryRemove,
87    PasswordQualityCheck(String),
88    Password(String),
89    CancelMFAReg,
90    TotpGenerate,
91    TotpVerify(u32, String),
92    TotpAcceptSha1,
93    TotpRemove(String),
94    BackupCodeGenerate,
95    BackupCodeRemove,
96    PasskeyInit,
97    PasskeyFinish(String, RegisterPublicKeyCredential),
98    PasskeyRemove(Uuid),
99    AttestedPasskeyInit,
100    AttestedPasskeyFinish(String, RegisterPublicKeyCredential),
101    AttestedPasskeyRemove(Uuid),
102    UnixPasswordRemove,
103    UnixPassword(String),
104    SshPublicKey(String, SshPublicKey),
105    SshPublicKeyRemove(String),
106}
107
108impl fmt::Debug for CURequest {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        let t = match self {
111            CURequest::PrimaryRemove => "CURequest::PrimaryRemove",
112            CURequest::PasswordQualityCheck(_) => "CURequest::PasswordQualityCheck",
113            CURequest::Password(_) => "CURequest::Password",
114            CURequest::CancelMFAReg => "CURequest::CancelMFAReg",
115            CURequest::TotpGenerate => "CURequest::TotpGenerate",
116            CURequest::TotpVerify(_, _) => "CURequest::TotpVerify",
117            CURequest::TotpAcceptSha1 => "CURequest::TotpAcceptSha1",
118            CURequest::TotpRemove(_) => "CURequest::TotpRemove",
119            CURequest::BackupCodeGenerate => "CURequest::BackupCodeGenerate",
120            CURequest::BackupCodeRemove => "CURequest::BackupCodeRemove",
121            CURequest::PasskeyInit => "CURequest::PasskeyInit",
122            CURequest::PasskeyFinish(_, _) => "CURequest::PasskeyFinish",
123            CURequest::PasskeyRemove(_) => "CURequest::PasskeyRemove",
124            CURequest::AttestedPasskeyInit => "CURequest::AttestedPasskeyInit",
125            CURequest::AttestedPasskeyFinish(_, _) => "CURequest::AttestedPasskeyFinish",
126            CURequest::AttestedPasskeyRemove(_) => "CURequest::AttestedPasskeyRemove",
127            CURequest::UnixPassword(_) => "CURequest::UnixPassword",
128            CURequest::UnixPasswordRemove => "CURequest::UnixPasswordRemove",
129            CURequest::SshPublicKey(_, _) => "CURequest::SSHKeySubmit",
130            CURequest::SshPublicKeyRemove(_) => "CURequest::SSHKeyRemove",
131        };
132        writeln!(f, "{t}")
133    }
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
137pub enum CURegState {
138    // Nothing in progress.
139    None,
140    TotpCheck(TotpSecret),
141    TotpTryAgain,
142    TotpNameTryAgain(String),
143    TotpInvalidSha1,
144    BackupCodes(Vec<String>),
145    #[schema(value_type = HashMap<String, Value>)]
146    Passkey(CreationChallengeResponse),
147    #[schema(value_type = HashMap<String, Value>)]
148    AttestedPasskey(CreationChallengeResponse),
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
152pub enum CUExtPortal {
153    None,
154    Hidden,
155    Some(Url),
156}
157
158#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq)]
159pub enum CUCredState {
160    Modifiable,
161    DeleteOnly,
162    AccessDeny,
163    PolicyDeny,
164    // Disabled,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
168pub enum CURegWarning {
169    MfaRequired,
170    PasskeyRequired,
171    AttestedPasskeyRequired,
172    AttestedResidentKeyRequired,
173    Unsatisfiable,
174    WebauthnAttestationUnsatisfiable,
175    WebauthnUserVerificationRequired,
176    NoValidCredentials,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
180pub struct CUStatus {
181    // Display values
182    pub spn: String,
183    pub displayname: String,
184    pub ext_cred_portal: CUExtPortal,
185    // Internal State Tracking
186    pub mfaregstate: CURegState,
187    // Display hints + The credential details.
188    pub can_commit: bool,
189    pub warnings: Vec<CURegWarning>,
190    pub dirty: bool,
191    pub primary: Option<CredentialDetail>,
192    pub primary_state: CUCredState,
193    pub passkeys: Vec<PasskeyDetail>,
194    pub passkeys_state: CUCredState,
195    pub attested_passkeys: Vec<PasskeyDetail>,
196    pub attested_passkeys_state: CUCredState,
197    pub attested_passkeys_allowed_devices: Vec<String>,
198
199    pub unixcred: Option<CredentialDetail>,
200    pub unixcred_state: CUCredState,
201
202    #[schema(value_type = BTreeMap<String, Value>)]
203    pub sshkeys: BTreeMap<String, SshPublicKey>,
204    pub sshkeys_state: CUCredState,
205}
206
207#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
208pub struct CredentialStatus {
209    pub creds: Vec<CredentialDetail>,
210}
211
212impl fmt::Display for CredentialStatus {
213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214        for cred in &self.creds {
215            writeln!(f, "---")?;
216            cred.fmt(f)?;
217        }
218        writeln!(f, "---")
219    }
220}
221
222#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
223pub enum CredentialDetailType {
224    Password,
225    GeneratedPassword,
226    Passkey(Vec<String>),
227    /// totp, webauthn
228    PasswordMfa(Vec<String>, Vec<String>, usize),
229}
230
231#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
232pub struct CredentialDetail {
233    pub uuid: Uuid,
234    pub type_: CredentialDetailType,
235}
236
237impl fmt::Display for CredentialDetail {
238    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239        writeln!(f, "uuid: {}", self.uuid)?;
240        /*
241        writeln!(f, "claims:")?;
242        for claim in &self.claims {
243            writeln!(f, " * {}", claim)?;
244        }
245        */
246        match &self.type_ {
247            CredentialDetailType::Password => writeln!(f, "password: set"),
248            CredentialDetailType::GeneratedPassword => writeln!(f, "generated password: set"),
249            CredentialDetailType::Passkey(labels) => {
250                if labels.is_empty() {
251                    writeln!(f, "passkeys: none registered")
252                } else {
253                    writeln!(f, "passkeys:")?;
254                    for label in labels {
255                        writeln!(f, " * {label}")?;
256                    }
257                    write!(f, "")
258                }
259            }
260            CredentialDetailType::PasswordMfa(totp_labels, wan_labels, backup_code) => {
261                writeln!(f, "password: set")?;
262
263                if !totp_labels.is_empty() {
264                    writeln!(f, "totp:")?;
265                    for label in totp_labels {
266                        writeln!(f, " * {label}")?;
267                    }
268                } else {
269                    writeln!(f, "totp: disabled")?;
270                }
271
272                if *backup_code > 0 {
273                    writeln!(f, "backup_code: enabled")?;
274                } else {
275                    writeln!(f, "backup_code: disabled")?;
276                }
277
278                if !wan_labels.is_empty() {
279                    // We no longer show the deprecated security key case by default.
280                    writeln!(f, " ⚠️  warning - security keys are deprecated.")?;
281                    writeln!(f, " ⚠️  you should re-enroll these to passkeys.")?;
282                    writeln!(f, "security keys:")?;
283                    for label in wan_labels {
284                        writeln!(f, " * {label}")?;
285                    }
286                    write!(f, "")
287                } else {
288                    write!(f, "")
289                }
290            }
291        }
292    }
293}
294
295#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
296pub struct PasskeyDetail {
297    pub uuid: Uuid,
298    pub tag: String,
299}
300
301#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
302pub struct BackupCodesView {
303    pub backup_codes: Vec<String>,
304}
305
306#[derive(Clone, Serialize, Deserialize, Debug, ToSchema, PartialEq, Eq, PartialOrd, Ord)]
307#[serde(rename_all = "lowercase")]
308pub enum PasswordFeedback {
309    // https://docs.rs/zxcvbn/latest/zxcvbn/feedback/enum.Suggestion.html
310    UseAFewWordsAvoidCommonPhrases,
311    NoNeedForSymbolsDigitsOrUppercaseLetters,
312    AddAnotherWordOrTwo,
313    CapitalizationDoesntHelpVeryMuch,
314    AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase,
315    ReversedWordsArentMuchHarderToGuess,
316    PredictableSubstitutionsDontHelpVeryMuch,
317    UseALongerKeyboardPatternWithMoreTurns,
318    AvoidRepeatedWordsAndCharacters,
319    AvoidSequences,
320    AvoidRecentYears,
321    AvoidYearsThatAreAssociatedWithYou,
322    AvoidDatesAndYearsThatAreAssociatedWithYou,
323    // https://docs.rs/zxcvbn/latest/zxcvbn/feedback/enum.Warning.html
324    StraightRowsOfKeysAreEasyToGuess,
325    ShortKeyboardPatternsAreEasyToGuess,
326    RepeatsLikeAaaAreEasyToGuess,
327    RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess,
328    ThisIsATop10Password,
329    ThisIsATop100Password,
330    ThisIsACommonPassword,
331    ThisIsSimilarToACommonlyUsedPassword,
332    SequencesLikeAbcAreEasyToGuess,
333    RecentYearsAreEasyToGuess,
334    AWordByItselfIsEasyToGuess,
335    DatesAreOftenEasyToGuess,
336    NamesAndSurnamesByThemselvesAreEasyToGuess,
337    CommonNamesAndSurnamesAreEasyToGuess,
338    // Custom
339    TooShort(u32),
340    TooLong(u32),
341    BadListed,
342    DontReusePasswords,
343}
344
345/// Human-readable PasswordFeedback result.
346impl fmt::Display for PasswordFeedback {
347    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348        match self {
349            PasswordFeedback::AddAnotherWordOrTwo => write!(f, "Add another word or two."),
350            PasswordFeedback::AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase => write!(
351                f,
352                "All uppercase is almost as easy to guess as all lowercase."
353            ),
354            PasswordFeedback::AvoidDatesAndYearsThatAreAssociatedWithYou => write!(
355                f,
356                "Avoid dates and years that are associated with you or your account."
357            ),
358            PasswordFeedback::AvoidRecentYears => write!(f, "Avoid recent years."),
359            PasswordFeedback::AvoidRepeatedWordsAndCharacters => {
360                write!(f, "Avoid repeated words and characters.")
361            }
362            PasswordFeedback::AvoidSequences => write!(f, "Avoid sequences of characters."),
363            PasswordFeedback::AvoidYearsThatAreAssociatedWithYou => {
364                write!(f, "Avoid years that are associated with you.")
365            }
366            PasswordFeedback::AWordByItselfIsEasyToGuess => {
367                write!(f, "A word by itself is easy to guess.")
368            }
369            PasswordFeedback::BadListed => write!(
370                f,
371                "This password has been compromised or otherwise blocked and can not be used."
372            ),
373            PasswordFeedback::CapitalizationDoesntHelpVeryMuch => {
374                write!(f, "Capitalization doesn't help very much.")
375            }
376            PasswordFeedback::CommonNamesAndSurnamesAreEasyToGuess => {
377                write!(f, "Common names and surnames are easy to guess.")
378            }
379            PasswordFeedback::DatesAreOftenEasyToGuess => {
380                write!(f, "Dates are often easy to guess.")
381            }
382            PasswordFeedback::DontReusePasswords => {
383                write!(
384                    f,
385                    "Don't reuse passwords that already exist on your account"
386                )
387            }
388            PasswordFeedback::NamesAndSurnamesByThemselvesAreEasyToGuess => {
389                write!(f, "Names and surnames by themselves are easy to guess.")
390            }
391            PasswordFeedback::NoNeedForSymbolsDigitsOrUppercaseLetters => {
392                write!(f, "No need for symbols, digits or upper-case letters.")
393            }
394            PasswordFeedback::PredictableSubstitutionsDontHelpVeryMuch => {
395                write!(f, "Predictable substitutions don't help very much.")
396            }
397            PasswordFeedback::RecentYearsAreEasyToGuess => {
398                write!(f, "Recent years are easy to guess.")
399            }
400            PasswordFeedback::RepeatsLikeAaaAreEasyToGuess => {
401                write!(f, "Repeats like 'aaa' are easy to guess.")
402            }
403            PasswordFeedback::RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess => write!(
404                f,
405                "Repeats like abcabcabc are only slightly harder to guess."
406            ),
407            PasswordFeedback::ReversedWordsArentMuchHarderToGuess => {
408                write!(f, "Reversed words aren't much harder to guess.")
409            }
410            PasswordFeedback::SequencesLikeAbcAreEasyToGuess => {
411                write!(f, "Sequences like 'abc' are easy to guess.")
412            }
413            PasswordFeedback::ShortKeyboardPatternsAreEasyToGuess => {
414                write!(f, "Short keyboard patterns are easy to guess.")
415            }
416            PasswordFeedback::StraightRowsOfKeysAreEasyToGuess => {
417                write!(f, "Straight rows of keys are easy to guess.")
418            }
419            PasswordFeedback::ThisIsACommonPassword => write!(f, "This is a common password."),
420            PasswordFeedback::ThisIsATop100Password => write!(f, "This is a top 100 password."),
421            PasswordFeedback::ThisIsATop10Password => write!(f, "This is a top 10 password."),
422            PasswordFeedback::ThisIsSimilarToACommonlyUsedPassword => {
423                write!(f, "This is similar to a commonly used password.")
424            }
425            PasswordFeedback::TooShort(minlength) => write!(
426                f,
427                "Password was too short, needs to be at least {minlength} characters long."
428            ),
429            PasswordFeedback::TooLong(maxlength) => write!(
430                f,
431                "Password was too long, must not be greater than {maxlength} characters long."
432            ),
433            PasswordFeedback::UseAFewWordsAvoidCommonPhrases => {
434                write!(f, "Use a few words and avoid common phrases.")
435            }
436            PasswordFeedback::UseALongerKeyboardPatternWithMoreTurns => {
437                write!(
438                    f,
439                    "The password included keyboard patterns across too much of a single row."
440                )
441            }
442        }
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    use super::{TotpAlgo, TotpSecret};
449
450    #[test]
451    fn totp_to_string() {
452        let totp = TotpSecret {
453            accountname: "william".to_string(),
454            issuer: "blackhats".to_string(),
455            secret: vec![0xaa, 0xbb, 0xcc, 0xdd],
456            step: 30,
457            algo: TotpAlgo::Sha256,
458            digits: 6,
459        };
460        let s = totp.to_uri();
461        assert_eq!(s,"otpauth://totp/blackhats:william?secret=VK54ZXI&issuer=blackhats&algorithm=SHA256&digits=6&period=30");
462
463        // check that invalid issuer/accounts are cleaned up.
464        let totp = TotpSecret {
465            accountname: "william:%3A".to_string(),
466            issuer: "blackhats australia".to_string(),
467            secret: vec![0xaa, 0xbb, 0xcc, 0xdd],
468            step: 30,
469            algo: TotpAlgo::Sha256,
470            digits: 6,
471        };
472        let s = totp.to_uri();
473        println!("{s}");
474        assert_eq!(s,"otpauth://totp/blackhats%20australia:william%3A%253A?secret=VK54ZXI&issuer=blackhats%20australia&algorithm=SHA256&digits=6&period=30");
475    }
476}