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