1pub mod actions;
43pub mod apdu;
44pub mod ccid;
45pub mod handler;
46
47pub use actions::UsbSmartCardProtocol;
48
49use std::net::SocketAddr;
50use std::sync::{Arc, Mutex as StdMutex};
51
52use anyhow::{anyhow, Result};
53use serde_json::{json, Value};
54use tokio::sync::mpsc;
55use tracing::{debug, info, warn};
56
57use crate::llm::action_helper::call_llm;
58use crate::llm::ollama_client::OllamaClient;
59use crate::llm::ActionResult;
60use crate::logging::emit::Log;
61use crate::protocol::Event;
62use crate::server::accept_bounded::{accept_bounded, ConnectionLimiter};
63use crate::server::connection::ConnectionId;
64use crate::server::usb::guard::{MAX_USBIP_CONNECTIONS, USBIP_NO_REFUSAL};
65use crate::state::app_state::AppState;
66use crate::state::ServerId;
67use crate::{console_debug, console_error};
68
69use actions::{
70 USB_SMARTCARD_APDU_RECEIVED_EVENT, USB_SMARTCARD_ATTACHED_EVENT, USB_SMARTCARD_DETACHED_EVENT,
71 USB_SMARTCARD_READER_READY_EVENT,
72};
73use apdu::{ApduCommand, ApduResponse};
74use handler::{CardState, PendingApdu, UsbCcidHandler};
75
76const DEFAULT_ATR: &[u8] = &[0x3B, 0x90, 0x11, 0x00];
82
83type SharedUsbHandler = Arc<StdMutex<Box<dyn usbip::UsbInterfaceHandler + Send>>>;
85
86pub struct UsbSmartCardServer;
87
88impl UsbSmartCardServer {
89 #[allow(clippy::too_many_arguments)]
91 pub async fn spawn_with_llm_actions(
92 listen_addr: SocketAddr,
93 llm_client: OllamaClient,
94 app_state: Arc<AppState>,
95 status_tx: mpsc::UnboundedSender<String>,
96 server_id: ServerId,
97 card_type: Option<String>,
98 first_byte_timeout_secs: Option<u64>,
99 idle_timeout_secs: Option<u64>,
100 ) -> Result<SocketAddr> {
101 let card_type = card_type.unwrap_or_else(|| "generic".to_string());
102
103 let listener =
106 crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
107 let local_addr = listener.local_addr()?;
108
109 Log::new(Some(&status_tx)).info(format!(
110 "USB smart card reader listening on {local_addr} (card_type={card_type}) - \
111 attach with: sudo usbip attach -r {} -b 0-0-0",
112 local_addr.ip()
113 ));
114
115 let card = Arc::new(StdMutex::new(CardState {
116 atr: DEFAULT_ATR.to_vec(),
117 card_present: true,
118 }));
119 let protocol = Arc::new(UsbSmartCardProtocol::new());
120
121 let ready_event = Event::new(
123 &USB_SMARTCARD_READER_READY_EVENT,
124 json!({
125 "listen_addr": local_addr.to_string(),
126 "card_type": card_type,
127 "atr_hex": hex::encode_upper(DEFAULT_ATR),
128 }),
129 );
130 match call_llm(
138 &llm_client,
139 &app_state,
140 server_id,
141 None, &ready_event,
143 protocol.as_ref(),
144 )
145 .await
146 {
147 Ok(execution) => {
148 for message in execution.messages {
149 let _ = status_tx.send(message);
150 }
151 Self::apply_card_config(execution.protocol_results, &card, None, &status_tx);
152 }
153 Err(e) => {
154 Log::new(Some(&status_tx)).error(format!(
155 "USB smart card startup configuration failed ({}); the reader is listening \
156 on {} with the default ATR {} and a card inserted",
157 e,
158 local_addr,
159 hex::encode_upper(DEFAULT_ATR)
160 ));
161 }
162 }
163
164 {
165 let snapshot = lock_card(&card);
166 debug!(
167 "Virtual smart card configured: atr={}, card_present={}",
168 hex::encode_upper(&snapshot.atr),
169 snapshot.card_present
170 );
171 }
172
173 let deadlines = crate::server::usb::guard::UsbIpDeadlines::from_secs(
177 first_byte_timeout_secs,
178 idle_timeout_secs,
179 );
180 let task_registrar = app_state.clone();
181 let limiter = ConnectionLimiter::new(MAX_USBIP_CONNECTIONS);
183 let accept_handle = tokio::spawn(async move {
184 loop {
185 let (stream, remote_addr, permit) = match accept_bounded(
186 &listener,
187 &limiter,
188 USBIP_NO_REFUSAL,
189 "USB smart card",
190 Some(&status_tx),
191 )
192 .await
193 {
194 Ok(accepted) => accepted,
195 Err(e) => {
196 Log::new(Some(&status_tx)).error(format!(
200 "USB smart card accept failed, stopping accept loop: {}",
201 e
202 ));
203 break;
204 }
205 };
206
207 let connection_id = ConnectionId::new(app_state.get_next_unified_id().await);
208 info!(
209 "USB/IP connection {} from {} (USB smart card)",
210 connection_id, remote_addr
211 );
212
213 {
214 use crate::state::server::{
215 ConnectionState, ConnectionStatus, ProtocolConnectionInfo,
216 };
217 let now = crate::utils::clock::Instant::now();
218 let conn_state = ConnectionState {
219 id: connection_id,
220 remote_addr,
221 local_addr: stream.local_addr().unwrap_or(local_addr),
222 bytes_sent: 0,
223 bytes_received: 0,
224 packets_sent: 0,
225 packets_received: 0,
226 last_activity: now,
227 status: ConnectionStatus::Active,
228 status_changed_at: now,
229 protocol_info: ProtocolConnectionInfo::new(json!({
230 "transport": "usbip",
231 "usb_class": "ccid",
232 "card_type": card_type,
233 })),
234 };
235 app_state
236 .add_connection_to_server(server_id, conn_state)
237 .await;
238 }
239 let _ = status_tx.send("__UPDATE_UI__".to_string());
240
241 let llm_client = llm_client.clone();
242 let conn_app_state = app_state.clone();
243 let status_tx = status_tx.clone();
244 let card = card.clone();
245 let protocol = protocol.clone();
246 let card_type = card_type.clone();
247 let task_owner = app_state.clone();
249 task_owner
250 .spawn_server_task(server_id, async move {
251 let _permit = permit;
254 if let Err(e) = Self::handle_connection(
255 stream,
256 connection_id,
257 server_id,
258 card,
259 card_type,
260 llm_client,
261 conn_app_state.clone(),
262 status_tx.clone(),
263 protocol,
264 deadlines,
265 )
266 .await
267 {
268 console_error!(
269 status_tx,
270 "USB smart card connection {} error: {}",
271 connection_id,
272 e
273 );
274 }
275 conn_app_state
276 .close_connection_on_server(server_id, connection_id)
277 .await;
278 let _ = status_tx.send("__UPDATE_UI__".to_string());
279 })
280 .await;
281 }
282 });
283
284 task_registrar
285 .register_server_task(server_id, accept_handle)
286 .await;
287
288 Ok(local_addr)
289 }
290
291 #[allow(clippy::too_many_arguments)]
293 async fn handle_connection(
294 stream: tokio::net::TcpStream,
295 connection_id: ConnectionId,
296 server_id: ServerId,
297 card: Arc<StdMutex<CardState>>,
298 card_type: String,
299 llm_client: OllamaClient,
300 app_state: Arc<AppState>,
301 status_tx: mpsc::UnboundedSender<String>,
302 protocol: Arc<UsbSmartCardProtocol>,
303 deadlines: crate::server::usb::guard::UsbIpDeadlines,
304 ) -> Result<()> {
305 let (apdu_tx, mut apdu_rx) = mpsc::unbounded_channel::<PendingApdu>();
308
309 let ccid_handler: SharedUsbHandler = Arc::new(StdMutex::new(Box::new(UsbCcidHandler::new(
310 card.clone(),
311 apdu_tx,
312 ))
313 as Box<dyn usbip::UsbInterfaceHandler + Send>));
314
315 let device = usbip::UsbDevice::new(0).with_interface(
316 usbip::ClassCode::SmartCard as u8,
317 0x00, 0x00, Some("NetGet Virtual CCID Reader"),
320 UsbCcidHandler::endpoints(),
321 ccid_handler.clone(),
322 );
323
324 let usbip_server = Arc::new(usbip::UsbIpServer::new_simulated(vec![device]));
325
326 let guard_status_tx = status_tx.clone();
333 let (import_tx, mut import_rx) = tokio::sync::oneshot::channel();
334 let mut usbip_task = tokio::spawn(async move {
335 match crate::server::usb::guard::run_guarded_usbip(
336 stream,
337 usbip_server,
338 "USB smart card",
339 connection_id.to_string(),
340 guard_status_tx,
341 Some(import_tx),
342 deadlines,
343 )
344 .await
345 {
346 Ok(()) => debug!(
347 "USB/IP session ended for smart card connection {}",
348 connection_id
349 ),
350 Err(e) => debug!(
351 "USB/IP session for smart card connection {} ended with error: {}",
352 connection_id, e
353 ),
354 }
355 });
356
357 let mut import_pending = true;
367 let mut imported = false;
368 loop {
369 tokio::select! {
370 attached = &mut import_rx, if import_pending => {
371 import_pending = false;
372 if attached.is_ok() {
373 imported = true;
374 let (atr_hex, card_present) = {
377 let snapshot = lock_card(&card);
378 (hex::encode_upper(&snapshot.atr), snapshot.card_present)
379 };
380 let attached_event = Event::new(
381 &USB_SMARTCARD_ATTACHED_EVENT,
382 json!({
383 "connection_id": connection_id.to_string(),
384 "card_type": card_type,
385 "card_present": card_present,
386 "atr_hex": atr_hex,
387 }),
388 );
389 match call_llm(
390 &llm_client,
391 &app_state,
392 server_id,
393 Some(connection_id),
394 &attached_event,
395 protocol.as_ref(),
396 )
397 .await
398 {
399 Ok(execution) => {
400 for message in execution.messages {
401 let _ = status_tx.send(message);
402 }
403 Self::apply_card_config(
404 execution.protocol_results,
405 &card,
406 Some(&ccid_handler),
407 &status_tx,
408 );
409 info!(
410 "USB smart card LLM call completed for connection {} (attach)",
411 connection_id
412 );
413 }
414 Err(e) => {
415 console_error!(
416 status_tx,
417 "USB smart card attach handler failed for {}: {}",
418 connection_id,
419 e
420 );
421 }
422 }
423 }
424 }
425 pending = apdu_rx.recv() => {
426 let Some(pending) = pending else { break };
427 let response = Self::answer_apdu(
428 &pending,
429 connection_id,
430 server_id,
431 &card_type,
432 &llm_client,
433 &app_state,
434 &status_tx,
435 protocol.as_ref(),
436 )
437 .await;
438
439 let status_word = response.status_word();
440 let bytes = response.into_bytes();
441 let _ = status_tx.send(format!(
442 "→ USB smart card response SW={} ({} data byte(s)) to {}",
443 status_word,
444 bytes.len().saturating_sub(2),
445 connection_id
446 ));
447 if with_ccid_handler(&ccid_handler, |h| {
448 h.queue_apdu_response(pending.slot, pending.seq, &bytes)
449 })
450 .is_none()
451 {
452 console_error!(
453 status_tx,
454 "USB smart card could not reach the CCID handler for {}; the host \
455 will not get an answer to seq {}",
456 connection_id,
457 pending.seq
458 );
459 }
460 }
461 _ = &mut usbip_task => break,
462 }
463 }
464
465 info!(
466 "USB smart card host detached on connection {}",
467 connection_id
468 );
469
470 if imported {
473 let detached_event = Event::new(
474 &USB_SMARTCARD_DETACHED_EVENT,
475 json!({ "connection_id": connection_id.to_string() }),
476 );
477 match call_llm(
478 &llm_client,
479 &app_state,
480 server_id,
481 Some(connection_id),
482 &detached_event,
483 protocol.as_ref(),
484 )
485 .await
486 {
487 Ok(execution) => {
488 for message in execution.messages {
489 let _ = status_tx.send(message);
490 }
491 info!(
492 "USB smart card LLM call completed for connection {} (detach)",
493 connection_id
494 );
495 }
496 Err(e) => {
497 console_error!(
498 status_tx,
499 "USB smart card detach handler failed for {}: {}",
500 connection_id,
501 e
502 );
503 }
504 }
505 }
506
507 Ok(())
508 }
509
510 #[allow(clippy::too_many_arguments)]
516 async fn answer_apdu(
517 pending: &PendingApdu,
518 connection_id: ConnectionId,
519 server_id: ServerId,
520 card_type: &str,
521 llm_client: &OllamaClient,
522 app_state: &Arc<AppState>,
523 status_tx: &mpsc::UnboundedSender<String>,
524 protocol: &UsbSmartCardProtocol,
525 ) -> ApduResponse {
526 let command = match ApduCommand::parse(&pending.apdu) {
527 Ok(command) => command,
528 Err(e) => {
529 console_error!(
530 status_tx,
531 "USB smart card host {} sent a malformed APDU: {}",
532 connection_id,
533 e
534 );
535 return ApduResponse::wrong_length();
536 }
537 };
538
539 let mut event_data = json!({
540 "connection_id": connection_id.to_string(),
541 "card_type": card_type,
542 "ins_name": command.ins_name(),
543 "cla": format!("{:02X}", command.cla),
544 "ins": format!("{:02X}", command.ins),
545 "p1": format!("{:02X}", command.p1),
546 "p2": format!("{:02X}", command.p2),
547 "lc": command.data.len(),
548 "data_hex": hex::encode_upper(&command.data),
549 "le": command.le,
550 });
551 if let Some(map) = event_data.as_object_mut() {
552 if let Some(text) = apdu::printable_text(&command.data) {
553 map.insert("data_text".into(), json!(text));
554 }
555 if command.is_select_by_aid() {
556 map.insert(
557 "application_id".into(),
558 json!(hex::encode_upper(&command.data)),
559 );
560 }
561 }
562
563 let event = Event::new(&USB_SMARTCARD_APDU_RECEIVED_EVENT, event_data);
564 console_debug!(
565 status_tx,
566 "USB smart card {} on {} (Lc={}, Le={:?})",
567 command.ins_name(),
568 connection_id,
569 command.data.len(),
570 command.le
571 );
572
573 let execution = match call_llm(
574 llm_client,
575 app_state,
576 server_id,
577 Some(connection_id),
578 &event,
579 protocol,
580 )
581 .await
582 {
583 Ok(execution) => execution,
584 Err(e) => {
585 console_error!(
589 status_tx,
590 "USB smart card handler failed for {} on {} \
591 decision=fail_closed_llm_error, answering 6F00: {}",
592 event.id(),
593 connection_id,
594 e
595 );
596 return ApduResponse::card_error();
597 }
598 };
599
600 for message in execution.messages {
601 let _ = status_tx.send(message);
602 }
603
604 let mut response = None;
605 let mut results = Vec::new();
606 flatten_results(execution.protocol_results, &mut results);
607 for result in results {
608 let ActionResult::Custom { name, data } = result else {
609 continue;
610 };
611 if name != "respond_to_apdu" {
612 continue;
613 }
614 match decode_apdu_response(&data) {
615 Ok(decoded) => {
616 if response.is_none() {
617 response = Some(decoded);
618 } else {
619 console_error!(
620 status_tx,
621 "USB smart card handler returned more than one respond_to_apdu for \
622 {}; ignoring the extra one",
623 connection_id
624 );
625 }
626 }
627 Err(e) => {
628 console_error!(
629 status_tx,
630 "USB smart card respond_to_apdu could not be decoded on {} \
631 decision=fail_closed_undecodable, falling through to 6F00: {}",
632 connection_id,
633 e
634 );
635 }
636 }
637 }
638
639 match response {
640 Some(response) => response,
641 None => {
642 console_error!(
643 status_tx,
644 "USB smart card handler produced no respond_to_apdu for {} on {} \
645 decision=model_silent, answering 6F00",
646 event.id(),
647 connection_id
648 );
649 ApduResponse::card_error()
650 }
651 }
652 }
653
654 fn apply_card_config(
657 results: Vec<ActionResult>,
658 card: &Arc<StdMutex<CardState>>,
659 ccid_handler: Option<&SharedUsbHandler>,
660 status_tx: &mpsc::UnboundedSender<String>,
661 ) {
662 let mut flattened = Vec::new();
663 flatten_results(results, &mut flattened);
664 for result in flattened {
665 let ActionResult::Custom { name, data } = result else {
666 continue;
667 };
668 match name.as_str() {
669 "set_atr" => match decode_atr(&data) {
670 Ok(atr) => {
671 let atr_hex = hex::encode_upper(&atr);
672 lock_card(card).atr = atr;
673 Log::new(Some(status_tx))
674 .info(format!("USB smart card ATR set: {atr_hex}"));
675 }
676 Err(e) => {
677 console_error!(status_tx, "USB smart card set_atr rejected: {}", e);
678 }
679 },
680 "set_card_present" => match data.get("present").and_then(|v| v.as_bool()) {
681 Some(present) => {
682 lock_card(card).card_present = present;
683 if let Some(handler) = ccid_handler {
684 with_ccid_handler(handler, |h| h.note_slot_change());
685 }
686 Log::new(Some(status_tx)).info(format!(
687 "USB smart card slot: card {}",
688 if present { "inserted" } else { "removed" }
689 ));
690 }
691 None => {
692 console_error!(
693 status_tx,
694 "USB smart card set_card_present had no boolean 'present'"
695 );
696 }
697 },
698 "respond_to_apdu" => {
699 console_error!(
700 status_tx,
701 "USB smart card respond_to_apdu is only valid in response to \
702 usb_smartcard_apdu_received; ignoring it"
703 );
704 }
705 other => {
706 warn!(
707 "USB smart card ignoring unhandled action result '{}'",
708 other
709 );
710 }
711 }
712 }
713 }
714}
715
716fn lock_card(card: &Arc<StdMutex<CardState>>) -> std::sync::MutexGuard<'_, CardState> {
719 match card.lock() {
720 Ok(guard) => guard,
721 Err(poisoned) => poisoned.into_inner(),
722 }
723}
724
725fn with_ccid_handler<R>(
727 handler: &SharedUsbHandler,
728 f: impl FnOnce(&mut UsbCcidHandler) -> R,
729) -> Option<R> {
730 let mut guard = match handler.lock() {
731 Ok(guard) => guard,
732 Err(poisoned) => poisoned.into_inner(),
733 };
734 let ccid = guard.as_any().downcast_mut::<UsbCcidHandler>()?;
735 Some(f(ccid))
736}
737
738fn flatten_results(results: Vec<ActionResult>, out: &mut Vec<ActionResult>) {
741 for result in results {
742 match result {
743 ActionResult::Multiple(nested) => flatten_results(nested, out),
744 other => out.push(other),
745 }
746 }
747}
748
749fn decode_atr(data: &Value) -> Result<Vec<u8>> {
751 let atr_hex = data
752 .get("atr_hex")
753 .and_then(|v| v.as_str())
754 .ok_or_else(|| anyhow!("Missing atr_hex"))?;
755 let atr = actions::parse_hex("atr_hex", atr_hex)?;
756 if atr.is_empty() {
757 return Err(anyhow!("ATR must not be empty"));
758 }
759 if atr.len() > ccid::MAX_PAYLOAD_LEN {
760 return Err(anyhow!(
761 "ATR is {} bytes; a CCID data block carries at most {}",
762 atr.len(),
763 ccid::MAX_PAYLOAD_LEN
764 ));
765 }
766 Ok(atr)
767}
768
769fn decode_apdu_response(data: &Value) -> Result<ApduResponse> {
771 let data_hex = data
772 .get("data_hex")
773 .and_then(|v| v.as_str())
774 .unwrap_or_default();
775 let body = if data_hex.is_empty() {
776 Vec::new()
777 } else {
778 actions::parse_hex("data_hex", data_hex)?
779 };
780
781 if body.len() + 2 > ccid::MAX_PAYLOAD_LEN {
783 return Err(anyhow!(
784 "Response body of {} bytes does not fit in a CCID data block (max {} including SW)",
785 body.len(),
786 ccid::MAX_PAYLOAD_LEN
787 ));
788 }
789
790 let sw1 = decode_status_byte(data, "sw1")?;
791 let sw2 = decode_status_byte(data, "sw2")?;
792 Ok(ApduResponse::new(body, sw1, sw2))
793}
794
795fn decode_status_byte(data: &Value, field: &str) -> Result<u8> {
803 let Some(value) = data.get(field).and_then(|v| v.as_str()) else {
804 return Err(anyhow!(
805 "'{field}' is missing from the respond_to_apdu payload; a status word is never \
806 defaulted, because the default would be success"
807 ));
808 };
809 match actions::parse_hex(field, value)?.as_slice() {
810 [byte] => Ok(*byte),
811 other => Err(anyhow!(
812 "'{field}' must be exactly one hex byte, got {} byte(s)",
813 other.len()
814 )),
815 }
816}