1use std::fmt;
2use std::ops::Not;
3
4use crate::utf16string::ZeroizedUtf16String;
5use crate::{Error, Secret, Utf16String, Utf16StringExt};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
8pub enum UsernameError {
9 MixedFormat,
10 InvalidUtf16,
11}
12
13impl std::error::Error for UsernameError {}
14
15impl fmt::Display for UsernameError {
16 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17 match self {
18 UsernameError::MixedFormat => write!(f, "mixed username format"),
19 UsernameError::InvalidUtf16 => write!(f, "invalid UTF-16 string"),
20 }
21 }
22}
23
24#[derive(Debug, Clone, Copy, Eq, PartialEq)]
28pub enum UserNameFormat {
29 UserPrincipalName,
33 DownLevelLogonName,
37}
38
39#[derive(Debug, Clone, Eq, PartialEq)]
41pub struct Username {
42 value: String,
43 format: UserNameFormat,
44 sep_idx: Option<usize>,
45}
46
47impl Username {
48 pub fn new_upn(account_name: &str, upn_suffix: &str) -> Result<Self, UsernameError> {
50 if account_name.contains(['\\']) {
52 return Err(UsernameError::MixedFormat);
53 }
54
55 if upn_suffix.contains(['\\', '@']) {
56 return Err(UsernameError::MixedFormat);
57 }
58
59 Ok(Self {
60 value: format!("{account_name}@{upn_suffix}"),
61 format: UserNameFormat::UserPrincipalName,
62 sep_idx: Some(account_name.len()),
63 })
64 }
65
66 pub fn new_down_level_logon_name(account_name: &str, netbios_domain_name: &str) -> Result<Self, UsernameError> {
68 if account_name.contains(['\\', '@']) {
69 return Err(UsernameError::MixedFormat);
70 }
71
72 if netbios_domain_name.contains(['\\', '@']) {
73 return Err(UsernameError::MixedFormat);
74 }
75
76 Ok(Self {
77 value: format!("{netbios_domain_name}\\{account_name}"),
78 format: UserNameFormat::DownLevelLogonName,
79 sep_idx: Some(netbios_domain_name.len()),
80 })
81 }
82
83 pub fn new(account_name: &str, netbios_domain_name: Option<&str>) -> Result<Self, UsernameError> {
90 match netbios_domain_name {
91 Some(netbios_domain_name) if !netbios_domain_name.is_empty() => {
92 Self::new_down_level_logon_name(account_name, netbios_domain_name)
93 }
94 _ => Self::parse(account_name),
95 }
96 }
97
98 pub fn parse(value: &str) -> Result<Self, UsernameError> {
103 match (value.split_once('\\'), value.rsplit_once('@')) {
104 (None, None) => Ok(Self {
105 value: value.to_owned(),
106 format: UserNameFormat::DownLevelLogonName,
107 sep_idx: None,
108 }),
109 (Some((netbios_domain_name, account_name)), _) => {
110 Self::new_down_level_logon_name(account_name, netbios_domain_name)
111 }
112 (_, Some((account_name, upn_suffix))) => Self::new_upn(account_name, upn_suffix),
113 }
114 }
115
116 pub fn inner(&self) -> &str {
118 &self.value
119 }
120
121 pub fn format(&self) -> UserNameFormat {
123 self.format
124 }
125
126 pub fn domain_name(&self) -> Option<&str> {
128 self.sep_idx.map(|idx| match self.format {
129 UserNameFormat::UserPrincipalName => &self.value[idx + 1..],
130 UserNameFormat::DownLevelLogonName => &self.value[..idx],
131 })
132 }
133
134 pub fn account_name(&self) -> &str {
136 if let Some(idx) = self.sep_idx {
137 match self.format {
138 UserNameFormat::UserPrincipalName => &self.value[..idx],
139 UserNameFormat::DownLevelLogonName => &self.value[idx + 1..],
140 }
141 } else {
142 &self.value
143 }
144 }
145}
146
147#[derive(Debug, Clone, Eq, PartialEq)]
153pub struct AuthIdentity {
154 pub username: Username,
155 pub password: Secret<String>,
156}
157
158#[derive(Clone, Eq, PartialEq, Default)]
160pub struct AuthIdentityBuffers {
161 pub user: Utf16String,
165 pub domain: Utf16String,
169 pub password: Secret<ZeroizedUtf16String>,
177}
178
179impl AuthIdentityBuffers {
180 pub fn new(user: Utf16String, domain: Utf16String, password: Utf16String) -> Self {
184 Self {
185 user,
186 domain,
187 password: ZeroizedUtf16String(password).into(),
188 }
189 }
190
191 pub fn is_empty(&self) -> bool {
192 self.user.is_empty()
193 }
194
195 pub fn from_utf8(user: &str, domain: &str, password: &str) -> Self {
199 Self {
200 user: user.into(),
201 domain: domain.into(),
202 password: ZeroizedUtf16String(Utf16String::from(password)).into(),
203 }
204 }
205
206 pub fn from_utf8_with_hash(user: &str, domain: &str, nt_hash: &crate::NtlmHash) -> Self {
208 Self {
209 user: user.into(),
210 domain: domain.into(),
211 password: ZeroizedUtf16String(Utf16String::from(nt_hash.to_sspi_password())).into(),
212 }
213 }
214}
215
216impl fmt::Debug for AuthIdentityBuffers {
217 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218 write!(f, "AuthIdentityBuffers {{ user: 0x")?;
219 self.user
220 .as_bytes_le()
221 .iter()
222 .try_for_each(|byte| write!(f, "{byte:02X}"))?;
223 write!(f, ", domain: 0x")?;
224 self.domain
225 .as_bytes_le()
226 .iter()
227 .try_for_each(|byte| write!(f, "{byte:02X}"))?;
228 write!(f, ", password: {:?} }}", self.password)?;
229
230 Ok(())
231 }
232}
233
234impl From<AuthIdentity> for AuthIdentityBuffers {
235 fn from(credentials: AuthIdentity) -> Self {
236 let password: &str = credentials.password.as_ref().as_ref();
237
238 Self {
239 user: credentials.username.account_name().into(),
240 domain: credentials.username.domain_name().unwrap_or_default().into(),
241 password: ZeroizedUtf16String(password.into()).into(),
242 }
243 }
244}
245
246impl TryFrom<&AuthIdentityBuffers> for AuthIdentity {
247 type Error = UsernameError;
248
249 fn try_from(credentials_buffers: &AuthIdentityBuffers) -> Result<Self, Self::Error> {
250 let account_name = credentials_buffers.user.to_string();
251
252 let domain_name = credentials_buffers
253 .domain
254 .is_empty()
255 .not()
256 .then(|| credentials_buffers.domain.to_string());
257
258 let username = Username::new(&account_name, domain_name.as_deref())?;
259 let password = credentials_buffers.password.as_ref().as_ref().to_string().into();
260
261 Ok(Self { username, password })
262 }
263}
264
265impl TryFrom<AuthIdentityBuffers> for AuthIdentity {
266 type Error = UsernameError;
267
268 fn try_from(credentials_buffers: AuthIdentityBuffers) -> Result<Self, Self::Error> {
269 AuthIdentity::try_from(&credentials_buffers)
270 }
271}
272
273#[cfg(feature = "scard")]
274mod scard_credentials {
275 #[cfg(not(target_arch = "wasm32"))]
276 use std::path::PathBuf;
277
278 use picky::key::PrivateKey;
279 use picky_asn1_der::Asn1DerError;
280 use picky_asn1_x509::Certificate;
281
282 use crate::secret::SecretPrivateKey;
283 use crate::utf16string::ZeroizedUtf16String;
284 use crate::{Error, ErrorKind, NonEmpty, Secret, Utf16String};
285
286 #[derive(Clone, Eq, PartialEq, Debug)]
288 pub struct CertificateRaw(Vec<u8>);
289
290 impl AsRef<[u8]> for CertificateRaw {
291 fn as_ref(&self) -> &[u8] {
292 self.0.as_ref()
293 }
294 }
295
296 impl TryFrom<Vec<u8>> for CertificateRaw {
297 type Error = Asn1DerError;
298
299 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
300 let _: Certificate = picky_asn1_der::from_bytes(value.as_ref())?;
301 Ok(Self(value))
302 }
303 }
304
305 impl From<CertificateRaw> for Vec<u8> {
306 fn from(value: CertificateRaw) -> Self {
307 value.0
308 }
309 }
310
311 impl TryFrom<&Certificate> for CertificateRaw {
312 type Error = Asn1DerError;
313
314 fn try_from(value: &Certificate) -> Result<Self, Self::Error> {
315 picky_asn1_der::to_vec(value).map(Self)
316 }
317 }
318
319 impl TryFrom<Certificate> for CertificateRaw {
320 type Error = Asn1DerError;
321
322 fn try_from(value: Certificate) -> Result<Self, Self::Error> {
323 Self::try_from(&value)
324 }
325 }
326
327 impl From<&CertificateRaw> for Certificate {
328 fn from(value: &CertificateRaw) -> Self {
329 picky_asn1_der::from_bytes(&value.0).expect("value.0 is convertible to Certificate (checked on creation)")
330 }
331 }
332
333 impl From<CertificateRaw> for Certificate {
334 fn from(value: CertificateRaw) -> Self {
335 Self::from(&value)
336 }
337 }
338
339 #[derive(Clone, Eq, PartialEq, Debug)]
341 pub enum SmartCardType {
342 Emulated {
346 scard_pin: Secret<Vec<u8>>,
350 },
351 #[cfg(not(target_arch = "wasm32"))]
352 SystemProvided {
356 pkcs11_module_path: PathBuf,
358 },
359 #[cfg(target_os = "windows")]
363 WindowsNative,
364 }
365
366 #[derive(Clone, Eq, PartialEq, Debug)]
368 pub struct SmartCardIdentityBuffers {
369 pub username: Utf16String,
371 pub certificate: CertificateRaw,
373 pub card_name: Option<NonEmpty<Utf16String>>,
375 pub reader_name: Utf16String,
377 pub container_name: Option<NonEmpty<Utf16String>>,
379 pub csp_name: Utf16String,
381 pub pin: Secret<ZeroizedUtf16String>,
383 pub private_key_pem: Option<NonEmpty<Utf16String>>,
385 pub scard_type: SmartCardType,
387 }
388
389 #[derive(Debug, Clone, PartialEq)]
391 pub struct SmartCardIdentity {
392 pub username: String,
394 pub certificate: Certificate,
396 pub reader_name: String,
398 pub card_name: Option<String>,
400 pub container_name: Option<String>,
402 pub csp_name: String,
404 pub pin: Secret<Vec<u8>>,
406 pub private_key: Option<SecretPrivateKey>,
408 pub scard_type: SmartCardType,
410 }
411
412 impl TryFrom<SmartCardIdentity> for SmartCardIdentityBuffers {
413 type Error = Error;
414
415 fn try_from(value: SmartCardIdentity) -> Result<Self, Self::Error> {
416 let private_key = if let Some(key) = value.private_key {
417 NonEmpty::new(Utf16String::from(key.as_ref().to_pem_str().map_err(|e| {
418 Error::new(
419 ErrorKind::InternalError,
420 format!("Unable to serialize a smart card private key: {e}"),
421 )
422 })?))
423 } else {
424 None
425 };
426
427 Ok(Self {
428 certificate: value.certificate.try_into()?,
429 reader_name: value.reader_name.into(),
430 pin: ZeroizedUtf16String(String::from_utf8_lossy(value.pin.as_ref()).as_ref().into()).into(),
431 username: value.username.into(),
432 card_name: value.card_name.and_then(|value| NonEmpty::new(value.into())),
433 container_name: value.container_name.and_then(|value| NonEmpty::new(value.into())),
434 csp_name: value.csp_name.into(),
435 private_key_pem: private_key,
436 scard_type: value.scard_type,
437 })
438 }
439 }
440
441 impl TryFrom<&SmartCardIdentityBuffers> for SmartCardIdentity {
442 type Error = Error;
443
444 fn try_from(value: &SmartCardIdentityBuffers) -> Result<Self, Self::Error> {
445 let private_key = if let Some(key) = &value.private_key_pem {
446 let pem_string = key.as_ref().to_string();
447
448 Some(SecretPrivateKey::new(PrivateKey::from_pem_str(&pem_string).map_err(
449 |e| {
450 Error::new(
451 ErrorKind::InternalError,
452 format!("Unable to create a PrivateKey from a PEM string: {e}"),
453 )
454 },
455 )?))
456 } else {
457 None
458 };
459
460 Ok(Self {
461 certificate: Certificate::from(&value.certificate),
462 reader_name: value.reader_name.to_string(),
463 pin: value.pin.as_ref().0.to_string().into_bytes().into(),
464 username: value.username.to_string(),
465 card_name: value.card_name.as_ref().map(NonEmpty::as_ref).map(ToString::to_string),
466 container_name: value
467 .container_name
468 .as_ref()
469 .map(NonEmpty::as_ref)
470 .map(ToString::to_string),
471 csp_name: value.csp_name.to_string(),
472 private_key,
473 scard_type: value.scard_type.clone(),
474 })
475 }
476 }
477}
478
479#[cfg(feature = "scard")]
480pub use self::scard_credentials::{CertificateRaw, SmartCardIdentity, SmartCardIdentityBuffers, SmartCardType};
481
482#[derive(Clone, Eq, PartialEq, Debug)]
484pub enum CredentialsBuffers {
485 AuthIdentity(AuthIdentityBuffers),
487 #[cfg(feature = "scard")]
488 SmartCard(SmartCardIdentityBuffers),
490}
491
492impl CredentialsBuffers {
493 pub fn into_auth_identity(self) -> Option<AuthIdentityBuffers> {
494 match self {
495 CredentialsBuffers::AuthIdentity(identity) => Some(identity),
496 #[cfg(feature = "scard")]
497 _ => None,
498 }
499 }
500
501 pub fn to_auth_identity(&self) -> Option<AuthIdentityBuffers> {
502 match self {
503 CredentialsBuffers::AuthIdentity(identity) => Some(identity.clone()),
504 #[cfg(feature = "scard")]
505 _ => None,
506 }
507 }
508
509 pub fn as_auth_identity(&self) -> Option<&AuthIdentityBuffers> {
510 match self {
511 CredentialsBuffers::AuthIdentity(identity) => Some(identity),
512 #[cfg(feature = "scard")]
513 _ => None,
514 }
515 }
516
517 pub fn as_mut_auth_identity(&mut self) -> Option<&mut AuthIdentityBuffers> {
518 match self {
519 CredentialsBuffers::AuthIdentity(identity) => Some(identity),
520 #[cfg(feature = "scard")]
521 _ => None,
522 }
523 }
524}
525
526#[derive(Clone, PartialEq, Debug)]
528pub enum Credentials {
529 AuthIdentity(AuthIdentity),
531 #[cfg(feature = "scard")]
533 SmartCard(Box<SmartCardIdentity>),
534}
535
536impl Credentials {
537 pub fn to_auth_identity(&self) -> Option<AuthIdentity> {
538 match self {
539 Credentials::AuthIdentity(identity) => Some(identity.clone()),
540 #[cfg(feature = "scard")]
541 _ => None,
542 }
543 }
544
545 pub fn auth_identity(self) -> Option<AuthIdentity> {
546 match self {
547 Credentials::AuthIdentity(identity) => Some(identity),
548 #[cfg(feature = "scard")]
549 _ => None,
550 }
551 }
552}
553
554#[cfg(feature = "scard")]
555impl From<SmartCardIdentity> for Credentials {
556 fn from(value: SmartCardIdentity) -> Self {
557 Self::SmartCard(Box::new(value))
558 }
559}
560
561impl From<AuthIdentity> for Credentials {
562 fn from(value: AuthIdentity) -> Self {
563 Self::AuthIdentity(value)
564 }
565}
566
567impl TryFrom<Credentials> for CredentialsBuffers {
568 type Error = Error;
569
570 fn try_from(value: Credentials) -> Result<Self, Self::Error> {
571 Ok(match value {
572 Credentials::AuthIdentity(identity) => Self::AuthIdentity(identity.into()),
573 #[cfg(feature = "scard")]
574 Credentials::SmartCard(identity) => Self::SmartCard((*identity).try_into()?),
575 })
576 }
577}
578
579#[cfg(test)]
580mod tests {
581 use proptest::prelude::*;
582
583 use super::*;
584
585 #[test]
586 fn username_format_conversion() {
587 proptest!(|(value in "[a-zA-Z0-9.]{1,3}@?\\\\?[a-zA-Z0-9.]{1,3}@?\\\\?[a-zA-Z0-9.]{1,3}")| {
588 let res = Username::parse(&value);
589 prop_assume!(res.is_ok());
590 let initial_username = res.unwrap();
591 assert_eq!(initial_username.inner(), value);
592
593 if let Some(domain_name) = initial_username.domain_name() {
594 let upn = Username::new_upn(initial_username.account_name(), domain_name).expect("UPN");
595 assert_eq!(upn.account_name(), initial_username.account_name());
596 assert_eq!(upn.domain_name(), initial_username.domain_name());
597 }
598
599 if !initial_username.account_name().contains('@') {
601 let netbios_name = Username::new(initial_username.account_name(), initial_username.domain_name()).expect("NetBIOS");
602 assert_eq!(netbios_name.format(), UserNameFormat::DownLevelLogonName);
603 assert_eq!(netbios_name.account_name(), initial_username.account_name());
604 assert_eq!(netbios_name.domain_name(), initial_username.domain_name());
605 }
606 })
607 }
608
609 fn check_round_trip_property(username: &Username) {
610 let round_trip = Username::parse(username.inner()).expect("round-trip parse");
611 assert_eq!(*username, round_trip);
612 }
613
614 #[test]
615 fn upn_round_trip() {
616 proptest!(|(account_name in "[a-zA-Z0-9@.]{1,3}", domain_name in "[a-z0-9.]{1,3}")| {
617 let username = Username::new_upn(&account_name, &domain_name).expect("UPN");
618
619 assert_eq!(username.account_name(), account_name);
620 assert_eq!(username.domain_name(), Some(domain_name.as_str()));
621 assert_eq!(username.format(), UserNameFormat::UserPrincipalName);
622
623 check_round_trip_property(&username);
624 })
625 }
626
627 #[test]
628 fn down_level_logon_name_round_trip() {
629 proptest!(|(account_name in "[a-zA-Z0-9.]{1,3}", domain_name in "[A-Z0-9.]{1,3}")| {
630 let username = Username::new_down_level_logon_name(&account_name, &domain_name).expect("down-level logon name");
631
632 assert_eq!(username.account_name(), account_name);
633 assert_eq!(username.domain_name(), Some(domain_name.as_str()));
634 assert_eq!(username.format(), UserNameFormat::DownLevelLogonName);
635
636 check_round_trip_property(&username);
637 })
638 }
639}