Skip to main content

rustsmb/
auth.rs

1//! Credential verification against the configured user database using the
2//! NTLM family of schemes ([MS-NLMP] §3.3).
3
4use smb_server_auth::crypto::{hmac_md5, nt_hash, ntlmv1_response};
5use smb_server_auth::ntlm::Type3;
6use std::collections::HashMap;
7
8/// Outcome of credential verification.
9#[derive(Debug, Clone)]
10pub struct AuthOutcome {
11    /// Credentials accepted.
12    pub ok: bool,
13    /// Principal was mapped to guest rather than a real account.
14    pub guest: bool,
15    /// Effective username.
16    pub user: String,
17    /// Exported SMB2 session key (16 bytes) when key exchange completed
18    /// against a verified secret ([MS-SMB2] §3.2.5.3.1). Signing keys are
19    /// derived from this; `None` disables signing for the session.
20    pub session_key: Option<[u8; 16]>,
21}
22
23/// Verify NTLMv2 (preferred), NTLMv1-with-ESS or LM responses.
24///
25/// `server.users` empty + `allow_guest` accepts any principal; otherwise only
26/// configured accounts (or explicit anonymous) succeed.
27#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // NTLM response/proof field widths ([MS-NLMP])
28pub fn authenticate_ntlmssp(
29    users: &HashMap<String, String>,
30    allow_guest: bool,
31    challenge: &[u8; 8],
32    t3: &Type3,
33) -> AuthOutcome {
34    let user = t3.user.trim().to_string();
35    const NO_KEY: Option<[u8; 16]> = None;
36
37    tracing::debug!(
38        user = %user,
39        domain = %t3.domain,
40        ntlm_len = t3.ntlm_response.len(),
41        lm_len = t3.lm_response.len(),
42        flags = format!("{:#010x}", t3.flags),
43        "ntlmssp authenticate"
44    );
45
46    // Anonymous / null session.
47    if t3.ntlm_response.is_empty() && t3.lm_response.is_empty()
48        || t3.flags & smb_server_auth::ntlm::NEGOTIATE_ANONYMOUS != 0
49    {
50        return AuthOutcome { ok: true, guest: true, user: "nobody".into(), session_key: NO_KEY };
51    }
52
53    if users.is_empty() && allow_guest {
54        // No user database configured: accept any principal. Without a
55        // shared secret the session key cannot be derived.
56        return AuthOutcome { ok: true, guest: false, user, session_key: NO_KEY };
57    }
58
59    let Some(pass) = users.get(&user.to_lowercase()).cloned() else {
60        return if allow_guest {
61            AuthOutcome { ok: true, guest: true, user, session_key: NO_KEY }
62        } else {
63            AuthOutcome { ok: false, guest: false, user, session_key: NO_KEY }
64        };
65    };
66
67    let nthash = nt_hash(&pass);
68
69    // NTLMv2 ([MS-NLMP] §3.3.2):
70    //   NTLMv2Hash = HMAC-MD5(NTHash, UPPER(user) || domain)
71    //   Proof      = HMAC-MD5(NTLMv2Hash, ServerChallenge || blob)
72    if t3.ntlm_response.len() >= 24 {
73        for identity in [
74            format!("{}{}", user.to_uppercase(), t3.domain),
75            format!("{}{}", user.to_uppercase(), ""),
76        ] {
77            let idb = utf16_le(&identity);
78            let ntv2 = hmac_md5(&nthash, &idb);
79            let mut msg = Vec::with_capacity(8 + t3.ntlm_response.len() - 16);
80            msg.extend_from_slice(challenge);
81            msg.extend_from_slice(&t3.ntlm_response[16..]);
82            let proof = hmac_md5(&ntv2, &msg);
83            if proof.as_slice() == &t3.ntlm_response[..16] {
84                // Derive keys from the SAME identity that verified.
85                let key = derive_session_key(&nthash, &idb, &proof, t3);
86                return AuthOutcome {
87                    ok: true,
88                    guest: false,
89                    user,
90                    session_key: key,
91                };
92            }
93        }
94    }
95
96    // NTLMv1 fallback: DES-based response over the expanded NT hash. No
97    // session-key derivation here (modern clients always use NTLMv2).
98    if t3.ntlm_response.len() == 24 {
99        let mut h21 = [0u8; 21];
100        h21[..16].copy_from_slice(&nthash);
101        let expect = ntlmv1_response(&h21, challenge);
102        if expect.as_slice() == &t3.ntlm_response[..24] {
103            return AuthOutcome { ok: true, guest: false, user, session_key: NO_KEY };
104        }
105    }
106
107    tracing::warn!(user = %user, domain = %t3.domain, ntlm_len = t3.ntlm_response.len(), "ntlm proof mismatch");
108    AuthOutcome { ok: false, guest: false, user, session_key: NO_KEY }
109}
110
111/// Exported session key ([MS-NLMP] §3.2.5.1.2):
112///   KeyExchangeKey     = HMAC-MD5(NTLMv2Hash, NTProofStr)
113///   ExportedSessionKey = RC4(KeyExchangeKey, EncryptedRandomSessionKey)
114/// When the client did not perform NEGOTIATE_KEY_EXCH the exported key
115/// equals KeyExchangeKey.
116#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] // NTLM session-key math ([MS-NLMP])
117fn derive_session_key(
118    nthash: &[u8; 16],
119    identity_utf16le: &[u8],
120    proof: &[u8],
121    t3: &Type3,
122) -> Option<[u8; 16]> {
123    use smb_server_auth::crypto::{hmac_md5, rc4};
124
125    let ntv2 = hmac_md5(nthash, identity_utf16le);
126    let key_exchange_key = hmac_md5(&ntv2, proof);
127
128    match t3.encrypted_session_key.as_slice() {
129        enc if enc.len() == 16 => {
130            let out = rc4(&key_exchange_key, enc);
131            Some(out.try_into().unwrap())
132        }
133        // No key exchange: exported == key exchange key.
134        _ => Some(key_exchange_key),
135    }
136}
137
138fn utf16_le(s: &str) -> Vec<u8> {
139    s.encode_utf16().flat_map(|u| u.to_le_bytes()).collect()
140}