use crate::error::{Error, Result};
use log::debug;
#[derive(Debug, Clone)]
pub struct CCache {
pub version: u16,
pub default_principal: CcachePrincipal,
pub credentials: Vec<CcacheCredential>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CcachePrincipal {
pub name_type: u32,
pub realm: String,
pub components: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct CcacheCredential {
pub client: CcachePrincipal,
pub server: CcachePrincipal,
pub key_etype: u16,
pub key_data: Vec<u8>,
pub authtime: u32,
pub starttime: u32,
pub endtime: u32,
pub renew_till: u32,
pub ticket: Vec<u8>,
}
pub fn load_ccache(path: Option<&std::path::Path>) -> Result<CCache> {
let path = match path {
Some(p) => p.to_path_buf(),
None => {
if let Ok(env_path) = std::env::var("KRB5CCNAME") {
let p = env_path.strip_prefix("FILE:").unwrap_or(&env_path);
std::path::PathBuf::from(p)
} else {
return Err(Error::invalid_data(
"ccache: no path specified and $KRB5CCNAME not set",
));
}
}
};
let data = std::fs::read(&path).map_err(|e| {
Error::invalid_data(format!("ccache: failed to read {}: {e}", path.display()))
})?;
parse_ccache(&data)
}
pub fn parse_ccache(data: &[u8]) -> Result<CCache> {
let mut pos = 0;
if data.len() < 2 {
return Err(Error::invalid_data("ccache: file too short for version"));
}
let version = read_u16(data, &mut pos)?;
if version != 0x0503 && version != 0x0504 {
return Err(Error::invalid_data(format!(
"ccache: unsupported version 0x{version:04x} (expected 0x0503 or 0x0504)"
)));
}
if version == 0x0504 {
let header_len = read_u16(data, &mut pos)? as usize;
if pos + header_len > data.len() {
return Err(Error::invalid_data(
"ccache: header extends past end of file",
));
}
pos += header_len;
}
let default_principal = read_principal(data, &mut pos)?;
let mut credentials = Vec::new();
while pos < data.len() {
match read_credential(data, &mut pos) {
Ok(cred) => credentials.push(cred),
Err(_) => break, }
}
debug!(
"ccache: parsed v{}, principal={}@{}, {} credentials",
version & 0xFF,
default_principal.components.join("/"),
default_principal.realm,
credentials.len()
);
Ok(CCache {
version,
default_principal,
credentials,
})
}
impl CCache {
pub fn find_service_ticket(
&self,
service: &str,
hostname: &str,
realm: &str,
) -> Option<&CcacheCredential> {
self.credentials.iter().find(|c| {
c.server.realm.eq_ignore_ascii_case(realm)
&& c.server.components.len() == 2
&& c.server.components[0].eq_ignore_ascii_case(service)
&& c.server.components[1].eq_ignore_ascii_case(hostname)
})
}
pub fn find_tgt(&self, realm: &str) -> Option<&CcacheCredential> {
self.credentials.iter().find(|c| {
c.server.realm.eq_ignore_ascii_case(realm)
&& c.server.components.len() == 2
&& c.server.components[0] == "krbtgt"
&& c.server.components[1].eq_ignore_ascii_case(realm)
})
}
}
fn read_u8(data: &[u8], pos: &mut usize) -> Result<u8> {
if *pos >= data.len() {
return Err(Error::invalid_data("ccache: unexpected end of data"));
}
let val = data[*pos];
*pos += 1;
Ok(val)
}
fn read_u16(data: &[u8], pos: &mut usize) -> Result<u16> {
if *pos + 2 > data.len() {
return Err(Error::invalid_data("ccache: unexpected end of data"));
}
let val = u16::from_be_bytes([data[*pos], data[*pos + 1]]);
*pos += 2;
Ok(val)
}
fn read_u32(data: &[u8], pos: &mut usize) -> Result<u32> {
if *pos + 4 > data.len() {
return Err(Error::invalid_data("ccache: unexpected end of data"));
}
let val = u32::from_be_bytes([data[*pos], data[*pos + 1], data[*pos + 2], data[*pos + 3]]);
*pos += 4;
Ok(val)
}
fn read_bytes(data: &[u8], pos: &mut usize, len: usize) -> Result<Vec<u8>> {
if *pos + len > data.len() {
return Err(Error::invalid_data("ccache: unexpected end of data"));
}
let val = data[*pos..*pos + len].to_vec();
*pos += len;
Ok(val)
}
fn read_string(data: &[u8], pos: &mut usize) -> Result<String> {
let len = read_u32(data, pos)? as usize;
let bytes = read_bytes(data, pos, len)?;
String::from_utf8(bytes).map_err(|_| Error::invalid_data("ccache: invalid UTF-8 in string"))
}
fn read_principal(data: &[u8], pos: &mut usize) -> Result<CcachePrincipal> {
let name_type = read_u32(data, pos)?;
let num_components = read_u32(data, pos)?;
let realm = read_string(data, pos)?;
let mut components = Vec::with_capacity(num_components as usize);
for _ in 0..num_components {
components.push(read_string(data, pos)?);
}
Ok(CcachePrincipal {
name_type,
realm,
components,
})
}
fn read_keyblock(data: &[u8], pos: &mut usize) -> Result<(u16, Vec<u8>)> {
let enctype = read_u16(data, pos)?;
let key_len = read_u32(data, pos)? as usize;
let key_data = read_bytes(data, pos, key_len)?;
Ok((enctype, key_data))
}
fn read_credential(data: &[u8], pos: &mut usize) -> Result<CcacheCredential> {
let client = read_principal(data, pos)?;
let server = read_principal(data, pos)?;
let (key_etype, key_data) = read_keyblock(data, pos)?;
let authtime = read_u32(data, pos)?;
let starttime = read_u32(data, pos)?;
let endtime = read_u32(data, pos)?;
let renew_till = read_u32(data, pos)?;
let _is_skey = read_u8(data, pos)?;
let _ticket_flags = read_u32(data, pos)?;
let addr_count = read_u32(data, pos)?;
for _ in 0..addr_count {
let _addr_type = read_u16(data, pos)?;
let addr_len = read_u32(data, pos)? as usize;
*pos += addr_len; }
let authdata_count = read_u32(data, pos)?;
for _ in 0..authdata_count {
let _ad_type = read_u16(data, pos)?;
let ad_len = read_u32(data, pos)? as usize;
*pos += ad_len; }
let ticket_len = read_u32(data, pos)? as usize;
let ticket = read_bytes(data, pos, ticket_len)?;
let second_ticket_len = read_u32(data, pos)? as usize;
let _second_ticket = read_bytes(data, pos, second_ticket_len)?;
Ok(CcacheCredential {
client,
server,
key_etype,
key_data,
authtime,
starttime,
endtime,
renew_till,
ticket,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_v4_ccache_from_fixture() {
let data = include_bytes!("../../../tests/fixtures/test.ccache");
let ccache = parse_ccache(data).expect("failed to parse v4 ccache");
assert_eq!(ccache.version, 0x0504);
assert_eq!(ccache.default_principal.realm, "TEST.LOCAL");
assert_eq!(ccache.default_principal.components, vec!["smbtest"]);
assert_eq!(ccache.credentials.len(), 2);
}
#[test]
fn parse_v3_ccache_from_fixture() {
let data = include_bytes!("../../../tests/fixtures/test_v3.ccache");
let ccache = parse_ccache(data).expect("failed to parse v3 ccache");
assert_eq!(ccache.version, 0x0503);
assert_eq!(ccache.default_principal.realm, "EXAMPLE.COM");
assert_eq!(ccache.default_principal.components, vec!["user"]);
assert_eq!(ccache.credentials.len(), 1);
}
#[test]
fn tgt_credential_has_correct_fields() {
let data = include_bytes!("../../../tests/fixtures/test.ccache");
let ccache = parse_ccache(data).unwrap();
let tgt = &ccache.credentials[0];
assert_eq!(tgt.client.realm, "TEST.LOCAL");
assert_eq!(tgt.client.components, vec!["smbtest"]);
assert_eq!(tgt.server.realm, "TEST.LOCAL");
assert_eq!(tgt.server.components, vec!["krbtgt", "TEST.LOCAL"]);
assert_eq!(tgt.key_etype, 23); assert_eq!(tgt.key_data.len(), 16);
assert_eq!(tgt.authtime, 1744100000);
assert_eq!(tgt.endtime, 1744200000);
}
#[test]
fn service_ticket_has_correct_fields() {
let data = include_bytes!("../../../tests/fixtures/test.ccache");
let ccache = parse_ccache(data).unwrap();
let svc = &ccache.credentials[1];
assert_eq!(svc.server.components, vec!["cifs", "server.test.local"]);
assert_eq!(svc.key_etype, 23);
assert_eq!(svc.key_data, (16u8..32).collect::<Vec<_>>());
}
#[test]
fn find_tgt_by_realm() {
let data = include_bytes!("../../../tests/fixtures/test.ccache");
let ccache = parse_ccache(data).unwrap();
let tgt = ccache.find_tgt("TEST.LOCAL");
assert!(tgt.is_some());
assert_eq!(tgt.unwrap().server.components[0], "krbtgt");
assert!(ccache.find_tgt("OTHER.REALM").is_none());
}
#[test]
fn find_service_ticket_by_spn() {
let data = include_bytes!("../../../tests/fixtures/test.ccache");
let ccache = parse_ccache(data).unwrap();
let svc = ccache.find_service_ticket("cifs", "server.test.local", "TEST.LOCAL");
assert!(svc.is_some());
assert_eq!(svc.unwrap().key_data, (16u8..32).collect::<Vec<_>>());
assert!(ccache
.find_service_ticket("cifs", "SERVER.TEST.LOCAL", "TEST.LOCAL")
.is_some());
assert!(ccache
.find_service_ticket("cifs", "other.test.local", "TEST.LOCAL")
.is_none());
assert!(ccache
.find_service_ticket("ldap", "server.test.local", "TEST.LOCAL")
.is_none());
}
#[test]
fn find_tgt_case_insensitive() {
let data = include_bytes!("../../../tests/fixtures/test.ccache");
let ccache = parse_ccache(data).unwrap();
assert!(ccache.find_tgt("test.local").is_some());
}
#[test]
fn v3_ccache_tgt_has_aes256_key() {
let data = include_bytes!("../../../tests/fixtures/test_v3.ccache");
let ccache = parse_ccache(data).unwrap();
let tgt = ccache.find_tgt("EXAMPLE.COM").unwrap();
assert_eq!(tgt.key_etype, 18); assert_eq!(tgt.key_data.len(), 32);
}
#[test]
fn reject_unsupported_version() {
let data = [0x05, 0x02]; let result = parse_ccache(&data);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("unsupported version"));
}
#[test]
fn reject_truncated_file() {
let result = parse_ccache(&[0x05]);
assert!(result.is_err());
}
#[test]
fn empty_credentials_list() {
let mut data = vec![0x05, 0x04, 0x00, 0x00]; data.extend_from_slice(&[0, 0, 0, 1]); data.extend_from_slice(&[0, 0, 0, 1]); data.extend_from_slice(&[0, 0, 0, 1]); data.push(b'R');
data.extend_from_slice(&[0, 0, 0, 1]); data.push(b'u');
let ccache = parse_ccache(&data).unwrap();
assert_eq!(ccache.credentials.len(), 0);
assert_eq!(ccache.default_principal.realm, "R");
assert_eq!(ccache.default_principal.components, vec!["u"]);
}
}