#[cfg(all(unix, feature = "libusb"))]
use anyhow::{bail, Context, Result};
#[cfg(all(unix, feature = "libusb"))]
use std::collections::HashMap;
#[cfg(all(unix, feature = "libusb"))]
use tracing::{debug, trace, warn};
#[cfg(all(unix, feature = "libusb"))]
pub const HID_PACKET_SIZE: usize = 64;
#[cfg(all(unix, feature = "libusb"))]
pub const BROADCAST_CHANNEL: u32 = 0xffffffff;
#[cfg(all(unix, feature = "libusb"))]
pub const INIT_PAYLOAD: usize = HID_PACKET_SIZE - 7;
#[cfg(all(unix, feature = "libusb"))]
pub const CONT_PAYLOAD: usize = HID_PACKET_SIZE - 5;
#[cfg(all(unix, feature = "libusb"))]
pub const MAX_MESSAGE_SIZE: usize = INIT_PAYLOAD + 128 * CONT_PAYLOAD;
#[cfg(all(unix, feature = "libusb"))]
pub const MAX_ASSEMBLING_CHANNELS: usize = 4;
#[cfg(all(unix, feature = "libusb"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum CtapHidCommand {
Ping = 0x01,
Msg = 0x03, Lock = 0x04,
Init = 0x06,
Wink = 0x08,
Cbor = 0x10, Cancel = 0x11,
Error = 0x3f,
Keepalive = 0x3b,
}
#[cfg(all(unix, feature = "libusb"))]
impl CtapHidCommand {
pub fn from_u8(value: u8) -> Option<Self> {
match value & !0x80 {
0x01 => Some(Self::Ping),
0x03 => Some(Self::Msg),
0x04 => Some(Self::Lock),
0x06 => Some(Self::Init),
0x08 => Some(Self::Wink),
0x10 => Some(Self::Cbor),
0x11 => Some(Self::Cancel),
0x3f => Some(Self::Error),
0x3b => Some(Self::Keepalive),
_ => None,
}
}
}
#[cfg(all(unix, feature = "libusb"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum CtapHidError {
InvalidCmd = 0x01,
InvalidPar = 0x02,
InvalidLen = 0x03,
InvalidSeq = 0x04,
MsgTimeout = 0x05,
ChannelBusy = 0x06,
LockRequired = 0x0a,
InvalidChannel = 0x0b,
Other = 0x7f,
}
#[cfg(all(unix, feature = "libusb"))]
#[derive(Debug, Clone, Copy)]
pub struct CtapHidRejection {
pub cid: u32,
pub error: CtapHidError,
}
#[cfg(all(unix, feature = "libusb"))]
impl std::fmt::Display for CtapHidRejection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"CTAPHID refusal on cid {:#010x}: {:?}",
self.cid, self.error
)
}
}
#[cfg(all(unix, feature = "libusb"))]
impl std::error::Error for CtapHidRejection {}
#[cfg(all(unix, feature = "libusb"))]
impl CtapHidRejection {
fn err(cid: u32, error: CtapHidError, context: String) -> anyhow::Error {
anyhow::Error::new(CtapHidRejection { cid, error }).context(context)
}
}
#[cfg(all(unix, feature = "libusb"))]
pub fn rejection_of(error: &anyhow::Error) -> CtapHidRejection {
error
.downcast_ref::<CtapHidRejection>()
.copied()
.unwrap_or(CtapHidRejection {
cid: BROADCAST_CHANNEL,
error: CtapHidError::InvalidSeq,
})
}
#[cfg(all(unix, feature = "libusb"))]
#[derive(Debug, Clone)]
pub struct CtapHidPacket {
pub cid: u32,
pub cmd_or_seq: u8,
pub data: Vec<u8>,
}
#[cfg(all(unix, feature = "libusb"))]
impl CtapHidPacket {
pub fn parse(data: &[u8]) -> Result<Self> {
if data.len() != HID_PACKET_SIZE {
bail!(
"Invalid packet size: {} (expected {})",
data.len(),
HID_PACKET_SIZE
);
}
let cid = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
let cmd_or_seq = data[4];
let is_init = (cmd_or_seq & 0x80) != 0;
let payload = if is_init {
data[7..].to_vec()
} else {
data[5..].to_vec()
};
Ok(Self {
cid,
cmd_or_seq,
data: payload,
})
}
pub fn get_byte_count(&self, data: &[u8]) -> Option<usize> {
if (self.cmd_or_seq & 0x80) != 0 && data.len() >= 7 {
let bcnt = u16::from_be_bytes([data[5], data[6]]) as usize;
Some(bcnt)
} else {
None
}
}
pub fn is_init(&self) -> bool {
(self.cmd_or_seq & 0x80) != 0
}
pub fn command(&self) -> Option<CtapHidCommand> {
if self.is_init() {
CtapHidCommand::from_u8(self.cmd_or_seq)
} else {
None
}
}
pub fn sequence(&self) -> Option<u8> {
if !self.is_init() {
Some(self.cmd_or_seq)
} else {
None
}
}
pub fn build_init(cid: u32, cmd: CtapHidCommand, data: &[u8]) -> Vec<u8> {
let mut packet = vec![0u8; HID_PACKET_SIZE];
packet[0..4].copy_from_slice(&cid.to_be_bytes());
packet[4] = (cmd as u8) | 0x80;
let bcnt = u16::try_from(data.len()).unwrap_or(MAX_MESSAGE_SIZE as u16);
packet[5..7].copy_from_slice(&bcnt.to_be_bytes());
let copy_len = data.len().min(INIT_PAYLOAD);
packet[7..7 + copy_len].copy_from_slice(&data[..copy_len]);
packet
}
pub fn build_cont(cid: u32, seq: u8, data: &[u8]) -> Vec<u8> {
let mut packet = vec![0u8; HID_PACKET_SIZE];
packet[0..4].copy_from_slice(&cid.to_be_bytes());
packet[4] = seq & 0x7f;
let copy_len = data.len().min(CONT_PAYLOAD);
packet[5..5 + copy_len].copy_from_slice(&data[..copy_len]);
packet
}
pub fn build_error(cid: u32, error: CtapHidError) -> Vec<u8> {
Self::build_init(cid, CtapHidCommand::Error, &[error as u8])
}
pub fn build_keepalive(cid: u32, status: KeepaliveStatus) -> Vec<u8> {
Self::build_init(cid, CtapHidCommand::Keepalive, &[status as u8])
}
}
#[cfg(all(unix, feature = "libusb"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum KeepaliveStatus {
Processing = 0x01,
UpNeeded = 0x02,
}
#[cfg(all(unix, feature = "libusb"))]
#[derive(Debug)]
pub struct CtapHidMessage {
pub cid: u32,
pub cmd: CtapHidCommand,
pub data: Vec<u8>,
expected_len: usize,
next_seq: u8,
}
#[cfg(all(unix, feature = "libusb"))]
impl CtapHidMessage {
pub fn new(
cid: u32,
cmd: CtapHidCommand,
bcnt: usize,
init_data: &[u8],
) -> std::result::Result<Self, anyhow::Error> {
if bcnt > MAX_MESSAGE_SIZE {
return Err(CtapHidRejection::err(
cid,
CtapHidError::InvalidLen,
format!(
"CTAPHID BCNT={bcnt} exceeds the {MAX_MESSAGE_SIZE}-byte maximum a 7-bit \
sequence number can frame"
),
));
}
let mut data = Vec::new();
let copy_len = init_data.len().min(bcnt);
data.extend_from_slice(&init_data[..copy_len]);
Ok(Self {
cid,
cmd,
data,
expected_len: bcnt,
next_seq: 0,
})
}
pub fn add_continuation(&mut self, seq: u8, cont_data: &[u8]) -> Result<()> {
if seq != self.next_seq {
return Err(CtapHidRejection::err(
self.cid,
CtapHidError::InvalidSeq,
format!(
"CTAPHID sequence mismatch on cid {:#010x}: expected {}, got {}",
self.cid, self.next_seq, seq
),
));
}
let remaining = self.expected_len.saturating_sub(self.data.len());
let copy_len = cont_data.len().min(remaining);
self.data.extend_from_slice(&cont_data[..copy_len]);
self.next_seq = (self.next_seq + 1) & 0x7f;
Ok(())
}
pub fn is_complete(&self) -> bool {
self.data.len() >= self.expected_len
}
pub fn into_data(mut self) -> Vec<u8> {
self.data.truncate(self.expected_len);
self.data
}
}
#[cfg(all(unix, feature = "libusb"))]
pub struct CtapHidHandler {
next_cid: u32,
active_messages: HashMap<u32, CtapHidMessage>,
}
#[cfg(all(unix, feature = "libusb"))]
impl CtapHidHandler {
pub fn new() -> Self {
Self {
next_cid: 1, active_messages: HashMap::new(),
}
}
pub fn allocate_channel(&mut self) -> u32 {
let cid = self.next_cid;
self.next_cid = self.next_cid.wrapping_add(1);
if self.next_cid == BROADCAST_CHANNEL {
self.next_cid = 1;
}
cid
}
pub fn process_packet(&mut self, raw_data: &[u8]) -> Result<Option<CtapHidMessage>> {
let packet = CtapHidPacket::parse(raw_data)?;
trace!(
"CTAPHID packet: cid={:#010x}, cmd_or_seq={:#04x}",
packet.cid,
packet.cmd_or_seq
);
if packet.is_init() {
let cmd = packet.command().context("Invalid command")?;
let bcnt = packet
.get_byte_count(raw_data)
.context("Missing byte count")?;
debug!(
"CTAPHID init: cid={:#010x}, cmd={:?}, bcnt={}",
packet.cid, cmd, bcnt
);
let message = CtapHidMessage::new(packet.cid, cmd, bcnt, &packet.data)?;
if message.is_complete() {
Ok(Some(message))
} else {
if !self.active_messages.contains_key(&packet.cid)
&& self.active_messages.len() >= MAX_ASSEMBLING_CHANNELS
{
warn!(
"CTAPHID: {} channels are already mid-message; refusing cid {:#010x}",
self.active_messages.len(),
packet.cid
);
return Err(CtapHidRejection::err(
packet.cid,
CtapHidError::ChannelBusy,
format!(
"CTAPHID refuses a {}th partially-assembled message; finish one \
before starting another",
MAX_ASSEMBLING_CHANNELS + 1
),
));
}
self.active_messages.insert(packet.cid, message);
Ok(None)
}
} else {
let seq = packet.sequence().context("Invalid sequence")?;
if let Some(message) = self.active_messages.get_mut(&packet.cid) {
message.add_continuation(seq, &packet.data)?;
if message.is_complete() {
Ok(self.active_messages.remove(&packet.cid))
} else {
Ok(None)
}
} else {
warn!(
"Continuation packet for unknown channel: {:#010x}",
packet.cid
);
Err(CtapHidRejection::err(
packet.cid,
CtapHidError::InvalidChannel,
format!(
"CTAPHID continuation for unopened channel {:#010x}",
packet.cid
),
))
}
}
}
pub fn fragment_response(&self, cid: u32, cmd: CtapHidCommand, data: &[u8]) -> Vec<Vec<u8>> {
if data.len() > MAX_MESSAGE_SIZE {
warn!(
"CTAPHID response of {} bytes on cid {:#010x} exceeds the {}-byte maximum; \
answering INVALID_LEN",
data.len(),
cid,
MAX_MESSAGE_SIZE
);
return vec![CtapHidPacket::build_error(cid, CtapHidError::InvalidLen)];
}
let mut packets = Vec::new();
packets.push(CtapHidPacket::build_init(cid, cmd, data));
if data.len() > INIT_PAYLOAD {
let mut offset = INIT_PAYLOAD;
let mut seq = 0u8;
while offset < data.len() {
let chunk = &data[offset..];
packets.push(CtapHidPacket::build_cont(cid, seq, chunk));
offset += CONT_PAYLOAD;
seq = (seq + 1) & 0x7f;
}
}
packets
}
pub fn assembling_channels(&self) -> usize {
self.active_messages.len()
}
}
#[cfg(all(unix, feature = "libusb"))]
impl Default for CtapHidHandler {
fn default() -> Self {
Self::new()
}
}