1 unstable release

0.1.0 Sep 19, 2023

#510 in Authentication

Download history 111/week @ 2024-01-11 72/week @ 2024-01-18 96/week @ 2024-01-25 51/week @ 2024-02-01 77/week @ 2024-02-08 68/week @ 2024-02-15 82/week @ 2024-02-22 61/week @ 2024-02-29 63/week @ 2024-03-07 41/week @ 2024-03-14 49/week @ 2024-03-21 125/week @ 2024-03-28 83/week @ 2024-04-04 80/week @ 2024-04-11 67/week @ 2024-04-18 61/week @ 2024-04-25

320 downloads per month
Used in legba

CC0 license

56KB
950 lines

ntlmclient

Simple NTLM client library for Rust.


lib.rs:

A simple NTLM client library for Rust.

Sample usage:

use base64::prelude::{BASE64_STANDARD, Engine};

const EWS_URL: &str = "https://example.com/EWS/Exchange.asmx";

async fn initialize_authed_client(username: &str, password: &str, domain: &str, local_hostname: &str) -> reqwest::Client {
    let nego_flags
        = ntlmclient::Flags::NEGOTIATE_UNICODE
        | ntlmclient::Flags::REQUEST_TARGET
        | ntlmclient::Flags::NEGOTIATE_NTLM
        | ntlmclient::Flags::NEGOTIATE_WORKSTATION_SUPPLIED
        ;
    let nego_msg = ntlmclient::Message::Negotiate(ntlmclient::NegotiateMessage {
        flags: nego_flags,
        supplied_domain: String::new(),
        supplied_workstation: local_hostname.to_owned(),
        os_version: Default::default(),
    });
    let nego_msg_bytes = nego_msg.to_bytes()
        .expect("failed to encode NTLM negotiation message");
    let nego_b64 = BASE64_STANDARD.encode(&nego_msg_bytes);

    let client = reqwest::Client::builder()
        .cookie_store(true)
        .build()
        .expect("failed to build client");
    let resp = client.get(EWS_URL)
        .header("Authorization", format!("NTLM {}", nego_b64))
        .send().await
        .expect("failed to send challenge request to Exchange");
    let challenge_header = resp.headers().get("www-authenticate")
        .expect("response missing challenge header");

    let challenge_b64 = challenge_header.to_str()
        .expect("challenge header not a string")
        .split(" ")
        .nth(1).expect("second chunk of challenge header missing");
    let challenge_bytes = BASE64_STANDARD.decode(challenge_b64)
        .expect("base64 decoding challenge message failed");
    let challenge = ntlmclient::Message::try_from(challenge_bytes.as_slice())
        .expect("decoding challenge message failed");
    let challenge_content = match challenge {
        ntlmclient::Message::Challenge(c) => c,
        other => panic!("wrong challenge message: {:?}", other),
    };
    let target_info_bytes: Vec<u8> = challenge_content.target_information
        .iter()
        .flat_map(|ie| ie.to_bytes())
        .collect();

    // calculate the response
    let creds = ntlmclient::Credentials {
        username: username.to_owned(),
        password: password.to_owned(),
        domain: domain.to_owned(),
    };
    let challenge_response = ntlmclient::respond_challenge_ntlm_v2(
        challenge_content.challenge,
        &target_info_bytes,
        ntlmclient::get_ntlm_time(),
        &creds,
    );

    // assemble the packet
    let auth_flags
        = ntlmclient::Flags::NEGOTIATE_UNICODE
        | ntlmclient::Flags::NEGOTIATE_NTLM
        ;
    let auth_msg = challenge_response.to_message(
        &creds,
        local_hostname,
        auth_flags,
    );
    let auth_msg_bytes = auth_msg.to_bytes()
        .expect("failed to encode NTLM authentication message");
    let auth_b64 = BASE64_STANDARD.encode(&auth_msg_bytes);

    client.get(EWS_URL)
        .header("Authorization", format!("NTLM {}", auth_b64))
        .send().await
        .expect("failed to send authentication request to Exchange")
        .error_for_status()
        .expect("error response to authentication message");

    // try calling again, without the auth stuff (thanks to cookies)
    client.get(EWS_URL)
        .send().await
        .expect("failed to send refresher request to Exchange")
        .error_for_status()
        .expect("error response to refresher message");

    client
}

Dependencies

~2–42MB
~597K SLoC