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 Ok(Some(names_key)) = hive.open_key("SAM\\Domains\\Account\\Users\\Names") else {
return results;
};
let Ok(Some(users_key)) = hive.open_key("SAM\\Domains\\Account\\Users") else {
return results;
};
let Ok(username_keys) = names_key.subkeys() else {
return results;
};
for name_key in username_keys {
let username = name_key.name();
if username.is_empty() {
continue;
}
let rid_opt = rid_and_f(&name_key, &users_key);
let Some((rid, f_data)) = rid_opt else {
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 rid_and_f(
name_key: &winreg_core::key::Key<'_>,
users_key: &winreg_core::key::Key<'_>,
) -> Option<(u32, Vec<u8>)> {
use winreg_format::flags::ValueType;
let ValueType::Unknown(rid) = name_key.value("").ok().flatten()?.data_type() else {
return None;
};
let rid_hex = format!("{rid:08X}");
let f_data = users_key
.subkey(&rid_hex)
.ok()
.flatten()?
.value("F")
.ok()
.flatten()?
.raw_data()
.ok()?;
Some((rid, f_data))
}
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]))
}