1use crate::errors::{AuthError, Result};
7use ring::digest::{Context, SHA256};
8use serde::{Deserialize, Serialize};
9
10const TACACS_MAJOR_VERSION: u8 = 0xC0; const TACACS_MINOR_VERSION_DEFAULT: u8 = 0x00;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[repr(u8)]
17pub enum TacacsPacketType {
18 Authentication = 0x01,
19 Authorization = 0x02,
20 Accounting = 0x03,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[repr(u8)]
26pub enum AuthenAction {
27 Login = 0x01,
28 ChangePassword = 0x02,
29 SendAuth = 0x04,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34#[repr(u8)]
35pub enum AuthenType {
36 Ascii = 0x01,
37 Pap = 0x02,
38 Chap = 0x03,
39 MSChap = 0x05,
40 MSChapV2 = 0x06,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45#[repr(u8)]
46pub enum AuthenService {
47 None = 0x00,
48 Login = 0x01,
49 Enable = 0x02,
50 Ppp = 0x03,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55#[repr(u8)]
56pub enum AuthenStatus {
57 Pass = 0x01,
58 Fail = 0x02,
59 GetData = 0x03,
60 GetUser = 0x04,
61 GetPass = 0x05,
62 Restart = 0x06,
63 Error = 0x07,
64 Follow = 0x21,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69#[repr(u8)]
70pub enum AuthorStatus {
71 PassAdd = 0x01,
72 PassReplace = 0x02,
73 Fail = 0x10,
74 Error = 0x11,
75 Follow = 0x21,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
80pub struct AcctFlags(pub u8);
81
82impl AcctFlags {
83 pub const START: Self = Self(0x02);
84 pub const STOP: Self = Self(0x04);
85 pub const WATCHDOG: Self = Self(0x08);
86
87 pub fn is_start(self) -> bool {
88 self.0 & 0x02 != 0
89 }
90 pub fn is_stop(self) -> bool {
91 self.0 & 0x04 != 0
92 }
93 pub fn is_watchdog(self) -> bool {
94 self.0 & 0x08 != 0
95 }
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct TacacsHeader {
101 pub version: u8,
102 pub packet_type: TacacsPacketType,
103 pub seq_no: u8,
104 pub flags: u8,
105 pub session_id: u32,
106 pub length: u32,
107}
108
109impl TacacsHeader {
110 pub fn new(packet_type: TacacsPacketType, seq_no: u8, session_id: u32, body_len: u32) -> Self {
112 Self {
113 version: TACACS_MAJOR_VERSION | TACACS_MINOR_VERSION_DEFAULT,
114 packet_type,
115 seq_no,
116 flags: 0x00, session_id,
118 length: body_len,
119 }
120 }
121
122 pub fn to_bytes(&self) -> [u8; 12] {
124 let mut buf = [0u8; 12];
125 buf[0] = self.version;
126 buf[1] = self.packet_type as u8;
127 buf[2] = self.seq_no;
128 buf[3] = self.flags;
129 buf[4..8].copy_from_slice(&self.session_id.to_be_bytes());
130 buf[8..12].copy_from_slice(&self.length.to_be_bytes());
131 buf
132 }
133
134 pub fn from_bytes(data: &[u8]) -> Result<Self> {
136 if data.len() < 12 {
137 return Err(AuthError::validation("TACACS+ header too short"));
138 }
139 let packet_type = match data[1] {
140 0x01 => TacacsPacketType::Authentication,
141 0x02 => TacacsPacketType::Authorization,
142 0x03 => TacacsPacketType::Accounting,
143 other => {
144 return Err(AuthError::validation(format!(
145 "Unknown TACACS+ packet type: {other:#x}"
146 )));
147 }
148 };
149 Ok(Self {
150 version: data[0],
151 packet_type,
152 seq_no: data[2],
153 flags: data[3],
154 session_id: u32::from_be_bytes([data[4], data[5], data[6], data[7]]),
155 length: u32::from_be_bytes([data[8], data[9], data[10], data[11]]),
156 })
157 }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct AuthenStartBody {
163 pub action: AuthenAction,
164 pub authen_type: AuthenType,
165 pub authen_service: AuthenService,
166 pub user: String,
167 pub port: String,
168 pub remote_address: String,
169 pub data: Vec<u8>,
170}
171
172impl AuthenStartBody {
173 pub fn to_bytes(&self) -> Vec<u8> {
175 let user_bytes = self.user.as_bytes();
176 let port_bytes = self.port.as_bytes();
177 let rem_bytes = self.remote_address.as_bytes();
178 let mut buf = Vec::with_capacity(
179 8 + user_bytes.len() + port_bytes.len() + rem_bytes.len() + self.data.len(),
180 );
181
182 buf.push(self.action as u8);
183 buf.push(0x01); buf.push(self.authen_type as u8);
185 buf.push(self.authen_service as u8);
186 buf.push(user_bytes.len() as u8);
187 buf.push(port_bytes.len() as u8);
188 buf.push(rem_bytes.len() as u8);
189 buf.push(self.data.len() as u8);
190 buf.extend_from_slice(user_bytes);
191 buf.extend_from_slice(port_bytes);
192 buf.extend_from_slice(rem_bytes);
193 buf.extend_from_slice(&self.data);
194
195 buf
196 }
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct AuthenReplyBody {
202 pub status: AuthenStatus,
203 pub flags: u8,
204 pub server_msg: String,
205 pub data: Vec<u8>,
206}
207
208impl AuthenReplyBody {
209 pub fn from_bytes(data: &[u8]) -> Result<Self> {
211 if data.len() < 6 {
212 return Err(AuthError::validation("TACACS+ authen reply too short"));
213 }
214 let status = match data[0] {
215 0x01 => AuthenStatus::Pass,
216 0x02 => AuthenStatus::Fail,
217 0x03 => AuthenStatus::GetData,
218 0x04 => AuthenStatus::GetUser,
219 0x05 => AuthenStatus::GetPass,
220 0x06 => AuthenStatus::Restart,
221 0x07 => AuthenStatus::Error,
222 0x21 => AuthenStatus::Follow,
223 other => {
224 return Err(AuthError::validation(format!(
225 "Unknown authen status: {other:#x}"
226 )));
227 }
228 };
229 let flags = data[1];
230 let server_msg_len = u16::from_be_bytes([data[2], data[3]]) as usize;
231 let data_len = u16::from_be_bytes([data[4], data[5]]) as usize;
232
233 if data.len() < 6 + server_msg_len + data_len {
234 return Err(AuthError::validation("TACACS+ authen reply truncated"));
235 }
236
237 let server_msg = String::from_utf8_lossy(&data[6..6 + server_msg_len]).to_string();
238 let reply_data = data[6 + server_msg_len..6 + server_msg_len + data_len].to_vec();
239
240 Ok(Self {
241 status,
242 flags,
243 server_msg,
244 data: reply_data,
245 })
246 }
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct AuthorRequestBody {
254 pub authen_method: u8,
255 pub authen_type: AuthenType,
256 pub authen_service: AuthenService,
257 pub user: String,
258 pub port: String,
259 pub remote_address: String,
260 pub args: Vec<String>,
262}
263
264impl AuthorRequestBody {
265 pub fn to_bytes(&self) -> Vec<u8> {
267 let user_bytes = self.user.as_bytes();
268 let port_bytes = self.port.as_bytes();
269 let rem_bytes = self.remote_address.as_bytes();
270 let arg_count = self.args.len() as u8;
271
272 let mut buf = Vec::new();
274 buf.push(self.authen_method);
275 buf.push(0x01); buf.push(self.authen_type as u8);
277 buf.push(self.authen_service as u8);
278 buf.push(user_bytes.len() as u8);
279 buf.push(port_bytes.len() as u8);
280 buf.push(rem_bytes.len() as u8);
281 buf.push(arg_count);
282
283 for arg in &self.args {
285 buf.push(arg.len() as u8);
286 }
287
288 buf.extend_from_slice(user_bytes);
290 buf.extend_from_slice(port_bytes);
291 buf.extend_from_slice(rem_bytes);
292 for arg in &self.args {
293 buf.extend_from_slice(arg.as_bytes());
294 }
295
296 buf
297 }
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct AuthorReplyBody {
303 pub status: AuthorStatus,
304 pub server_msg: String,
305 pub data: Vec<u8>,
306 pub args: Vec<String>,
307}
308
309impl AuthorReplyBody {
310 pub fn from_bytes(data: &[u8]) -> Result<Self> {
312 if data.len() < 6 {
313 return Err(AuthError::validation("TACACS+ author reply too short"));
314 }
315 let status = match data[0] {
316 0x01 => AuthorStatus::PassAdd,
317 0x02 => AuthorStatus::PassReplace,
318 0x10 => AuthorStatus::Fail,
319 0x11 => AuthorStatus::Error,
320 0x21 => AuthorStatus::Follow,
321 other => {
322 return Err(AuthError::validation(format!(
323 "Unknown author status: {other:#x}"
324 )));
325 }
326 };
327
328 let arg_count = data[1] as usize;
329 let server_msg_len = u16::from_be_bytes([data[2], data[3]]) as usize;
330 let data_len = u16::from_be_bytes([data[4], data[5]]) as usize;
331
332 let mut offset = 6;
333 if data.len() < offset + arg_count {
335 return Err(AuthError::validation(
336 "TACACS+ author reply truncated (arg lengths)",
337 ));
338 }
339 let arg_lens: Vec<usize> = data[offset..offset + arg_count]
340 .iter()
341 .map(|&b| b as usize)
342 .collect();
343 offset += arg_count;
344
345 if data.len() < offset + server_msg_len {
347 return Err(AuthError::validation(
348 "TACACS+ author reply truncated (msg)",
349 ));
350 }
351 let server_msg =
352 String::from_utf8_lossy(&data[offset..offset + server_msg_len]).to_string();
353 offset += server_msg_len;
354
355 if data.len() < offset + data_len {
357 return Err(AuthError::validation(
358 "TACACS+ author reply truncated (data)",
359 ));
360 }
361 let reply_data = data[offset..offset + data_len].to_vec();
362 offset += data_len;
363
364 let mut args = Vec::with_capacity(arg_count);
366 for &len in &arg_lens {
367 if data.len() < offset + len {
368 return Err(AuthError::validation(
369 "TACACS+ author reply truncated (args)",
370 ));
371 }
372 args.push(String::from_utf8_lossy(&data[offset..offset + len]).to_string());
373 offset += len;
374 }
375
376 Ok(Self {
377 status,
378 server_msg,
379 data: reply_data,
380 args,
381 })
382 }
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct AcctRequestBody {
390 pub flags: AcctFlags,
391 pub authen_method: u8,
392 pub authen_type: AuthenType,
393 pub authen_service: AuthenService,
394 pub user: String,
395 pub port: String,
396 pub remote_address: String,
397 pub args: Vec<String>,
399}
400
401impl AcctRequestBody {
402 pub fn to_bytes(&self) -> Vec<u8> {
404 let user_bytes = self.user.as_bytes();
405 let port_bytes = self.port.as_bytes();
406 let rem_bytes = self.remote_address.as_bytes();
407 let arg_count = self.args.len() as u8;
408
409 let mut buf = Vec::new();
410 buf.push(self.flags.0);
411 buf.push(self.authen_method);
412 buf.push(0x01); buf.push(self.authen_type as u8);
414 buf.push(self.authen_service as u8);
415 buf.push(user_bytes.len() as u8);
416 buf.push(port_bytes.len() as u8);
417 buf.push(rem_bytes.len() as u8);
418 buf.push(arg_count);
419
420 for arg in &self.args {
422 buf.push(arg.len() as u8);
423 }
424
425 buf.extend_from_slice(user_bytes);
427 buf.extend_from_slice(port_bytes);
428 buf.extend_from_slice(rem_bytes);
429 for arg in &self.args {
430 buf.extend_from_slice(arg.as_bytes());
431 }
432
433 buf
434 }
435}
436
437#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
439#[repr(u8)]
440pub enum AcctStatus {
441 Success = 0x01,
442 Error = 0x02,
443 Follow = 0x21,
444}
445
446#[derive(Debug, Clone, Serialize, Deserialize)]
448pub struct AcctReplyBody {
449 pub status: AcctStatus,
450 pub server_msg: String,
451 pub data: Vec<u8>,
452}
453
454impl AcctReplyBody {
455 pub fn from_bytes(data: &[u8]) -> Result<Self> {
457 if data.len() < 5 {
458 return Err(AuthError::validation("TACACS+ acct reply too short"));
459 }
460 let server_msg_len = u16::from_be_bytes([data[0], data[1]]) as usize;
461 let data_len = u16::from_be_bytes([data[2], data[3]]) as usize;
462 let status = match data[4] {
463 0x01 => AcctStatus::Success,
464 0x02 => AcctStatus::Error,
465 0x21 => AcctStatus::Follow,
466 other => {
467 return Err(AuthError::validation(format!(
468 "Unknown acct status: {other:#x}"
469 )));
470 }
471 };
472
473 let offset = 5;
474 if data.len() < offset + server_msg_len + data_len {
475 return Err(AuthError::validation("TACACS+ acct reply truncated"));
476 }
477 let server_msg =
478 String::from_utf8_lossy(&data[offset..offset + server_msg_len]).to_string();
479 let reply_data = data[offset + server_msg_len..offset + server_msg_len + data_len].to_vec();
480
481 Ok(Self {
482 status,
483 server_msg,
484 data: reply_data,
485 })
486 }
487}
488
489pub fn obfuscate(header: &TacacsHeader, secret: &[u8], body: &mut [u8]) {
495 if secret.is_empty() || body.is_empty() {
496 return;
497 }
498
499 let mut pad = Vec::new();
500 let session_id_bytes = header.session_id.to_be_bytes();
501
502 let mut ctx = Context::new(&SHA256);
504 ctx.update(&session_id_bytes);
505 ctx.update(secret);
506 ctx.update(&[header.version]);
507 ctx.update(&[header.seq_no]);
508 let digest = ctx.finish();
509 pad.extend_from_slice(digest.as_ref());
510
511 while pad.len() < body.len() {
513 let mut ctx = Context::new(&SHA256);
514 ctx.update(&session_id_bytes);
515 ctx.update(secret);
516 ctx.update(&[header.version]);
517 ctx.update(&[header.seq_no]);
518 let prev_start = pad.len().saturating_sub(32);
519 ctx.update(&pad[prev_start..]);
520 let digest = ctx.finish();
521 pad.extend_from_slice(digest.as_ref());
522 }
523
524 for (i, b) in body.iter_mut().enumerate() {
526 *b ^= pad[i];
527 }
528}
529
530pub fn generate_session_id() -> Result<u32> {
532 use ring::rand::{SecureRandom, SystemRandom};
533 let rng = SystemRandom::new();
534 let mut buf = [0u8; 4];
535 rng.fill(&mut buf)
536 .map_err(|_| AuthError::crypto("Failed to generate session ID".to_string()))?;
537 Ok(u32::from_be_bytes(buf))
538}
539
540#[cfg(test)]
541mod tests {
542 use super::*;
543
544 #[test]
545 fn test_header_roundtrip() {
546 let h = TacacsHeader::new(TacacsPacketType::Authentication, 1, 0xDEADBEEF, 42);
547 let bytes = h.to_bytes();
548 assert_eq!(bytes.len(), 12);
549 let h2 = TacacsHeader::from_bytes(&bytes).unwrap();
550 assert_eq!(h2.packet_type, TacacsPacketType::Authentication);
551 assert_eq!(h2.session_id, 0xDEADBEEF);
552 assert_eq!(h2.length, 42);
553 assert_eq!(h2.seq_no, 1);
554 }
555
556 #[test]
557 fn test_header_too_short() {
558 assert!(TacacsHeader::from_bytes(&[0; 5]).is_err());
559 }
560
561 #[test]
562 fn test_header_unknown_type() {
563 let mut bytes = TacacsHeader::new(TacacsPacketType::Authentication, 1, 1, 0).to_bytes();
564 bytes[1] = 0xFF;
565 assert!(TacacsHeader::from_bytes(&bytes).is_err());
566 }
567
568 #[test]
569 fn test_authen_start_body_serialization() {
570 let body = AuthenStartBody {
571 action: AuthenAction::Login,
572 authen_type: AuthenType::Pap,
573 authen_service: AuthenService::Login,
574 user: "admin".to_string(),
575 port: "tty0".to_string(),
576 remote_address: "10.0.0.1".to_string(),
577 data: b"password".to_vec(),
578 };
579 let bytes = body.to_bytes();
580 assert_eq!(bytes[0], AuthenAction::Login as u8);
581 assert_eq!(bytes[2], AuthenType::Pap as u8);
582 assert_eq!(bytes[4], 5); }
584
585 #[test]
586 fn test_authen_reply_parsing() {
587 let mut data = vec![0x01, 0x00]; data.extend_from_slice(&3u16.to_be_bytes()); data.extend_from_slice(&0u16.to_be_bytes()); data.extend_from_slice(b"OK!");
591
592 let reply = AuthenReplyBody::from_bytes(&data).unwrap();
593 assert_eq!(reply.status, AuthenStatus::Pass);
594 assert_eq!(reply.server_msg, "OK!");
595 assert!(reply.data.is_empty());
596 }
597
598 #[test]
599 fn test_authen_reply_too_short() {
600 assert!(AuthenReplyBody::from_bytes(&[0x01, 0x00]).is_err());
601 }
602
603 #[test]
604 fn test_obfuscate_deobfuscate_roundtrip() {
605 let header = TacacsHeader::new(TacacsPacketType::Authentication, 1, 12345, 11);
606 let secret = b"shared-secret";
607 let original = b"hello world".to_vec();
608 let mut encrypted = original.clone();
609
610 obfuscate(&header, secret, &mut encrypted);
611 assert_ne!(encrypted, original, "obfuscation should change data");
612
613 obfuscate(&header, secret, &mut encrypted);
614 assert_eq!(
615 encrypted, original,
616 "double obfuscation should restore data"
617 );
618 }
619
620 #[test]
621 fn test_obfuscate_empty_secret_noop() {
622 let header = TacacsHeader::new(TacacsPacketType::Authentication, 1, 1, 5);
623 let mut data = b"hello".to_vec();
624 let orig = data.clone();
625 obfuscate(&header, b"", &mut data);
626 assert_eq!(data, orig);
627 }
628
629 #[test]
630 fn test_generate_session_id() {
631 let id1 = generate_session_id().unwrap();
632 let id2 = generate_session_id().unwrap();
633 assert!(id1 != 0 || id2 != 0);
636 }
637
638 #[test]
639 fn test_acct_flags() {
640 let start = AcctFlags::START;
641 assert!(start.is_start());
642 assert!(!start.is_stop());
643 assert!(!start.is_watchdog());
644
645 let stop = AcctFlags::STOP;
646 assert!(stop.is_stop());
647 }
648
649 #[test]
650 fn test_packet_type_values() {
651 assert_eq!(TacacsPacketType::Authentication as u8, 0x01);
652 assert_eq!(TacacsPacketType::Authorization as u8, 0x02);
653 assert_eq!(TacacsPacketType::Accounting as u8, 0x03);
654 }
655
656 #[test]
659 fn test_author_request_serialization() {
660 let body = AuthorRequestBody {
661 authen_method: 0x06, authen_type: AuthenType::Pap,
663 authen_service: AuthenService::Login,
664 user: "admin".to_string(),
665 port: "tty0".to_string(),
666 remote_address: "10.0.0.1".to_string(),
667 args: vec!["service=shell".to_string(), "cmd=show".to_string()],
668 };
669 let bytes = body.to_bytes();
670 assert_eq!(bytes[0], 0x06); assert_eq!(bytes[7], 2); assert_eq!(bytes[8], 13); assert_eq!(bytes[9], 8); }
676
677 #[test]
678 fn test_author_reply_parsing() {
679 let mut data = vec![0x01]; data.push(2); data.extend_from_slice(&2u16.to_be_bytes()); data.extend_from_slice(&0u16.to_be_bytes()); data.push(6); data.push(6); data.extend_from_slice(b"OK"); data.extend_from_slice(b"priv=1role=a"); let reply = AuthorReplyBody::from_bytes(&data).unwrap();
691 assert_eq!(reply.status, AuthorStatus::PassAdd);
692 assert_eq!(reply.server_msg, "OK");
693 assert_eq!(reply.args.len(), 2);
694 assert_eq!(reply.args[0], "priv=1");
695 assert_eq!(reply.args[1], "role=a");
696 }
697
698 #[test]
699 fn test_author_reply_too_short() {
700 assert!(AuthorReplyBody::from_bytes(&[0x01]).is_err());
701 }
702
703 #[test]
706 fn test_acct_request_serialization() {
707 let body = AcctRequestBody {
708 flags: AcctFlags::START,
709 authen_method: 0x06,
710 authen_type: AuthenType::Pap,
711 authen_service: AuthenService::Login,
712 user: "admin".to_string(),
713 port: "tty0".to_string(),
714 remote_address: "10.0.0.1".to_string(),
715 args: vec!["task_id=1".to_string()],
716 };
717 let bytes = body.to_bytes();
718 assert_eq!(bytes[0], AcctFlags::START.0); assert_eq!(bytes[8], 1); }
721
722 #[test]
723 fn test_acct_reply_parsing() {
724 let mut data = Vec::new();
725 data.extend_from_slice(&4u16.to_be_bytes()); data.extend_from_slice(&0u16.to_be_bytes()); data.push(0x01); data.extend_from_slice(b"Done"); let reply = AcctReplyBody::from_bytes(&data).unwrap();
731 assert_eq!(reply.status, AcctStatus::Success);
732 assert_eq!(reply.server_msg, "Done");
733 }
734
735 #[test]
736 fn test_acct_reply_too_short() {
737 assert!(AcctReplyBody::from_bytes(&[0x00, 0x01]).is_err());
738 }
739}