use anyhow::{bail, Context, Result};
pub const REPORT_LEN: usize = 64;
const BROADCAST_CID: [u8; 4] = [0xff; 4];
const TYPE_INIT: u8 = 0x80;
const CTAPHID_INIT: u8 = 0x06;
const CTAPHID_KEEPALIVE: u8 = 0x3b;
const CTAPHID_ERROR: u8 = 0x3f;
pub const YK_READ_CONFIG: u8 = 0x42;
const TAG_SERIAL: u8 = 0x02;
const MAX_REPORTS: usize = 64;
pub trait ReportChannel {
fn write_report(&mut self, report: &[u8; REPORT_LEN]) -> Result<()>;
fn read_report(&mut self) -> Result<[u8; REPORT_LEN]>;
}
fn send(ch: &mut dyn ReportChannel, cid: [u8; 4], cmd: u8, data: &[u8]) -> Result<()> {
let mut report = [0u8; REPORT_LEN];
report[..4].copy_from_slice(&cid);
report[4] = TYPE_INIT | cmd;
report[5..7].copy_from_slice(&(data.len() as u16).to_be_bytes());
let first = data.len().min(REPORT_LEN - 7);
report[7..7 + first].copy_from_slice(&data[..first]);
ch.write_report(&report)?;
for (seq, chunk) in data[first..].chunks(REPORT_LEN - 5).enumerate() {
let mut report = [0u8; REPORT_LEN];
report[..4].copy_from_slice(&cid);
report[4] = seq as u8;
report[5..5 + chunk.len()].copy_from_slice(chunk);
ch.write_report(&report)?;
}
Ok(())
}
fn recv(ch: &mut dyn ReportChannel, cid: [u8; 4], cmd: u8) -> Result<Vec<u8>> {
for _ in 0..MAX_REPORTS {
let report = ch.read_report()?;
if report[..4] != cid {
continue;
}
match report[4] {
c if c == TYPE_INIT | CTAPHID_KEEPALIVE => continue,
c if c == TYPE_INIT | CTAPHID_ERROR => bail!("CTAPHID error 0x{:02x}", report[7]),
c if c == TYPE_INIT | cmd => {}
c => bail!("unexpected CTAPHID reply 0x{c:02x}"),
}
let size = u16::from_be_bytes([report[5], report[6]]) as usize;
let mut data = report[7..].to_vec();
data.truncate(size);
while data.len() < size {
let cont = ch.read_report()?;
if cont[..4] == cid {
let take = (size - data.len()).min(REPORT_LEN - 5);
data.extend_from_slice(&cont[5..5 + take]);
}
}
return Ok(data);
}
bail!("no CTAPHID reply")
}
fn init(ch: &mut dyn ReportChannel) -> Result<[u8; 4]> {
let nonce: [u8; 8] = rand::random();
send(ch, BROADCAST_CID, CTAPHID_INIT, &nonce)?;
for _ in 0..MAX_REPORTS {
let reply = recv(ch, BROADCAST_CID, CTAPHID_INIT)?;
if reply.len() >= 12 && reply[..8] == nonce {
return Ok([reply[8], reply[9], reply[10], reply[11]]);
}
}
bail!("no CTAPHID INIT reply")
}
fn find_tlv(mut data: &[u8], tag: u8) -> Option<&[u8]> {
while data.len() >= 2 {
let (len, off) = match data[1] {
0x81 if data.len() >= 3 => (data[2] as usize, 3),
0x82 if data.len() >= 4 => (u16::from_be_bytes([data[2], data[3]]) as usize, 4),
l if l < 0x80 => (l as usize, 2),
_ => return None,
};
let value = data.get(off..off + len)?;
if data[0] == tag {
return Some(value);
}
data = &data[off + len..];
}
None
}
pub fn parse_device_info_serial(info: &[u8]) -> Result<Option<u32>> {
let Some((&len, tlvs)) = info.split_first() else {
bail!("empty device info");
};
if len as usize != tlvs.len() {
bail!("invalid device info length");
}
Ok(find_tlv(tlvs, TAG_SERIAL).map(|v| v.iter().fold(0u32, |n, &b| (n << 8) | u32::from(b))))
}
pub fn read_serial(ch: &mut dyn ReportChannel) -> Result<Option<u32>> {
let cid = init(ch)?;
send(ch, cid, YK_READ_CONFIG, &[0x00])?;
let info = recv(ch, cid, YK_READ_CONFIG).context("reading the YubiKey's device info")?;
parse_device_info_serial(&info)
}
#[cfg(target_os = "linux")]
pub struct Hidraw(std::fs::File);
#[cfg(target_os = "linux")]
impl Hidraw {
pub fn open(path: &str) -> Result<Self> {
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.with_context(|| format!("opening {path}"))?;
Ok(Self(file))
}
}
#[cfg(target_os = "linux")]
impl ReportChannel for Hidraw {
fn write_report(&mut self, report: &[u8; REPORT_LEN]) -> Result<()> {
use std::io::Write as _;
let mut buf = [0u8; REPORT_LEN + 1];
buf[1..].copy_from_slice(report);
self.0.write_all(&buf).context("writing to the FIDO device")
}
fn read_report(&mut self) -> Result<[u8; REPORT_LEN]> {
use std::io::Read as _;
let mut buf = [0u8; REPORT_LEN];
let n = self
.0
.read(&mut buf)
.context("reading from the FIDO device")?;
if n != REPORT_LEN {
bail!("short HID report ({n} bytes)");
}
Ok(buf)
}
}
#[cfg(any(test, feature = "test-utils", feature = "test-fixture"))]
pub struct IoChannel {
io: super::ffi::FidoDevIo,
handle: *mut std::ffi::c_void,
}
#[cfg(any(test, feature = "test-utils", feature = "test-fixture"))]
impl IoChannel {
pub fn open(io: &super::ffi::FidoDevIo) -> Result<Self> {
let handle = unsafe { (io.open)(c"emulator".as_ptr()) };
if handle.is_null() {
bail!("the emulator did not open");
}
Ok(Self { io: *io, handle })
}
}
#[cfg(any(test, feature = "test-utils", feature = "test-fixture"))]
impl Drop for IoChannel {
fn drop(&mut self) {
unsafe { (self.io.close)(self.handle) }
}
}
#[cfg(any(test, feature = "test-utils", feature = "test-fixture"))]
impl ReportChannel for IoChannel {
fn write_report(&mut self, report: &[u8; REPORT_LEN]) -> Result<()> {
let mut buf = [0u8; REPORT_LEN + 1];
buf[1..].copy_from_slice(report);
let n = unsafe { (self.io.write)(self.handle, buf.as_ptr(), buf.len()) };
if n != buf.len() as std::ffi::c_int {
bail!("emulator write failed");
}
Ok(())
}
fn read_report(&mut self) -> Result<[u8; REPORT_LEN]> {
let mut buf = [0u8; REPORT_LEN];
let n = unsafe { (self.io.read)(self.handle, buf.as_mut_ptr(), REPORT_LEN, -1) };
if n != REPORT_LEN as std::ffi::c_int {
bail!("emulator read failed");
}
Ok(buf)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn device_info_serial() {
let tlvs = [
0x01, 0x02, 0x02, 0x3b, 0x02, 0x04, 0x01, 0xec, 0x98, 0x5d, 0x05, 0x03, 0x05, 0x07,
0x01,
];
let mut info = vec![tlvs.len() as u8];
info.extend_from_slice(&tlvs);
assert_eq!(parse_device_info_serial(&info).unwrap(), Some(32_282_717));
assert!(parse_device_info_serial(&[5, 1, 2]).is_err());
}
#[test]
fn device_info_without_serial() {
let tlvs = [0x05, 0x03, 0x05, 0x07, 0x01];
let mut info = vec![tlvs.len() as u8];
info.extend_from_slice(&tlvs);
assert_eq!(parse_device_info_serial(&info).unwrap(), None);
}
}