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