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