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