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)]
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 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 }
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 pub spn: String,
183 pub displayname: String,
184 pub ext_cred_portal: CUExtPortal,
185 pub mfaregstate: CURegState,
187 pub can_commit: bool,
189 pub warnings: Vec<CURegWarning>,
190 pub primary: Option<CredentialDetail>,
191 pub primary_state: CUCredState,
192 pub passkeys: Vec<PasskeyDetail>,
193 pub passkeys_state: CUCredState,
194 pub attested_passkeys: Vec<PasskeyDetail>,
195 pub attested_passkeys_state: CUCredState,
196 pub attested_passkeys_allowed_devices: Vec<String>,
197
198 pub unixcred: Option<CredentialDetail>,
199 pub unixcred_state: CUCredState,
200
201 #[schema(value_type = BTreeMap<String, Value>)]
202 pub sshkeys: BTreeMap<String, SshPublicKey>,
203 pub sshkeys_state: CUCredState,
204}
205
206#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
207pub struct CredentialStatus {
208 pub creds: Vec<CredentialDetail>,
209}
210
211impl fmt::Display for CredentialStatus {
212 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213 for cred in &self.creds {
214 writeln!(f, "---")?;
215 cred.fmt(f)?;
216 }
217 writeln!(f, "---")
218 }
219}
220
221#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
222pub enum CredentialDetailType {
223 Password,
224 GeneratedPassword,
225 Passkey(Vec<String>),
226 PasswordMfa(Vec<String>, Vec<String>, usize),
228}
229
230#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
231pub struct CredentialDetail {
232 pub uuid: Uuid,
233 pub type_: CredentialDetailType,
234}
235
236impl fmt::Display for CredentialDetail {
237 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238 writeln!(f, "uuid: {}", self.uuid)?;
239 match &self.type_ {
246 CredentialDetailType::Password => writeln!(f, "password: set"),
247 CredentialDetailType::GeneratedPassword => writeln!(f, "generated password: set"),
248 CredentialDetailType::Passkey(labels) => {
249 if labels.is_empty() {
250 writeln!(f, "passkeys: none registered")
251 } else {
252 writeln!(f, "passkeys:")?;
253 for label in labels {
254 writeln!(f, " * {label}")?;
255 }
256 write!(f, "")
257 }
258 }
259 CredentialDetailType::PasswordMfa(totp_labels, wan_labels, backup_code) => {
260 writeln!(f, "password: set")?;
261
262 if !totp_labels.is_empty() {
263 writeln!(f, "totp:")?;
264 for label in totp_labels {
265 writeln!(f, " * {label}")?;
266 }
267 } else {
268 writeln!(f, "totp: disabled")?;
269 }
270
271 if *backup_code > 0 {
272 writeln!(f, "backup_code: enabled")?;
273 } else {
274 writeln!(f, "backup_code: disabled")?;
275 }
276
277 if !wan_labels.is_empty() {
278 writeln!(f, " ⚠️ warning - security keys are deprecated.")?;
280 writeln!(f, " ⚠️ you should re-enroll these to passkeys.")?;
281 writeln!(f, "security keys:")?;
282 for label in wan_labels {
283 writeln!(f, " * {label}")?;
284 }
285 write!(f, "")
286 } else {
287 write!(f, "")
288 }
289 }
290 }
291 }
292}
293
294#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
295pub struct PasskeyDetail {
296 pub uuid: Uuid,
297 pub tag: String,
298}
299
300#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
301pub struct BackupCodesView {
302 pub backup_codes: Vec<String>,
303}
304
305#[derive(Clone, Serialize, Deserialize, Debug, ToSchema, PartialEq, Eq, PartialOrd, Ord)]
306#[serde(rename_all = "lowercase")]
307pub enum PasswordFeedback {
308 UseAFewWordsAvoidCommonPhrases,
310 NoNeedForSymbolsDigitsOrUppercaseLetters,
311 AddAnotherWordOrTwo,
312 CapitalizationDoesntHelpVeryMuch,
313 AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase,
314 ReversedWordsArentMuchHarderToGuess,
315 PredictableSubstitutionsDontHelpVeryMuch,
316 UseALongerKeyboardPatternWithMoreTurns,
317 AvoidRepeatedWordsAndCharacters,
318 AvoidSequences,
319 AvoidRecentYears,
320 AvoidYearsThatAreAssociatedWithYou,
321 AvoidDatesAndYearsThatAreAssociatedWithYou,
322 StraightRowsOfKeysAreEasyToGuess,
324 ShortKeyboardPatternsAreEasyToGuess,
325 RepeatsLikeAaaAreEasyToGuess,
326 RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess,
327 ThisIsATop10Password,
328 ThisIsATop100Password,
329 ThisIsACommonPassword,
330 ThisIsSimilarToACommonlyUsedPassword,
331 SequencesLikeAbcAreEasyToGuess,
332 RecentYearsAreEasyToGuess,
333 AWordByItselfIsEasyToGuess,
334 DatesAreOftenEasyToGuess,
335 NamesAndSurnamesByThemselvesAreEasyToGuess,
336 CommonNamesAndSurnamesAreEasyToGuess,
337 TooShort(u32),
339 TooLong(u32),
340 BadListed,
341 DontReusePasswords,
342}
343
344impl fmt::Display for PasswordFeedback {
346 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347 match self {
348 PasswordFeedback::AddAnotherWordOrTwo => write!(f, "Add another word or two."),
349 PasswordFeedback::AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase => write!(
350 f,
351 "All uppercase is almost as easy to guess as all lowercase."
352 ),
353 PasswordFeedback::AvoidDatesAndYearsThatAreAssociatedWithYou => write!(
354 f,
355 "Avoid dates and years that are associated with you or your account."
356 ),
357 PasswordFeedback::AvoidRecentYears => write!(f, "Avoid recent years."),
358 PasswordFeedback::AvoidRepeatedWordsAndCharacters => {
359 write!(f, "Avoid repeated words and characters.")
360 }
361 PasswordFeedback::AvoidSequences => write!(f, "Avoid sequences of characters."),
362 PasswordFeedback::AvoidYearsThatAreAssociatedWithYou => {
363 write!(f, "Avoid years that are associated with you.")
364 }
365 PasswordFeedback::AWordByItselfIsEasyToGuess => {
366 write!(f, "A word by itself is easy to guess.")
367 }
368 PasswordFeedback::BadListed => write!(
369 f,
370 "This password has been compromised or otherwise blocked and can not be used."
371 ),
372 PasswordFeedback::CapitalizationDoesntHelpVeryMuch => {
373 write!(f, "Capitalization doesn't help very much.")
374 }
375 PasswordFeedback::CommonNamesAndSurnamesAreEasyToGuess => {
376 write!(f, "Common names and surnames are easy to guess.")
377 }
378 PasswordFeedback::DatesAreOftenEasyToGuess => {
379 write!(f, "Dates are often easy to guess.")
380 }
381 PasswordFeedback::DontReusePasswords => {
382 write!(
383 f,
384 "Don't reuse passwords that already exist on your account"
385 )
386 }
387 PasswordFeedback::NamesAndSurnamesByThemselvesAreEasyToGuess => {
388 write!(f, "Names and surnames by themselves are easy to guess.")
389 }
390 PasswordFeedback::NoNeedForSymbolsDigitsOrUppercaseLetters => {
391 write!(f, "No need for symbols, digits or upper-case letters.")
392 }
393 PasswordFeedback::PredictableSubstitutionsDontHelpVeryMuch => {
394 write!(f, "Predictable substitutions don't help very much.")
395 }
396 PasswordFeedback::RecentYearsAreEasyToGuess => {
397 write!(f, "Recent years are easy to guess.")
398 }
399 PasswordFeedback::RepeatsLikeAaaAreEasyToGuess => {
400 write!(f, "Repeats like 'aaa' are easy to guess.")
401 }
402 PasswordFeedback::RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess => write!(
403 f,
404 "Repeats like abcabcabc are only slightly harder to guess."
405 ),
406 PasswordFeedback::ReversedWordsArentMuchHarderToGuess => {
407 write!(f, "Reversed words aren't much harder to guess.")
408 }
409 PasswordFeedback::SequencesLikeAbcAreEasyToGuess => {
410 write!(f, "Sequences like 'abc' are easy to guess.")
411 }
412 PasswordFeedback::ShortKeyboardPatternsAreEasyToGuess => {
413 write!(f, "Short keyboard patterns are easy to guess.")
414 }
415 PasswordFeedback::StraightRowsOfKeysAreEasyToGuess => {
416 write!(f, "Straight rows of keys are easy to guess.")
417 }
418 PasswordFeedback::ThisIsACommonPassword => write!(f, "This is a common password."),
419 PasswordFeedback::ThisIsATop100Password => write!(f, "This is a top 100 password."),
420 PasswordFeedback::ThisIsATop10Password => write!(f, "This is a top 10 password."),
421 PasswordFeedback::ThisIsSimilarToACommonlyUsedPassword => {
422 write!(f, "This is similar to a commonly used password.")
423 }
424 PasswordFeedback::TooShort(minlength) => write!(
425 f,
426 "Password was too short, needs to be at least {minlength} characters long."
427 ),
428 PasswordFeedback::TooLong(maxlength) => write!(
429 f,
430 "Password was too long, must not be greater than {maxlength} characters long."
431 ),
432 PasswordFeedback::UseAFewWordsAvoidCommonPhrases => {
433 write!(f, "Use a few words and avoid common phrases.")
434 }
435 PasswordFeedback::UseALongerKeyboardPatternWithMoreTurns => {
436 write!(
437 f,
438 "The password included keyboard patterns across too much of a single row."
439 )
440 }
441 }
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 use super::{TotpAlgo, TotpSecret};
448
449 #[test]
450 fn totp_to_string() {
451 let totp = TotpSecret {
452 accountname: "william".to_string(),
453 issuer: "blackhats".to_string(),
454 secret: vec![0xaa, 0xbb, 0xcc, 0xdd],
455 step: 30,
456 algo: TotpAlgo::Sha256,
457 digits: 6,
458 };
459 let s = totp.to_uri();
460 assert_eq!(s,"otpauth://totp/blackhats:william?secret=VK54ZXI&issuer=blackhats&algorithm=SHA256&digits=6&period=30");
461
462 let totp = TotpSecret {
464 accountname: "william:%3A".to_string(),
465 issuer: "blackhats australia".to_string(),
466 secret: vec![0xaa, 0xbb, 0xcc, 0xdd],
467 step: 30,
468 algo: TotpAlgo::Sha256,
469 digits: 6,
470 };
471 let s = totp.to_uri();
472 println!("{s}");
473 assert_eq!(s,"otpauth://totp/blackhats%20australia:william%3A%253A?secret=VK54ZXI&issuer=blackhats%20australia&algorithm=SHA256&digits=6&period=30");
474 }
475}