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