pub mod actions;
pub mod apdu;
pub mod ccid;
pub mod handler;
pub use actions::UsbSmartCardProtocol;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex as StdMutex};
use anyhow::{anyhow, Result};
use serde_json::{json, Value};
use tokio::sync::mpsc;
use tracing::{debug, info, warn};
use crate::llm::action_helper::call_llm;
use crate::llm::ollama_client::OllamaClient;
use crate::llm::ActionResult;
use crate::logging::emit::Log;
use crate::protocol::Event;
use crate::server::accept_bounded::{accept_bounded, ConnectionLimiter};
use crate::server::connection::ConnectionId;
use crate::server::usb::guard::{MAX_USBIP_CONNECTIONS, USBIP_NO_REFUSAL};
use crate::state::app_state::AppState;
use crate::state::ServerId;
use crate::{console_debug, console_error};
use actions::{
USB_SMARTCARD_APDU_RECEIVED_EVENT, USB_SMARTCARD_ATTACHED_EVENT, USB_SMARTCARD_DETACHED_EVENT,
USB_SMARTCARD_READER_READY_EVENT,
};
use apdu::{ApduCommand, ApduResponse};
use handler::{CardState, PendingApdu, UsbCcidHandler};
const DEFAULT_ATR: &[u8] = &[0x3B, 0x90, 0x11, 0x00];
type SharedUsbHandler = Arc<StdMutex<Box<dyn usbip::UsbInterfaceHandler + Send>>>;
pub struct UsbSmartCardServer;
impl UsbSmartCardServer {
#[allow(clippy::too_many_arguments)]
pub async fn spawn_with_llm_actions(
listen_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: ServerId,
card_type: Option<String>,
first_byte_timeout_secs: Option<u64>,
idle_timeout_secs: Option<u64>,
) -> Result<SocketAddr> {
let card_type = card_type.unwrap_or_else(|| "generic".to_string());
let listener =
crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
let local_addr = listener.local_addr()?;
Log::new(Some(&status_tx)).info(format!(
"USB smart card reader listening on {local_addr} (card_type={card_type}) - \
attach with: sudo usbip attach -r {} -b 0-0-0",
local_addr.ip()
));
let card = Arc::new(StdMutex::new(CardState {
atr: DEFAULT_ATR.to_vec(),
card_present: true,
}));
let protocol = Arc::new(UsbSmartCardProtocol::new());
let ready_event = Event::new(
&USB_SMARTCARD_READER_READY_EVENT,
json!({
"listen_addr": local_addr.to_string(),
"card_type": card_type,
"atr_hex": hex::encode_upper(DEFAULT_ATR),
}),
);
match call_llm(
&llm_client,
&app_state,
server_id,
None, &ready_event,
protocol.as_ref(),
)
.await
{
Ok(execution) => {
for message in execution.messages {
let _ = status_tx.send(message);
}
Self::apply_card_config(execution.protocol_results, &card, None, &status_tx);
}
Err(e) => {
Log::new(Some(&status_tx)).error(format!(
"USB smart card startup configuration failed ({}); the reader is listening \
on {} with the default ATR {} and a card inserted",
e,
local_addr,
hex::encode_upper(DEFAULT_ATR)
));
}
}
{
let snapshot = lock_card(&card);
debug!(
"Virtual smart card configured: atr={}, card_present={}",
hex::encode_upper(&snapshot.atr),
snapshot.card_present
);
}
let deadlines = crate::server::usb::guard::UsbIpDeadlines::from_secs(
first_byte_timeout_secs,
idle_timeout_secs,
);
let task_registrar = app_state.clone();
let limiter = ConnectionLimiter::new(MAX_USBIP_CONNECTIONS);
let accept_handle = tokio::spawn(async move {
loop {
let (stream, remote_addr, permit) = match accept_bounded(
&listener,
&limiter,
USBIP_NO_REFUSAL,
"USB smart card",
Some(&status_tx),
)
.await
{
Ok(accepted) => accepted,
Err(e) => {
Log::new(Some(&status_tx)).error(format!(
"USB smart card accept failed, stopping accept loop: {}",
e
));
break;
}
};
let connection_id = ConnectionId::new(app_state.get_next_unified_id().await);
info!(
"USB/IP connection {} from {} (USB smart card)",
connection_id, remote_addr
);
{
use crate::state::server::{
ConnectionState, ConnectionStatus, ProtocolConnectionInfo,
};
let now = crate::utils::clock::Instant::now();
let conn_state = ConnectionState {
id: connection_id,
remote_addr,
local_addr: stream.local_addr().unwrap_or(local_addr),
bytes_sent: 0,
bytes_received: 0,
packets_sent: 0,
packets_received: 0,
last_activity: now,
status: ConnectionStatus::Active,
status_changed_at: now,
protocol_info: ProtocolConnectionInfo::new(json!({
"transport": "usbip",
"usb_class": "ccid",
"card_type": card_type,
})),
};
app_state
.add_connection_to_server(server_id, conn_state)
.await;
}
let _ = status_tx.send("__UPDATE_UI__".to_string());
let llm_client = llm_client.clone();
let conn_app_state = app_state.clone();
let status_tx = status_tx.clone();
let card = card.clone();
let protocol = protocol.clone();
let card_type = card_type.clone();
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
let _permit = permit;
if let Err(e) = Self::handle_connection(
stream,
connection_id,
server_id,
card,
card_type,
llm_client,
conn_app_state.clone(),
status_tx.clone(),
protocol,
deadlines,
)
.await
{
console_error!(
status_tx,
"USB smart card connection {} error: {}",
connection_id,
e
);
}
conn_app_state
.close_connection_on_server(server_id, connection_id)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
})
.await;
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(local_addr)
}
#[allow(clippy::too_many_arguments)]
async fn handle_connection(
stream: tokio::net::TcpStream,
connection_id: ConnectionId,
server_id: ServerId,
card: Arc<StdMutex<CardState>>,
card_type: String,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
protocol: Arc<UsbSmartCardProtocol>,
deadlines: crate::server::usb::guard::UsbIpDeadlines,
) -> Result<()> {
let (apdu_tx, mut apdu_rx) = mpsc::unbounded_channel::<PendingApdu>();
let ccid_handler: SharedUsbHandler = Arc::new(StdMutex::new(Box::new(UsbCcidHandler::new(
card.clone(),
apdu_tx,
))
as Box<dyn usbip::UsbInterfaceHandler + Send>));
let device = usbip::UsbDevice::new(0).with_interface(
usbip::ClassCode::SmartCard as u8,
0x00, 0x00, Some("NetGet Virtual CCID Reader"),
UsbCcidHandler::endpoints(),
ccid_handler.clone(),
);
let usbip_server = Arc::new(usbip::UsbIpServer::new_simulated(vec![device]));
let guard_status_tx = status_tx.clone();
let (import_tx, mut import_rx) = tokio::sync::oneshot::channel();
let mut usbip_task = tokio::spawn(async move {
match crate::server::usb::guard::run_guarded_usbip(
stream,
usbip_server,
"USB smart card",
connection_id.to_string(),
guard_status_tx,
Some(import_tx),
deadlines,
)
.await
{
Ok(()) => debug!(
"USB/IP session ended for smart card connection {}",
connection_id
),
Err(e) => debug!(
"USB/IP session for smart card connection {} ended with error: {}",
connection_id, e
),
}
});
let mut import_pending = true;
let mut imported = false;
loop {
tokio::select! {
attached = &mut import_rx, if import_pending => {
import_pending = false;
if attached.is_ok() {
imported = true;
let (atr_hex, card_present) = {
let snapshot = lock_card(&card);
(hex::encode_upper(&snapshot.atr), snapshot.card_present)
};
let attached_event = Event::new(
&USB_SMARTCARD_ATTACHED_EVENT,
json!({
"connection_id": connection_id.to_string(),
"card_type": card_type,
"card_present": card_present,
"atr_hex": atr_hex,
}),
);
match call_llm(
&llm_client,
&app_state,
server_id,
Some(connection_id),
&attached_event,
protocol.as_ref(),
)
.await
{
Ok(execution) => {
for message in execution.messages {
let _ = status_tx.send(message);
}
Self::apply_card_config(
execution.protocol_results,
&card,
Some(&ccid_handler),
&status_tx,
);
info!(
"USB smart card LLM call completed for connection {} (attach)",
connection_id
);
}
Err(e) => {
console_error!(
status_tx,
"USB smart card attach handler failed for {}: {}",
connection_id,
e
);
}
}
}
}
pending = apdu_rx.recv() => {
let Some(pending) = pending else { break };
let response = Self::answer_apdu(
&pending,
connection_id,
server_id,
&card_type,
&llm_client,
&app_state,
&status_tx,
protocol.as_ref(),
)
.await;
let status_word = response.status_word();
let bytes = response.into_bytes();
let _ = status_tx.send(format!(
"→ USB smart card response SW={} ({} data byte(s)) to {}",
status_word,
bytes.len().saturating_sub(2),
connection_id
));
if with_ccid_handler(&ccid_handler, |h| {
h.queue_apdu_response(pending.slot, pending.seq, &bytes)
})
.is_none()
{
console_error!(
status_tx,
"USB smart card could not reach the CCID handler for {}; the host \
will not get an answer to seq {}",
connection_id,
pending.seq
);
}
}
_ = &mut usbip_task => break,
}
}
info!(
"USB smart card host detached on connection {}",
connection_id
);
if imported {
let detached_event = Event::new(
&USB_SMARTCARD_DETACHED_EVENT,
json!({ "connection_id": connection_id.to_string() }),
);
match call_llm(
&llm_client,
&app_state,
server_id,
Some(connection_id),
&detached_event,
protocol.as_ref(),
)
.await
{
Ok(execution) => {
for message in execution.messages {
let _ = status_tx.send(message);
}
info!(
"USB smart card LLM call completed for connection {} (detach)",
connection_id
);
}
Err(e) => {
console_error!(
status_tx,
"USB smart card detach handler failed for {}: {}",
connection_id,
e
);
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn answer_apdu(
pending: &PendingApdu,
connection_id: ConnectionId,
server_id: ServerId,
card_type: &str,
llm_client: &OllamaClient,
app_state: &Arc<AppState>,
status_tx: &mpsc::UnboundedSender<String>,
protocol: &UsbSmartCardProtocol,
) -> ApduResponse {
let command = match ApduCommand::parse(&pending.apdu) {
Ok(command) => command,
Err(e) => {
console_error!(
status_tx,
"USB smart card host {} sent a malformed APDU: {}",
connection_id,
e
);
return ApduResponse::wrong_length();
}
};
let mut event_data = json!({
"connection_id": connection_id.to_string(),
"card_type": card_type,
"ins_name": command.ins_name(),
"cla": format!("{:02X}", command.cla),
"ins": format!("{:02X}", command.ins),
"p1": format!("{:02X}", command.p1),
"p2": format!("{:02X}", command.p2),
"lc": command.data.len(),
"data_hex": hex::encode_upper(&command.data),
"le": command.le,
});
if let Some(map) = event_data.as_object_mut() {
if let Some(text) = apdu::printable_text(&command.data) {
map.insert("data_text".into(), json!(text));
}
if command.is_select_by_aid() {
map.insert(
"application_id".into(),
json!(hex::encode_upper(&command.data)),
);
}
}
let event = Event::new(&USB_SMARTCARD_APDU_RECEIVED_EVENT, event_data);
console_debug!(
status_tx,
"USB smart card {} on {} (Lc={}, Le={:?})",
command.ins_name(),
connection_id,
command.data.len(),
command.le
);
let execution = match call_llm(
llm_client,
app_state,
server_id,
Some(connection_id),
&event,
protocol,
)
.await
{
Ok(execution) => execution,
Err(e) => {
console_error!(
status_tx,
"USB smart card handler failed for {} on {} \
decision=fail_closed_llm_error, answering 6F00: {}",
event.id(),
connection_id,
e
);
return ApduResponse::card_error();
}
};
for message in execution.messages {
let _ = status_tx.send(message);
}
let mut response = None;
let mut results = Vec::new();
flatten_results(execution.protocol_results, &mut results);
for result in results {
let ActionResult::Custom { name, data } = result else {
continue;
};
if name != "respond_to_apdu" {
continue;
}
match decode_apdu_response(&data) {
Ok(decoded) => {
if response.is_none() {
response = Some(decoded);
} else {
console_error!(
status_tx,
"USB smart card handler returned more than one respond_to_apdu for \
{}; ignoring the extra one",
connection_id
);
}
}
Err(e) => {
console_error!(
status_tx,
"USB smart card respond_to_apdu could not be decoded on {} \
decision=fail_closed_undecodable, falling through to 6F00: {}",
connection_id,
e
);
}
}
}
match response {
Some(response) => response,
None => {
console_error!(
status_tx,
"USB smart card handler produced no respond_to_apdu for {} on {} \
decision=model_silent, answering 6F00",
event.id(),
connection_id
);
ApduResponse::card_error()
}
}
}
fn apply_card_config(
results: Vec<ActionResult>,
card: &Arc<StdMutex<CardState>>,
ccid_handler: Option<&SharedUsbHandler>,
status_tx: &mpsc::UnboundedSender<String>,
) {
let mut flattened = Vec::new();
flatten_results(results, &mut flattened);
for result in flattened {
let ActionResult::Custom { name, data } = result else {
continue;
};
match name.as_str() {
"set_atr" => match decode_atr(&data) {
Ok(atr) => {
let atr_hex = hex::encode_upper(&atr);
lock_card(card).atr = atr;
Log::new(Some(status_tx))
.info(format!("USB smart card ATR set: {atr_hex}"));
}
Err(e) => {
console_error!(status_tx, "USB smart card set_atr rejected: {}", e);
}
},
"set_card_present" => match data.get("present").and_then(|v| v.as_bool()) {
Some(present) => {
lock_card(card).card_present = present;
if let Some(handler) = ccid_handler {
with_ccid_handler(handler, |h| h.note_slot_change());
}
Log::new(Some(status_tx)).info(format!(
"USB smart card slot: card {}",
if present { "inserted" } else { "removed" }
));
}
None => {
console_error!(
status_tx,
"USB smart card set_card_present had no boolean 'present'"
);
}
},
"respond_to_apdu" => {
console_error!(
status_tx,
"USB smart card respond_to_apdu is only valid in response to \
usb_smartcard_apdu_received; ignoring it"
);
}
other => {
warn!(
"USB smart card ignoring unhandled action result '{}'",
other
);
}
}
}
}
}
fn lock_card(card: &Arc<StdMutex<CardState>>) -> std::sync::MutexGuard<'_, CardState> {
match card.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
}
}
fn with_ccid_handler<R>(
handler: &SharedUsbHandler,
f: impl FnOnce(&mut UsbCcidHandler) -> R,
) -> Option<R> {
let mut guard = match handler.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
let ccid = guard.as_any().downcast_mut::<UsbCcidHandler>()?;
Some(f(ccid))
}
fn flatten_results(results: Vec<ActionResult>, out: &mut Vec<ActionResult>) {
for result in results {
match result {
ActionResult::Multiple(nested) => flatten_results(nested, out),
other => out.push(other),
}
}
}
fn decode_atr(data: &Value) -> Result<Vec<u8>> {
let atr_hex = data
.get("atr_hex")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("Missing atr_hex"))?;
let atr = actions::parse_hex("atr_hex", atr_hex)?;
if atr.is_empty() {
return Err(anyhow!("ATR must not be empty"));
}
if atr.len() > ccid::MAX_PAYLOAD_LEN {
return Err(anyhow!(
"ATR is {} bytes; a CCID data block carries at most {}",
atr.len(),
ccid::MAX_PAYLOAD_LEN
));
}
Ok(atr)
}
fn decode_apdu_response(data: &Value) -> Result<ApduResponse> {
let data_hex = data
.get("data_hex")
.and_then(|v| v.as_str())
.unwrap_or_default();
let body = if data_hex.is_empty() {
Vec::new()
} else {
actions::parse_hex("data_hex", data_hex)?
};
if body.len() + 2 > ccid::MAX_PAYLOAD_LEN {
return Err(anyhow!(
"Response body of {} bytes does not fit in a CCID data block (max {} including SW)",
body.len(),
ccid::MAX_PAYLOAD_LEN
));
}
let sw1 = decode_status_byte(data, "sw1")?;
let sw2 = decode_status_byte(data, "sw2")?;
Ok(ApduResponse::new(body, sw1, sw2))
}
fn decode_status_byte(data: &Value, field: &str) -> Result<u8> {
let Some(value) = data.get(field).and_then(|v| v.as_str()) else {
return Err(anyhow!(
"'{field}' is missing from the respond_to_apdu payload; a status word is never \
defaulted, because the default would be success"
));
};
match actions::parse_hex(field, value)?.as_slice() {
[byte] => Ok(*byte),
other => Err(anyhow!(
"'{field}' must be exactly one hex byte, got {} byte(s)",
other.len()
)),
}
}