use crate::consts::{Capability, HIDCmd};
use crate::crypto::ECDHSecret;
use crate::ctap2::client_data::{Challenge, WebauthnType};
use crate::ctap2::commands::client_pin::{GetKeyAgreement, PinAuth};
use crate::ctap2::commands::get_info::{AuthenticatorInfo, GetInfo};
use crate::ctap2::commands::get_version::GetVersion;
use crate::ctap2::commands::make_credentials::{
MakeCredentials, MakeCredentialsExtensions, MakeCredentialsOptions,
};
use crate::ctap2::commands::selection::Selection;
use crate::ctap2::commands::{
CommandError, PinAuthCommand, Request, RequestCtap1, RequestCtap2, Retryable, StatusCode,
};
use crate::ctap2::server::{
PublicKeyCredentialParameters, RelyingParty, RelyingPartyWrapper, User,
};
use crate::transport::device_selector::BlinkResult;
use crate::transport::errors::{ApduErrorStatus, HIDError};
use crate::transport::hid::HIDDevice;
use crate::util::io_err;
use crate::CollectedClientData;
use std::thread;
use std::time::Duration;
pub mod device_selector;
pub mod errors;
pub mod hid;
#[cfg(all(
any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"),
not(test)
))]
pub mod hidproto;
#[cfg(all(target_os = "linux", not(test)))]
#[path = "linux/mod.rs"]
pub mod platform;
#[cfg(all(target_os = "freebsd", not(test)))]
#[path = "freebsd/mod.rs"]
pub mod platform;
#[cfg(all(target_os = "netbsd", not(test)))]
#[path = "netbsd/mod.rs"]
pub mod platform;
#[cfg(all(target_os = "openbsd", not(test)))]
#[path = "openbsd/mod.rs"]
pub mod platform;
#[cfg(all(target_os = "macos", not(test)))]
#[path = "macos/mod.rs"]
pub mod platform;
#[cfg(all(target_os = "windows", not(test)))]
#[path = "windows/mod.rs"]
pub mod platform;
#[cfg(not(any(
target_os = "linux",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "macos",
target_os = "windows",
test
)))]
#[path = "stub/mod.rs"]
pub mod platform;
#[cfg(test)]
#[path = "mock/mod.rs"]
pub mod platform;
#[derive(Debug)]
pub enum Nonce {
CreateRandom,
Use([u8; 8]),
}
pub trait FidoDevice: HIDDevice {
fn send_msg<'msg, Out, Req: Request<Out>>(&mut self, msg: &'msg Req) -> Result<Out, HIDError> {
if !self.initialized() {
return Err(HIDError::DeviceNotInitialized);
}
if self.supports_ctap2() && msg.is_ctap2_request() {
self.send_cbor(msg)
} else {
self.send_apdu(msg)
}
}
fn send_cbor<'msg, Req: RequestCtap2>(
&mut self,
msg: &'msg Req,
) -> Result<Req::Output, HIDError> {
debug!("sending {:?} to {:?}", msg, self);
let mut data = msg.wire_format(self)?;
let mut buf: Vec<u8> = Vec::with_capacity(data.len() + 1);
buf.push(Req::command() as u8);
buf.append(&mut data);
let buf = buf;
let (cmd, resp) = self.sendrecv(HIDCmd::Cbor, &buf)?;
debug!(
"got from Device {:?} status={:?}: {:?}",
self.id(),
cmd,
resp
);
if cmd == HIDCmd::Cbor {
Ok(msg.handle_response_ctap2(self, &resp)?)
} else {
Err(HIDError::UnexpectedCmd(cmd.into()))
}
}
fn send_apdu<'msg, Req: RequestCtap1>(
&mut self,
msg: &'msg Req,
) -> Result<Req::Output, HIDError> {
debug!("sending {:?} to {:?}", msg, self);
let data = msg.apdu_format(self)?;
loop {
let (cmd, mut data) = self.sendrecv(HIDCmd::Msg, &data)?;
debug!(
"got from Device {:?} status={:?}: {:?}",
self.id(),
cmd,
data
);
if cmd == HIDCmd::Msg {
if data.len() < 2 {
return Err(io_err("Unexpected Response: shorter than expected").into());
}
let split_at = data.len() - 2;
let status = data.split_off(split_at);
let status = ApduErrorStatus::from([status[0], status[1]]);
match msg.handle_response_ctap1(status, &data) {
Ok(out) => return Ok(out),
Err(Retryable::Retry) => {
thread::sleep(Duration::from_millis(100));
}
Err(Retryable::Error(e)) => return Err(e),
}
} else {
return Err(HIDError::UnexpectedCmd(cmd.into()));
}
}
}
fn init(&mut self, nonce: Nonce) -> Result<(), HIDError> {
let resp = <Self as HIDDevice>::initialize(self, nonce)?;
if self.supports_ctap2() {
let command = GetInfo::default();
let info = self.send_cbor(&command)?;
debug!("{:?} infos: {:?}", self.id(), info);
self.set_authenticator_info(info);
}
if self.supports_ctap1() {
let command = GetVersion::default();
self.send_apdu(&command)?;
}
Ok(resp)
}
fn block_and_blink(&mut self) -> BlinkResult {
let resp;
let supports_select_cmd = self
.get_authenticator_info()
.map_or(false, |i| i.versions.contains(&String::from("FIDO_2_1")));
if supports_select_cmd {
let msg = Selection {};
resp = self.send_cbor(&msg);
} else {
let mut msg = MakeCredentials::new(
CollectedClientData {
webauthn_type: WebauthnType::Create,
challenge: Challenge::new(vec![0, 1, 2, 3, 4]),
origin: String::new(),
cross_origin: false,
token_binding: None,
},
RelyingPartyWrapper::Data(RelyingParty {
id: String::from("make.me.blink"),
..Default::default()
}),
Some(User {
id: vec![0],
name: Some(String::from("make.me.blink")),
..Default::default()
}),
vec![PublicKeyCredentialParameters {
alg: crate::COSEAlgorithm::ES256,
}],
vec![],
MakeCredentialsOptions::default(),
MakeCredentialsExtensions::default(),
None,
);
msg.set_pin_auth(Some(PinAuth::empty_pin_auth()));
info!("Trying to blink: {:?}", &msg);
resp = self.send_msg(&msg).map(|_| ());
}
match resp {
Ok(_)
| Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinInvalid, _)))
| Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthInvalid, _)))
| Err(HIDError::Command(CommandError::StatusCode(StatusCode::PinNotSet, _))) => {
BlinkResult::DeviceSelected
}
Err(HIDError::Command(CommandError::StatusCode(StatusCode::KeepaliveCancel, _)))
| Err(HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, _)))
| Err(HIDError::Command(CommandError::StatusCode(StatusCode::UserActionTimeout, _))) => {
debug!("Device {:?} got cancelled", &self);
BlinkResult::Cancelled
}
e => {
info!("Device {:?} received unexpected answer, so we assume an error occurred and we are NOT using this device (assuming the request was cancelled): {:?}", &self, e);
BlinkResult::Cancelled
}
}
}
fn supports_ctap1(&self) -> bool {
!self.get_device_info().cap_flags.contains(Capability::NMSG)
}
fn supports_ctap2(&self) -> bool {
self.get_device_info().cap_flags.contains(Capability::CBOR)
}
fn establish_shared_secret(&mut self) -> Result<(ECDHSecret, AuthenticatorInfo), HIDError> {
if !self.supports_ctap2() {
return Err(HIDError::UnsupportedCommand);
}
let info = if let Some(authenticator_info) = self.get_authenticator_info().cloned() {
authenticator_info
} else {
let info_command = GetInfo::default();
let info = self.send_cbor(&info_command)?;
debug!("infos: {:?}", info);
self.set_authenticator_info(info.clone());
info
};
let pin_command = GetKeyAgreement::new(&info)?;
let device_key_agreement = self.send_cbor(&pin_command)?;
let shared_secret = device_key_agreement.shared_secret()?;
self.set_shared_secret(shared_secret.clone());
Ok((shared_secret, info))
}
}