use regex::Regex;
use std::collections::HashMap;
use windows_sys::Win32::Foundation::{FILETIME, SYSTEMTIME};
use windows_sys::Win32::Security::Credentials::{
CredEnumerateW, CredFree, CREDENTIALW, CRED_ENUMERATE_ALL_CREDENTIALS, CRED_PERSIST, CRED_TYPE,
};
use windows_sys::Win32::Storage::FileSystem::FileTimeToLocalFileTime;
use windows_sys::Win32::System::Time::{LocalFileTimeToLocalSystemTime, TIME_ZONE_INFORMATION};
use super::error::{Error as ErrorCode, Result};
use super::search::{CredentialSearch, CredentialSearchApi, CredentialSearchResult};
static DAYS: [&str; 7] = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
static MONTHS: [&str; 12] = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
pub struct WinCredential {
pub username: String,
pub target_name: String,
pub target_alias: String,
pub comment: String,
pub cred_type: CRED_TYPE,
pub last_written: HumanTime,
pub persist: CRED_PERSIST,
}
pub struct HumanTime {
pub day_of_week: String,
pub day: u16,
pub hour: u16,
pub minute: u16,
pub second: u16,
pub month: String,
pub year: u16,
}
impl std::fmt::Display for HumanTime {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}, {} {}, {} at {:02}:{:02}:{:02}",
self.day_of_week, self.day, self.month, self.year, self.hour, self.minute, self.second
)
}
}
enum WinSearchType {
Target,
Service,
User,
}
pub struct WinCredentialSearch {}
pub fn default_credential_search() -> Box<CredentialSearch> {
Box::new(WinCredentialSearch {})
}
impl CredentialSearchApi for WinCredentialSearch {
fn by(&self, by: &str, query: &str) -> CredentialSearchResult {
let mut count = 0;
let results = search_type(by, query)?;
let mut outer_map: HashMap<String, HashMap<String, String>> = HashMap::new();
for result in results {
count += 1;
let mut inner_map: HashMap<String, String> = HashMap::new();
inner_map.insert("Comment".to_string(), result.comment.clone());
inner_map.insert("User".to_string(), result.username.clone());
inner_map.insert("Type".to_string(), match_cred_type(result.cred_type)?);
inner_map.insert("Last Written".to_string(), result.last_written.to_string());
inner_map.insert("Persist".to_string(), match_persist_type(result.persist)?);
inner_map.insert("Target".to_string(), result.target_name.to_string());
outer_map.insert(count.to_string(), inner_map);
}
Ok(outer_map)
}
}
fn search_type(by: &str, query: &str) -> Result<Vec<WinCredential>> {
let search_type = match by.to_ascii_lowercase().as_str() {
"target" => WinSearchType::Target,
"service" => WinSearchType::Service,
"user" => WinSearchType::User,
_ => {
return Err(ErrorCode::SearchError(
"Invalid search parameter, not Target, Service, or User".to_string(),
))
}
};
search(&search_type, query)
}
fn search(search_type: &WinSearchType, search_parameter: &str) -> Result<Vec<WinCredential>> {
let credentials = get_all_credentials();
let re = format!(r#"(?i){}"#, search_parameter);
let regex = match Regex::new(re.as_str()) {
Ok(regex) => regex,
Err(err) => return Err(ErrorCode::SearchError(format!("Regex Error, {}", err))),
};
let mut results = Vec::new();
for credential in credentials {
let haystack = match search_type {
WinSearchType::Target => &credential.target_name,
WinSearchType::Service => &credential.comment,
WinSearchType::User => &credential.username,
};
if regex.is_match(haystack) {
results.push(credential);
}
}
if results.is_empty() {
Err(ErrorCode::NoResults)
} else {
Ok(results)
}
}
fn get_all_credentials() -> Vec<WinCredential> {
let mut entries: Vec<WinCredential> = Vec::new();
let mut count = 0;
let mut credentials_ptr = std::ptr::null_mut();
unsafe {
CredEnumerateW(
std::ptr::null(),
CRED_ENUMERATE_ALL_CREDENTIALS,
&mut count,
&mut credentials_ptr,
);
}
let credentials =
unsafe { std::slice::from_raw_parts::<&CREDENTIALW>(credentials_ptr as _, count as usize) };
for credential in credentials {
let target_name = unsafe { from_wstr(credential.TargetName) };
let index = target_name.find('=').unwrap_or(0);
let target_name = target_name[index + 1..].to_string();
let username = if unsafe { from_wstr(credential.UserName) }.is_empty() {
String::from("NO USER")
} else {
unsafe { from_wstr(credential.UserName) }
};
let target_alias = unsafe { from_wstr(credential.TargetAlias) };
let comment = unsafe { from_wstr(credential.Comment) };
let cred_type = credential.Type;
let last_written = unsafe { get_last_written(credential.LastWritten) };
let persist = credential.Persist;
entries.push(WinCredential {
username,
target_name,
target_alias,
comment,
cred_type,
last_written,
persist,
});
}
unsafe {
CredFree(std::mem::transmute::<
*mut *mut CREDENTIALW,
*const std::ffi::c_void,
>(credentials_ptr))
};
entries
}
unsafe fn get_last_written(last_written: FILETIME) -> HumanTime {
let mut local_filetime: FILETIME = std::mem::zeroed();
let mut system_time: SYSTEMTIME = std::mem::zeroed();
let local: TIME_ZONE_INFORMATION = std::mem::zeroed();
FileTimeToLocalFileTime(&last_written, &mut local_filetime as *mut FILETIME);
LocalFileTimeToLocalSystemTime(&local, &local_filetime, &mut system_time as *mut SYSTEMTIME);
HumanTime {
hour: system_time.wHour,
minute: system_time.wMinute,
second: system_time.wSecond,
day_of_week: DAYS[system_time.wDayOfWeek as usize].to_string(),
day: system_time.wDay,
month: MONTHS[system_time.wMonth as usize - 1].to_string(),
year: system_time.wYear,
}
}
fn match_cred_type(credential: u32) -> Result<String> {
match credential {
1 => Ok("Generic".to_string()),
2 => Ok("Domain Password".to_string()),
3 => Ok("Domain Certificate".to_string()),
4 => Ok("Domain Visible Password".to_string()),
5 => Ok("Generic Certificate".to_string()),
6 => Ok("Domain Extended".to_string()),
7 => Ok("Maximum".to_string()),
1007 => Ok("Maximum Ex".to_string()),
_ => Err(ErrorCode::Unexpected("cred_type".to_string())),
}
}
fn match_persist_type(credential: u32) -> Result<String> {
match credential {
0 => Ok("None".to_string()),
1 => Ok("Session".to_string()),
2 => Ok("Local Machine".to_string()),
3 => Ok("Enterprise".to_string()),
_ => Err(ErrorCode::Unexpected("persist_type".to_string())),
}
}
unsafe fn from_wstr(ws: *const u16) -> String {
if ws.is_null() {
return String::new();
}
let len = (0..).take_while(|&i| *ws.offset(i) != 0).count();
if len == 0 {
return String::new();
}
let slice = std::slice::from_raw_parts(ws, len);
String::from_utf16_lossy(slice)
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use std::iter::once;
use byteorder::{ByteOrder, LittleEndian};
use windows_sys::Win32::Foundation::FILETIME;
use windows_sys::Win32::Security::Credentials::{
CredDeleteW, CredFree, CredReadW, CredWriteW, CREDENTIALW, CREDENTIAL_ATTRIBUTEW,
CRED_FLAGS, CRED_PERSIST_ENTERPRISE, CRED_TYPE_GENERIC,
};
use crate::{tests::generate_random_string, Search};
use crate::{Error, Limit, List};
use super::{get_last_written, match_cred_type, match_persist_type};
fn to_wstr(s: &str) -> Vec<u16> {
s.encode_utf16().chain(once(0)).collect()
}
fn delete_credential(name: &str) {
unsafe {
CredDeleteW(
to_wstr(&name).as_ptr(),
CRED_TYPE_GENERIC,
CRED_TYPE_GENERIC,
)
};
}
fn create_credential(name: &str, user: Option<&str>) {
let mut user = if user == None {
to_wstr(&name)
} else {
to_wstr(user.unwrap())
};
let mut target_name = to_wstr(&name);
let mut target_alias = to_wstr(&name);
let mut comment = to_wstr(&name);
let last_written = FILETIME {
dwLowDateTime: 0,
dwHighDateTime: 0,
};
let blob_u16 = to_wstr("password");
let mut blob = vec![0; blob_u16.len() * 2];
LittleEndian::write_u16_into(&blob_u16, &mut blob);
let blob_len = blob.len() as u32;
let attributes: *mut CREDENTIAL_ATTRIBUTEW = std::ptr::null_mut();
let mut credential = CREDENTIALW {
Flags: CRED_FLAGS::default(),
Type: CRED_TYPE_GENERIC,
TargetName: target_name.as_mut_ptr(),
Comment: comment.as_mut_ptr(),
LastWritten: last_written,
CredentialBlobSize: blob_len,
CredentialBlob: blob.as_mut_ptr(),
Persist: CRED_PERSIST_ENTERPRISE,
AttributeCount: 0,
Attributes: attributes,
TargetAlias: target_alias.as_mut_ptr(),
UserName: user.as_mut_ptr(),
};
let p_credential: *const CREDENTIALW = &mut credential;
unsafe { CredWriteW(p_credential, 0) };
}
fn test_search(by: &str) {
let name = generate_random_string();
println!("test-search by {}\nname {}\n", &by, &name);
create_credential(&name, None);
let mut r_credential: *mut CREDENTIALW = std::ptr::null_mut();
let last_written_filetime = unsafe {
CredReadW(
to_wstr(&name).as_ptr(),
CRED_TYPE_GENERIC,
CRED_FLAGS::default(),
&mut r_credential,
);
let read_credential = *r_credential;
CredFree(r_credential as *mut _);
read_credential.LastWritten
};
let expected = format!(
"1\nTarget: {}\nLast Written: {}\nType: {}\nPersist: {}\nUser: {}\nComment: {}\n",
name,
unsafe { get_last_written(last_written_filetime) },
match_cred_type(CRED_TYPE_GENERIC).expect("Failed to match expected cred type"),
match_persist_type(CRED_PERSIST_ENTERPRISE)
.expect("Failed to match expected persist type"),
name,
name,
);
let search_result = match by.to_ascii_lowercase().as_str() {
"user" => Search::new()
.expect("Error creating test search")
.by_user(&name.clone()),
"target" => Search::new()
.expect("Error creating test search")
.by_target(&name.clone()),
"service" => Search::new()
.expect("Error creating test search")
.by_service(&name.clone()),
_ => panic!("Unexpected search by parameter"),
};
let list = List::list_credentials(&search_result, Limit::All);
let result_set: HashSet<&str> = list.lines().collect();
let actual_set: HashSet<&str> = expected.lines().collect();
delete_credential(&name);
assert_eq!(result_set, actual_set);
}
#[test]
fn test_search_by_user() {
test_search("user")
}
#[test]
fn test_search_by_service() {
test_search("service")
}
#[test]
fn test_search_by_target() {
test_search("target")
}
#[test]
fn test_max_result() {
let name1 = generate_random_string();
let name2 = generate_random_string();
let name3 = generate_random_string();
let name4 = generate_random_string();
println!(
"test-max-result:\nname1 {}\nname2 {}\nname3 {}\nname4 {}",
&name1.clone(),
&name2.clone(),
&name3.clone(),
&name4.clone()
);
create_credential(&name1, Some("test-user"));
create_credential(&name2, Some("test-user"));
create_credential(&name3, Some("test-user"));
create_credential(&name4, Some("test-user"));
let search = Search::new()
.expect("Error creating test-max-result search")
.by_user("test-user");
let list = List::list_credentials(&search, Limit::Max(1));
let lines = list.lines().count();
delete_credential(&name1);
delete_credential(&name2);
delete_credential(&name3);
delete_credential(&name4);
assert_eq!(7, lines);
}
#[test]
fn no_results() {
let name = generate_random_string();
let result = Search::new()
.expect("Failed to build new search")
.by_user(&name);
assert!(
matches!(result.unwrap_err(), Error::NoResults),
"Returned an empty value"
);
}
#[test]
fn last_written_filetime() {
let test_filetimes: Vec<FILETIME> = vec![
FILETIME {
dwLowDateTime: 0xFE39E000,
dwHighDateTime: 0x01D5C09A,
}, FILETIME {
dwLowDateTime: 0x21082000,
dwHighDateTime: 0x01D5D8F7,
}, FILETIME {
dwLowDateTime: 0xEF02E000,
dwHighDateTime: 0x01D5EFC0,
}, FILETIME {
dwLowDateTime: 0x11D12000,
dwHighDateTime: 0x01D6081D,
}, FILETIME {
dwLowDateTime: 0x0A35A000,
dwHighDateTime: 0x01D61FB0,
}, FILETIME {
dwLowDateTime: 0x2D03E000,
dwHighDateTime: 0x01D6380C,
}, FILETIME {
dwLowDateTime: 0x25686000,
dwHighDateTime: 0x01D64F9F,
}, FILETIME {
dwLowDateTime: 0x4836A000,
dwHighDateTime: 0x01D667FB,
}, FILETIME {
dwLowDateTime: 0x6B04E000,
dwHighDateTime: 0x01D68057,
}, FILETIME {
dwLowDateTime: 0x63696000,
dwHighDateTime: 0x01D697EA,
}, FILETIME {
dwLowDateTime: 0x8637A000,
dwHighDateTime: 0x01D6B046,
}, FILETIME {
dwLowDateTime: 0x7E9C2000,
dwHighDateTime: 0x01D6C7D9,
}, ];
let expected_months: Vec<&str> = vec![
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
let expected_day_of_weeks: Vec<&str> = vec![
"Wednesday",
"Saturday",
"Sunday",
"Wednesday",
"Friday",
"Monday",
"Wednesday",
"Saturday",
"Tuesday",
"Thursday",
"Sunday",
"Tuesday",
];
for (i, ft) in test_filetimes.iter().enumerate() {
let human_time = unsafe { get_last_written(*ft) };
println!("Testing FILETIME {}", human_time.to_string());
assert_eq!(
human_time.month, expected_months[i],
"Month string does not match expected value"
);
assert_eq!(
human_time.day_of_week, expected_day_of_weeks[i],
"Day of week string does not match expected value"
);
assert_eq!(
human_time.year, 2020,
"Year does not match expected value of 2020"
);
assert_eq!(human_time.day, 1, "Day does not match expected value of 1");
}
}
}