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