Skip to main content

sspi/
auth_identity.rs

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/// Enumeration of the supported [User Name Formats].
25///
26/// [User Name Formats]: https://learn.microsoft.com/en-us/windows/win32/secauthn/user-name-formats
27#[derive(Debug, Clone, Copy, Eq, PartialEq)]
28pub enum UserNameFormat {
29    /// [User principal name] (UPN) format is used to specify an Internet-style name, such as UserName@Example.Microsoft.com.
30    ///
31    /// [User principal name]: https://learn.microsoft.com/en-us/windows/win32/secauthn/user-name-formats#user-principal-name
32    UserPrincipalName,
33    /// The [down-level logon name] format is used to specify a domain and a user account in that domain, for example, DOMAIN\UserName.
34    ///
35    /// [down-level logon name]: https://learn.microsoft.com/en-us/windows/win32/secauthn/user-name-formats#down-level-logon-name
36    DownLevelLogonName,
37}
38
39/// A username formatted as either UPN or Down-Level Logon Name
40#[derive(Debug, Clone, Eq, PartialEq)]
41pub struct Username {
42    value: String,
43    format: UserNameFormat,
44    sep_idx: Option<usize>,
45}
46
47impl Username {
48    /// Builds a user principal name from an account name and an UPN suffix
49    pub fn new_upn(account_name: &str, upn_suffix: &str) -> Result<Self, UsernameError> {
50        // NOTE: AD usernames may contain `@`
51        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    /// Builds a down-level logon name from an account name and a NetBIOS domain name
67    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    /// Attempts to guess the right name format for the account name/domain combo
84    ///
85    /// If no netbios domain name is provided, or if it is an empty string, the username will
86    /// be parsed as either a user principal name or a down-level logon name.
87    ///
88    /// It falls back to a down-level logon name when the format can’t be guessed.
89    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    /// Parses the value in order to find if the value is a user principal name or a down-level logon name
99    ///
100    /// If there is no `\` or `@` separator, the value is considered to be a down-level logon name with
101    /// an empty NetBIOS domain.
102    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    /// Returns the internal representation, as-is
117    pub fn inner(&self) -> &str {
118        &self.value
119    }
120
121    /// Returns the [`UserNameFormat`] for this username
122    pub fn format(&self) -> UserNameFormat {
123        self.format
124    }
125
126    /// May return an UPN suffix or NetBIOS domain name depending on the internal format
127    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    /// Returns the account name
135    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/// Allows you to pass a particular user name and password to the run-time library for the purpose of authentication
148///
149/// # MSDN
150///
151/// * [SEC_WINNT_AUTH_IDENTITY_W structure](https://docs.microsoft.com/en-us/windows/win32/api/sspi/ns-sspi-sec_winnt_auth_identity_w)
152#[derive(Debug, Clone, Eq, PartialEq)]
153pub struct AuthIdentity {
154    pub username: Username,
155    pub password: Secret<String>,
156}
157
158/// Auth identity buffers for password-based logon.
159#[derive(Clone, Eq, PartialEq, Default)]
160pub struct AuthIdentityBuffers {
161    /// Username.
162    ///
163    /// Must be UTF-16 encoded.
164    pub user: Utf16String,
165    /// Domain.
166    ///
167    /// Must be UTF-16 encoded.
168    pub domain: Utf16String,
169    /// Password.
170    ///
171    /// Must be UTF-16 encoded.
172    ///
173    /// If the password is an NT hash, it should be prefixed with [`NTLM_HASH_PREFIX`](crate::NTLM_HASH_PREFIX) followed by the hash in hexadecimal format.
174    ///
175    /// See [`NtlmHash`](crate::NtlmHash) for more details.
176    pub password: Secret<ZeroizedUtf16String>,
177}
178
179impl AuthIdentityBuffers {
180    /// Creates a new [AuthIdentityBuffers] object based on provided credentials.
181    ///
182    /// Provided credentials must be UTF-16 encoded.
183    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    /// Creates a new [AuthIdentityBuffers] object based on UTF-8 credentials.
196    ///
197    /// It converts the provided credentials to UTF-16 byte vectors automatically.
198    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    /// Creates a new [AuthIdentityBuffers] object based on UTF-8 username and domain, and NT hash for the password.
207    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    /// DER-encoded x509 certificate.
287    #[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    /// Smart card type.
340    #[derive(Clone, Eq, PartialEq, Debug)]
341    pub enum SmartCardType {
342        /// Emulated smart card.
343        ///
344        /// No real device is used. All smart card functionality is emulated using the [winscard] crate.
345        Emulated {
346            /// Emulated smart card PIN code.
347            ///
348            /// This is smart card PIN code, not the PIN code provided by the user.
349            scard_pin: Secret<Vec<u8>>,
350        },
351        #[cfg(not(target_arch = "wasm32"))]
352        /// System-provided smart card.
353        ///
354        /// Real smart card device in use.
355        SystemProvided {
356            /// Path to the PKCS11 module.
357            pkcs11_module_path: PathBuf,
358        },
359        /// System-provided smart card, but the Windows native API will be used for accessing smart card.
360        ///
361        /// Available only on Windows.
362        #[cfg(target_os = "windows")]
363        WindowsNative,
364    }
365
366    /// Represents raw data needed for smart card authentication
367    #[derive(Clone, Eq, PartialEq, Debug)]
368    pub struct SmartCardIdentityBuffers {
369        /// UTF-16 encoded username
370        pub username: Utf16String,
371        /// DER-encoded X509 certificate
372        pub certificate: CertificateRaw,
373        /// UTF-16 encoded smart card name
374        pub card_name: Option<NonEmpty<Utf16String>>,
375        /// UTF-16 encoded smart card reader name
376        pub reader_name: Utf16String,
377        /// UTF-16 encoded smart card key container name
378        pub container_name: Option<NonEmpty<Utf16String>>,
379        /// UTF-16 encoded smart card CSP name
380        pub csp_name: Utf16String,
381        /// UTF-16 encoded smart card PIN code
382        pub pin: Secret<ZeroizedUtf16String>,
383        /// UTF-16 string with PEM-encoded RSA 2048-bit private key
384        pub private_key_pem: Option<NonEmpty<Utf16String>>,
385        /// Smart card type.
386        pub scard_type: SmartCardType,
387    }
388
389    /// Represents data needed for smart card authentication
390    #[derive(Debug, Clone, PartialEq)]
391    pub struct SmartCardIdentity {
392        /// Username
393        pub username: String,
394        /// X509 certificate
395        pub certificate: Certificate,
396        /// Smart card reader name
397        pub reader_name: String,
398        /// Smart card name
399        pub card_name: Option<String>,
400        /// Smart card key container name
401        pub container_name: Option<String>,
402        /// Smart card CSP name
403        pub csp_name: String,
404        /// ASCII encoded mart card PIN code
405        pub pin: Secret<Vec<u8>>,
406        /// RSA 2048-bit private key
407        pub private_key: Option<SecretPrivateKey>,
408        /// Smart card type.
409        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/// Generic enum that encapsulates raw credentials for any type of authentication
483#[derive(Clone, Eq, PartialEq, Debug)]
484pub enum CredentialsBuffers {
485    /// Raw auth identity buffers for the password based authentication
486    AuthIdentity(AuthIdentityBuffers),
487    #[cfg(feature = "scard")]
488    /// Raw smart card identity buffers for the smart card based authentication
489    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/// Generic enum that encapsulates credentials for any type of authentication
527#[derive(Clone, PartialEq, Debug)]
528pub enum Credentials {
529    /// Auth identity for the password based authentication
530    AuthIdentity(AuthIdentity),
531    /// Smart card identity for the smart card based authentication
532    #[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            // A down-level user name can't contain a @ in the account name
600            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}