Initial commit
This commit is contained in:
596
src/ctap/hid/mod.rs
Normal file
596
src/ctap/hid/mod.rs
Normal file
@@ -0,0 +1,596 @@
|
||||
// Copyright 2019 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
mod receive;
|
||||
mod send;
|
||||
|
||||
use self::receive::MessageAssembler;
|
||||
use self::send::HidPacketIterator;
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
use super::ctap1;
|
||||
use super::status_code::Ctap2StatusCode;
|
||||
use super::timed_permission::TimedPermission;
|
||||
use super::CtapState;
|
||||
use crate::timer::{ClockValue, Duration, Timestamp};
|
||||
use alloc::vec::Vec;
|
||||
#[cfg(feature = "debug_ctap")]
|
||||
use core::fmt::Write;
|
||||
use crypto::rng256::Rng256;
|
||||
#[cfg(feature = "debug_ctap")]
|
||||
use libtock::console::Console;
|
||||
|
||||
// CTAP specification (version 20190130) section 8.1
|
||||
// TODO: Channel allocation, section 8.1.3?
|
||||
// TODO: Transaction timeout, section 8.1.5.2
|
||||
|
||||
pub type HidPacket = [u8; 64];
|
||||
pub type ChannelID = [u8; 4];
|
||||
|
||||
pub enum ProcessedPacket<'a> {
|
||||
InitPacket {
|
||||
cmd: u8,
|
||||
len: usize,
|
||||
data: &'a [u8; 57],
|
||||
},
|
||||
ContinuationPacket {
|
||||
seq: u8,
|
||||
data: &'a [u8; 59],
|
||||
},
|
||||
}
|
||||
|
||||
// An assembled CTAPHID command.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Message {
|
||||
// Channel ID.
|
||||
pub cid: ChannelID,
|
||||
// Command.
|
||||
pub cmd: u8,
|
||||
// Bytes of the message.
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct CtapHid {
|
||||
assembler: MessageAssembler,
|
||||
// The specification (version 20190130) only requires unique CIDs ; the allocation algorithm is
|
||||
// vendor specific.
|
||||
// We allocate them incrementally, that is all `cid` such that 1 <= cid <= allocated_cids are
|
||||
// allocated.
|
||||
// In packets, the ids are then encoded with the native endianness (with the
|
||||
// u32::to/from_ne_bytes methods).
|
||||
allocated_cids: usize,
|
||||
pub wink_permission: TimedPermission,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub enum KeepaliveStatus {
|
||||
Processing,
|
||||
UpNeeded,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
// TODO(kaczmarczyck) disable the warning in the end
|
||||
impl CtapHid {
|
||||
// CTAP specification (version 20190130) section 8.1.3
|
||||
const CHANNEL_RESERVED: ChannelID = [0, 0, 0, 0];
|
||||
const CHANNEL_BROADCAST: ChannelID = [0xFF, 0xFF, 0xFF, 0xFF];
|
||||
const TYPE_INIT_BIT: u8 = 0x80;
|
||||
const PACKET_TYPE_MASK: u8 = 0x80;
|
||||
|
||||
// CTAP specification (version 20190130) section 8.1.9
|
||||
const COMMAND_PING: u8 = 0x01;
|
||||
const COMMAND_MSG: u8 = 0x03;
|
||||
const COMMAND_INIT: u8 = 0x06;
|
||||
const COMMAND_CBOR: u8 = 0x10;
|
||||
pub const COMMAND_CANCEL: u8 = 0x11;
|
||||
const COMMAND_KEEPALIVE: u8 = 0x3B;
|
||||
const COMMAND_ERROR: u8 = 0x3F;
|
||||
// TODO: optional lock command
|
||||
const COMMAND_LOCK: u8 = 0x04;
|
||||
const COMMAND_WINK: u8 = 0x08;
|
||||
const COMMAND_VENDOR_FIRST: u8 = 0x40;
|
||||
const COMMAND_VENDOR_LAST: u8 = 0x7F;
|
||||
|
||||
// CTAP specification (version 20190130) section 8.1.9.1.6
|
||||
const ERR_INVALID_CMD: u8 = 0x01;
|
||||
const ERR_INVALID_PAR: u8 = 0x02;
|
||||
const ERR_INVALID_LEN: u8 = 0x03;
|
||||
const ERR_INVALID_SEQ: u8 = 0x04;
|
||||
const ERR_MSG_TIMEOUT: u8 = 0x05;
|
||||
const ERR_CHANNEL_BUSY: u8 = 0x06;
|
||||
const ERR_LOCK_REQUIRED: u8 = 0x0A;
|
||||
const ERR_INVALID_CHANNEL: u8 = 0x0B;
|
||||
const ERR_OTHER: u8 = 0x7F;
|
||||
|
||||
// CTAP specification (version 20190130) section 8.1.9.1.3
|
||||
const PROTOCOL_VERSION: u8 = 2;
|
||||
|
||||
// The device version number is vendor-defined. For now we define them to be zero.
|
||||
// TODO: Update with device version?
|
||||
const DEVICE_VERSION_MAJOR: u8 = 0;
|
||||
const DEVICE_VERSION_MINOR: u8 = 0;
|
||||
const DEVICE_VERSION_BUILD: u8 = 0;
|
||||
|
||||
const CAPABILITY_WINK: u8 = 0x01;
|
||||
const CAPABILITY_CBOR: u8 = 0x04;
|
||||
const CAPABILITY_NMSG: u8 = 0x08;
|
||||
// Capabilitites currently supported by this device.
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
const CAPABILITIES: u8 = CtapHid::CAPABILITY_WINK | CtapHid::CAPABILITY_CBOR;
|
||||
#[cfg(not(feature = "with_ctap1"))]
|
||||
const CAPABILITIES: u8 =
|
||||
CtapHid::CAPABILITY_WINK | CtapHid::CAPABILITY_CBOR | CtapHid::CAPABILITY_NMSG;
|
||||
|
||||
// TODO: Is this timeout duration specified?
|
||||
const TIMEOUT_DURATION: Duration<isize> = Duration::from_ms(100);
|
||||
const WINK_TIMEOUT_DURATION: Duration<isize> = Duration::from_ms(5000);
|
||||
|
||||
pub fn new() -> CtapHid {
|
||||
CtapHid {
|
||||
assembler: MessageAssembler::new(),
|
||||
allocated_cids: 0,
|
||||
wink_permission: TimedPermission::waiting(),
|
||||
}
|
||||
}
|
||||
|
||||
// Process an incoming USB HID packet, and optionally returns a list of outgoing packets to
|
||||
// send as a reply.
|
||||
pub fn process_hid_packet<R, CheckUserPresence>(
|
||||
&mut self,
|
||||
packet: &HidPacket,
|
||||
clock_value: ClockValue,
|
||||
ctap_state: &mut CtapState<R, CheckUserPresence>,
|
||||
) -> HidPacketIterator
|
||||
where
|
||||
R: Rng256,
|
||||
CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>,
|
||||
{
|
||||
// TODO: Send COMMAND_KEEPALIVE every 100ms?
|
||||
match self
|
||||
.assembler
|
||||
.parse_packet(packet, Timestamp::<isize>::from_clock_value(clock_value))
|
||||
{
|
||||
Ok(Some(message)) => {
|
||||
#[cfg(feature = "debug_ctap")]
|
||||
writeln!(&mut Console::new(), "Received message: {:02x?}", message).unwrap();
|
||||
|
||||
let cid = message.cid;
|
||||
if !self.has_valid_channel(&message) {
|
||||
#[cfg(feature = "debug_ctap")]
|
||||
writeln!(&mut Console::new(), "Invalid channel: {:02x?}", cid).unwrap();
|
||||
return CtapHid::error_message(cid, CtapHid::ERR_INVALID_CHANNEL);
|
||||
}
|
||||
// If another command arrives, stop winking to prevent accidential button touches.
|
||||
self.wink_permission = TimedPermission::waiting();
|
||||
|
||||
match message.cmd {
|
||||
// CTAP specification (version 20190130) section 8.1.9.1.1
|
||||
CtapHid::COMMAND_MSG => {
|
||||
// If we don't have CTAP1 backward compatibilty, this command in invalid.
|
||||
#[cfg(not(feature = "with_ctap1"))]
|
||||
return CtapHid::error_message(cid, CtapHid::ERR_INVALID_CMD);
|
||||
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
match ctap1::Ctap1Command::process_command(
|
||||
&message.payload,
|
||||
ctap_state,
|
||||
clock_value,
|
||||
) {
|
||||
Ok(payload) => CtapHid::ctap1_success_message(cid, &payload),
|
||||
Err(ctap1_status_code) => {
|
||||
CtapHid::ctap1_error_message(cid, ctap1_status_code)
|
||||
}
|
||||
}
|
||||
}
|
||||
// CTAP specification (version 20190130) section 8.1.9.1.2
|
||||
CtapHid::COMMAND_CBOR => {
|
||||
// CTAP specification (version 20190130) section 8.1.5.1
|
||||
// Each transaction is atomic, so we process the command directly here and
|
||||
// don't handle any other packet in the meantime.
|
||||
// TODO: Send keep-alive packets in the meantime.
|
||||
let response = ctap_state.process_command(&message.payload, cid);
|
||||
if let Some(iterator) = CtapHid::split_message(Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_CBOR,
|
||||
payload: response,
|
||||
}) {
|
||||
iterator
|
||||
} else {
|
||||
// Handle the case of a payload > 7609 bytes.
|
||||
// Although this shouldn't happen if the FIDO2 commands are implemented
|
||||
// correctly, we reply with a vendor specific code instead of silently
|
||||
// ignoring the error.
|
||||
//
|
||||
// The error payload that we send instead is 1 <= 7609 bytes, so it is
|
||||
// safe to unwrap() the result.
|
||||
CtapHid::split_message(Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_CBOR,
|
||||
payload: vec![
|
||||
Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_TOO_LONG as u8,
|
||||
],
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
// CTAP specification (version 20190130) section 8.1.9.1.3
|
||||
CtapHid::COMMAND_INIT => {
|
||||
if cid == CtapHid::CHANNEL_BROADCAST {
|
||||
if message.payload.len() != 8 {
|
||||
return CtapHid::error_message(cid, CtapHid::ERR_INVALID_LEN);
|
||||
}
|
||||
|
||||
// TODO: Prevent allocating 2^32 channels.
|
||||
self.allocated_cids += 1;
|
||||
let allocated_cid = (self.allocated_cids as u32).to_ne_bytes();
|
||||
|
||||
let mut payload = vec![0; 17];
|
||||
payload[..8].copy_from_slice(&message.payload);
|
||||
payload[8..12].copy_from_slice(&allocated_cid);
|
||||
payload[12] = CtapHid::PROTOCOL_VERSION;
|
||||
payload[13] = CtapHid::DEVICE_VERSION_MAJOR;
|
||||
payload[14] = CtapHid::DEVICE_VERSION_MINOR;
|
||||
payload[15] = CtapHid::DEVICE_VERSION_BUILD;
|
||||
payload[16] = CtapHid::CAPABILITIES;
|
||||
|
||||
// This unwrap is safe because the payload length is 17 <= 7609 bytes.
|
||||
CtapHid::split_message(Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_INIT,
|
||||
payload,
|
||||
})
|
||||
.unwrap()
|
||||
} else {
|
||||
// Sync the channel and discard the current transaction.
|
||||
// TODO: The specification (version 20190130) wording isn't clear about
|
||||
// the payload format in this case.
|
||||
//
|
||||
// This unwrap is safe because the payload length is 0 <= 7609 bytes.
|
||||
CtapHid::split_message(Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_INIT,
|
||||
payload: vec![],
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
// CTAP specification (version 20190130) section 8.1.9.1.4
|
||||
CtapHid::COMMAND_PING => {
|
||||
// Pong the same message.
|
||||
// This unwrap is safe because if we could parse the incoming message, it's
|
||||
// payload length must be <= 7609 bytes.
|
||||
CtapHid::split_message(message).unwrap()
|
||||
}
|
||||
// CTAP specification (version 20190130) section 8.1.9.1.5
|
||||
CtapHid::COMMAND_CANCEL => {
|
||||
// Authenticators MUST NOT reply to this message.
|
||||
// CANCEL is handled during user presence checks in main.
|
||||
HidPacketIterator::none()
|
||||
}
|
||||
// Optional commands
|
||||
// CTAP specification (version 20190130) section 8.1.9.2.1
|
||||
CtapHid::COMMAND_WINK => {
|
||||
if !message.payload.is_empty() {
|
||||
return CtapHid::error_message(cid, CtapHid::ERR_INVALID_LEN);
|
||||
}
|
||||
self.wink_permission =
|
||||
TimedPermission::granted(clock_value, CtapHid::WINK_TIMEOUT_DURATION);
|
||||
CtapHid::split_message(Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_WINK,
|
||||
payload: vec![],
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
// CTAP specification (version 20190130) section 8.1.9.2.2
|
||||
// TODO: implement LOCK
|
||||
_ => {
|
||||
// Unknown or unsupported command.
|
||||
CtapHid::error_message(cid, CtapHid::ERR_INVALID_CMD)
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
// Waiting for more packets to assemble the message, nothing to send for now.
|
||||
HidPacketIterator::none()
|
||||
}
|
||||
Err((cid, error)) => {
|
||||
if !self.is_allocated_channel(cid) {
|
||||
CtapHid::error_message(cid, CtapHid::ERR_INVALID_CHANNEL)
|
||||
} else {
|
||||
match error {
|
||||
receive::Error::UnexpectedChannel => {
|
||||
CtapHid::error_message(cid, CtapHid::ERR_CHANNEL_BUSY)
|
||||
}
|
||||
receive::Error::UnexpectedInit => {
|
||||
// TODO: Should we send another error code in this case?
|
||||
// Technically, we were expecting a sequence number and got another
|
||||
// byte, although the command/seqnum bit has higher-level semantics
|
||||
// than sequence numbers.
|
||||
CtapHid::error_message(cid, CtapHid::ERR_INVALID_SEQ)
|
||||
}
|
||||
receive::Error::UnexpectedContinuation => {
|
||||
// CTAP specification (version 20190130) section 8.1.5.4
|
||||
// Spurious continuation packets will be ignored.
|
||||
HidPacketIterator::none()
|
||||
}
|
||||
receive::Error::UnexpectedSeq => {
|
||||
CtapHid::error_message(cid, CtapHid::ERR_INVALID_SEQ)
|
||||
}
|
||||
receive::Error::Timeout => {
|
||||
CtapHid::error_message(cid, CtapHid::ERR_MSG_TIMEOUT)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn has_valid_channel(&self, message: &Message) -> bool {
|
||||
match message.cid {
|
||||
// Only INIT commands use the broadcast channel.
|
||||
CtapHid::CHANNEL_BROADCAST => message.cmd == CtapHid::COMMAND_INIT,
|
||||
// Check that the channel is allocated.
|
||||
_ => self.is_allocated_channel(message.cid),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_allocated_channel(&self, cid: ChannelID) -> bool {
|
||||
cid != CtapHid::CHANNEL_RESERVED && u32::from_ne_bytes(cid) as usize <= self.allocated_cids
|
||||
}
|
||||
|
||||
fn error_message(cid: ChannelID, error_code: u8) -> HidPacketIterator {
|
||||
// This unwrap is safe because the payload length is 1 <= 7609 bytes.
|
||||
CtapHid::split_message(Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_ERROR,
|
||||
payload: vec![error_code],
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn process_single_packet(packet: &HidPacket) -> (&ChannelID, ProcessedPacket) {
|
||||
let (cid, rest) = array_refs![packet, 4, 60];
|
||||
if rest[0] & CtapHid::PACKET_TYPE_MASK != 0 {
|
||||
let cmd = rest[0] & !CtapHid::PACKET_TYPE_MASK;
|
||||
let len = (rest[1] as usize) << 8 | (rest[2] as usize);
|
||||
(
|
||||
cid,
|
||||
ProcessedPacket::InitPacket {
|
||||
cmd,
|
||||
len,
|
||||
data: array_ref!(rest, 3, 57),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(
|
||||
cid,
|
||||
ProcessedPacket::ContinuationPacket {
|
||||
seq: rest[0],
|
||||
data: array_ref!(rest, 1, 59),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn split_message(message: Message) -> Option<HidPacketIterator> {
|
||||
#[cfg(feature = "debug_ctap")]
|
||||
writeln!(&mut Console::new(), "Sending message: {:02x?}", message).unwrap();
|
||||
HidPacketIterator::new(message)
|
||||
}
|
||||
|
||||
pub fn keepalive(cid: ChannelID, status: KeepaliveStatus) -> HidPacketIterator {
|
||||
let status_code = match status {
|
||||
KeepaliveStatus::Processing => 1,
|
||||
KeepaliveStatus::UpNeeded => 2,
|
||||
};
|
||||
// This unwrap is safe because the payload length is 1 <= 7609 bytes.
|
||||
CtapHid::split_message(Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_KEEPALIVE,
|
||||
payload: vec![status_code],
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
fn ctap1_error_message(
|
||||
cid: ChannelID,
|
||||
error_code: ctap1::Ctap1StatusCode,
|
||||
) -> HidPacketIterator {
|
||||
// This unwrap is safe because the payload length is 2 <= 7609 bytes
|
||||
let code: u16 = error_code.into();
|
||||
CtapHid::split_message(Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_MSG,
|
||||
payload: code.to_be_bytes().to_vec(),
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ctap1")]
|
||||
fn ctap1_success_message(cid: ChannelID, payload: &[u8]) -> HidPacketIterator {
|
||||
let mut response = payload.to_vec();
|
||||
let code: u16 = ctap1::Ctap1StatusCode::SW_NO_ERROR.into();
|
||||
response.extend_from_slice(&code.to_be_bytes());
|
||||
CtapHid::split_message(Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_MSG,
|
||||
payload: response,
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crypto::rng256::ThreadRng256;
|
||||
|
||||
const CLOCK_FREQUENCY_HZ: usize = 32768;
|
||||
// Except for tests for timeouts (done in ctap1.rs), transactions are time independant.
|
||||
const DUMMY_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ);
|
||||
const DUMMY_TIMESTAMP: Timestamp<isize> = Timestamp::from_ms(0);
|
||||
|
||||
fn process_messages<CheckUserPresence>(
|
||||
ctap_hid: &mut CtapHid,
|
||||
ctap_state: &mut CtapState<ThreadRng256, CheckUserPresence>,
|
||||
request: Vec<Message>,
|
||||
) -> Option<Vec<Message>>
|
||||
where
|
||||
CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>,
|
||||
{
|
||||
let mut result = Vec::new();
|
||||
let mut assembler_reply = MessageAssembler::new();
|
||||
for msg_request in request {
|
||||
for pkt_request in HidPacketIterator::new(msg_request).unwrap() {
|
||||
for pkt_reply in
|
||||
ctap_hid.process_hid_packet(&pkt_request, DUMMY_CLOCK_VALUE, ctap_state)
|
||||
{
|
||||
match assembler_reply.parse_packet(&pkt_reply, DUMMY_TIMESTAMP) {
|
||||
Ok(Some(message)) => result.push(message),
|
||||
Ok(None) => (),
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
|
||||
fn cid_from_init<CheckUserPresence>(
|
||||
ctap_hid: &mut CtapHid,
|
||||
ctap_state: &mut CtapState<ThreadRng256, CheckUserPresence>,
|
||||
) -> ChannelID
|
||||
where
|
||||
CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>,
|
||||
{
|
||||
let nonce = vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0];
|
||||
let reply = process_messages(
|
||||
ctap_hid,
|
||||
ctap_state,
|
||||
vec![Message {
|
||||
cid: CtapHid::CHANNEL_BROADCAST,
|
||||
cmd: CtapHid::COMMAND_INIT,
|
||||
payload: nonce.clone(),
|
||||
}],
|
||||
);
|
||||
|
||||
let mut cid_in_payload: ChannelID = Default::default();
|
||||
if let Some(messages) = reply {
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert!(messages[0].payload.len() >= 12);
|
||||
assert_eq!(nonce, &messages[0].payload[..8]);
|
||||
cid_in_payload.copy_from_slice(&messages[0].payload[8..12]);
|
||||
} else {
|
||||
panic!("The init process was not successful to generate a valid channel ID.")
|
||||
}
|
||||
cid_in_payload
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_assemble() {
|
||||
for payload_len in 0..7609 {
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x00,
|
||||
payload: vec![0xFF; payload_len],
|
||||
};
|
||||
|
||||
let mut messages = Vec::new();
|
||||
let mut assembler = MessageAssembler::new();
|
||||
for packet in HidPacketIterator::new(message.clone()).unwrap() {
|
||||
match assembler.parse_packet(&packet, DUMMY_TIMESTAMP) {
|
||||
Ok(Some(msg)) => messages.push(msg),
|
||||
Ok(None) => (),
|
||||
Err(_) => panic!("Couldn't assemble packet: {:02x?}", &packet as &[u8]),
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(messages, vec![message]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_init() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let user_immediately_present = |_| Ok(());
|
||||
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
|
||||
let mut ctap_hid = CtapHid::new();
|
||||
|
||||
let reply = process_messages(
|
||||
&mut ctap_hid,
|
||||
&mut ctap_state,
|
||||
vec![Message {
|
||||
cid: CtapHid::CHANNEL_BROADCAST,
|
||||
cmd: CtapHid::COMMAND_INIT,
|
||||
payload: vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0],
|
||||
}],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
reply,
|
||||
Some(vec![Message {
|
||||
cid: CtapHid::CHANNEL_BROADCAST,
|
||||
cmd: CtapHid::COMMAND_INIT,
|
||||
payload: vec![
|
||||
0x12, // Nonce
|
||||
0x34,
|
||||
0x56,
|
||||
0x78,
|
||||
0x9A,
|
||||
0xBC,
|
||||
0xDE,
|
||||
0xF0,
|
||||
0x01, // Allocated CID
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x02, // Protocol version
|
||||
0x00, // Device version
|
||||
0x00,
|
||||
0x00,
|
||||
CtapHid::CAPABILITIES
|
||||
]
|
||||
}])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_ping() {
|
||||
let mut rng = ThreadRng256 {};
|
||||
let user_immediately_present = |_| Ok(());
|
||||
let mut ctap_state = CtapState::new(&mut rng, user_immediately_present);
|
||||
let mut ctap_hid = CtapHid::new();
|
||||
let cid = cid_from_init(&mut ctap_hid, &mut ctap_state);
|
||||
|
||||
let reply = process_messages(
|
||||
&mut ctap_hid,
|
||||
&mut ctap_state,
|
||||
vec![Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_PING,
|
||||
payload: vec![0x99, 0x99],
|
||||
}],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
reply,
|
||||
Some(vec![Message {
|
||||
cid,
|
||||
cmd: CtapHid::COMMAND_PING,
|
||||
payload: vec![0x99, 0x99]
|
||||
}])
|
||||
);
|
||||
}
|
||||
}
|
||||
590
src/ctap/hid/receive.rs
Normal file
590
src/ctap/hid/receive.rs
Normal file
@@ -0,0 +1,590 @@
|
||||
// Copyright 2019 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::{ChannelID, CtapHid, HidPacket, Message, ProcessedPacket};
|
||||
use crate::timer::Timestamp;
|
||||
use alloc::vec::Vec;
|
||||
use core::mem::swap;
|
||||
|
||||
// A structure to assemble CTAPHID commands from a series of incoming USB HID packets.
|
||||
pub struct MessageAssembler {
|
||||
// Whether this is waiting to receive an initialization packet.
|
||||
idle: bool,
|
||||
// Current channel ID.
|
||||
cid: ChannelID,
|
||||
// Timestamp of the last packet received on the current channel.
|
||||
last_timestamp: Timestamp<isize>,
|
||||
// Current command.
|
||||
cmd: u8,
|
||||
// Sequence number expected for the next packet.
|
||||
seq: u8,
|
||||
// Number of bytes left to fill the current message.
|
||||
remaining_payload_len: usize,
|
||||
// Buffer for the current payload.
|
||||
payload: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum Error {
|
||||
// Expected a continuation packet on a specific channel, got a packet on another channel.
|
||||
UnexpectedChannel,
|
||||
// Expected a continuation packet, got an init packet.
|
||||
UnexpectedInit,
|
||||
// Expected an init packet, got a continuation packet.
|
||||
UnexpectedContinuation,
|
||||
// Expected a continuation packet with a specific sequence number, got another sequence number.
|
||||
UnexpectedSeq,
|
||||
// This packet arrived after a timeout.
|
||||
Timeout,
|
||||
}
|
||||
|
||||
impl MessageAssembler {
|
||||
pub fn new() -> MessageAssembler {
|
||||
MessageAssembler {
|
||||
idle: true,
|
||||
cid: [0, 0, 0, 0],
|
||||
last_timestamp: Timestamp::from_ms(0),
|
||||
cmd: 0,
|
||||
seq: 0,
|
||||
remaining_payload_len: 0,
|
||||
payload: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// Resets the message assembler to the idle state.
|
||||
// The caller can reset the assembler for example due to a timeout.
|
||||
pub fn reset(&mut self) {
|
||||
self.idle = true;
|
||||
self.cid = [0, 0, 0, 0];
|
||||
self.last_timestamp = Timestamp::from_ms(0);
|
||||
self.cmd = 0;
|
||||
self.seq = 0;
|
||||
self.remaining_payload_len = 0;
|
||||
self.payload.clear();
|
||||
}
|
||||
|
||||
// Returns:
|
||||
// - An Ok() result if the packet was parsed correctly. This contains either Some(Vec<u8>) if a
|
||||
// full message was assembled after this packet, or None if more packets are needed to fill the
|
||||
// message.
|
||||
// - An Err() result if there was a parsing error.
|
||||
// TODO: Implement timeouts. For example, have the caller pass us a timestamp of when this
|
||||
// packet was received.
|
||||
pub fn parse_packet(
|
||||
&mut self,
|
||||
packet: &HidPacket,
|
||||
timestamp: Timestamp<isize>,
|
||||
) -> Result<Option<Message>, (ChannelID, Error)> {
|
||||
// TODO: Support non-full-speed devices (i.e. packet len != 64)? This isn't recommended by
|
||||
// section 8.8.1
|
||||
let (cid, processed_packet) = CtapHid::process_single_packet(&packet);
|
||||
|
||||
if !self.idle && timestamp - self.last_timestamp >= CtapHid::TIMEOUT_DURATION {
|
||||
// The current channel timed out.
|
||||
// Save the channel ID and reset the state.
|
||||
let current_cid = self.cid;
|
||||
self.reset();
|
||||
|
||||
// If the packet is from the timed-out channel, send back a timeout error.
|
||||
// Otherwise, proceed with processing the packet.
|
||||
if *cid == current_cid {
|
||||
return Err((*cid, Error::Timeout));
|
||||
}
|
||||
}
|
||||
|
||||
if self.idle {
|
||||
// Expecting an initialization packet.
|
||||
match processed_packet {
|
||||
ProcessedPacket::InitPacket { cmd, len, data } => {
|
||||
Ok(self.accept_init_packet(*cid, cmd, len, data, timestamp))
|
||||
}
|
||||
ProcessedPacket::ContinuationPacket { .. } => {
|
||||
// CTAP specification (version 20190130) section 8.1.5.4
|
||||
// Spurious continuation packets will be ignored.
|
||||
Err((*cid, Error::UnexpectedContinuation))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Expecting a continuation packet from the current channel.
|
||||
|
||||
// CTAP specification (version 20190130) section 8.1.5.1
|
||||
// Reject packets from other channels.
|
||||
if *cid != self.cid {
|
||||
return Err((*cid, Error::UnexpectedChannel));
|
||||
}
|
||||
|
||||
match processed_packet {
|
||||
// Unexpected initialization packet.
|
||||
ProcessedPacket::InitPacket { cmd, len, data } => {
|
||||
self.reset();
|
||||
if cmd == CtapHid::COMMAND_INIT {
|
||||
Ok(self.accept_init_packet(*cid, cmd, len, data, timestamp))
|
||||
} else {
|
||||
Err((*cid, Error::UnexpectedInit))
|
||||
}
|
||||
}
|
||||
ProcessedPacket::ContinuationPacket { seq, data } => {
|
||||
if seq != self.seq {
|
||||
// Reject packets with the wrong sequence number.
|
||||
self.reset();
|
||||
Err((*cid, Error::UnexpectedSeq))
|
||||
} else {
|
||||
// Update the last timestamp.
|
||||
self.last_timestamp = timestamp;
|
||||
// Increment the sequence number for the next packet.
|
||||
self.seq += 1;
|
||||
Ok(self.append_payload(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn accept_init_packet(
|
||||
&mut self,
|
||||
cid: ChannelID,
|
||||
cmd: u8,
|
||||
len: usize,
|
||||
data: &[u8],
|
||||
timestamp: Timestamp<isize>,
|
||||
) -> Option<Message> {
|
||||
// TODO: Should invalid commands/payload lengths be rejected early, i.e. as soon as the
|
||||
// initialization packet is received, or should we build a message and then catch the
|
||||
// error?
|
||||
// The specification (version 20190130) isn't clear on this point.
|
||||
self.cid = cid;
|
||||
self.last_timestamp = timestamp;
|
||||
self.cmd = cmd;
|
||||
self.seq = 0;
|
||||
self.remaining_payload_len = len;
|
||||
self.append_payload(data)
|
||||
}
|
||||
|
||||
fn append_payload(&mut self, data: &[u8]) -> Option<Message> {
|
||||
if data.len() < self.remaining_payload_len {
|
||||
self.payload.extend_from_slice(data);
|
||||
self.idle = false;
|
||||
self.remaining_payload_len -= data.len();
|
||||
None
|
||||
} else {
|
||||
self.payload
|
||||
.extend_from_slice(&data[..self.remaining_payload_len]);
|
||||
self.idle = true;
|
||||
let mut payload = Vec::new();
|
||||
swap(&mut self.payload, &mut payload);
|
||||
Some(Message {
|
||||
cid: self.cid,
|
||||
cmd: self.cmd,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::timer::Duration;
|
||||
|
||||
// Except for tests that exercise timeouts, all packets are synchronized at the same dummy
|
||||
// timestamp.
|
||||
const DUMMY_TIMESTAMP: Timestamp<isize> = Timestamp::from_ms(0);
|
||||
|
||||
fn byte_extend(bytes: &[u8], padding: u8) -> HidPacket {
|
||||
let len = bytes.len();
|
||||
assert!(len <= 64);
|
||||
let mut result = [0; 64];
|
||||
result[..len].copy_from_slice(bytes);
|
||||
for byte in result[len..].iter_mut() {
|
||||
*byte = padding;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn zero_extend(bytes: &[u8]) -> HidPacket {
|
||||
byte_extend(bytes, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_payload() {
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x80]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x00,
|
||||
payload: vec![]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_one_packet() {
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x80, 0x00, 0x10]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x00,
|
||||
payload: vec![0x00; 0x10]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nonzero_padding() {
|
||||
// CTAP specification (version 20190130) section 8.1.4
|
||||
// It is written that "Unused bytes SHOULD be set to zero", so we test that non-zero
|
||||
// padding is accepted as well.
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x80, 0x00, 0x10], 0xFF),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x00,
|
||||
payload: vec![0xFF; 0x10]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_two_packets() {
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x00]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x01,
|
||||
payload: vec![0x00; 0x40]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_three_packets() {
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x80]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x00]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x01]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x01,
|
||||
payload: vec![0x00; 0x80]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_packets() {
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x1D, 0xB9]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
for seq in 0..0x7F {
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, seq]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x7F]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x01,
|
||||
payload: vec![0x00; 0x1DB9]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_messages() {
|
||||
// Check that after yielding a message, the assembler is ready to process new messages.
|
||||
let mut assembler = MessageAssembler::new();
|
||||
for i in 0..10 {
|
||||
// Introduce some variability in the messages.
|
||||
let cmd = 2 * i;
|
||||
let byte = 3 * i;
|
||||
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x80 | cmd, 0x00, 0x80], byte),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x00], byte),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x01], byte),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd,
|
||||
payload: vec![byte; 0x80]
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_channel_switch() {
|
||||
// Check that the assembler can process messages from multiple channels, sequentially.
|
||||
let mut assembler = MessageAssembler::new();
|
||||
for i in 0..10 {
|
||||
// Introduce some variability in the messages.
|
||||
let cid = 0x78 + i;
|
||||
let cmd = 2 * i;
|
||||
let byte = 3 * i;
|
||||
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&byte_extend(&[0x12, 0x34, 0x56, cid, 0x80 | cmd, 0x00, 0x80], byte),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&byte_extend(&[0x12, 0x34, 0x56, cid, 0x00], byte),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&byte_extend(&[0x12, 0x34, 0x56, cid, 0x01], byte),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, cid],
|
||||
cmd,
|
||||
payload: vec![byte; 0x80]
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unexpected_channel() {
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
|
||||
// Check that many sorts of packets on another channel are ignored.
|
||||
for cmd in 0..=0xFF {
|
||||
for byte in 0..=0xFF {
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&byte_extend(&[0x12, 0x34, 0x56, 0x9A, cmd, 0x00], byte),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Err(([0x12, 0x34, 0x56, 0x9A], Error::UnexpectedChannel))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x00]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x01,
|
||||
payload: vec![0x00; 0x40]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spurious_continuation_packets() {
|
||||
// CTAP specification (version 20190130) section 8.1.5.4
|
||||
// Spurious continuation packets appearing without a prior initialization packet will be
|
||||
// ignored.
|
||||
let mut assembler = MessageAssembler::new();
|
||||
for i in 0..0x80 {
|
||||
// Some legit packet.
|
||||
let byte = 2 * i;
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x80, 0x00, 0x10], byte),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x00,
|
||||
payload: vec![byte; 0x10]
|
||||
}))
|
||||
);
|
||||
|
||||
// Spurious continuation packet.
|
||||
let seq = i;
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, seq]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Err(([0x12, 0x34, 0x56, 0x78], Error::UnexpectedContinuation))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unexpected_init() {
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x80]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Err(([0x12, 0x34, 0x56, 0x78], Error::UnexpectedInit))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unexpected_seq() {
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x01]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Err(([0x12, 0x34, 0x56, 0x78], Error::UnexpectedSeq))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timed_out_packet() {
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x40]),
|
||||
DUMMY_TIMESTAMP
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x00]),
|
||||
DUMMY_TIMESTAMP + CtapHid::TIMEOUT_DURATION
|
||||
),
|
||||
Err(([0x12, 0x34, 0x56, 0x78], Error::Timeout))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_just_in_time_packets() {
|
||||
let mut timestamp = DUMMY_TIMESTAMP;
|
||||
// Delay between each packet is just below the threshold.
|
||||
let delay = CtapHid::TIMEOUT_DURATION - Duration::from_ms(1);
|
||||
|
||||
let mut assembler = MessageAssembler::new();
|
||||
assert_eq!(
|
||||
assembler.parse_packet(
|
||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x1D, 0xB9]),
|
||||
timestamp
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
for seq in 0..0x7F {
|
||||
timestamp += delay;
|
||||
assert_eq!(
|
||||
assembler.parse_packet(&zero_extend(&[0x12, 0x34, 0x56, 0x78, seq]), timestamp),
|
||||
Ok(None)
|
||||
);
|
||||
}
|
||||
timestamp += delay;
|
||||
assert_eq!(
|
||||
assembler.parse_packet(&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x7F]), timestamp),
|
||||
Ok(Some(Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x01,
|
||||
payload: vec![0x00; 0x1DB9]
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: more tests
|
||||
}
|
||||
296
src/ctap/hid/send.rs
Normal file
296
src/ctap/hid/send.rs
Normal file
@@ -0,0 +1,296 @@
|
||||
// Copyright 2019 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::{CtapHid, HidPacket, Message};
|
||||
|
||||
pub struct HidPacketIterator(Option<MessageSplitter>);
|
||||
|
||||
impl HidPacketIterator {
|
||||
pub fn new(message: Message) -> Option<HidPacketIterator> {
|
||||
let splitter = MessageSplitter::new(message);
|
||||
if splitter.is_some() {
|
||||
Some(HidPacketIterator(splitter))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn none() -> HidPacketIterator {
|
||||
HidPacketIterator(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for HidPacketIterator {
|
||||
type Item = HidPacket;
|
||||
|
||||
fn next(&mut self) -> Option<HidPacket> {
|
||||
match &mut self.0 {
|
||||
Some(splitter) => splitter.next(),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MessageSplitter {
|
||||
message: Message,
|
||||
packet: HidPacket,
|
||||
seq: Option<u8>,
|
||||
i: usize,
|
||||
}
|
||||
|
||||
impl MessageSplitter {
|
||||
// Try to split this message into an iterator of HID packets. This fails if the message is too
|
||||
// long to fit into a sequence of HID packets (which is limited to 7609 bytes).
|
||||
pub fn new(message: Message) -> Option<MessageSplitter> {
|
||||
if message.payload.len() > 7609 {
|
||||
None
|
||||
} else {
|
||||
// Cache the CID, as it is constant for all packets in this message.
|
||||
let mut packet = [0; 64];
|
||||
packet[..4].copy_from_slice(&message.cid);
|
||||
|
||||
Some(MessageSplitter {
|
||||
message,
|
||||
packet,
|
||||
seq: None,
|
||||
i: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Copy as many bytes as possible from data to dst, and return how many bytes are copied.
|
||||
// Contrary to copy_from_slice, this doesn't require slices of the same length.
|
||||
// All unused bytes in dst are set to zero, as if the data was padded with zeros to match.
|
||||
fn consume_data(dst: &mut [u8], data: &[u8]) -> usize {
|
||||
let dst_len = dst.len();
|
||||
let data_len = data.len();
|
||||
|
||||
if data_len <= dst_len {
|
||||
// data fits in dst, copy all the bytes.
|
||||
dst[..data_len].copy_from_slice(data);
|
||||
for byte in dst[data_len..].iter_mut() {
|
||||
*byte = 0;
|
||||
}
|
||||
data_len
|
||||
} else {
|
||||
// Fill all of dst.
|
||||
dst.copy_from_slice(&data[..dst_len]);
|
||||
dst_len
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for MessageSplitter {
|
||||
type Item = HidPacket;
|
||||
|
||||
fn next(&mut self) -> Option<HidPacket> {
|
||||
let payload_len = self.message.payload.len();
|
||||
match self.seq {
|
||||
None => {
|
||||
// First, send an initialization packet.
|
||||
self.packet[4] = self.message.cmd | CtapHid::TYPE_INIT_BIT;
|
||||
self.packet[5] = (payload_len >> 8) as u8;
|
||||
self.packet[6] = payload_len as u8;
|
||||
|
||||
self.seq = Some(0);
|
||||
self.i =
|
||||
MessageSplitter::consume_data(&mut self.packet[7..], &self.message.payload);
|
||||
Some(self.packet)
|
||||
}
|
||||
Some(seq) => {
|
||||
// Send the next continuation packet, if any.
|
||||
if self.i < payload_len {
|
||||
self.packet[4] = seq;
|
||||
self.seq = Some(seq + 1);
|
||||
self.i += MessageSplitter::consume_data(
|
||||
&mut self.packet[5..],
|
||||
&self.message.payload[self.i..],
|
||||
);
|
||||
Some(self.packet)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
fn assert_packet_output_equality(message: Message, expected_packets: Vec<HidPacket>) {
|
||||
let packets: Vec<HidPacket> = HidPacketIterator::new(message).unwrap().collect();
|
||||
assert_eq!(packets.len(), expected_packets.len());
|
||||
for (packet, expected_packet) in packets.iter().zip(expected_packets.iter()) {
|
||||
assert_eq!(packet as &[u8], expected_packet as &[u8]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_packet_iterator_single_packet() {
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x4C,
|
||||
payload: vec![0xAA, 0xBB],
|
||||
};
|
||||
let expected_packets: Vec<HidPacket> = vec![[
|
||||
0x12, 0x34, 0x56, 0x78, 0xCC, 0x00, 0x02, 0xAA, 0xBB, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
]];
|
||||
assert_packet_output_equality(message, expected_packets);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_packet_iterator_big_single_packet() {
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x4C,
|
||||
payload: vec![0xAA; 64 - 7],
|
||||
};
|
||||
let expected_packets: Vec<HidPacket> = vec![[
|
||||
0x12, 0x34, 0x56, 0x78, 0xCC, 0x00, 0x39, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
]];
|
||||
assert_packet_output_equality(message, expected_packets);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_packet_iterator_two_packets() {
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x4C,
|
||||
payload: vec![0xAA; 64 - 7 + 1],
|
||||
};
|
||||
let expected_packets: Vec<HidPacket> = vec![
|
||||
[
|
||||
0x12, 0x34, 0x56, 0x78, 0xCC, 0x00, 0x3A, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
],
|
||||
[
|
||||
0x12, 0x34, 0x56, 0x78, 0x00, 0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
],
|
||||
];
|
||||
assert_packet_output_equality(message, expected_packets);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_packet_iterator_two_full_packets() {
|
||||
let mut payload = vec![0xAA; 64 - 7];
|
||||
payload.extend(vec![0xBB; 64 - 5]);
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0x4C,
|
||||
payload,
|
||||
};
|
||||
let expected_packets: Vec<HidPacket> = vec![
|
||||
[
|
||||
0x12, 0x34, 0x56, 0x78, 0xCC, 0x00, 0x74, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
||||
],
|
||||
[
|
||||
0x12, 0x34, 0x56, 0x78, 0x00, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
|
||||
0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
|
||||
0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
|
||||
0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
|
||||
0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB,
|
||||
],
|
||||
];
|
||||
assert_packet_output_equality(message, expected_packets);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_packet_iterator_max_packets() {
|
||||
let mut payload = vec![0xFF; 64 - 7];
|
||||
for i in 0..128 {
|
||||
payload.extend(vec![i + 1; 64 - 5]);
|
||||
}
|
||||
|
||||
// Sanity check for the length of the payload.
|
||||
assert_eq!((64 - 7) + 128 * (64 - 5), 0x1db9);
|
||||
assert_eq!(7609, 0x1db9);
|
||||
assert_eq!(payload.len(), 0x1db9);
|
||||
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0xAB,
|
||||
payload,
|
||||
};
|
||||
|
||||
let mut expected_packets = Vec::new();
|
||||
expected_packets.push([
|
||||
0x12, 0x34, 0x56, 0x78, 0xAB, 0x1D, 0xB9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
]);
|
||||
for i in 0..128 {
|
||||
let mut packet: HidPacket = [0; 64];
|
||||
packet[0] = 0x12;
|
||||
packet[1] = 0x34;
|
||||
packet[2] = 0x56;
|
||||
packet[3] = 0x78;
|
||||
packet[4] = i;
|
||||
for byte in packet.iter_mut().skip(5) {
|
||||
*byte = i + 1;
|
||||
}
|
||||
expected_packets.push(packet);
|
||||
}
|
||||
|
||||
assert_packet_output_equality(message, expected_packets);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_packet_iterator_payload_one_too_large() {
|
||||
let payload = vec![0xFF; (64 - 7) + 128 * (64 - 5) + 1];
|
||||
assert_eq!(payload.len(), 0x1dba);
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0xAB,
|
||||
payload,
|
||||
};
|
||||
assert!(HidPacketIterator::new(message).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hid_packet_iterator_payload_way_too_large() {
|
||||
// Check that overflow of u16 doesn't bypass the size limit.
|
||||
let payload = vec![0xFF; 0x10000];
|
||||
let message = Message {
|
||||
cid: [0x12, 0x34, 0x56, 0x78],
|
||||
cmd: 0xAB,
|
||||
payload,
|
||||
};
|
||||
assert!(HidPacketIterator::new(message).is_none());
|
||||
}
|
||||
|
||||
// TODO(kaczmarczyck) implement and test limits (maximum bytes and packets)
|
||||
}
|
||||
Reference in New Issue
Block a user