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 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 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 None,
132 TotpCheck(TotpSecret),
133 TotpTryAgain,
134 TotpNameTryAgain(String),
135 TotpInvalidSha1,
136 BackupCodes(Vec<String>),
137 Passkey(CreationChallengeResponse),
138 AttestedPasskey(CreationChallengeResponse),
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
142pub enum CUExtPortal {
143 None,
144 Hidden,
145 Some(Url),
146}
147
148#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq)]
149pub enum CUCredState {
150 Modifiable,
151 DeleteOnly,
152 AccessDeny,
153 PolicyDeny,
154 }
156
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
158pub enum CURegWarning {
159 MfaRequired,
160 PasskeyRequired,
161 AttestedPasskeyRequired,
162 AttestedResidentKeyRequired,
163 Unsatisfiable,
164 WebauthnAttestationUnsatisfiable,
165 WebauthnUserVerificationRequired,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
169pub struct CUStatus {
170 pub spn: String,
172 pub displayname: String,
173 pub ext_cred_portal: CUExtPortal,
174 pub mfaregstate: CURegState,
176 pub can_commit: bool,
178 pub warnings: Vec<CURegWarning>,
179 pub primary: Option<CredentialDetail>,
180 pub primary_state: CUCredState,
181 pub passkeys: Vec<PasskeyDetail>,
182 pub passkeys_state: CUCredState,
183 pub attested_passkeys: Vec<PasskeyDetail>,
184 pub attested_passkeys_state: CUCredState,
185 pub attested_passkeys_allowed_devices: Vec<String>,
186
187 pub unixcred: Option<CredentialDetail>,
188 pub unixcred_state: CUCredState,
189
190 pub sshkeys: BTreeMap<String, SshPublicKey>,
191 pub sshkeys_state: CUCredState,
192}
193
194#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
195pub struct CredentialStatus {
196 pub creds: Vec<CredentialDetail>,
197}
198
199impl fmt::Display for CredentialStatus {
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 for cred in &self.creds {
202 writeln!(f, "---")?;
203 cred.fmt(f)?;
204 }
205 writeln!(f, "---")
206 }
207}
208
209#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
210pub enum CredentialDetailType {
211 Password,
212 GeneratedPassword,
213 Passkey(Vec<String>),
214 PasswordMfa(Vec<String>, Vec<String>, usize),
216}
217
218#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
219pub struct CredentialDetail {
220 pub uuid: Uuid,
221 pub type_: CredentialDetailType,
222}
223
224impl fmt::Display for CredentialDetail {
225 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226 writeln!(f, "uuid: {}", self.uuid)?;
227 match &self.type_ {
234 CredentialDetailType::Password => writeln!(f, "password: set"),
235 CredentialDetailType::GeneratedPassword => writeln!(f, "generated password: set"),
236 CredentialDetailType::Passkey(labels) => {
237 if labels.is_empty() {
238 writeln!(f, "passkeys: none registered")
239 } else {
240 writeln!(f, "passkeys:")?;
241 for label in labels {
242 writeln!(f, " * {label}")?;
243 }
244 write!(f, "")
245 }
246 }
247 CredentialDetailType::PasswordMfa(totp_labels, wan_labels, backup_code) => {
248 writeln!(f, "password: set")?;
249
250 if !totp_labels.is_empty() {
251 writeln!(f, "totp:")?;
252 for label in totp_labels {
253 writeln!(f, " * {label}")?;
254 }
255 } else {
256 writeln!(f, "totp: disabled")?;
257 }
258
259 if *backup_code > 0 {
260 writeln!(f, "backup_code: enabled")?;
261 } else {
262 writeln!(f, "backup_code: disabled")?;
263 }
264
265 if !wan_labels.is_empty() {
266 writeln!(f, " ⚠️ warning - security keys are deprecated.")?;
268 writeln!(f, " ⚠️ you should re-enroll these to passkeys.")?;
269 writeln!(f, "security keys:")?;
270 for label in wan_labels {
271 writeln!(f, " * {label}")?;
272 }
273 write!(f, "")
274 } else {
275 write!(f, "")
276 }
277 }
278 }
279 }
280}
281
282#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
283pub struct PasskeyDetail {
284 pub uuid: Uuid,
285 pub tag: String,
286}
287
288#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
289pub struct BackupCodesView {
290 pub backup_codes: Vec<String>,
291}
292
293#[derive(Serialize, Deserialize, Debug, ToSchema, PartialEq, Eq, PartialOrd, Ord)]
294#[serde(rename_all = "lowercase")]
295pub enum PasswordFeedback {
296 UseAFewWordsAvoidCommonPhrases,
298 NoNeedForSymbolsDigitsOrUppercaseLetters,
299 AddAnotherWordOrTwo,
300 CapitalizationDoesntHelpVeryMuch,
301 AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase,
302 ReversedWordsArentMuchHarderToGuess,
303 PredictableSubstitutionsDontHelpVeryMuch,
304 UseALongerKeyboardPatternWithMoreTurns,
305 AvoidRepeatedWordsAndCharacters,
306 AvoidSequences,
307 AvoidRecentYears,
308 AvoidYearsThatAreAssociatedWithYou,
309 AvoidDatesAndYearsThatAreAssociatedWithYou,
310 StraightRowsOfKeysAreEasyToGuess,
312 ShortKeyboardPatternsAreEasyToGuess,
313 RepeatsLikeAaaAreEasyToGuess,
314 RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess,
315 ThisIsATop10Password,
316 ThisIsATop100Password,
317 ThisIsACommonPassword,
318 ThisIsSimilarToACommonlyUsedPassword,
319 SequencesLikeAbcAreEasyToGuess,
320 RecentYearsAreEasyToGuess,
321 AWordByItselfIsEasyToGuess,
322 DatesAreOftenEasyToGuess,
323 NamesAndSurnamesByThemselvesAreEasyToGuess,
324 CommonNamesAndSurnamesAreEasyToGuess,
325 TooShort(u32),
327 BadListed,
328 DontReusePasswords,
329}
330
331impl fmt::Display for PasswordFeedback {
333 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334 match self {
335 PasswordFeedback::AddAnotherWordOrTwo => write!(f, "Add another word or two."),
336 PasswordFeedback::AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase => write!(
337 f,
338 "All uppercase is almost as easy to guess as all lowercase."
339 ),
340 PasswordFeedback::AvoidDatesAndYearsThatAreAssociatedWithYou => write!(
341 f,
342 "Avoid dates and years that are associated with you or your account."
343 ),
344 PasswordFeedback::AvoidRecentYears => write!(f, "Avoid recent years."),
345 PasswordFeedback::AvoidRepeatedWordsAndCharacters => {
346 write!(f, "Avoid repeated words and characters.")
347 }
348 PasswordFeedback::AvoidSequences => write!(f, "Avoid sequences of characters."),
349 PasswordFeedback::AvoidYearsThatAreAssociatedWithYou => {
350 write!(f, "Avoid years that are associated with you.")
351 }
352 PasswordFeedback::AWordByItselfIsEasyToGuess => {
353 write!(f, "A word by itself is easy to guess.")
354 }
355 PasswordFeedback::BadListed => write!(
356 f,
357 "This password has been compromised or otherwise blocked and can not be used."
358 ),
359 PasswordFeedback::CapitalizationDoesntHelpVeryMuch => {
360 write!(f, "Capitalization doesn't help very much.")
361 }
362 PasswordFeedback::CommonNamesAndSurnamesAreEasyToGuess => {
363 write!(f, "Common names and surnames are easy to guess.")
364 }
365 PasswordFeedback::DatesAreOftenEasyToGuess => {
366 write!(f, "Dates are often easy to guess.")
367 }
368 PasswordFeedback::DontReusePasswords => {
369 write!(
370 f,
371 "Don't reuse passwords that already exist on your account"
372 )
373 }
374 PasswordFeedback::NamesAndSurnamesByThemselvesAreEasyToGuess => {
375 write!(f, "Names and surnames by themselves are easy to guess.")
376 }
377 PasswordFeedback::NoNeedForSymbolsDigitsOrUppercaseLetters => {
378 write!(f, "No need for symbols, digits or upper-case letters.")
379 }
380 PasswordFeedback::PredictableSubstitutionsDontHelpVeryMuch => {
381 write!(f, "Predictable substitutions don't help very much.")
382 }
383 PasswordFeedback::RecentYearsAreEasyToGuess => {
384 write!(f, "Recent years are easy to guess.")
385 }
386 PasswordFeedback::RepeatsLikeAaaAreEasyToGuess => {
387 write!(f, "Repeats like 'aaa' are easy to guess.")
388 }
389 PasswordFeedback::RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess => write!(
390 f,
391 "Repeats like abcabcabc are only slightly harder to guess."
392 ),
393 PasswordFeedback::ReversedWordsArentMuchHarderToGuess => {
394 write!(f, "Reversed words aren't much harder to guess.")
395 }
396 PasswordFeedback::SequencesLikeAbcAreEasyToGuess => {
397 write!(f, "Sequences like 'abc' are easy to guess.")
398 }
399 PasswordFeedback::ShortKeyboardPatternsAreEasyToGuess => {
400 write!(f, "Short keyboard patterns are easy to guess.")
401 }
402 PasswordFeedback::StraightRowsOfKeysAreEasyToGuess => {
403 write!(f, "Straight rows of keys are easy to guess.")
404 }
405 PasswordFeedback::ThisIsACommonPassword => write!(f, "This is a common password."),
406 PasswordFeedback::ThisIsATop100Password => write!(f, "This is a top 100 password."),
407 PasswordFeedback::ThisIsATop10Password => write!(f, "This is a top 10 password."),
408 PasswordFeedback::ThisIsSimilarToACommonlyUsedPassword => {
409 write!(f, "This is similar to a commonly used password.")
410 }
411 PasswordFeedback::TooShort(minlength) => write!(
412 f,
413 "Password was too short, needs to be at least {minlength} characters long."
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 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}