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