use core::ffi::c_void;
use core::mem::size_of;
use core::ptr;
use windows::Win32::Foundation::{HANDLE, LUID as WinLuid, NTSTATUS, UNICODE_STRING};
use windows::Win32::Security::Authentication::Identity::{
LsaCallAuthenticationPackage, LsaConnectUntrusted, LsaDeregisterLogonProcess,
LsaFreeReturnBuffer, LsaLookupAuthenticationPackage, LSA_STRING,
};
use windows_core::{PSTR, PWSTR};
use crate::error::{Error, Result};
use crate::types::{AuthPackageId, KrbCred, Luid, TicketCacheInfo};
const KERB_QUERY_TICKET_CACHE: u32 = 1;
const KERB_RETRIEVE_TICKET: u32 = 4;
const KERB_PURGE_TICKET_CACHE: u32 = 6;
const KERB_SUBMIT_TICKET: u32 = 19;
const KERB_RETRIEVE_TICKET_AS_KERB_CRED: u32 = 0x8;
const KERB_ETYPE_DEFAULT: i32 = 0;
#[repr(C)]
#[derive(Clone, Copy)]
struct SecHandle {
lower: usize,
upper: usize,
}
#[repr(C)]
struct KerbRetrieveTktRequest {
message_type: u32,
logon_id: WinLuid,
target_name: UNICODE_STRING,
ticket_flags: u32,
cache_options: u32,
encryption_type: i32,
credentials_handle: SecHandle,
}
#[repr(C)]
struct KerbCryptoKey {
key_type: i32,
length: u32,
value: *mut u8,
}
#[repr(C)]
struct KerbExternalTicket {
service_name: *mut c_void, target_name: *mut c_void,
client_name: *mut c_void,
domain_name: UNICODE_STRING,
target_domain_name: UNICODE_STRING,
alt_target_domain_name: UNICODE_STRING,
session_key: KerbCryptoKey,
ticket_flags: u32,
flags: u32,
key_expiration_time: i64,
start_time: i64,
end_time: i64,
renew_until: i64,
time_skew: i64,
encoded_ticket_size: u32,
encoded_ticket: *mut u8,
}
#[repr(C)]
struct KerbRetrieveTktResponse {
ticket: KerbExternalTicket,
}
#[repr(C)]
struct KerbQueryTktCacheRequest {
message_type: u32,
logon_id: WinLuid,
}
#[repr(C)]
struct KerbTicketCacheInfoRaw {
server_name: UNICODE_STRING,
realm_name: UNICODE_STRING,
start_time: i64,
end_time: i64,
renew_time: i64,
encryption_type: i32,
ticket_flags: u32,
}
#[repr(C)]
struct KerbQueryTktCacheResponse {
message_type: u32,
count_of_tickets: u32,
}
#[repr(C)]
struct KerbCryptoKey32 {
key_type: i32,
length: u32,
offset: u32,
}
#[repr(C)]
struct KerbSubmitTktRequest {
message_type: u32,
logon_id: WinLuid,
flags: u32,
key: KerbCryptoKey32,
kerb_cred_size: u32,
kerb_cred_offset: u32,
}
#[repr(C)]
struct KerbPurgeTktCacheRequest {
message_type: u32,
logon_id: WinLuid,
server_name: UNICODE_STRING,
realm_name: UNICODE_STRING,
}
#[derive(Debug)]
pub struct Lsa;
#[derive(Debug)]
pub struct LsaHandle {
handle: HANDLE,
}
unsafe impl Send for LsaHandle {}
unsafe impl Sync for LsaHandle {}
impl Lsa {
pub fn connect_untrusted() -> Result<LsaHandle> {
let mut h = HANDLE::default();
let status = unsafe { LsaConnectUntrusted(&mut h) };
check(status, "LsaConnectUntrusted")?;
Ok(LsaHandle { handle: h })
}
}
impl Drop for LsaHandle {
fn drop(&mut self) {
if !self.handle.is_invalid() {
unsafe {
let _ = LsaDeregisterLogonProcess(self.handle);
}
}
}
}
impl LsaHandle {
pub fn kerberos_package(&self) -> Result<AuthPackageId> {
const NAME: &[u8] = b"Kerberos";
let lsa_str = LSA_STRING {
Length: NAME.len() as u16,
MaximumLength: NAME.len() as u16,
Buffer: PSTR(NAME.as_ptr() as *mut u8),
};
let mut pkg: u32 = 0;
let status = unsafe { LsaLookupAuthenticationPackage(self.handle, &lsa_str, &mut pkg) };
check(status, "LsaLookupAuthenticationPackage")?;
Ok(AuthPackageId(pkg))
}
pub fn retrieve_tgt(&self, pkg: AuthPackageId, luid: Option<Luid>) -> Result<KrbCred> {
let req = KerbRetrieveTktRequest {
message_type: KERB_RETRIEVE_TICKET,
logon_id: to_win_luid(luid),
target_name: UNICODE_STRING::default(),
ticket_flags: 0,
cache_options: KERB_RETRIEVE_TICKET_AS_KERB_CRED,
encryption_type: KERB_ETYPE_DEFAULT,
credentials_handle: SecHandle { lower: 0, upper: 0 },
};
let response = self.call(pkg, &req, size_of::<KerbRetrieveTktRequest>() as u32)?;
let bytes = unsafe {
let resp_ptr = response.ptr as *const KerbRetrieveTktResponse;
if response.len < size_of::<KerbRetrieveTktResponse>() {
return Err(Error::Malformed("retrieve response short"));
}
let ticket = &(*resp_ptr).ticket;
let size = ticket.encoded_ticket_size as usize;
if size == 0 || ticket.encoded_ticket.is_null() {
return Err(Error::Malformed("retrieve response has no ticket"));
}
core::slice::from_raw_parts(ticket.encoded_ticket, size).to_vec()
};
Ok(KrbCred::new(bytes))
}
pub fn query_ticket_cache(
&self,
pkg: AuthPackageId,
luid: Option<Luid>,
) -> Result<Vec<TicketCacheInfo>> {
let req = KerbQueryTktCacheRequest {
message_type: KERB_QUERY_TICKET_CACHE,
logon_id: to_win_luid(luid),
};
let response = self.call(pkg, &req, size_of::<KerbQueryTktCacheRequest>() as u32)?;
unsafe {
if response.len < size_of::<KerbQueryTktCacheResponse>() {
return Err(Error::Malformed("query response header short"));
}
let header = &*(response.ptr as *const KerbQueryTktCacheResponse);
let n = header.count_of_tickets as usize;
let stride = size_of::<KerbTicketCacheInfoRaw>();
let needed = size_of::<KerbQueryTktCacheResponse>()
.checked_add(n.checked_mul(stride).ok_or(Error::Overflow)?)
.ok_or(Error::Overflow)?;
if response.len < needed {
return Err(Error::Malformed("query response trailing entries short"));
}
let entries_ptr = (response.ptr as *const u8)
.add(size_of::<KerbQueryTktCacheResponse>())
as *const KerbTicketCacheInfoRaw;
let mut out = Vec::with_capacity(n);
for i in 0..n {
let raw = &*entries_ptr.add(i);
out.push(TicketCacheInfo {
server_name: unicode_string_to_string(&raw.server_name),
realm_name: unicode_string_to_string(&raw.realm_name),
start_time_ft: raw.start_time,
end_time_ft: raw.end_time,
renew_time_ft: raw.renew_time,
encryption_type: raw.encryption_type,
ticket_flags: raw.ticket_flags,
});
}
Ok(out)
}
}
pub fn submit_ticket(
&self,
pkg: AuthPackageId,
cred: &KrbCred,
luid: Option<Luid>,
) -> Result<()> {
let hdr_len = size_of::<KerbSubmitTktRequest>();
let total = hdr_len
.checked_add(cred.encoded.len())
.ok_or(Error::Overflow)?;
if total > u32::MAX as usize {
return Err(Error::Overflow);
}
let mut buf: Vec<u8> = vec![0u8; total];
{
let hdr = KerbSubmitTktRequest {
message_type: KERB_SUBMIT_TICKET,
logon_id: to_win_luid(luid),
flags: 0,
key: KerbCryptoKey32 {
key_type: 0,
length: 0,
offset: 0,
},
kerb_cred_size: cred.encoded.len() as u32,
kerb_cred_offset: hdr_len as u32,
};
unsafe {
ptr::write(buf.as_mut_ptr() as *mut KerbSubmitTktRequest, hdr);
}
}
if !cred.encoded.is_empty() {
buf[hdr_len..].copy_from_slice(&cred.encoded);
}
let _ = self.call_raw(pkg, buf.as_ptr() as *const c_void, total as u32)?;
Ok(())
}
pub fn purge_ticket_cache(
&self,
pkg: AuthPackageId,
luid: Option<Luid>,
target_spn: Option<&str>,
) -> Result<()> {
let spn_utf16: Vec<u16> = match target_spn {
None => Vec::new(),
Some(s) => s.encode_utf16().collect(),
};
let spn_bytes = spn_utf16.len().checked_mul(2).ok_or(Error::Overflow)?;
if spn_bytes > u16::MAX as usize - 2 {
return Err(Error::StringTooLong);
}
let hdr_len = size_of::<KerbPurgeTktCacheRequest>();
let total = hdr_len.checked_add(spn_bytes).ok_or(Error::Overflow)?;
let mut buf: Vec<u8> = vec![0u8; total];
if spn_bytes > 0 {
unsafe {
ptr::copy_nonoverlapping(
spn_utf16.as_ptr() as *const u8,
buf.as_mut_ptr().add(hdr_len),
spn_bytes,
);
}
}
let server_name = if spn_bytes == 0 {
UNICODE_STRING::default()
} else {
UNICODE_STRING {
Length: spn_bytes as u16,
MaximumLength: spn_bytes as u16,
Buffer: PWSTR(unsafe { buf.as_mut_ptr().add(hdr_len) as *mut u16 }),
}
};
let hdr = KerbPurgeTktCacheRequest {
message_type: KERB_PURGE_TICKET_CACHE,
logon_id: to_win_luid(luid),
server_name,
realm_name: UNICODE_STRING::default(),
};
unsafe {
ptr::write(buf.as_mut_ptr() as *mut KerbPurgeTktCacheRequest, hdr);
}
let _ = self.call_raw(pkg, buf.as_ptr() as *const c_void, total as u32)?;
Ok(())
}
fn call<T>(&self, pkg: AuthPackageId, req: &T, len: u32) -> Result<LsaResponse> {
self.call_raw(pkg, req as *const T as *const c_void, len)
}
fn call_raw(&self, pkg: AuthPackageId, buf: *const c_void, len: u32) -> Result<LsaResponse> {
let mut ret_buf: *mut c_void = ptr::null_mut();
let mut ret_len: u32 = 0;
let mut proto_status: i32 = 0;
let status = unsafe {
LsaCallAuthenticationPackage(
self.handle,
pkg.0,
buf,
len,
Some(&mut ret_buf),
Some(&mut ret_len),
Some(&mut proto_status),
)
};
check(status, "LsaCallAuthenticationPackage")?;
if proto_status != 0 {
if !ret_buf.is_null() {
unsafe {
let _ = LsaFreeReturnBuffer(ret_buf);
}
}
return Err(Error::Protocol(proto_status as u32));
}
Ok(LsaResponse {
ptr: ret_buf,
len: ret_len as usize,
})
}
}
struct LsaResponse {
ptr: *mut c_void,
len: usize,
}
impl Drop for LsaResponse {
fn drop(&mut self) {
if !self.ptr.is_null() {
unsafe {
let _ = LsaFreeReturnBuffer(self.ptr);
}
}
}
}
fn to_win_luid(luid: Option<Luid>) -> WinLuid {
match luid {
None => WinLuid {
LowPart: 0,
HighPart: 0,
},
Some(l) => WinLuid {
LowPart: l.low_part,
HighPart: l.high_part,
},
}
}
fn check(status: NTSTATUS, api: &'static str) -> Result<()> {
if status.0 == 0 {
Ok(())
} else {
Err(Error::Lsa {
api,
status: status.0 as u32,
})
}
}
fn unicode_string_to_string(us: &UNICODE_STRING) -> String {
if us.Buffer.is_null() || us.Length == 0 {
return String::new();
}
let len_words = (us.Length as usize) / 2;
let slice = unsafe { core::slice::from_raw_parts(us.Buffer.0, len_words) };
String::from_utf16_lossy(slice)
}
#[cfg(target_pointer_width = "64")]
const _: () = {
assert!(size_of::<KerbSubmitTktRequest>() == 36);
assert!(size_of::<KerbQueryTktCacheRequest>() == 12);
};