use std::io::Cursor;
use winreg_core::hive::Hive;
use winreg_core::key::filetime_to_datetime;
#[derive(Debug, Clone, serde::Serialize)]
pub struct SamUserEntry {
pub username: String,
pub rid: u32,
pub last_login: Option<String>,
pub password_last_set: Option<String>,
pub account_expires: Option<String>,
pub login_count: u16,
pub account_flags: u32,
pub is_disabled: bool,
pub is_locked: bool,
}
const F_LAST_LOGIN_OFF: usize = 8;
const F_PASSWORD_LAST_SET_OFF: usize = 16;
const F_ACCOUNT_EXPIRES_OFF: usize = 24;
const F_ACCOUNT_FLAGS_OFF: usize = 56;
const F_LOGIN_COUNT_OFF: usize = 66;
const ACCOUNT_DISABLED: u32 = 0x0001;
const ACCOUNT_LOCKED: u32 = 0x0010;
pub fn parse(hive: &Hive<Cursor<Vec<u8>>>) -> Vec<SamUserEntry> {
let mut results = Vec::new();
let names_key = match hive.open_key("SAM\\Domains\\Account\\Users\\Names") {
Ok(Some(k)) => k,
_ => return results,
};
let users_key = match hive.open_key("SAM\\Domains\\Account\\Users") {
Ok(Some(k)) => k,
_ => return results,
};
let username_keys = match names_key.subkeys() {
Ok(v) => v,
Err(_) => return results,
};
for name_key in username_keys {
let username = name_key.name();
if username.is_empty() {
continue;
}
let rid_opt = find_rid_for_username(&users_key, &username);
let (rid, f_data) = match rid_opt {
Some((r, d)) => (r, d),
None => {
results.push(SamUserEntry {
username,
rid: 0,
last_login: None,
password_last_set: None,
account_expires: None,
login_count: 0,
account_flags: 0,
is_disabled: false,
is_locked: false,
});
continue;
}
};
let entry = parse_f_record(&username, rid, &f_data);
results.push(entry);
}
results
}
fn find_rid_for_username(
users_key: &winreg_core::key::Key<'_>,
username: &str,
) -> Option<(u32, Vec<u8>)> {
let subkeys = users_key.subkeys().ok()?;
for sub in subkeys {
let name = sub.name();
if name.eq_ignore_ascii_case("Names") {
continue;
}
let rid = u32::from_str_radix(&name, 16).ok()?;
let f_val = match sub.value("F") {
Ok(Some(v)) => v,
_ => continue,
};
let f_data = f_val.raw_data().unwrap_or_default();
let _ = username; return Some((rid, f_data));
}
None
}
fn parse_f_record(username: &str, rid: u32, f: &[u8]) -> SamUserEntry {
let last_login = read_filetime(f, F_LAST_LOGIN_OFF);
let password_last_set = read_filetime(f, F_PASSWORD_LAST_SET_OFF);
let account_expires = read_filetime(f, F_ACCOUNT_EXPIRES_OFF);
let account_flags = read_u32(f, F_ACCOUNT_FLAGS_OFF);
let login_count = read_u16(f, F_LOGIN_COUNT_OFF);
SamUserEntry {
username: username.to_string(),
rid,
last_login,
password_last_set,
account_expires,
login_count,
account_flags,
is_disabled: (account_flags & ACCOUNT_DISABLED) != 0,
is_locked: (account_flags & ACCOUNT_LOCKED) != 0,
}
}
fn read_filetime(data: &[u8], offset: usize) -> Option<String> {
if offset + 8 > data.len() {
return None;
}
let ft = u64::from_le_bytes(data[offset..offset + 8].try_into().ok()?);
let dt = filetime_to_datetime(ft)?;
Some(dt.format("%Y-%m-%dT%H:%M:%SZ").to_string())
}
fn read_u32(data: &[u8], offset: usize) -> u32 {
if offset + 4 > data.len() {
return 0;
}
u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap_or([0; 4]))
}
fn read_u16(data: &[u8], offset: usize) -> u16 {
if offset + 2 > data.len() {
return 0;
}
u16::from_le_bytes(data[offset..offset + 2].try_into().unwrap_or([0; 2]))
}