use log::{debug, trace};
use crate::Error;
use digest::{Digest, KeyInit};
use hmac::{Hmac, Mac};
type HmacMd5 = Hmac<md5::Md5>;
const NTLM_SIGNATURE: &[u8; 8] = b"NTLMSSP\0";
const MSG_TYPE_NEGOTIATE: u32 = 0x0000_0001;
const MSG_TYPE_CHALLENGE: u32 = 0x0000_0002;
const MSG_TYPE_AUTHENTICATE: u32 = 0x0000_0003;
const NTLMSSP_NEGOTIATE_UNICODE: u32 = 0x0000_0001;
const NTLMSSP_REQUEST_TARGET: u32 = 0x0000_0004;
const NTLMSSP_NEGOTIATE_SIGN: u32 = 0x0000_0010;
const NTLMSSP_NEGOTIATE_SEAL: u32 = 0x0000_0020;
const NTLMSSP_NEGOTIATE_NTLM: u32 = 0x0000_0200;
const NTLMSSP_NEGOTIATE_ALWAYS_SIGN: u32 = 0x0000_8000;
const NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY: u32 = 0x0008_0000;
const NTLMSSP_NEGOTIATE_TARGET_INFO: u32 = 0x0080_0000;
const NTLMSSP_NEGOTIATE_128: u32 = 0x2000_0000;
const NTLMSSP_NEGOTIATE_KEY_EXCH: u32 = 0x4000_0000;
const NTLMSSP_NEGOTIATE_56: u32 = 0x8000_0000;
const DEFAULT_NEGOTIATE_FLAGS: u32 = NTLMSSP_NEGOTIATE_UNICODE
| NTLMSSP_REQUEST_TARGET
| NTLMSSP_NEGOTIATE_NTLM
| NTLMSSP_NEGOTIATE_ALWAYS_SIGN
| NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY
| NTLMSSP_NEGOTIATE_TARGET_INFO
| NTLMSSP_NEGOTIATE_128
| NTLMSSP_NEGOTIATE_KEY_EXCH
| NTLMSSP_NEGOTIATE_56
| NTLMSSP_NEGOTIATE_SIGN
| NTLMSSP_NEGOTIATE_SEAL;
const MSV_AV_EOL: u16 = 0x0000;
#[cfg(test)]
const MSV_AV_NB_COMPUTER_NAME: u16 = 0x0001;
#[cfg(test)]
const MSV_AV_NB_DOMAIN_NAME: u16 = 0x0002;
#[allow(dead_code)]
const MSV_AV_DNS_COMPUTER_NAME: u16 = 0x0003;
#[allow(dead_code)]
const MSV_AV_DNS_DOMAIN_NAME: u16 = 0x0004;
const MSV_AV_FLAGS: u16 = 0x0006;
const MSV_AV_TIMESTAMP: u16 = 0x0007;
#[allow(dead_code)]
const MSV_AV_TARGET_NAME: u16 = 0x0009;
pub struct NtlmCredentials {
pub username: String,
pub password: String,
pub domain: String,
}
pub struct NtlmAuthenticator {
credentials: NtlmCredentials,
negotiate_bytes: Option<Vec<u8>>,
challenge_bytes: Option<Vec<u8>>,
session_key: Option<Vec<u8>>,
#[cfg(test)]
test_client_challenge: Option<[u8; 8]>,
#[cfg(test)]
test_random_session_key: Option<[u8; 16]>,
#[cfg(test)]
test_timestamp: Option<u64>,
}
impl NtlmAuthenticator {
pub fn new(credentials: NtlmCredentials) -> Self {
Self {
credentials,
negotiate_bytes: None,
challenge_bytes: None,
session_key: None,
#[cfg(test)]
test_client_challenge: None,
#[cfg(test)]
test_random_session_key: None,
#[cfg(test)]
test_timestamp: None,
}
}
pub fn negotiate(&mut self) -> Vec<u8> {
let mut buf = Vec::with_capacity(32);
buf.extend_from_slice(NTLM_SIGNATURE);
buf.extend_from_slice(&MSG_TYPE_NEGOTIATE.to_le_bytes());
buf.extend_from_slice(&DEFAULT_NEGOTIATE_FLAGS.to_le_bytes());
buf.extend_from_slice(&[0u8; 8]);
buf.extend_from_slice(&[0u8; 8]);
debug!("ntlm: negotiate message built, len={}", buf.len());
self.negotiate_bytes = Some(buf.clone());
buf
}
pub fn authenticate(&mut self, challenge_bytes: &[u8]) -> Result<Vec<u8>, Error> {
debug!("ntlm: processing challenge, len={}", challenge_bytes.len());
self.challenge_bytes = Some(challenge_bytes.to_vec());
let challenge = parse_challenge_message(challenge_bytes)?;
trace!(
"ntlm: challenge flags=0x{:08x}, target_info_len={}",
challenge.negotiate_flags,
challenge.target_info.len()
);
let nt_hash = compute_nt_hash(&self.credentials.password);
let ntlmv2_hash = compute_ntlmv2_hash(
&nt_hash,
&self.credentials.username,
&self.credentials.domain,
);
let timestamp = self.get_timestamp(&challenge);
let client_challenge = self.get_client_challenge();
let has_timestamp = find_av_pair(&challenge.target_info, MSV_AV_TIMESTAMP).is_some();
let auth_target_info = build_auth_target_info(&challenge.target_info, has_timestamp);
let temp = build_temp(timestamp, &client_challenge, &auth_target_info);
let nt_proof_str = {
let mut mac =
HmacMd5::new_from_slice(&ntlmv2_hash).expect("HMAC accepts any key length");
mac.update(&challenge.server_challenge);
mac.update(&temp);
mac.finalize().into_bytes().to_vec()
};
let mut nt_challenge_response = nt_proof_str.clone();
nt_challenge_response.extend_from_slice(&temp);
let session_base_key = {
let mut mac =
HmacMd5::new_from_slice(&ntlmv2_hash).expect("HMAC accepts any key length");
mac.update(&nt_proof_str);
mac.finalize().into_bytes().to_vec()
};
let negotiate_flags = challenge.negotiate_flags;
let key_exch = (negotiate_flags & NTLMSSP_NEGOTIATE_KEY_EXCH) != 0
&& ((negotiate_flags & NTLMSSP_NEGOTIATE_SIGN) != 0
|| (negotiate_flags & NTLMSSP_NEGOTIATE_SEAL) != 0);
let (exported_session_key, encrypted_random_session_key) = if key_exch {
let random_key = self.get_random_session_key();
let encrypted = rc4_encrypt(&session_base_key, &random_key);
(random_key.to_vec(), encrypted)
} else {
(session_base_key.clone(), Vec::new())
};
let lm_challenge_response = if has_timestamp {
vec![0u8; 24]
} else {
let mut mac =
HmacMd5::new_from_slice(&ntlmv2_hash).expect("HMAC accepts any key length");
mac.update(&challenge.server_challenge);
mac.update(&client_challenge);
let proof = mac.finalize().into_bytes();
let mut resp = proof.to_vec();
resp.extend_from_slice(&client_challenge);
resp
};
let auth_msg = build_authenticate_message(
negotiate_flags,
&self.credentials.domain,
&self.credentials.username,
&lm_challenge_response,
&nt_challenge_response,
&encrypted_random_session_key,
has_timestamp,
);
let final_msg = if has_timestamp {
let negotiate_bytes = self.negotiate_bytes.as_ref().ok_or_else(|| {
Error::invalid_data("negotiate() must be called before authenticate()")
})?;
let mic = compute_mic(
&exported_session_key,
negotiate_bytes,
challenge_bytes,
&auth_msg,
);
let mut patched = auth_msg;
patched[72..88].copy_from_slice(&mic);
patched
} else {
auth_msg
};
self.session_key = Some(exported_session_key);
debug!(
"ntlm: authenticate message built, len={}, mic={}",
final_msg.len(),
has_timestamp
);
Ok(final_msg)
}
pub fn session_key(&self) -> Option<&[u8]> {
self.session_key.as_deref()
}
fn get_timestamp(&self, challenge: &ChallengeMessage) -> u64 {
#[cfg(test)]
if let Some(ts) = self.test_timestamp {
return ts;
}
if let Some(ts_bytes) = find_av_pair(&challenge.target_info, MSV_AV_TIMESTAMP) {
if ts_bytes.len() == 8 {
return u64::from_le_bytes(ts_bytes.try_into().unwrap());
}
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
(now.as_secs() + 11_644_473_600) * 10_000_000 + u64::from(now.subsec_nanos()) / 100
}
fn get_client_challenge(&self) -> [u8; 8] {
#[cfg(test)]
if let Some(cc) = self.test_client_challenge {
return cc;
}
let mut challenge = [0u8; 8];
getrandom::fill(&mut challenge).expect("system RNG failed");
challenge
}
fn get_random_session_key(&self) -> [u8; 16] {
#[cfg(test)]
if let Some(rsk) = self.test_random_session_key {
return rsk;
}
let mut key = [0u8; 16];
getrandom::fill(&mut key).expect("system RNG failed");
key
}
}
struct ChallengeMessage {
negotiate_flags: u32,
server_challenge: [u8; 8],
target_info: Vec<u8>,
}
fn parse_challenge_message(data: &[u8]) -> Result<ChallengeMessage, Error> {
if data.len() < 32 {
return Err(Error::invalid_data("CHALLENGE_MESSAGE too short"));
}
if &data[0..8] != NTLM_SIGNATURE {
return Err(Error::invalid_data(
"invalid NTLM signature in CHALLENGE_MESSAGE",
));
}
let msg_type = u32::from_le_bytes(data[8..12].try_into().unwrap());
if msg_type != MSG_TYPE_CHALLENGE {
return Err(Error::invalid_data(format!(
"expected CHALLENGE_MESSAGE type 2, got {}",
msg_type
)));
}
let negotiate_flags = u32::from_le_bytes(data[20..24].try_into().unwrap());
let mut server_challenge = [0u8; 8];
server_challenge.copy_from_slice(&data[24..32]);
let target_info = if data.len() >= 48 {
let ti_len = u16::from_le_bytes(data[40..42].try_into().unwrap()) as usize;
let ti_offset = u32::from_le_bytes(data[44..48].try_into().unwrap()) as usize;
if ti_len > 0 && ti_offset + ti_len <= data.len() {
data[ti_offset..ti_offset + ti_len].to_vec()
} else {
Vec::new()
}
} else {
Vec::new()
};
Ok(ChallengeMessage {
negotiate_flags,
server_challenge,
target_info,
})
}
fn find_av_pair(target_info: &[u8], av_id: u16) -> Option<Vec<u8>> {
let mut offset = 0;
while offset + 4 <= target_info.len() {
let id = u16::from_le_bytes(target_info[offset..offset + 2].try_into().unwrap());
let len =
u16::from_le_bytes(target_info[offset + 2..offset + 4].try_into().unwrap()) as usize;
if id == av_id {
if offset + 4 + len <= target_info.len() {
return Some(target_info[offset + 4..offset + 4 + len].to_vec());
}
return None;
}
if id == MSV_AV_EOL {
break;
}
offset += 4 + len;
}
None
}
fn parse_av_pairs(target_info: &[u8]) -> Vec<(u16, Vec<u8>)> {
let mut pairs = Vec::new();
let mut offset = 0;
while offset + 4 <= target_info.len() {
let id = u16::from_le_bytes(target_info[offset..offset + 2].try_into().unwrap());
let len =
u16::from_le_bytes(target_info[offset + 2..offset + 4].try_into().unwrap()) as usize;
if id == MSV_AV_EOL {
pairs.push((id, Vec::new()));
break;
}
if offset + 4 + len > target_info.len() {
break;
}
pairs.push((id, target_info[offset + 4..offset + 4 + len].to_vec()));
offset += 4 + len;
}
pairs
}
fn build_auth_target_info(challenge_target_info: &[u8], has_timestamp: bool) -> Vec<u8> {
let pairs = parse_av_pairs(challenge_target_info);
let mut result = Vec::new();
for (id, value) in &pairs {
if *id == MSV_AV_EOL {
continue;
}
if *id == MSV_AV_FLAGS && has_timestamp {
continue;
}
result.extend_from_slice(&id.to_le_bytes());
result.extend_from_slice(&(value.len() as u16).to_le_bytes());
result.extend_from_slice(value);
}
if has_timestamp {
let existing_flags = pairs
.iter()
.find(|(id, _)| *id == MSV_AV_FLAGS)
.map(|(_, v)| {
if v.len() >= 4 {
u32::from_le_bytes(v[..4].try_into().unwrap())
} else {
0
}
})
.unwrap_or(0);
let flags = existing_flags | 0x0000_0002; result.extend_from_slice(&MSV_AV_FLAGS.to_le_bytes());
result.extend_from_slice(&4u16.to_le_bytes());
result.extend_from_slice(&flags.to_le_bytes());
}
result.extend_from_slice(&MSV_AV_EOL.to_le_bytes());
result.extend_from_slice(&0u16.to_le_bytes());
result
}
fn compute_nt_hash(password: &str) -> Vec<u8> {
let unicode_password: Vec<u8> = password
.encode_utf16()
.flat_map(|u| u.to_le_bytes())
.collect();
let mut hasher = md4::Md4::new();
hasher.update(&unicode_password);
hasher.finalize().to_vec()
}
fn compute_ntlmv2_hash(nt_hash: &[u8], username: &str, domain: &str) -> Vec<u8> {
let user_upper: Vec<u8> = username
.to_uppercase()
.encode_utf16()
.flat_map(|u| u.to_le_bytes())
.collect();
let domain_unicode: Vec<u8> = domain
.encode_utf16()
.flat_map(|u| u.to_le_bytes())
.collect();
let mut mac = HmacMd5::new_from_slice(nt_hash).expect("HMAC accepts any key length");
mac.update(&user_upper);
mac.update(&domain_unicode);
mac.finalize().into_bytes().to_vec()
}
fn build_temp(timestamp: u64, client_challenge: &[u8; 8], target_info: &[u8]) -> Vec<u8> {
let mut temp = Vec::new();
temp.push(0x01); temp.push(0x01); temp.extend_from_slice(&[0u8; 6]); temp.extend_from_slice(×tamp.to_le_bytes()); temp.extend_from_slice(client_challenge); temp.extend_from_slice(&[0u8; 4]); temp.extend_from_slice(target_info); temp.extend_from_slice(&[0u8; 4]); temp
}
fn rc4_encrypt(key: &[u8], data: &[u8]) -> Vec<u8> {
let mut s: Vec<u8> = (0..=255).collect();
let mut j: u8 = 0;
for i in 0..256 {
j = j.wrapping_add(s[i]).wrapping_add(key[i % key.len()]);
s.swap(i, j as usize);
}
let mut i: u8 = 0;
j = 0;
data.iter()
.map(|&byte| {
i = i.wrapping_add(1);
j = j.wrapping_add(s[i as usize]);
s.swap(i as usize, j as usize);
byte ^ s[s[i as usize].wrapping_add(s[j as usize]) as usize]
})
.collect()
}
fn compute_mic(
exported_session_key: &[u8],
negotiate_bytes: &[u8],
challenge_bytes: &[u8],
authenticate_bytes: &[u8],
) -> Vec<u8> {
let mut mac =
HmacMd5::new_from_slice(exported_session_key).expect("HMAC accepts any key length");
mac.update(negotiate_bytes);
mac.update(challenge_bytes);
mac.update(authenticate_bytes);
mac.finalize().into_bytes().to_vec()
}
fn encode_utf16le(s: &str) -> Vec<u8> {
s.encode_utf16().flat_map(|u| u.to_le_bytes()).collect()
}
fn build_authenticate_message(
negotiate_flags: u32,
domain: &str,
username: &str,
lm_challenge_response: &[u8],
nt_challenge_response: &[u8],
encrypted_random_session_key: &[u8],
include_mic: bool,
) -> Vec<u8> {
let domain_bytes = encode_utf16le(domain);
let user_bytes = encode_utf16le(username);
let workstation_bytes: Vec<u8> = Vec::new();
let header_size = if include_mic { 88 } else { 72 };
let domain_offset = header_size;
let user_offset = domain_offset + domain_bytes.len();
let workstation_offset = user_offset + user_bytes.len();
let lm_offset = workstation_offset + workstation_bytes.len();
let nt_offset = lm_offset + lm_challenge_response.len();
let session_key_offset = nt_offset + nt_challenge_response.len();
let mut buf = Vec::with_capacity(session_key_offset + encrypted_random_session_key.len());
buf.extend_from_slice(NTLM_SIGNATURE);
buf.extend_from_slice(&MSG_TYPE_AUTHENTICATE.to_le_bytes());
buf.extend_from_slice(&(lm_challenge_response.len() as u16).to_le_bytes());
buf.extend_from_slice(&(lm_challenge_response.len() as u16).to_le_bytes());
buf.extend_from_slice(&(lm_offset as u32).to_le_bytes());
buf.extend_from_slice(&(nt_challenge_response.len() as u16).to_le_bytes());
buf.extend_from_slice(&(nt_challenge_response.len() as u16).to_le_bytes());
buf.extend_from_slice(&(nt_offset as u32).to_le_bytes());
buf.extend_from_slice(&(domain_bytes.len() as u16).to_le_bytes());
buf.extend_from_slice(&(domain_bytes.len() as u16).to_le_bytes());
buf.extend_from_slice(&(domain_offset as u32).to_le_bytes());
buf.extend_from_slice(&(user_bytes.len() as u16).to_le_bytes());
buf.extend_from_slice(&(user_bytes.len() as u16).to_le_bytes());
buf.extend_from_slice(&(user_offset as u32).to_le_bytes());
buf.extend_from_slice(&(workstation_bytes.len() as u16).to_le_bytes());
buf.extend_from_slice(&(workstation_bytes.len() as u16).to_le_bytes());
buf.extend_from_slice(&(workstation_offset as u32).to_le_bytes());
buf.extend_from_slice(&(encrypted_random_session_key.len() as u16).to_le_bytes());
buf.extend_from_slice(&(encrypted_random_session_key.len() as u16).to_le_bytes());
buf.extend_from_slice(&(session_key_offset as u32).to_le_bytes());
buf.extend_from_slice(&negotiate_flags.to_le_bytes());
buf.extend_from_slice(&[0u8; 8]);
if include_mic {
buf.extend_from_slice(&[0u8; 16]);
}
buf.extend_from_slice(&domain_bytes);
buf.extend_from_slice(&user_bytes);
buf.extend_from_slice(&workstation_bytes);
buf.extend_from_slice(lm_challenge_response);
buf.extend_from_slice(nt_challenge_response);
buf.extend_from_slice(encrypted_random_session_key);
buf
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_USER: &str = "User";
const TEST_PASSWORD: &str = "Password";
const TEST_DOMAIN: &str = "Domain";
const TEST_SERVER_CHALLENGE: [u8; 8] = [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef];
const TEST_CLIENT_CHALLENGE: [u8; 8] = [0xaa; 8];
const TEST_RANDOM_SESSION_KEY: [u8; 16] = [0x55; 16];
const TEST_TIME: u64 = 0;
#[test]
fn nt_hash_of_password() {
let expected = [
0xa4, 0xf4, 0x9c, 0x40, 0x65, 0x10, 0xbd, 0xca, 0xb6, 0x82, 0x4e, 0xe7, 0xc3, 0x0f,
0xd8, 0x52,
];
let hash = compute_nt_hash(TEST_PASSWORD);
assert_eq!(hash, expected);
}
#[test]
fn ntlmv2_hash_computation() {
let expected = [
0x0c, 0x86, 0x8a, 0x40, 0x3b, 0xfd, 0x7a, 0x93, 0xa3, 0x00, 0x1e, 0xf2, 0x2e, 0xf0,
0x2e, 0x3f,
];
let nt_hash = compute_nt_hash(TEST_PASSWORD);
let ntlmv2_hash = compute_ntlmv2_hash(&nt_hash, TEST_USER, TEST_DOMAIN);
assert_eq!(ntlmv2_hash, expected);
}
#[test]
fn nt_proof_str_computation() {
let expected_nt_proof_str = [
0x68, 0xcd, 0x0a, 0xb8, 0x51, 0xe5, 0x1c, 0x96, 0xaa, 0xbc, 0x92, 0x7b, 0xeb, 0xef,
0x6a, 0x1c,
];
let nt_hash = compute_nt_hash(TEST_PASSWORD);
let ntlmv2_hash = compute_ntlmv2_hash(&nt_hash, TEST_USER, TEST_DOMAIN);
let target_info = build_test_target_info();
let temp = build_temp(TEST_TIME, &TEST_CLIENT_CHALLENGE, &target_info);
let mut mac = HmacMd5::new_from_slice(&ntlmv2_hash).expect("HMAC accepts any key length");
mac.update(&TEST_SERVER_CHALLENGE);
mac.update(&temp);
let nt_proof_str = mac.finalize().into_bytes().to_vec();
assert_eq!(nt_proof_str, expected_nt_proof_str);
}
#[test]
fn session_base_key_computation() {
let expected = [
0x8d, 0xe4, 0x0c, 0xca, 0xdb, 0xc1, 0x4a, 0x82, 0xf1, 0x5c, 0xb0, 0xad, 0x0d, 0xe9,
0x5c, 0xa3,
];
let nt_hash = compute_nt_hash(TEST_PASSWORD);
let ntlmv2_hash = compute_ntlmv2_hash(&nt_hash, TEST_USER, TEST_DOMAIN);
let target_info = build_test_target_info();
let temp = build_temp(TEST_TIME, &TEST_CLIENT_CHALLENGE, &target_info);
let mut mac = HmacMd5::new_from_slice(&ntlmv2_hash).expect("HMAC accepts any key length");
mac.update(&TEST_SERVER_CHALLENGE);
mac.update(&temp);
let nt_proof_str = mac.finalize().into_bytes().to_vec();
let mut mac = HmacMd5::new_from_slice(&ntlmv2_hash).expect("HMAC accepts any key length");
mac.update(&nt_proof_str);
let session_base_key = mac.finalize().into_bytes().to_vec();
assert_eq!(session_base_key, expected);
}
#[test]
fn rc4_encrypted_session_key() {
let expected = [
0xc5, 0xda, 0xd2, 0x54, 0x4f, 0xc9, 0x79, 0x90, 0x94, 0xce, 0x1c, 0xe9, 0x0b, 0xc9,
0xd0, 0x3e,
];
let session_base_key = [
0x8d, 0xe4, 0x0c, 0xca, 0xdb, 0xc1, 0x4a, 0x82, 0xf1, 0x5c, 0xb0, 0xad, 0x0d, 0xe9,
0x5c, 0xa3,
];
let result = rc4_encrypt(&session_base_key, &TEST_RANDOM_SESSION_KEY);
assert_eq!(result, expected);
}
#[test]
fn rc4_roundtrip() {
let key = b"test key";
let data = b"hello, world!";
let encrypted = rc4_encrypt(key, data);
let decrypted = rc4_encrypt(key, &encrypted);
assert_eq!(decrypted, data);
}
#[test]
fn parse_av_pairs_from_target_info() {
let target_info = build_test_target_info();
let pairs = parse_av_pairs(&target_info);
assert_eq!(pairs.len(), 3);
assert_eq!(pairs[0].0, MSV_AV_NB_DOMAIN_NAME);
let domain = String::from_utf16(
&pairs[0]
.1
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect::<Vec<_>>(),
)
.unwrap();
assert_eq!(domain, "Domain");
assert_eq!(pairs[1].0, MSV_AV_NB_COMPUTER_NAME);
assert_eq!(pairs[2].0, MSV_AV_EOL);
}
#[test]
fn find_av_pair_present() {
let target_info = build_test_target_info();
let domain = find_av_pair(&target_info, MSV_AV_NB_DOMAIN_NAME);
assert!(domain.is_some());
}
#[test]
fn find_av_pair_absent() {
let target_info = build_test_target_info();
let timestamp = find_av_pair(&target_info, MSV_AV_TIMESTAMP);
assert!(timestamp.is_none());
}
#[test]
fn detect_timestamp_in_target_info() {
let mut target_info = Vec::new();
let domain_bytes = encode_utf16le("Domain");
target_info.extend_from_slice(&MSV_AV_NB_DOMAIN_NAME.to_le_bytes());
target_info.extend_from_slice(&(domain_bytes.len() as u16).to_le_bytes());
target_info.extend_from_slice(&domain_bytes);
target_info.extend_from_slice(&MSV_AV_TIMESTAMP.to_le_bytes());
target_info.extend_from_slice(&8u16.to_le_bytes());
target_info.extend_from_slice(&0u64.to_le_bytes());
target_info.extend_from_slice(&MSV_AV_EOL.to_le_bytes());
target_info.extend_from_slice(&0u16.to_le_bytes());
assert!(find_av_pair(&target_info, MSV_AV_TIMESTAMP).is_some());
}
#[test]
fn negotiate_message_has_correct_signature() {
let mut auth = NtlmAuthenticator::new(NtlmCredentials {
username: TEST_USER.to_string(),
password: TEST_PASSWORD.to_string(),
domain: TEST_DOMAIN.to_string(),
});
let msg = auth.negotiate();
assert_eq!(&msg[0..8], NTLM_SIGNATURE);
}
#[test]
fn negotiate_message_has_correct_type() {
let mut auth = NtlmAuthenticator::new(NtlmCredentials {
username: TEST_USER.to_string(),
password: TEST_PASSWORD.to_string(),
domain: TEST_DOMAIN.to_string(),
});
let msg = auth.negotiate();
let msg_type = u32::from_le_bytes(msg[8..12].try_into().unwrap());
assert_eq!(msg_type, MSG_TYPE_NEGOTIATE);
}
#[test]
fn negotiate_message_has_expected_flags() {
let mut auth = NtlmAuthenticator::new(NtlmCredentials {
username: TEST_USER.to_string(),
password: TEST_PASSWORD.to_string(),
domain: TEST_DOMAIN.to_string(),
});
let msg = auth.negotiate();
let flags = u32::from_le_bytes(msg[12..16].try_into().unwrap());
assert_ne!(flags & NTLMSSP_NEGOTIATE_UNICODE, 0);
assert_ne!(flags & NTLMSSP_NEGOTIATE_NTLM, 0);
assert_ne!(flags & NTLMSSP_NEGOTIATE_KEY_EXCH, 0);
assert_ne!(flags & NTLMSSP_NEGOTIATE_128, 0);
}
#[test]
fn negotiate_message_minimum_size() {
let mut auth = NtlmAuthenticator::new(NtlmCredentials {
username: String::new(),
password: String::new(),
domain: String::new(),
});
let msg = auth.negotiate();
assert_eq!(msg.len(), 32);
}
#[test]
fn parse_challenge_message_from_spec() {
let challenge_bytes = build_test_challenge_message();
let challenge = parse_challenge_message(&challenge_bytes).unwrap();
assert_eq!(challenge.server_challenge, TEST_SERVER_CHALLENGE);
assert!(!challenge.target_info.is_empty());
}
#[test]
fn parse_challenge_message_rejects_wrong_signature() {
let mut bad = build_test_challenge_message();
bad[0] = 0x00; assert!(parse_challenge_message(&bad).is_err());
}
#[test]
fn parse_challenge_message_rejects_wrong_type() {
let mut bad = build_test_challenge_message();
bad[8] = 0x01;
assert!(parse_challenge_message(&bad).is_err());
}
#[test]
fn parse_challenge_message_rejects_too_short() {
assert!(parse_challenge_message(&[0u8; 16]).is_err());
}
#[test]
fn full_negotiate_authenticate_flow_no_timestamp() {
let mut auth = NtlmAuthenticator::new(NtlmCredentials {
username: TEST_USER.to_string(),
password: TEST_PASSWORD.to_string(),
domain: TEST_DOMAIN.to_string(),
});
auth.test_client_challenge = Some(TEST_CLIENT_CHALLENGE);
auth.test_random_session_key = Some(TEST_RANDOM_SESSION_KEY);
auth.test_timestamp = Some(TEST_TIME);
let _negotiate = auth.negotiate();
let challenge_bytes = build_test_challenge_message();
let authenticate = auth.authenticate(&challenge_bytes).unwrap();
assert_eq!(&authenticate[0..8], NTLM_SIGNATURE);
let msg_type = u32::from_le_bytes(authenticate[8..12].try_into().unwrap());
assert_eq!(msg_type, MSG_TYPE_AUTHENTICATE);
assert!(auth.session_key().is_some());
assert_eq!(auth.session_key().unwrap().len(), 16);
}
#[test]
fn full_flow_with_timestamp_includes_mic() {
let mut auth = NtlmAuthenticator::new(NtlmCredentials {
username: TEST_USER.to_string(),
password: TEST_PASSWORD.to_string(),
domain: TEST_DOMAIN.to_string(),
});
auth.test_client_challenge = Some(TEST_CLIENT_CHALLENGE);
auth.test_random_session_key = Some(TEST_RANDOM_SESSION_KEY);
auth.test_timestamp = Some(TEST_TIME);
let _negotiate = auth.negotiate();
let challenge_bytes = build_test_challenge_message_with_timestamp();
let authenticate = auth.authenticate(&challenge_bytes).unwrap();
assert_eq!(&authenticate[0..8], NTLM_SIGNATURE);
let msg_type = u32::from_le_bytes(authenticate[8..12].try_into().unwrap());
assert_eq!(msg_type, MSG_TYPE_AUTHENTICATE);
let mic = &authenticate[72..88];
assert_ne!(
mic, &[0u8; 16],
"MIC should be non-zero when timestamp is present"
);
assert!(auth.session_key().is_some());
}
#[test]
fn session_key_not_available_before_authenticate() {
let auth = NtlmAuthenticator::new(NtlmCredentials {
username: TEST_USER.to_string(),
password: TEST_PASSWORD.to_string(),
domain: TEST_DOMAIN.to_string(),
});
assert!(auth.session_key().is_none());
}
#[test]
fn authenticate_without_negotiate_and_timestamp_still_works() {
let mut auth = NtlmAuthenticator::new(NtlmCredentials {
username: TEST_USER.to_string(),
password: TEST_PASSWORD.to_string(),
domain: TEST_DOMAIN.to_string(),
});
auth.test_client_challenge = Some(TEST_CLIENT_CHALLENGE);
auth.test_random_session_key = Some(TEST_RANDOM_SESSION_KEY);
auth.test_timestamp = Some(TEST_TIME);
let challenge_bytes = build_test_challenge_message();
let result = auth.authenticate(&challenge_bytes);
assert!(result.is_ok());
}
#[test]
fn authenticate_with_timestamp_requires_negotiate() {
let mut auth = NtlmAuthenticator::new(NtlmCredentials {
username: TEST_USER.to_string(),
password: TEST_PASSWORD.to_string(),
domain: TEST_DOMAIN.to_string(),
});
auth.test_client_challenge = Some(TEST_CLIENT_CHALLENGE);
auth.test_random_session_key = Some(TEST_RANDOM_SESSION_KEY);
auth.test_timestamp = Some(TEST_TIME);
let challenge_bytes = build_test_challenge_message_with_timestamp();
let result = auth.authenticate(&challenge_bytes);
assert!(result.is_err());
}
#[test]
fn empty_domain() {
let mut auth = NtlmAuthenticator::new(NtlmCredentials {
username: TEST_USER.to_string(),
password: TEST_PASSWORD.to_string(),
domain: String::new(),
});
auth.test_client_challenge = Some(TEST_CLIENT_CHALLENGE);
auth.test_random_session_key = Some(TEST_RANDOM_SESSION_KEY);
auth.test_timestamp = Some(TEST_TIME);
let _negotiate = auth.negotiate();
let challenge_bytes = build_test_challenge_message();
let result = auth.authenticate(&challenge_bytes);
assert!(result.is_ok());
}
#[test]
fn unicode_username_with_special_characters() {
let mut auth = NtlmAuthenticator::new(NtlmCredentials {
username: "Us\u{00e9}r".to_string(), password: TEST_PASSWORD.to_string(),
domain: TEST_DOMAIN.to_string(),
});
auth.test_client_challenge = Some(TEST_CLIENT_CHALLENGE);
auth.test_random_session_key = Some(TEST_RANDOM_SESSION_KEY);
auth.test_timestamp = Some(TEST_TIME);
let _negotiate = auth.negotiate();
let challenge_bytes = build_test_challenge_message();
let result = auth.authenticate(&challenge_bytes);
assert!(result.is_ok());
}
#[test]
fn build_auth_target_info_adds_flags_when_timestamp_present() {
let mut target_info = Vec::new();
let domain_bytes = encode_utf16le("Domain");
target_info.extend_from_slice(&MSV_AV_NB_DOMAIN_NAME.to_le_bytes());
target_info.extend_from_slice(&(domain_bytes.len() as u16).to_le_bytes());
target_info.extend_from_slice(&domain_bytes);
target_info.extend_from_slice(&MSV_AV_TIMESTAMP.to_le_bytes());
target_info.extend_from_slice(&8u16.to_le_bytes());
target_info.extend_from_slice(&0u64.to_le_bytes());
target_info.extend_from_slice(&MSV_AV_EOL.to_le_bytes());
target_info.extend_from_slice(&0u16.to_le_bytes());
let auth_info = build_auth_target_info(&target_info, true);
let pairs = parse_av_pairs(&auth_info);
let flags_pair = pairs.iter().find(|(id, _)| *id == MSV_AV_FLAGS);
assert!(flags_pair.is_some(), "MsvAvFlags should be present");
let flags_value = u32::from_le_bytes(flags_pair.unwrap().1[..4].try_into().unwrap());
assert_ne!(flags_value & 0x2, 0, "MIC bit should be set in MsvAvFlags");
}
#[test]
fn build_auth_target_info_no_flags_when_no_timestamp() {
let target_info = build_test_target_info();
let auth_info = build_auth_target_info(&target_info, false);
let pairs = parse_av_pairs(&auth_info);
let flags_pair = pairs.iter().find(|(id, _)| *id == MSV_AV_FLAGS);
assert!(flags_pair.is_none());
}
#[test]
fn lm_challenge_response_is_zeroed_when_timestamp_present() {
let mut auth = NtlmAuthenticator::new(NtlmCredentials {
username: TEST_USER.to_string(),
password: TEST_PASSWORD.to_string(),
domain: TEST_DOMAIN.to_string(),
});
auth.test_client_challenge = Some(TEST_CLIENT_CHALLENGE);
auth.test_random_session_key = Some(TEST_RANDOM_SESSION_KEY);
auth.test_timestamp = Some(TEST_TIME);
let _negotiate = auth.negotiate();
let challenge_bytes = build_test_challenge_message_with_timestamp();
let authenticate = auth.authenticate(&challenge_bytes).unwrap();
let lm_len = u16::from_le_bytes(authenticate[12..14].try_into().unwrap()) as usize;
let lm_offset = u32::from_le_bytes(authenticate[16..20].try_into().unwrap()) as usize;
assert_eq!(lm_len, 24);
let lm_data = &authenticate[lm_offset..lm_offset + lm_len];
assert_eq!(lm_data, &[0u8; 24]);
}
#[test]
fn authenticate_message_contains_correct_domain_and_user() {
let mut auth = NtlmAuthenticator::new(NtlmCredentials {
username: TEST_USER.to_string(),
password: TEST_PASSWORD.to_string(),
domain: TEST_DOMAIN.to_string(),
});
auth.test_client_challenge = Some(TEST_CLIENT_CHALLENGE);
auth.test_random_session_key = Some(TEST_RANDOM_SESSION_KEY);
auth.test_timestamp = Some(TEST_TIME);
let _negotiate = auth.negotiate();
let challenge_bytes = build_test_challenge_message();
let authenticate = auth.authenticate(&challenge_bytes).unwrap();
let domain_len = u16::from_le_bytes(authenticate[28..30].try_into().unwrap()) as usize;
let domain_offset = u32::from_le_bytes(authenticate[32..36].try_into().unwrap()) as usize;
let domain_bytes = &authenticate[domain_offset..domain_offset + domain_len];
let domain = String::from_utf16(
&domain_bytes
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect::<Vec<_>>(),
)
.unwrap();
assert_eq!(domain, TEST_DOMAIN);
let user_len = u16::from_le_bytes(authenticate[36..38].try_into().unwrap()) as usize;
let user_offset = u32::from_le_bytes(authenticate[40..44].try_into().unwrap()) as usize;
let user_bytes = &authenticate[user_offset..user_offset + user_len];
let user = String::from_utf16(
&user_bytes
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect::<Vec<_>>(),
)
.unwrap();
assert_eq!(user, TEST_USER);
}
#[test]
fn ntlmv2_full_known_answer_lm_response() {
let expected = [
0x86, 0xc3, 0x50, 0x97, 0xac, 0x9c, 0xec, 0x10, 0x25, 0x54, 0x76, 0x4a, 0x57, 0xcc,
0xcc, 0x19, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
];
let nt_hash = compute_nt_hash(TEST_PASSWORD);
let ntlmv2_hash = compute_ntlmv2_hash(&nt_hash, TEST_USER, TEST_DOMAIN);
let mut mac = HmacMd5::new_from_slice(&ntlmv2_hash).expect("HMAC accepts any key length");
mac.update(&TEST_SERVER_CHALLENGE);
mac.update(&TEST_CLIENT_CHALLENGE);
let proof = mac.finalize().into_bytes();
let mut resp = proof.to_vec();
resp.extend_from_slice(&TEST_CLIENT_CHALLENGE);
assert_eq!(resp, expected);
}
fn build_test_target_info() -> Vec<u8> {
let mut info = Vec::new();
let domain_bytes = encode_utf16le("Domain");
info.extend_from_slice(&MSV_AV_NB_DOMAIN_NAME.to_le_bytes());
info.extend_from_slice(&(domain_bytes.len() as u16).to_le_bytes());
info.extend_from_slice(&domain_bytes);
let server_bytes = encode_utf16le("Server");
info.extend_from_slice(&MSV_AV_NB_COMPUTER_NAME.to_le_bytes());
info.extend_from_slice(&(server_bytes.len() as u16).to_le_bytes());
info.extend_from_slice(&server_bytes);
info.extend_from_slice(&MSV_AV_EOL.to_le_bytes());
info.extend_from_slice(&0u16.to_le_bytes());
info
}
fn build_test_challenge_message() -> Vec<u8> {
let target_info = build_test_target_info();
let target_name = encode_utf16le("Server");
let flags: u32 = 0xe28a8233;
build_challenge_message_bytes(flags, &target_name, &target_info)
}
fn build_test_challenge_message_with_timestamp() -> Vec<u8> {
let mut target_info = Vec::new();
let domain_bytes = encode_utf16le("Domain");
target_info.extend_from_slice(&MSV_AV_NB_DOMAIN_NAME.to_le_bytes());
target_info.extend_from_slice(&(domain_bytes.len() as u16).to_le_bytes());
target_info.extend_from_slice(&domain_bytes);
let server_bytes = encode_utf16le("Server");
target_info.extend_from_slice(&MSV_AV_NB_COMPUTER_NAME.to_le_bytes());
target_info.extend_from_slice(&(server_bytes.len() as u16).to_le_bytes());
target_info.extend_from_slice(&server_bytes);
target_info.extend_from_slice(&MSV_AV_TIMESTAMP.to_le_bytes());
target_info.extend_from_slice(&8u16.to_le_bytes());
target_info.extend_from_slice(&0u64.to_le_bytes());
target_info.extend_from_slice(&MSV_AV_EOL.to_le_bytes());
target_info.extend_from_slice(&0u16.to_le_bytes());
let target_name = encode_utf16le("Server");
let flags: u32 = 0xe28a8233;
build_challenge_message_bytes(flags, &target_name, &target_info)
}
fn build_challenge_message_bytes(
flags: u32,
target_name: &[u8],
target_info: &[u8],
) -> Vec<u8> {
let header_size = 56;
let target_name_offset = header_size;
let target_info_offset = target_name_offset + target_name.len();
let mut buf = Vec::with_capacity(target_info_offset + target_info.len());
buf.extend_from_slice(NTLM_SIGNATURE);
buf.extend_from_slice(&MSG_TYPE_CHALLENGE.to_le_bytes());
buf.extend_from_slice(&(target_name.len() as u16).to_le_bytes());
buf.extend_from_slice(&(target_name.len() as u16).to_le_bytes());
buf.extend_from_slice(&(target_name_offset as u32).to_le_bytes());
buf.extend_from_slice(&flags.to_le_bytes());
buf.extend_from_slice(&TEST_SERVER_CHALLENGE);
buf.extend_from_slice(&[0u8; 8]);
buf.extend_from_slice(&(target_info.len() as u16).to_le_bytes());
buf.extend_from_slice(&(target_info.len() as u16).to_le_bytes());
buf.extend_from_slice(&(target_info_offset as u32).to_le_bytes());
buf.extend_from_slice(&[0x06, 0x00, 0x70, 0x17, 0x00, 0x00, 0x00, 0x0f]);
buf.extend_from_slice(target_name);
buf.extend_from_slice(target_info);
buf
}
}