use std::{
ffi::CString,
time::Duration,
};
use hidapi::{
HidApi,
HidDevice,
};
use rand::Rng as _;
use crate::error::{
CtapStatus,
Error,
Result,
};
pub mod cmd {
pub const CTAPHID_INIT: u8 = 0x86;
pub const CTAPHID_CBOR: u8 = 0x90;
pub const CTAPHID_CANCEL: u8 = 0x91;
pub const CTAPHID_WINK: u8 = 0x88;
pub const CTAPHID_KEEPALIVE: u8 = 0xBB;
pub const CTAPHID_ERROR: u8 = 0xBF;
}
pub const REPORT_SIZE: usize = 64;
const BROADCAST_CID: [u8; 4] = [0xFF, 0xFF, 0xFF, 0xFF];
const READ_TIMEOUT: Duration = Duration::from_mins(1);
const INIT_FRAME_DATA: usize = REPORT_SIZE - 7;
const CONT_FRAME_DATA: usize = REPORT_SIZE - 5;
const MAX_PAYLOAD: usize = INIT_FRAME_DATA + 128 * CONT_FRAME_DATA;
pub struct Transport {
device: HidDevice,
cid: [u8; 4],
firmware_version: (u8, u8, u8),
}
struct InitResponse {
cid: [u8; 4],
firmware_version: (u8, u8, u8),
}
impl Transport {
pub fn open(path: &str) -> Result<Self> {
log::trace!("Transport::open path={path}");
let api = HidApi::new().map_err(|err| Error::Hid(err.to_string()))?;
let cpath = CString::new(path).map_err(|err| Error::Hid(format!("bad path: {err}")))?;
let device = api
.open_path(&cpath)
.map_err(|err| Error::Hid(err.to_string()))?;
let init = ctaphid_init(&device)?;
log::trace!("Transport::open ok cid={:02x?}", init.cid);
Ok(Self {
device,
cid: init.cid,
firmware_version: init.firmware_version,
})
}
#[must_use]
pub const fn firmware_version(&self) -> (u8, u8, u8) {
self.firmware_version
}
pub fn transact(
&mut self,
payload: &[u8],
on_keepalive: Option<&mut dyn FnMut(KeepAlive)>,
) -> Result<Vec<u8>> {
self.send_message(cmd::CTAPHID_CBOR, payload)?;
let mut body = self.recv_message(cmd::CTAPHID_CBOR, on_keepalive)?;
let status_byte = body
.first()
.copied()
.ok_or(Error::Parse("CBOR response missing status byte"))?;
let status = CtapStatus::from_byte(status_byte);
if status == CtapStatus::Ok {
body.drain(..1);
Ok(body)
} else {
Err(Error::Ctap(status))
}
}
pub fn wink(&mut self) -> Result<()> {
self.send_message(cmd::CTAPHID_WINK, &[])?;
self.recv_message(cmd::CTAPHID_WINK, None)?;
Ok(())
}
pub fn cancel(&self) -> Result<()> {
self.send_message(cmd::CTAPHID_CANCEL, &[])
}
pub fn vendor_command(&self, command: u8, payload: &[u8]) -> Result<Vec<u8>> {
if command & 0x80 == 0 {
return Err(Error::Parse(
"vendor_command requires an init-frame command byte (bit 7 set)",
));
}
self.send_message(command, payload)?;
self.recv_message(command, None)
}
fn send_message(&self, command: u8, payload: &[u8]) -> Result<()> {
log::trace!(
"send_message cid={:02x?} cmd=0x{:02x} bcnt={}",
self.cid,
command,
payload.len()
);
if payload.len() > MAX_PAYLOAD {
return Err(Error::Parse(
"payload exceeds CTAPHID wire ceiling (7609 bytes)",
));
}
let bcnt =
u16::try_from(payload.len()).map_err(|_| Error::Parse("payload exceeds u16 BCNT"))?;
let (init_chunk, mut rest) = payload.split_at(payload.len().min(INIT_FRAME_DATA));
let mut frame = [0_u8; REPORT_SIZE + 1];
frame[1..5].copy_from_slice(&self.cid);
frame[5] = command;
frame[6..8].copy_from_slice(&bcnt.to_be_bytes());
frame[8..8 + init_chunk.len()].copy_from_slice(init_chunk);
self.write_frame(&frame)?;
let mut seq = 0_u8;
while !rest.is_empty() {
let (chunk, tail) = rest.split_at(rest.len().min(CONT_FRAME_DATA));
rest = tail;
frame.fill(0);
frame[1..5].copy_from_slice(&self.cid);
frame[5] = seq & 0x7F;
frame[6..6 + chunk.len()].copy_from_slice(chunk);
self.write_frame(&frame)?;
seq = seq.wrapping_add(1);
}
Ok(())
}
fn recv_message(
&self,
expected_cmd: u8,
mut on_keepalive: Option<&mut dyn FnMut(KeepAlive)>,
) -> Result<Vec<u8>> {
log::trace!(
"recv_message: waiting for cid={:02x?} expected_cmd=0x{:02x}",
self.cid,
expected_cmd
);
loop {
let frame = self.read_frame()?;
log::trace!(
"recv_message: got frame cid={:02x?} cmd=0x{:02x} bcnt={}",
&frame[0..4],
frame[4],
u16::from_be_bytes([frame[5], frame[6]])
);
if frame[0..4] != self.cid {
log::trace!("recv_message: cid mismatch, skipping");
continue;
}
let cmd_byte = frame[4];
match cmd_byte {
byte if byte == cmd::CTAPHID_KEEPALIVE => {
if let Some(keepalive) = on_keepalive.as_mut() {
keepalive(KeepAlive::from_byte(frame[7]));
}
continue;
},
byte if byte == cmd::CTAPHID_ERROR => {
let status = CtapStatus::from_byte(frame[7]);
return Err(Error::Ctap(status));
},
byte if byte == expected_cmd => {},
_ => return Err(Error::Parse("unexpected CTAPHID command byte in response")),
}
let bcnt = u16::from_be_bytes([frame[5], frame[6]]) as usize;
let mut body = Vec::<u8>::with_capacity(bcnt);
let init_take = bcnt.min(INIT_FRAME_DATA);
body.extend_from_slice(&frame[7..7 + init_take]);
let mut seq = 0_u8;
while body.len() < bcnt {
let cont = self.read_frame()?;
if cont[0..4] != self.cid {
continue;
}
if cont[4] & 0x80 != 0 {
return Err(Error::Parse(
"expected CTAPHID continuation frame, got init",
));
}
if cont[4] != seq {
return Err(Error::Parse("CTAPHID continuation sequence skew"));
}
let remaining = bcnt - body.len();
let take = remaining.min(CONT_FRAME_DATA);
body.extend_from_slice(&cont[5..5 + take]);
seq = seq.wrapping_add(1);
}
return Ok(body);
}
}
fn write_frame(&self, frame: &[u8; REPORT_SIZE + 1]) -> Result<()> {
write_hid_frame(&self.device, frame)
}
fn read_frame(&self) -> Result<[u8; REPORT_SIZE]> {
read_hid_frame(&self.device)
}
}
fn write_hid_frame(device: &HidDevice, frame: &[u8; REPORT_SIZE + 1]) -> Result<()> {
let written = device
.write(frame)
.map_err(|err| Error::Hid(err.to_string()))?;
if written != frame.len() {
return Err(Error::Hid(format!(
"short HID write: {written} of {}",
frame.len()
)));
}
Ok(())
}
fn read_hid_frame(device: &HidDevice) -> Result<[u8; REPORT_SIZE]> {
log::trace!("read_hid_frame: blocking on read_timeout");
let mut buf = [0_u8; REPORT_SIZE];
let read = device
.read_timeout(
&mut buf,
i32::try_from(READ_TIMEOUT.as_millis()).unwrap_or(i32::MAX),
)
.map_err(|err| Error::Hid(err.to_string()))?;
log::trace!("read_hid_frame: returned {read} bytes");
if read == 0 {
return Err(Error::Hid("HID read timed out".into()));
}
if read != REPORT_SIZE {
return Err(Error::Hid(format!(
"short HID read: {read} of {REPORT_SIZE}"
)));
}
Ok(buf)
}
fn ctaphid_init(device: &HidDevice) -> Result<InitResponse> {
let mut nonce = [0_u8; 8];
rand::rng().fill_bytes(&mut nonce);
log::trace!("ctaphid_init: sending INIT nonce={nonce:02x?}");
let mut frame = [0_u8; REPORT_SIZE + 1];
frame[1..5].copy_from_slice(&BROADCAST_CID);
frame[5] = cmd::CTAPHID_INIT;
frame[6..8].copy_from_slice(&8_u16.to_be_bytes());
frame[8..16].copy_from_slice(&nonce);
write_hid_frame(device, &frame)?;
loop {
let resp = read_hid_frame(device)?;
if resp[0..4] != BROADCAST_CID {
continue;
}
if resp[4] != cmd::CTAPHID_INIT {
return Err(Error::Parse(
"unexpected CTAPHID command byte in INIT response",
));
}
let bcnt = u16::from_be_bytes([resp[5], resp[6]]) as usize;
if bcnt < 17 {
return Err(Error::Parse("INIT response too short"));
}
let body = &resp[7..7 + bcnt.min(REPORT_SIZE - 7)];
if body[0..8] != nonce {
return Err(Error::Parse("INIT response nonce mismatch"));
}
return Ok(InitResponse {
cid: [body[8], body[9], body[10], body[11]],
firmware_version: (body[13], body[14], body[15]),
});
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum KeepAlive {
Processing,
UserPresenceNeeded,
Other(u8),
}
impl KeepAlive {
#[must_use]
pub const fn from_byte(byte: u8) -> Self {
match byte {
0x01 => Self::Processing,
0x02 => Self::UserPresenceNeeded,
other => Self::Other(other),
}
}
}
impl From<u8> for KeepAlive {
fn from(byte: u8) -> Self {
Self::from_byte(byte)
}
}