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(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 BadListed,
340 DontReusePasswords,
341}
342
343impl fmt::Display for PasswordFeedback {
345 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
346 match self {
347 PasswordFeedback::AddAnotherWordOrTwo => write!(f, "Add another word or two."),
348 PasswordFeedback::AllUppercaseIsAlmostAsEasyToGuessAsAllLowercase => write!(
349 f,
350 "All uppercase is almost as easy to guess as all lowercase."
351 ),
352 PasswordFeedback::AvoidDatesAndYearsThatAreAssociatedWithYou => write!(
353 f,
354 "Avoid dates and years that are associated with you or your account."
355 ),
356 PasswordFeedback::AvoidRecentYears => write!(f, "Avoid recent years."),
357 PasswordFeedback::AvoidRepeatedWordsAndCharacters => {
358 write!(f, "Avoid repeated words and characters.")
359 }
360 PasswordFeedback::AvoidSequences => write!(f, "Avoid sequences of characters."),
361 PasswordFeedback::AvoidYearsThatAreAssociatedWithYou => {
362 write!(f, "Avoid years that are associated with you.")
363 }
364 PasswordFeedback::AWordByItselfIsEasyToGuess => {
365 write!(f, "A word by itself is easy to guess.")
366 }
367 PasswordFeedback::BadListed => write!(
368 f,
369 "This password has been compromised or otherwise blocked and can not be used."
370 ),
371 PasswordFeedback::CapitalizationDoesntHelpVeryMuch => {
372 write!(f, "Capitalization doesn't help very much.")
373 }
374 PasswordFeedback::CommonNamesAndSurnamesAreEasyToGuess => {
375 write!(f, "Common names and surnames are easy to guess.")
376 }
377 PasswordFeedback::DatesAreOftenEasyToGuess => {
378 write!(f, "Dates are often easy to guess.")
379 }
380 PasswordFeedback::DontReusePasswords => {
381 write!(
382 f,
383 "Don't reuse passwords that already exist on your account"
384 )
385 }
386 PasswordFeedback::NamesAndSurnamesByThemselvesAreEasyToGuess => {
387 write!(f, "Names and surnames by themselves are easy to guess.")
388 }
389 PasswordFeedback::NoNeedForSymbolsDigitsOrUppercaseLetters => {
390 write!(f, "No need for symbols, digits or upper-case letters.")
391 }
392 PasswordFeedback::PredictableSubstitutionsDontHelpVeryMuch => {
393 write!(f, "Predictable substitutions don't help very much.")
394 }
395 PasswordFeedback::RecentYearsAreEasyToGuess => {
396 write!(f, "Recent years are easy to guess.")
397 }
398 PasswordFeedback::RepeatsLikeAaaAreEasyToGuess => {
399 write!(f, "Repeats like 'aaa' are easy to guess.")
400 }
401 PasswordFeedback::RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess => write!(
402 f,
403 "Repeats like abcabcabc are only slightly harder to guess."
404 ),
405 PasswordFeedback::ReversedWordsArentMuchHarderToGuess => {
406 write!(f, "Reversed words aren't much harder to guess.")
407 }
408 PasswordFeedback::SequencesLikeAbcAreEasyToGuess => {
409 write!(f, "Sequences like 'abc' are easy to guess.")
410 }
411 PasswordFeedback::ShortKeyboardPatternsAreEasyToGuess => {
412 write!(f, "Short keyboard patterns are easy to guess.")
413 }
414 PasswordFeedback::StraightRowsOfKeysAreEasyToGuess => {
415 write!(f, "Straight rows of keys are easy to guess.")
416 }
417 PasswordFeedback::ThisIsACommonPassword => write!(f, "This is a common password."),
418 PasswordFeedback::ThisIsATop100Password => write!(f, "This is a top 100 password."),
419 PasswordFeedback::ThisIsATop10Password => write!(f, "This is a top 10 password."),
420 PasswordFeedback::ThisIsSimilarToACommonlyUsedPassword => {
421 write!(f, "This is similar to a commonly used password.")
422 }
423 PasswordFeedback::TooShort(minlength) => write!(
424 f,
425 "Password was too short, needs to be at least {minlength} characters long."
426 ),
427 PasswordFeedback::UseAFewWordsAvoidCommonPhrases => {
428 write!(f, "Use a few words and avoid common phrases.")
429 }
430 PasswordFeedback::UseALongerKeyboardPatternWithMoreTurns => {
431 write!(
432 f,
433 "The password included keyboard patterns across too much of a single row."
434 )
435 }
436 }
437 }
438}
439
440#[cfg(test)]
441mod tests {
442 use super::{TotpAlgo, TotpSecret};
443
444 #[test]
445 fn totp_to_string() {
446 let totp = TotpSecret {
447 accountname: "william".to_string(),
448 issuer: "blackhats".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 assert_eq!(s,"otpauth://totp/blackhats:william?secret=VK54ZXI&issuer=blackhats&algorithm=SHA256&digits=6&period=30");
456
457 let totp = TotpSecret {
459 accountname: "william:%3A".to_string(),
460 issuer: "blackhats australia".to_string(),
461 secret: vec![0xaa, 0xbb, 0xcc, 0xdd],
462 step: 30,
463 algo: TotpAlgo::Sha256,
464 digits: 6,
465 };
466 let s = totp.to_uri();
467 println!("{s}");
468 assert_eq!(s,"otpauth://totp/blackhats%20australia:william%3A%253A?secret=VK54ZXI&issuer=blackhats%20australia&algorithm=SHA256&digits=6&period=30");
469 }
470}