1#![allow(dead_code)]
15
16#[cfg(feature = "alloc")]
17extern crate alloc;
18
19#[cfg(feature = "alloc")]
20use alloc::{string::String, vec::Vec};
21
22use super::protocol::{EncryptionType, KerberosClient, KerberosTime, PrincipalName};
23use crate::error::KernelError;
24
25const CCACHE_VERSION: u16 = 0x0504;
31
32const MAX_CACHE_ENTRIES: usize = 64;
34
35#[cfg(feature = "alloc")]
41#[derive(Debug, Clone)]
42pub struct CcacheEntry {
43 pub client_principal: PrincipalName,
45 pub server_principal: PrincipalName,
47 pub session_key: Vec<u8>,
49 pub session_key_etype: EncryptionType,
51 pub auth_time: KerberosTime,
53 pub start_time: KerberosTime,
55 pub end_time: KerberosTime,
57 pub renew_till: Option<KerberosTime>,
59 pub ticket_data: Vec<u8>,
61 pub flags: u32,
63}
64
65#[cfg(feature = "alloc")]
66impl CcacheEntry {
67 pub fn is_expired(&self) -> bool {
69 self.end_time.has_expired()
70 }
71
72 pub fn is_renewable(&self) -> bool {
74 if let Some(ref renew_till) = self.renew_till {
75 !renew_till.has_expired()
76 } else {
77 false
78 }
79 }
80
81 pub fn matches_server(&self, server: &PrincipalName) -> bool {
83 self.server_principal == *server
84 }
85
86 pub fn remaining_secs(&self) -> u64 {
88 let now = crate::arch::timer::get_timestamp_secs();
89 self.end_time.timestamp.saturating_sub(now)
90 }
91}
92
93#[cfg(feature = "alloc")]
108#[derive(Debug, Clone)]
109pub struct CcacheFile {
110 pub version: u16,
112 pub default_principal: PrincipalName,
114 pub entries: Vec<CcacheEntry>,
116}
117
118#[cfg(feature = "alloc")]
119impl CcacheFile {
120 pub fn new(default_principal: PrincipalName) -> Self {
122 Self {
123 version: CCACHE_VERSION,
124 default_principal,
125 entries: Vec::new(),
126 }
127 }
128
129 pub fn serialize(&self) -> Vec<u8> {
131 let mut out = Vec::new();
132
133 out.extend_from_slice(&self.version.to_be_bytes());
135
136 let header_len: u16 = 0; out.extend_from_slice(&header_len.to_be_bytes());
139
140 Self::serialize_principal(&self.default_principal, &mut out);
142
143 for entry in &self.entries {
145 self.serialize_credential(entry, &mut out);
146 }
147
148 out
149 }
150
151 pub fn deserialize(data: &[u8]) -> Result<Self, KernelError> {
153 if data.len() < 4 {
154 return Err(KernelError::InvalidArgument {
155 name: "ccache",
156 value: "too short",
157 });
158 }
159
160 let mut pos = 0;
161
162 let version = u16::from_be_bytes([data[pos], data[pos + 1]]);
164 pos += 2;
165
166 if version != CCACHE_VERSION {
167 return Err(KernelError::InvalidArgument {
168 name: "ccache_version",
169 value: "unsupported version",
170 });
171 }
172
173 let header_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
175 pos += 2;
176
177 if pos + header_len > data.len() {
179 return Err(KernelError::InvalidArgument {
180 name: "ccache_header",
181 value: "truncated",
182 });
183 }
184 pos += header_len;
185
186 let (default_principal, consumed) = Self::deserialize_principal(data, pos)?;
188 pos += consumed;
189
190 let mut entries = Vec::new();
192 while pos < data.len() {
193 match Self::deserialize_credential(data, pos) {
194 Ok((entry, consumed)) => {
195 entries.push(entry);
196 pos += consumed;
197 }
198 Err(_) => break,
199 }
200 }
201
202 Ok(Self {
203 version,
204 default_principal,
205 entries,
206 })
207 }
208
209 fn serialize_principal(principal: &PrincipalName, out: &mut Vec<u8>) {
211 out.extend_from_slice(&(principal.name_type as u32).to_be_bytes());
213 out.extend_from_slice(&(principal.name_string.len() as u32).to_be_bytes());
215 for component in &principal.name_string {
217 out.extend_from_slice(&(component.len() as u32).to_be_bytes());
218 out.extend_from_slice(component.as_bytes());
219 }
220 }
221
222 fn deserialize_principal(
224 data: &[u8],
225 start: usize,
226 ) -> Result<(PrincipalName, usize), KernelError> {
227 let mut pos = start;
228
229 if pos + 8 > data.len() {
230 return Err(KernelError::InvalidArgument {
231 name: "ccache_principal",
232 value: "truncated",
233 });
234 }
235
236 let name_type_val =
237 u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
238 pos += 4;
239
240 let num_components =
241 u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
242 pos += 4;
243
244 let mut name_string = Vec::new();
245 for _ in 0..num_components {
246 if pos + 4 > data.len() {
247 return Err(KernelError::InvalidArgument {
248 name: "ccache_component",
249 value: "truncated",
250 });
251 }
252 let comp_len =
253 u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
254 as usize;
255 pos += 4;
256
257 if pos + comp_len > data.len() {
258 return Err(KernelError::InvalidArgument {
259 name: "ccache_component_data",
260 value: "truncated",
261 });
262 }
263 let s = core::str::from_utf8(&data[pos..pos + comp_len]).map_err(|_| {
264 KernelError::InvalidArgument {
265 name: "ccache_component",
266 value: "invalid utf8",
267 }
268 })?;
269 name_string.push(String::from(s));
270 pos += comp_len;
271 }
272
273 let name_type = match name_type_val {
274 1 => super::protocol::NameType::Principal,
275 2 => super::protocol::NameType::SrvInst,
276 3 => super::protocol::NameType::SrvHst,
277 _ => super::protocol::NameType::Principal,
278 };
279
280 Ok((
281 PrincipalName {
282 name_type,
283 name_string,
284 },
285 pos - start,
286 ))
287 }
288
289 fn serialize_credential(&self, entry: &CcacheEntry, out: &mut Vec<u8>) {
291 Self::serialize_principal(&entry.client_principal, out);
293 Self::serialize_principal(&entry.server_principal, out);
295 out.extend_from_slice(&(entry.session_key_etype as u16).to_be_bytes());
297 out.extend_from_slice(&(entry.session_key.len() as u32).to_be_bytes());
298 out.extend_from_slice(&entry.session_key);
299 out.extend_from_slice(&(entry.auth_time.timestamp as u32).to_be_bytes());
301 out.extend_from_slice(&(entry.start_time.timestamp as u32).to_be_bytes());
302 out.extend_from_slice(&(entry.end_time.timestamp as u32).to_be_bytes());
303 let renew = entry.renew_till.map_or(0u32, |t| t.timestamp as u32);
304 out.extend_from_slice(&renew.to_be_bytes());
305 out.extend_from_slice(&entry.flags.to_be_bytes());
307 out.extend_from_slice(&(entry.ticket_data.len() as u32).to_be_bytes());
309 out.extend_from_slice(&entry.ticket_data);
310 }
311
312 fn deserialize_credential(
314 data: &[u8],
315 start: usize,
316 ) -> Result<(CcacheEntry, usize), KernelError> {
317 let mut pos = start;
318
319 let (client_principal, consumed) = Self::deserialize_principal(data, pos)?;
321 pos += consumed;
322
323 let (server_principal, consumed) = Self::deserialize_principal(data, pos)?;
325 pos += consumed;
326
327 if pos + 6 > data.len() {
329 return Err(KernelError::InvalidArgument {
330 name: "ccache_cred",
331 value: "truncated key",
332 });
333 }
334 let etype_val = u16::from_be_bytes([data[pos], data[pos + 1]]);
335 pos += 2;
336 let key_len =
337 u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
338 pos += 4;
339
340 if pos + key_len > data.len() {
341 return Err(KernelError::InvalidArgument {
342 name: "ccache_cred",
343 value: "truncated key data",
344 });
345 }
346 let session_key = data[pos..pos + key_len].to_vec();
347 pos += key_len;
348
349 if pos + 16 > data.len() {
351 return Err(KernelError::InvalidArgument {
352 name: "ccache_cred",
353 value: "truncated times",
354 });
355 }
356 let auth_time =
357 u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
358 pos += 4;
359 let start_time =
360 u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
361 pos += 4;
362 let end_time = u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
363 pos += 4;
364 let renew_till_val =
365 u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
366 pos += 4;
367
368 if pos + 4 > data.len() {
370 return Err(KernelError::InvalidArgument {
371 name: "ccache_cred",
372 value: "truncated flags",
373 });
374 }
375 let flags = u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
376 pos += 4;
377
378 if pos + 4 > data.len() {
380 return Err(KernelError::InvalidArgument {
381 name: "ccache_cred",
382 value: "truncated ticket len",
383 });
384 }
385 let ticket_len =
386 u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
387 pos += 4;
388
389 if pos + ticket_len > data.len() {
390 return Err(KernelError::InvalidArgument {
391 name: "ccache_cred",
392 value: "truncated ticket data",
393 });
394 }
395 let ticket_data = data[pos..pos + ticket_len].to_vec();
396 pos += ticket_len;
397
398 let session_key_etype =
399 EncryptionType::from_i64(etype_val as i64).unwrap_or(EncryptionType::Aes256CtsHmacSha1);
400
401 let renew_till = if renew_till_val > 0 {
402 Some(KerberosTime::from_timestamp(renew_till_val as u64))
403 } else {
404 None
405 };
406
407 Ok((
408 CcacheEntry {
409 client_principal,
410 server_principal,
411 session_key,
412 session_key_etype,
413 auth_time: KerberosTime::from_timestamp(auth_time as u64),
414 start_time: KerberosTime::from_timestamp(start_time as u64),
415 end_time: KerberosTime::from_timestamp(end_time as u64),
416 renew_till,
417 ticket_data,
418 flags,
419 },
420 pos - start,
421 ))
422 }
423}
424
425#[cfg(feature = "alloc")]
433pub struct TicketCache {
434 default_principal: Option<PrincipalName>,
436 entries: Vec<CcacheEntry>,
438}
439
440#[cfg(feature = "alloc")]
441impl Default for TicketCache {
442 fn default() -> Self {
443 Self::new()
444 }
445}
446
447#[cfg(feature = "alloc")]
448impl TicketCache {
449 pub fn new() -> Self {
451 Self {
452 default_principal: None,
453 entries: Vec::new(),
454 }
455 }
456
457 pub fn set_default_principal(&mut self, principal: PrincipalName) {
459 self.default_principal = Some(principal);
460 }
461
462 pub fn default_principal(&self) -> Option<&PrincipalName> {
464 self.default_principal.as_ref()
465 }
466
467 pub fn store_ticket(&mut self, entry: CcacheEntry) {
472 if let Some(existing) = self
474 .entries
475 .iter_mut()
476 .find(|e| e.server_principal == entry.server_principal)
477 {
478 *existing = entry;
479 return;
480 }
481
482 if self.entries.len() >= MAX_CACHE_ENTRIES {
484 self.entries.remove(0);
485 }
486
487 self.entries.push(entry);
488 }
489
490 pub fn lookup_ticket(&self, server: &PrincipalName) -> Option<&CcacheEntry> {
492 self.entries
493 .iter()
494 .find(|e| e.matches_server(server) && !e.is_expired())
495 }
496
497 pub fn remove_ticket(&mut self, server: &PrincipalName) -> bool {
499 let initial_len = self.entries.len();
500 self.entries.retain(|e| !e.matches_server(server));
501 self.entries.len() < initial_len
502 }
503
504 pub fn purge_expired(&mut self) -> usize {
506 let initial_len = self.entries.len();
507 self.entries.retain(|e| !e.is_expired());
508 initial_len - self.entries.len()
509 }
510
511 pub fn list_tickets(&self) -> &[CcacheEntry] {
513 &self.entries
514 }
515
516 pub fn len(&self) -> usize {
518 self.entries.len()
519 }
520
521 pub fn is_empty(&self) -> bool {
523 self.entries.is_empty()
524 }
525
526 pub fn clear(&mut self) {
528 self.entries.clear();
529 self.default_principal = None;
530 }
531
532 pub fn serialize(&self) -> Option<Vec<u8>> {
534 let principal = self.default_principal.as_ref()?;
535 let file = CcacheFile {
536 version: CCACHE_VERSION,
537 default_principal: principal.clone(),
538 entries: self.entries.clone(),
539 };
540 Some(file.serialize())
541 }
542
543 pub fn deserialize(data: &[u8]) -> Result<Self, KernelError> {
545 let file = CcacheFile::deserialize(data)?;
546 Ok(Self {
547 default_principal: Some(file.default_principal),
548 entries: file.entries,
549 })
550 }
551}
552
553#[cfg(feature = "alloc")]
562pub fn kinit_command(
563 username: &str,
564 realm: &str,
565 password: &str,
566 cache: &mut TicketCache,
567) -> Vec<u8> {
568 let mut client = KerberosClient::new(username, realm, password);
569 let principal = PrincipalName::new_principal(username);
570 cache.set_default_principal(principal);
571 client.request_tgt()
572}
573
574#[cfg(feature = "alloc")]
576pub fn klist_command(cache: &TicketCache) -> Vec<String> {
577 let mut lines = Vec::new();
578
579 if let Some(principal) = cache.default_principal() {
580 let mut header = String::from("Default principal: ");
581 header.push_str(&principal.to_text());
582 lines.push(header);
583 } else {
584 lines.push(String::from("No default principal"));
585 }
586
587 lines.push(String::new());
588
589 if cache.is_empty() {
590 lines.push(String::from("No cached tickets"));
591 return lines;
592 }
593
594 lines.push(String::from(
595 " Server Expires Flags",
596 ));
597 lines.push(String::from(
598 " ------ ------- -----",
599 ));
600
601 for entry in cache.list_tickets() {
602 let server = entry.server_principal.to_text();
603 let remaining = entry.remaining_secs();
604 let expired_marker = if entry.is_expired() { " [EXPIRED]" } else { "" };
605
606 let mut line = String::from(" ");
607 line.push_str(&server);
608
609 let pad = if server.len() < 32 {
611 32 - server.len()
612 } else {
613 2
614 };
615 for _ in 0..pad {
616 line.push(' ');
617 }
618
619 let hours = remaining / 3600;
621 let minutes = (remaining % 3600) / 60;
622 let mut time_str = String::new();
623 push_u64(&mut time_str, hours);
625 time_str.push('h');
626 push_u64(&mut time_str, minutes);
627 time_str.push('m');
628 line.push_str(&time_str);
629 line.push_str(expired_marker);
630
631 lines.push(line);
632 }
633
634 lines
635}
636
637#[cfg(feature = "alloc")]
639fn push_u64(s: &mut String, mut val: u64) {
640 if val == 0 {
641 s.push('0');
642 return;
643 }
644 let mut digits = [0u8; 20];
645 let mut count = 0;
646 while val > 0 {
647 digits[count] = (val % 10) as u8;
648 val /= 10;
649 count += 1;
650 }
651 for i in (0..count).rev() {
652 s.push((b'0' + digits[i]) as char);
653 }
654}
655
656#[cfg(feature = "alloc")]
658pub fn kdestroy_command(cache: &mut TicketCache) {
659 cache.clear();
660}
661
662#[cfg(feature = "alloc")]
669pub struct KerberosAuthBackend {
670 realm: String,
672}
673
674#[cfg(feature = "alloc")]
675impl KerberosAuthBackend {
676 pub fn new(realm: &str) -> Self {
678 Self {
679 realm: String::from(realm),
680 }
681 }
682
683 pub fn has_valid_ticket(&self, cache: &TicketCache, username: &str) -> bool {
685 let krbtgt = PrincipalName::krbtgt(&self.realm);
686 if let Some(entry) = cache.lookup_ticket(&krbtgt) {
687 if entry
689 .client_principal
690 .name_string
691 .first()
692 .map(|s| s.as_str())
693 == Some(username)
694 {
695 return !entry.is_expired();
696 }
697 }
698 false
699 }
700
701 pub fn realm(&self) -> &str {
703 &self.realm
704 }
705}
706
707#[cfg(test)]
712mod tests {
713 #[allow(unused_imports)]
714 use alloc::vec;
715
716 use super::*;
717
718 fn make_test_entry(server: &str, end_secs: u64) -> CcacheEntry {
719 CcacheEntry {
720 client_principal: PrincipalName::new_principal("testuser"),
721 server_principal: PrincipalName::new_service("krbtgt", server),
722 session_key: vec![0x42; 32],
723 session_key_etype: EncryptionType::Aes256CtsHmacSha1,
724 auth_time: KerberosTime::from_timestamp(1000),
725 start_time: KerberosTime::from_timestamp(1000),
726 end_time: KerberosTime::from_timestamp(end_secs),
727 renew_till: None,
728 ticket_data: vec![0xDE, 0xAD],
729 flags: 0x4000_0000,
730 }
731 }
732
733 #[test]
734 fn test_ticket_cache_store_lookup() {
735 let mut cache = TicketCache::new();
736 let entry = make_test_entry("EXAMPLE.COM", u64::MAX);
737 cache.store_ticket(entry);
738
739 let server = PrincipalName::new_service("krbtgt", "EXAMPLE.COM");
740 let found = cache.lookup_ticket(&server);
741 assert!(found.is_some());
742 assert_eq!(found.unwrap().session_key.len(), 32);
743 }
744
745 #[test]
746 fn test_ticket_cache_remove() {
747 let mut cache = TicketCache::new();
748 cache.store_ticket(make_test_entry("EXAMPLE.COM", u64::MAX));
749
750 let server = PrincipalName::new_service("krbtgt", "EXAMPLE.COM");
751 assert!(cache.remove_ticket(&server));
752 assert!(cache.is_empty());
753 }
754
755 #[test]
756 fn test_ticket_cache_replace_existing() {
757 let mut cache = TicketCache::new();
758 cache.store_ticket(make_test_entry("EXAMPLE.COM", u64::MAX / 2));
760 cache.store_ticket(make_test_entry("EXAMPLE.COM", u64::MAX / 2 + 1));
761
762 assert_eq!(cache.len(), 1);
763 let server = PrincipalName::new_service("krbtgt", "EXAMPLE.COM");
764 let found = cache.lookup_ticket(&server).unwrap();
765 assert_eq!(found.end_time.timestamp, u64::MAX / 2 + 1);
766 }
767
768 #[test]
769 fn test_ticket_cache_clear() {
770 let mut cache = TicketCache::new();
771 cache.set_default_principal(PrincipalName::new_principal("alice"));
772 cache.store_ticket(make_test_entry("EXAMPLE.COM", u64::MAX));
773 cache.store_ticket(make_test_entry("OTHER.COM", u64::MAX));
774
775 assert_eq!(cache.len(), 2);
776 cache.clear();
777 assert!(cache.is_empty());
778 assert!(cache.default_principal().is_none());
779 }
780
781 #[test]
782 fn test_ccache_serialize_deserialize() {
783 let principal = PrincipalName::new_principal("alice");
784 let mut file = CcacheFile::new(principal.clone());
785 file.entries.push(make_test_entry("EXAMPLE.COM", 5000));
786
787 let serialized = file.serialize();
788 assert!(!serialized.is_empty());
789
790 let deserialized = CcacheFile::deserialize(&serialized).unwrap();
791 assert_eq!(deserialized.version, CCACHE_VERSION);
792 assert_eq!(deserialized.default_principal.name_string[0], "alice");
793 assert_eq!(deserialized.entries.len(), 1);
794 }
795
796 #[test]
797 fn test_ccache_roundtrip_multiple_entries() {
798 let principal = PrincipalName::new_principal("bob");
799 let mut file = CcacheFile::new(principal);
800 file.entries.push(make_test_entry("REALM1.COM", 5000));
801 file.entries.push(make_test_entry("REALM2.COM", 6000));
802
803 let serialized = file.serialize();
804 let deserialized = CcacheFile::deserialize(&serialized).unwrap();
805 assert_eq!(deserialized.entries.len(), 2);
806 assert_eq!(
807 deserialized.entries[0].server_principal.name_string[1],
808 "REALM1.COM"
809 );
810 assert_eq!(
811 deserialized.entries[1].server_principal.name_string[1],
812 "REALM2.COM"
813 );
814 }
815
816 #[test]
817 fn test_kinit_produces_bytes() {
818 let mut cache = TicketCache::new();
819 let req = kinit_command("alice", "EXAMPLE.COM", "password", &mut cache);
820 assert!(!req.is_empty());
821 assert!(cache.default_principal().is_some());
822 }
823
824 #[test]
825 fn test_klist_empty_cache() {
826 let cache = TicketCache::new();
827 let lines = klist_command(&cache);
828 assert!(lines.iter().any(|l| l.contains("No cached tickets")));
829 }
830
831 #[test]
832 fn test_kdestroy() {
833 let mut cache = TicketCache::new();
834 cache.store_ticket(make_test_entry("EXAMPLE.COM", u64::MAX));
835 kdestroy_command(&mut cache);
836 assert!(cache.is_empty());
837 }
838
839 #[test]
840 fn test_auth_backend() {
841 let backend = KerberosAuthBackend::new("EXAMPLE.COM");
842 assert_eq!(backend.realm(), "EXAMPLE.COM");
843
844 let cache = TicketCache::new();
845 assert!(!backend.has_valid_ticket(&cache, "alice"));
846 }
847}