new process_message function and command enum
This commit is contained in:
committed by
kaczmarczyck
parent
3d3689dc23
commit
c595980a3b
@@ -25,14 +25,12 @@ use ctap2::ctap::command::{
|
|||||||
};
|
};
|
||||||
use ctap2::ctap::hid::receive::MessageAssembler;
|
use ctap2::ctap::hid::receive::MessageAssembler;
|
||||||
use ctap2::ctap::hid::send::HidPacketIterator;
|
use ctap2::ctap::hid::send::HidPacketIterator;
|
||||||
use ctap2::ctap::hid::{ChannelID, HidPacket, Message};
|
use ctap2::ctap::hid::{ChannelID, CtapHidCommand, HidPacket, Message};
|
||||||
use ctap2::env::test::TestEnv;
|
use ctap2::env::test::TestEnv;
|
||||||
use ctap2::Ctap;
|
use ctap2::Ctap;
|
||||||
use libtock_drivers::timer::{ClockValue, Timestamp};
|
use libtock_drivers::timer::{ClockValue, Timestamp};
|
||||||
|
|
||||||
const COMMAND_INIT: u8 = 0x06;
|
|
||||||
const CHANNEL_BROADCAST: ChannelID = [0xFF, 0xFF, 0xFF, 0xFF];
|
const CHANNEL_BROADCAST: ChannelID = [0xFF, 0xFF, 0xFF, 0xFF];
|
||||||
const PACKET_TYPE_MASK: u8 = 0x80;
|
|
||||||
|
|
||||||
const CLOCK_FREQUENCY_HZ: usize = 32768;
|
const CLOCK_FREQUENCY_HZ: usize = 32768;
|
||||||
const DUMMY_TIMESTAMP: Timestamp<isize> = Timestamp::from_ms(0);
|
const DUMMY_TIMESTAMP: Timestamp<isize> = Timestamp::from_ms(0);
|
||||||
@@ -53,13 +51,14 @@ fn raw_to_message(data: &[u8]) -> Message {
|
|||||||
cid[..data.len()].copy_from_slice(data);
|
cid[..data.len()].copy_from_slice(data);
|
||||||
Message {
|
Message {
|
||||||
cid,
|
cid,
|
||||||
cmd: 0,
|
// Arbitrary command.
|
||||||
|
cmd: CtapHidCommand::Cbor,
|
||||||
payload: vec![],
|
payload: vec![],
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Message {
|
Message {
|
||||||
cid: array_ref!(data, 0, 4).clone(),
|
cid: array_ref!(data, 0, 4).clone(),
|
||||||
cmd: data[4],
|
cmd: CtapHidCommand::from(data[4]),
|
||||||
payload: data[5..].to_vec(),
|
payload: data[5..].to_vec(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +70,7 @@ fn initialize(ctap: &mut Ctap<TestEnv>) -> ChannelID {
|
|||||||
let nonce = vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0];
|
let nonce = vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0];
|
||||||
let message = Message {
|
let message = Message {
|
||||||
cid: CHANNEL_BROADCAST,
|
cid: CHANNEL_BROADCAST,
|
||||||
cmd: COMMAND_INIT,
|
cmd: CtapHidCommand::Init,
|
||||||
payload: nonce,
|
payload: nonce,
|
||||||
};
|
};
|
||||||
let mut assembler_reply = MessageAssembler::new();
|
let mut assembler_reply = MessageAssembler::new();
|
||||||
@@ -168,7 +167,7 @@ pub fn process_ctap_specific_type(data: &[u8], input_type: InputType) {
|
|||||||
|
|
||||||
// Splits the given data as HID packets and reassembles it, verifying that the original input message is reconstructed.
|
// Splits the given data as HID packets and reassembles it, verifying that the original input message is reconstructed.
|
||||||
pub fn split_assemble_hid_packets(data: &[u8]) {
|
pub fn split_assemble_hid_packets(data: &[u8]) {
|
||||||
let mut message = raw_to_message(data);
|
let message = raw_to_message(data);
|
||||||
if let Some(hid_packet_iterator) = HidPacketIterator::new(message.clone()) {
|
if let Some(hid_packet_iterator) = HidPacketIterator::new(message.clone()) {
|
||||||
let mut assembler = MessageAssembler::new();
|
let mut assembler = MessageAssembler::new();
|
||||||
let packets: Vec<HidPacket> = hid_packet_iterator.collect();
|
let packets: Vec<HidPacket> = hid_packet_iterator.collect();
|
||||||
@@ -176,7 +175,6 @@ pub fn split_assemble_hid_packets(data: &[u8]) {
|
|||||||
for packet in first_packets {
|
for packet in first_packets {
|
||||||
assert_eq!(assembler.parse_packet(packet, DUMMY_TIMESTAMP), Ok(None));
|
assert_eq!(assembler.parse_packet(packet, DUMMY_TIMESTAMP), Ok(None));
|
||||||
}
|
}
|
||||||
message.cmd &= !PACKET_TYPE_MASK;
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
assembler.parse_packet(last_packet, DUMMY_TIMESTAMP),
|
assembler.parse_packet(last_packet, DUMMY_TIMESTAMP),
|
||||||
Ok(Some(message))
|
Ok(Some(message))
|
||||||
|
|||||||
@@ -32,13 +32,50 @@ use core::fmt::Write;
|
|||||||
use libtock_drivers::console::Console;
|
use libtock_drivers::console::Console;
|
||||||
use libtock_drivers::timer::{ClockValue, Duration, Timestamp};
|
use libtock_drivers::timer::{ClockValue, Duration, Timestamp};
|
||||||
|
|
||||||
// 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 HidPacket = [u8; 64];
|
||||||
pub type ChannelID = [u8; 4];
|
pub type ChannelID = [u8; 4];
|
||||||
|
|
||||||
|
/// CTAPHID commands
|
||||||
|
///
|
||||||
|
/// See section 11.2.9. of FIDO 2.1 (2021-06-15).
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum CtapHidCommand {
|
||||||
|
Ping = 0x01,
|
||||||
|
Msg = 0x03,
|
||||||
|
// Lock is optional and may be used in the future.
|
||||||
|
Lock = 0x04,
|
||||||
|
Init = 0x06,
|
||||||
|
Wink = 0x08,
|
||||||
|
Cbor = 0x10,
|
||||||
|
Cancel = 0x11,
|
||||||
|
Keepalive = 0x3B,
|
||||||
|
Error = 0x3F,
|
||||||
|
// VendorFirst and VendorLast describe a range, and are not commands themselves.
|
||||||
|
_VendorFirst = 0x40,
|
||||||
|
_VendorLast = 0x7F,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u8> for CtapHidCommand {
|
||||||
|
fn from(cmd: u8) -> Self {
|
||||||
|
match cmd {
|
||||||
|
x if x == CtapHidCommand::Ping as u8 => CtapHidCommand::Ping,
|
||||||
|
x if x == CtapHidCommand::Msg as u8 => CtapHidCommand::Msg,
|
||||||
|
x if x == CtapHidCommand::Lock as u8 => CtapHidCommand::Lock,
|
||||||
|
x if x == CtapHidCommand::Init as u8 => CtapHidCommand::Init,
|
||||||
|
x if x == CtapHidCommand::Wink as u8 => CtapHidCommand::Wink,
|
||||||
|
x if x == CtapHidCommand::Cbor as u8 => CtapHidCommand::Cbor,
|
||||||
|
x if x == CtapHidCommand::Cancel as u8 => CtapHidCommand::Cancel,
|
||||||
|
x if x == CtapHidCommand::Keepalive as u8 => CtapHidCommand::Keepalive,
|
||||||
|
// This includes the actual error code 0x3F. Error is not used for incoming packets in
|
||||||
|
// the specification, so we can safely reuse it for unknown bytes.
|
||||||
|
_ => CtapHidCommand::Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes the structure of a parsed HID packet.
|
||||||
|
///
|
||||||
|
/// A packet is either an Init or a Continuation packet.
|
||||||
pub enum ProcessedPacket<'a> {
|
pub enum ProcessedPacket<'a> {
|
||||||
InitPacket {
|
InitPacket {
|
||||||
cmd: u8,
|
cmd: u8,
|
||||||
@@ -51,72 +88,64 @@ pub enum ProcessedPacket<'a> {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// An assembled CTAPHID command.
|
/// An assembled CTAPHID command.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
// Channel ID.
|
// Channel ID.
|
||||||
pub cid: ChannelID,
|
pub cid: ChannelID,
|
||||||
// Command.
|
// Command.
|
||||||
pub cmd: u8,
|
pub cmd: CtapHidCommand,
|
||||||
// Bytes of the message.
|
// Bytes of the message.
|
||||||
pub payload: Vec<u8>,
|
pub payload: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A keepalive packet reports the reason why a command does not finish.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub enum KeepaliveStatus {
|
||||||
|
Processing = 0x01,
|
||||||
|
UpNeeded = 0x02,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Holds all state for receiving and sending HID packets.
|
||||||
|
///
|
||||||
|
/// This includes
|
||||||
|
/// - state from not fully processed messages,
|
||||||
|
/// - all allocated channels,
|
||||||
|
/// - information about requested winks.
|
||||||
|
///
|
||||||
|
/// The wink information can be polled to decide to i.e. blink LEDs.
|
||||||
pub struct CtapHid {
|
pub struct CtapHid {
|
||||||
assembler: MessageAssembler,
|
assembler: MessageAssembler,
|
||||||
// The specification (version 20190130) only requires unique CIDs ; the allocation algorithm is
|
// The specification only requires unique CIDs, the allocation algorithm is vendor specific.
|
||||||
// vendor specific.
|
|
||||||
// We allocate them incrementally, that is all `cid` such that 1 <= cid <= allocated_cids are
|
// We allocate them incrementally, that is all `cid` such that 1 <= cid <= allocated_cids are
|
||||||
// allocated.
|
// allocated.
|
||||||
// In packets, the ID encoding is Big Endian to match what is used throughout CTAP (with the
|
// In packets, the ID encoding is Big Endian to match what is used throughout CTAP (with the
|
||||||
// u32::to/from_be_bytes methods).
|
// u32::to/from_be_bytes methods).
|
||||||
|
// TODO(kaczmarczyck) We might want to limit or timeout open channels.
|
||||||
allocated_cids: usize,
|
allocated_cids: usize,
|
||||||
pub(crate) wink_permission: TimedPermission,
|
pub(crate) wink_permission: TimedPermission,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub enum KeepaliveStatus {
|
|
||||||
Processing,
|
|
||||||
UpNeeded,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
// TODO(kaczmarczyck) disable the warning in the end
|
|
||||||
impl CtapHid {
|
impl CtapHid {
|
||||||
// CTAP specification (version 20190130) section 8.1.3
|
// We implement CTAP 2.1 from 2021-06-15. Please see section
|
||||||
|
// 11.2. USB Human Interface Device (USB HID)
|
||||||
const CHANNEL_RESERVED: ChannelID = [0, 0, 0, 0];
|
const CHANNEL_RESERVED: ChannelID = [0, 0, 0, 0];
|
||||||
const CHANNEL_BROADCAST: ChannelID = [0xFF, 0xFF, 0xFF, 0xFF];
|
const CHANNEL_BROADCAST: ChannelID = [0xFF, 0xFF, 0xFF, 0xFF];
|
||||||
const TYPE_INIT_BIT: u8 = 0x80;
|
const TYPE_INIT_BIT: u8 = 0x80;
|
||||||
const PACKET_TYPE_MASK: 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_CMD: u8 = 0x01;
|
||||||
const ERR_INVALID_PAR: u8 = 0x02;
|
const _ERR_INVALID_PAR: u8 = 0x02;
|
||||||
const ERR_INVALID_LEN: u8 = 0x03;
|
const ERR_INVALID_LEN: u8 = 0x03;
|
||||||
const ERR_INVALID_SEQ: u8 = 0x04;
|
const ERR_INVALID_SEQ: u8 = 0x04;
|
||||||
const ERR_MSG_TIMEOUT: u8 = 0x05;
|
const ERR_MSG_TIMEOUT: u8 = 0x05;
|
||||||
const ERR_CHANNEL_BUSY: u8 = 0x06;
|
const ERR_CHANNEL_BUSY: u8 = 0x06;
|
||||||
const ERR_LOCK_REQUIRED: u8 = 0x0A;
|
const _ERR_LOCK_REQUIRED: u8 = 0x0A;
|
||||||
const ERR_INVALID_CHANNEL: u8 = 0x0B;
|
const ERR_INVALID_CHANNEL: u8 = 0x0B;
|
||||||
const ERR_OTHER: u8 = 0x7F;
|
const _ERR_OTHER: u8 = 0x7F;
|
||||||
|
|
||||||
// CTAP specification (version 20190130) section 8.1.9.1.3
|
// See section 11.2.9.1.3. CTAPHID_INIT (0x06).
|
||||||
const PROTOCOL_VERSION: u8 = 2;
|
const PROTOCOL_VERSION: u8 = 2;
|
||||||
|
|
||||||
// The device version number is vendor-defined.
|
// The device version number is vendor-defined.
|
||||||
const DEVICE_VERSION_MAJOR: u8 = 1;
|
const DEVICE_VERSION_MAJOR: u8 = 1;
|
||||||
const DEVICE_VERSION_MINOR: u8 = 0;
|
const DEVICE_VERSION_MINOR: u8 = 0;
|
||||||
@@ -124,6 +153,7 @@ impl CtapHid {
|
|||||||
|
|
||||||
const CAPABILITY_WINK: u8 = 0x01;
|
const CAPABILITY_WINK: u8 = 0x01;
|
||||||
const CAPABILITY_CBOR: u8 = 0x04;
|
const CAPABILITY_CBOR: u8 = 0x04;
|
||||||
|
#[cfg(not(feature = "with_ctap1"))]
|
||||||
const CAPABILITY_NMSG: u8 = 0x08;
|
const CAPABILITY_NMSG: u8 = 0x08;
|
||||||
// Capabilitites currently supported by this device.
|
// Capabilitites currently supported by this device.
|
||||||
#[cfg(feature = "with_ctap1")]
|
#[cfg(feature = "with_ctap1")]
|
||||||
@@ -136,6 +166,7 @@ impl CtapHid {
|
|||||||
const TIMEOUT_DURATION: Duration<isize> = Duration::from_ms(100);
|
const TIMEOUT_DURATION: Duration<isize> = Duration::from_ms(100);
|
||||||
const WINK_TIMEOUT_DURATION: Duration<isize> = Duration::from_ms(5000);
|
const WINK_TIMEOUT_DURATION: Duration<isize> = Duration::from_ms(5000);
|
||||||
|
|
||||||
|
/// Creates a new idle HID state.
|
||||||
pub fn new() -> CtapHid {
|
pub fn new() -> CtapHid {
|
||||||
CtapHid {
|
CtapHid {
|
||||||
assembler: MessageAssembler::new(),
|
assembler: MessageAssembler::new(),
|
||||||
@@ -144,21 +175,14 @@ impl CtapHid {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process an incoming USB HID packet, and optionally returns a list of outgoing packets to
|
/// Processes an assembled incoming message and returns an outgoing message if necessary.
|
||||||
// send as a reply.
|
pub fn process_message(
|
||||||
pub fn process_hid_packet(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
env: &mut impl Env,
|
env: &mut impl Env,
|
||||||
packet: &HidPacket,
|
message: Message,
|
||||||
clock_value: ClockValue,
|
clock_value: ClockValue,
|
||||||
ctap_state: &mut CtapState,
|
ctap_state: &mut CtapState,
|
||||||
) -> HidPacketIterator {
|
) -> Option<Message> {
|
||||||
// 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")]
|
#[cfg(feature = "debug_ctap")]
|
||||||
writeln!(&mut Console::new(), "Received message: {:02x?}", message).unwrap();
|
writeln!(&mut Console::new(), "Received message: {:02x?}", message).unwrap();
|
||||||
|
|
||||||
@@ -166,17 +190,17 @@ impl CtapHid {
|
|||||||
if !self.has_valid_channel(&message) {
|
if !self.has_valid_channel(&message) {
|
||||||
#[cfg(feature = "debug_ctap")]
|
#[cfg(feature = "debug_ctap")]
|
||||||
writeln!(&mut Console::new(), "Invalid channel: {:02x?}", cid).unwrap();
|
writeln!(&mut Console::new(), "Invalid channel: {:02x?}", cid).unwrap();
|
||||||
return CtapHid::error_message(cid, CtapHid::ERR_INVALID_CHANNEL);
|
return Some(CtapHid::error_message(cid, CtapHid::ERR_INVALID_CHANNEL));
|
||||||
}
|
}
|
||||||
// If another command arrives, stop winking to prevent accidential button touches.
|
// If another command arrives, stop winking to prevent accidential button touches.
|
||||||
self.wink_permission = TimedPermission::waiting();
|
self.wink_permission = TimedPermission::waiting();
|
||||||
|
|
||||||
match message.cmd {
|
match message.cmd {
|
||||||
// CTAP specification (version 20190130) section 8.1.9.1.1
|
// CTAP 2.1 from 2021-06-15, section 11.2.9.1.1.
|
||||||
CtapHid::COMMAND_MSG => {
|
CtapHidCommand::Msg => {
|
||||||
// If we don't have CTAP1 backward compatibilty, this command is invalid.
|
// If we don't have CTAP1 backward compatibilty, this command is invalid.
|
||||||
#[cfg(not(feature = "with_ctap1"))]
|
#[cfg(not(feature = "with_ctap1"))]
|
||||||
return CtapHid::error_message(cid, CtapHid::ERR_INVALID_CMD);
|
return Some(CtapHid::error_message(cid, CtapHid::ERR_INVALID_CMD));
|
||||||
|
|
||||||
#[cfg(feature = "with_ctap1")]
|
#[cfg(feature = "with_ctap1")]
|
||||||
match ctap1::Ctap1Command::process_command(
|
match ctap1::Ctap1Command::process_command(
|
||||||
@@ -185,48 +209,28 @@ impl CtapHid {
|
|||||||
ctap_state,
|
ctap_state,
|
||||||
clock_value,
|
clock_value,
|
||||||
) {
|
) {
|
||||||
Ok(payload) => CtapHid::ctap1_success_message(cid, &payload),
|
Ok(payload) => Some(CtapHid::ctap1_success_message(cid, &payload)),
|
||||||
Err(ctap1_status_code) => {
|
Err(ctap1_status_code) => {
|
||||||
CtapHid::ctap1_error_message(cid, ctap1_status_code)
|
Some(CtapHid::ctap1_error_message(cid, ctap1_status_code))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// CTAP specification (version 20190130) section 8.1.9.1.2
|
// CTAP 2.1 from 2021-06-15, section 11.2.9.1.2.
|
||||||
CtapHid::COMMAND_CBOR => {
|
CtapHidCommand::Cbor => {
|
||||||
// CTAP specification (version 20190130) section 8.1.5.1
|
|
||||||
// Each transaction is atomic, so we process the command directly here and
|
// Each transaction is atomic, so we process the command directly here and
|
||||||
// don't handle any other packet in the meantime.
|
// don't handle any other packet in the meantime.
|
||||||
// TODO: Send keep-alive packets in the meantime.
|
// TODO: Send "Processing" type keep-alive packets in the meantime.
|
||||||
let response =
|
let response = ctap_state.process_command(env, &message.payload, cid, clock_value);
|
||||||
ctap_state.process_command(env, &message.payload, cid, clock_value);
|
Some(Message {
|
||||||
if let Some(iterator) = CtapHid::split_message(Message {
|
|
||||||
cid,
|
cid,
|
||||||
cmd: CtapHid::COMMAND_CBOR,
|
cmd: CtapHidCommand::Cbor,
|
||||||
payload: response,
|
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_INTERNAL_ERROR as u8,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
}
|
// CTAP 2.1 from 2021-06-15, section 11.2.9.1.3.
|
||||||
// CTAP specification (version 20190130) section 8.1.9.1.3
|
CtapHidCommand::Init => {
|
||||||
CtapHid::COMMAND_INIT => {
|
|
||||||
if message.payload.len() != 8 {
|
if message.payload.len() != 8 {
|
||||||
return CtapHid::error_message(cid, CtapHid::ERR_INVALID_LEN);
|
return Some(CtapHid::error_message(cid, CtapHid::ERR_INVALID_LEN));
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_cid = if cid == CtapHid::CHANNEL_BROADCAST {
|
let new_cid = if cid == CtapHid::CHANNEL_BROADCAST {
|
||||||
@@ -247,95 +251,103 @@ impl CtapHid {
|
|||||||
payload[15] = CtapHid::DEVICE_VERSION_BUILD;
|
payload[15] = CtapHid::DEVICE_VERSION_BUILD;
|
||||||
payload[16] = CtapHid::CAPABILITIES;
|
payload[16] = CtapHid::CAPABILITIES;
|
||||||
|
|
||||||
// This unwrap is safe because the payload length is 17 <= 7609 bytes.
|
Some(Message {
|
||||||
CtapHid::split_message(Message {
|
|
||||||
cid,
|
cid,
|
||||||
cmd: CtapHid::COMMAND_INIT,
|
cmd: CtapHidCommand::Init,
|
||||||
payload,
|
payload,
|
||||||
})
|
})
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
// CTAP specification (version 20190130) section 8.1.9.1.4
|
// CTAP 2.1 from 2021-06-15, section 11.2.9.1.4.
|
||||||
CtapHid::COMMAND_PING => {
|
CtapHidCommand::Ping => {
|
||||||
// Pong the same message.
|
// Pong the same message.
|
||||||
// This unwrap is safe because if we could parse the incoming message, it's
|
Some(message)
|
||||||
// payload length must be <= 7609 bytes.
|
|
||||||
CtapHid::split_message(message).unwrap()
|
|
||||||
}
|
}
|
||||||
// CTAP specification (version 20190130) section 8.1.9.1.5
|
// CTAP 2.1 from 2021-06-15, section 11.2.9.1.5.
|
||||||
CtapHid::COMMAND_CANCEL => {
|
CtapHidCommand::Cancel => {
|
||||||
// Authenticators MUST NOT reply to this message.
|
// Authenticators MUST NOT reply to this message.
|
||||||
// CANCEL is handled during user presence checks in main.
|
// CANCEL is handled during user presence checks in main.
|
||||||
HidPacketIterator::none()
|
None
|
||||||
}
|
}
|
||||||
// Optional commands
|
// CTAP 2.1 from 2021-06-15, section 11.2.9.2.1.
|
||||||
// CTAP specification (version 20190130) section 8.1.9.2.1
|
CtapHidCommand::Wink => {
|
||||||
CtapHid::COMMAND_WINK => {
|
|
||||||
if !message.payload.is_empty() {
|
if !message.payload.is_empty() {
|
||||||
return CtapHid::error_message(cid, CtapHid::ERR_INVALID_LEN);
|
return Some(CtapHid::error_message(cid, CtapHid::ERR_INVALID_LEN));
|
||||||
}
|
}
|
||||||
self.wink_permission =
|
self.wink_permission =
|
||||||
TimedPermission::granted(clock_value, CtapHid::WINK_TIMEOUT_DURATION);
|
TimedPermission::granted(clock_value, CtapHid::WINK_TIMEOUT_DURATION);
|
||||||
CtapHid::split_message(Message {
|
// The response is empty like the request.
|
||||||
cid,
|
Some(message)
|
||||||
cmd: CtapHid::COMMAND_WINK,
|
|
||||||
payload: vec![],
|
|
||||||
})
|
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
// CTAP specification (version 20190130) section 8.1.9.2.2
|
|
||||||
// TODO: implement LOCK
|
|
||||||
_ => {
|
_ => {
|
||||||
// Unknown or unsupported command.
|
// Unknown or unsupported command.
|
||||||
CtapHid::error_message(cid, CtapHid::ERR_INVALID_CMD)
|
Some(CtapHid::error_message(cid, CtapHid::ERR_INVALID_CMD))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Processes an incoming USB HID packet, and returns an iterator for all outgoing packets.
|
||||||
|
pub fn process_hid_packet(
|
||||||
|
&mut self,
|
||||||
|
env: &mut impl Env,
|
||||||
|
packet: &HidPacket,
|
||||||
|
clock_value: ClockValue,
|
||||||
|
ctap_state: &mut CtapState,
|
||||||
|
) -> HidPacketIterator {
|
||||||
|
let reponse_message = match self
|
||||||
|
.assembler
|
||||||
|
.parse_packet(packet, Timestamp::<isize>::from_clock_value(clock_value))
|
||||||
|
{
|
||||||
|
Ok(Some(message)) => self.process_message(env, message, clock_value, ctap_state),
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
// Waiting for more packets to assemble the message, nothing to send for now.
|
// Waiting for more packets to assemble the message, nothing to send for now.
|
||||||
HidPacketIterator::none()
|
None
|
||||||
}
|
}
|
||||||
Err((cid, error)) => {
|
Err((cid, error)) => {
|
||||||
if !self.is_allocated_channel(cid)
|
if !self.is_allocated_channel(cid)
|
||||||
&& error != receive::Error::UnexpectedContinuation
|
&& error != receive::Error::UnexpectedContinuation
|
||||||
{
|
{
|
||||||
CtapHid::error_message(cid, CtapHid::ERR_INVALID_CHANNEL)
|
Some(CtapHid::error_message(cid, CtapHid::ERR_INVALID_CHANNEL))
|
||||||
} else {
|
} else {
|
||||||
match error {
|
match error {
|
||||||
receive::Error::UnexpectedChannel => {
|
receive::Error::UnexpectedChannel => {
|
||||||
CtapHid::error_message(cid, CtapHid::ERR_CHANNEL_BUSY)
|
Some(CtapHid::error_message(cid, CtapHid::ERR_CHANNEL_BUSY))
|
||||||
}
|
}
|
||||||
receive::Error::UnexpectedInit => {
|
receive::Error::UnexpectedInit => {
|
||||||
// TODO: Should we send another error code in this case?
|
// TODO: Should we send another error code in this case?
|
||||||
// Technically, we were expecting a sequence number and got another
|
// Technically, we were expecting a sequence number and got another
|
||||||
// byte, although the command/seqnum bit has higher-level semantics
|
// byte, although the command/seqnum bit has higher-level semantics
|
||||||
// than sequence numbers.
|
// than sequence numbers.
|
||||||
CtapHid::error_message(cid, CtapHid::ERR_INVALID_SEQ)
|
Some(CtapHid::error_message(cid, CtapHid::ERR_INVALID_SEQ))
|
||||||
}
|
}
|
||||||
receive::Error::UnexpectedContinuation => {
|
receive::Error::UnexpectedContinuation => {
|
||||||
// CTAP specification (version 20190130) section 8.1.5.4
|
// CTAP specification (version 20190130) section 8.1.5.4
|
||||||
// Spurious continuation packets will be ignored.
|
// Spurious continuation packets will be ignored.
|
||||||
HidPacketIterator::none()
|
None
|
||||||
}
|
}
|
||||||
receive::Error::UnexpectedSeq => {
|
receive::Error::UnexpectedSeq => {
|
||||||
CtapHid::error_message(cid, CtapHid::ERR_INVALID_SEQ)
|
Some(CtapHid::error_message(cid, CtapHid::ERR_INVALID_SEQ))
|
||||||
}
|
}
|
||||||
receive::Error::UnexpectedLen => {
|
receive::Error::UnexpectedLen => {
|
||||||
CtapHid::error_message(cid, CtapHid::ERR_INVALID_LEN)
|
Some(CtapHid::error_message(cid, CtapHid::ERR_INVALID_LEN))
|
||||||
}
|
}
|
||||||
receive::Error::Timeout => {
|
receive::Error::Timeout => {
|
||||||
CtapHid::error_message(cid, CtapHid::ERR_MSG_TIMEOUT)
|
Some(CtapHid::error_message(cid, CtapHid::ERR_MSG_TIMEOUT))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
if let Some(message) = reponse_message {
|
||||||
|
CtapHid::split_message(message)
|
||||||
|
} else {
|
||||||
|
HidPacketIterator::none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_valid_channel(&self, message: &Message) -> bool {
|
fn has_valid_channel(&self, message: &Message) -> bool {
|
||||||
match message.cid {
|
match message.cid {
|
||||||
// Only INIT commands use the broadcast channel.
|
// Only INIT commands use the broadcast channel.
|
||||||
CtapHid::CHANNEL_BROADCAST => message.cmd == CtapHid::COMMAND_INIT,
|
CtapHid::CHANNEL_BROADCAST => message.cmd == CtapHidCommand::Init,
|
||||||
// Check that the channel is allocated.
|
// Check that the channel is allocated.
|
||||||
_ => self.is_allocated_channel(message.cid),
|
_ => self.is_allocated_channel(message.cid),
|
||||||
}
|
}
|
||||||
@@ -345,16 +357,15 @@ impl CtapHid {
|
|||||||
cid != CtapHid::CHANNEL_RESERVED && u32::from_be_bytes(cid) as usize <= self.allocated_cids
|
cid != CtapHid::CHANNEL_RESERVED && u32::from_be_bytes(cid) as usize <= self.allocated_cids
|
||||||
}
|
}
|
||||||
|
|
||||||
fn error_message(cid: ChannelID, error_code: u8) -> HidPacketIterator {
|
fn error_message(cid: ChannelID, error_code: u8) -> Message {
|
||||||
// This unwrap is safe because the payload length is 1 <= 7609 bytes.
|
Message {
|
||||||
CtapHid::split_message(Message {
|
|
||||||
cid,
|
cid,
|
||||||
cmd: CtapHid::COMMAND_ERROR,
|
cmd: CtapHidCommand::Error,
|
||||||
payload: vec![error_code],
|
payload: vec![error_code],
|
||||||
})
|
}
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper function to parse a raw packet.
|
||||||
pub fn process_single_packet(packet: &HidPacket) -> (&ChannelID, ProcessedPacket) {
|
pub fn process_single_packet(packet: &HidPacket) -> (&ChannelID, ProcessedPacket) {
|
||||||
let (cid, rest) = array_refs![packet, 4, 60];
|
let (cid, rest) = array_refs![packet, 4, 60];
|
||||||
if rest[0] & CtapHid::PACKET_TYPE_MASK != 0 {
|
if rest[0] & CtapHid::PACKET_TYPE_MASK != 0 {
|
||||||
@@ -379,24 +390,40 @@ impl CtapHid {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn split_message(message: Message) -> Option<HidPacketIterator> {
|
/// Splits the message and unwraps the result.
|
||||||
|
///
|
||||||
|
/// Unwrapping handles the case of payload lengths > 7609 bytes. All responses are fixed
|
||||||
|
/// length, with the exception of:
|
||||||
|
/// - PING, but here output equals the (validated) input,
|
||||||
|
/// - CBOR, where long responses are conceivable.
|
||||||
|
///
|
||||||
|
/// Long CBOR responses should not happen, but we might not catch all edge cases, like for
|
||||||
|
/// example long user names that are part of the output of an assertion. These cases should be
|
||||||
|
/// correctly handled by the CTAP implementation. It is therefore an internal error from the
|
||||||
|
/// HID perspective.
|
||||||
|
fn split_message(message: Message) -> HidPacketIterator {
|
||||||
#[cfg(feature = "debug_ctap")]
|
#[cfg(feature = "debug_ctap")]
|
||||||
writeln!(&mut Console::new(), "Sending message: {:02x?}", message).unwrap();
|
writeln!(&mut Console::new(), "Sending message: {:02x?}", message).unwrap();
|
||||||
HidPacketIterator::new(message)
|
let cid = message.cid;
|
||||||
|
HidPacketIterator::new(message).unwrap_or_else(|| {
|
||||||
|
// The error payload is 1 <= 7609 bytes, so unwrap() is safe.
|
||||||
|
HidPacketIterator::new(Message {
|
||||||
|
cid,
|
||||||
|
cmd: CtapHidCommand::Cbor,
|
||||||
|
payload: vec![Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR as u8],
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates the HID response packets for a keepalive status.
|
||||||
pub fn keepalive(cid: ChannelID, status: KeepaliveStatus) -> HidPacketIterator {
|
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.
|
// This unwrap is safe because the payload length is 1 <= 7609 bytes.
|
||||||
CtapHid::split_message(Message {
|
CtapHid::split_message(Message {
|
||||||
cid,
|
cid,
|
||||||
cmd: CtapHid::COMMAND_KEEPALIVE,
|
cmd: CtapHidCommand::Keepalive,
|
||||||
payload: vec![status_code],
|
payload: vec![status as u8],
|
||||||
})
|
})
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn should_wink(&self, now: ClockValue) -> bool {
|
pub fn should_wink(&self, now: ClockValue) -> bool {
|
||||||
@@ -404,31 +431,25 @@ impl CtapHid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "with_ctap1")]
|
#[cfg(feature = "with_ctap1")]
|
||||||
fn ctap1_error_message(
|
fn ctap1_error_message(cid: ChannelID, error_code: ctap1::Ctap1StatusCode) -> 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();
|
let code: u16 = error_code.into();
|
||||||
CtapHid::split_message(Message {
|
Message {
|
||||||
cid,
|
cid,
|
||||||
cmd: CtapHid::COMMAND_MSG,
|
cmd: CtapHidCommand::Msg,
|
||||||
payload: code.to_be_bytes().to_vec(),
|
payload: code.to_be_bytes().to_vec(),
|
||||||
})
|
}
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "with_ctap1")]
|
#[cfg(feature = "with_ctap1")]
|
||||||
fn ctap1_success_message(cid: ChannelID, payload: &[u8]) -> HidPacketIterator {
|
fn ctap1_success_message(cid: ChannelID, payload: &[u8]) -> Message {
|
||||||
let mut response = payload.to_vec();
|
let mut response = payload.to_vec();
|
||||||
let code: u16 = ctap1::Ctap1StatusCode::SW_SUCCESS.into();
|
let code: u16 = ctap1::Ctap1StatusCode::SW_SUCCESS.into();
|
||||||
response.extend_from_slice(&code.to_be_bytes());
|
response.extend_from_slice(&code.to_be_bytes());
|
||||||
CtapHid::split_message(Message {
|
Message {
|
||||||
cid,
|
cid,
|
||||||
cmd: CtapHid::COMMAND_MSG,
|
cmd: CtapHidCommand::Msg,
|
||||||
payload: response,
|
payload: response,
|
||||||
})
|
}
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,7 +499,7 @@ mod test {
|
|||||||
ctap_state,
|
ctap_state,
|
||||||
vec![Message {
|
vec![Message {
|
||||||
cid: CtapHid::CHANNEL_BROADCAST,
|
cid: CtapHid::CHANNEL_BROADCAST,
|
||||||
cmd: CtapHid::COMMAND_INIT,
|
cmd: CtapHidCommand::Init,
|
||||||
payload: nonce.clone(),
|
payload: nonce.clone(),
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
@@ -500,7 +521,7 @@ mod test {
|
|||||||
for payload_len in 0..7609 {
|
for payload_len in 0..7609 {
|
||||||
let message = Message {
|
let message = Message {
|
||||||
cid: [0x12, 0x34, 0x56, 0x78],
|
cid: [0x12, 0x34, 0x56, 0x78],
|
||||||
cmd: 0x00,
|
cmd: CtapHidCommand::Cbor,
|
||||||
payload: vec![0xFF; payload_len],
|
payload: vec![0xFF; payload_len],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -552,7 +573,7 @@ mod test {
|
|||||||
&mut ctap_state,
|
&mut ctap_state,
|
||||||
vec![Message {
|
vec![Message {
|
||||||
cid: CtapHid::CHANNEL_BROADCAST,
|
cid: CtapHid::CHANNEL_BROADCAST,
|
||||||
cmd: CtapHid::COMMAND_INIT,
|
cmd: CtapHidCommand::Init,
|
||||||
payload: vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0],
|
payload: vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0],
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
@@ -561,7 +582,7 @@ mod test {
|
|||||||
reply,
|
reply,
|
||||||
Some(vec![Message {
|
Some(vec![Message {
|
||||||
cid: CtapHid::CHANNEL_BROADCAST,
|
cid: CtapHid::CHANNEL_BROADCAST,
|
||||||
cmd: CtapHid::COMMAND_INIT,
|
cmd: CtapHidCommand::Init,
|
||||||
payload: vec![
|
payload: vec![
|
||||||
0x12, // Nonce
|
0x12, // Nonce
|
||||||
0x34,
|
0x34,
|
||||||
@@ -623,7 +644,7 @@ mod test {
|
|||||||
result,
|
result,
|
||||||
vec![Message {
|
vec![Message {
|
||||||
cid,
|
cid,
|
||||||
cmd: CtapHid::COMMAND_INIT,
|
cmd: CtapHidCommand::Init,
|
||||||
payload: vec![
|
payload: vec![
|
||||||
0x12, // Nonce
|
0x12, // Nonce
|
||||||
0x34,
|
0x34,
|
||||||
@@ -660,7 +681,7 @@ mod test {
|
|||||||
&mut ctap_state,
|
&mut ctap_state,
|
||||||
vec![Message {
|
vec![Message {
|
||||||
cid,
|
cid,
|
||||||
cmd: CtapHid::COMMAND_PING,
|
cmd: CtapHidCommand::Ping,
|
||||||
payload: vec![0x99, 0x99],
|
payload: vec![0x99, 0x99],
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
@@ -669,7 +690,7 @@ mod test {
|
|||||||
reply,
|
reply,
|
||||||
Some(vec![Message {
|
Some(vec![Message {
|
||||||
cid,
|
cid,
|
||||||
cmd: CtapHid::COMMAND_PING,
|
cmd: CtapHidCommand::Ping,
|
||||||
payload: vec![0x99, 0x99]
|
payload: vec![0x99, 0x99]
|
||||||
}])
|
}])
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use super::super::customization::MAX_MSG_SIZE;
|
use super::super::customization::MAX_MSG_SIZE;
|
||||||
use super::{ChannelID, CtapHid, HidPacket, Message, ProcessedPacket};
|
use super::{ChannelID, CtapHid, CtapHidCommand, HidPacket, Message, ProcessedPacket};
|
||||||
use alloc::vec::Vec;
|
use alloc::vec::Vec;
|
||||||
use core::mem::swap;
|
use core::mem::swap;
|
||||||
use libtock_drivers::timer::Timestamp;
|
use libtock_drivers::timer::Timestamp;
|
||||||
@@ -131,7 +131,7 @@ impl MessageAssembler {
|
|||||||
// Unexpected initialization packet.
|
// Unexpected initialization packet.
|
||||||
ProcessedPacket::InitPacket { cmd, len, data } => {
|
ProcessedPacket::InitPacket { cmd, len, data } => {
|
||||||
self.reset();
|
self.reset();
|
||||||
if cmd == CtapHid::COMMAND_INIT {
|
if cmd == CtapHidCommand::Init as u8 {
|
||||||
self.parse_init_packet(*cid, cmd, len, data, timestamp)
|
self.parse_init_packet(*cid, cmd, len, data, timestamp)
|
||||||
} else {
|
} else {
|
||||||
Err((*cid, Error::UnexpectedInit))
|
Err((*cid, Error::UnexpectedInit))
|
||||||
@@ -189,7 +189,7 @@ impl MessageAssembler {
|
|||||||
swap(&mut self.payload, &mut payload);
|
swap(&mut self.payload, &mut payload);
|
||||||
Some(Message {
|
Some(Message {
|
||||||
cid: self.cid,
|
cid: self.cid,
|
||||||
cmd: self.cmd,
|
cmd: CtapHidCommand::from(self.cmd),
|
||||||
payload,
|
payload,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -225,12 +225,12 @@ mod test {
|
|||||||
let mut assembler = MessageAssembler::new();
|
let mut assembler = MessageAssembler::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
assembler.parse_packet(
|
assembler.parse_packet(
|
||||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x80]),
|
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x90]),
|
||||||
DUMMY_TIMESTAMP
|
DUMMY_TIMESTAMP
|
||||||
),
|
),
|
||||||
Ok(Some(Message {
|
Ok(Some(Message {
|
||||||
cid: [0x12, 0x34, 0x56, 0x78],
|
cid: [0x12, 0x34, 0x56, 0x78],
|
||||||
cmd: 0x00,
|
cmd: CtapHidCommand::Cbor,
|
||||||
payload: vec![]
|
payload: vec![]
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -241,12 +241,12 @@ mod test {
|
|||||||
let mut assembler = MessageAssembler::new();
|
let mut assembler = MessageAssembler::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
assembler.parse_packet(
|
assembler.parse_packet(
|
||||||
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x80, 0x00, 0x10]),
|
&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x90, 0x00, 0x10]),
|
||||||
DUMMY_TIMESTAMP
|
DUMMY_TIMESTAMP
|
||||||
),
|
),
|
||||||
Ok(Some(Message {
|
Ok(Some(Message {
|
||||||
cid: [0x12, 0x34, 0x56, 0x78],
|
cid: [0x12, 0x34, 0x56, 0x78],
|
||||||
cmd: 0x00,
|
cmd: CtapHidCommand::Cbor,
|
||||||
payload: vec![0x00; 0x10]
|
payload: vec![0x00; 0x10]
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -260,12 +260,12 @@ mod test {
|
|||||||
let mut assembler = MessageAssembler::new();
|
let mut assembler = MessageAssembler::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
assembler.parse_packet(
|
assembler.parse_packet(
|
||||||
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x80, 0x00, 0x10], 0xFF),
|
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x90, 0x00, 0x10], 0xFF),
|
||||||
DUMMY_TIMESTAMP
|
DUMMY_TIMESTAMP
|
||||||
),
|
),
|
||||||
Ok(Some(Message {
|
Ok(Some(Message {
|
||||||
cid: [0x12, 0x34, 0x56, 0x78],
|
cid: [0x12, 0x34, 0x56, 0x78],
|
||||||
cmd: 0x00,
|
cmd: CtapHidCommand::Cbor,
|
||||||
payload: vec![0xFF; 0x10]
|
payload: vec![0xFF; 0x10]
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -288,7 +288,7 @@ mod test {
|
|||||||
),
|
),
|
||||||
Ok(Some(Message {
|
Ok(Some(Message {
|
||||||
cid: [0x12, 0x34, 0x56, 0x78],
|
cid: [0x12, 0x34, 0x56, 0x78],
|
||||||
cmd: 0x01,
|
cmd: CtapHidCommand::Ping,
|
||||||
payload: vec![0x00; 0x40]
|
payload: vec![0x00; 0x40]
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -318,7 +318,7 @@ mod test {
|
|||||||
),
|
),
|
||||||
Ok(Some(Message {
|
Ok(Some(Message {
|
||||||
cid: [0x12, 0x34, 0x56, 0x78],
|
cid: [0x12, 0x34, 0x56, 0x78],
|
||||||
cmd: 0x01,
|
cmd: CtapHidCommand::Ping,
|
||||||
payload: vec![0x00; 0x80]
|
payload: vec![0x00; 0x80]
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -350,7 +350,7 @@ mod test {
|
|||||||
),
|
),
|
||||||
Ok(Some(Message {
|
Ok(Some(Message {
|
||||||
cid: [0x12, 0x34, 0x56, 0x78],
|
cid: [0x12, 0x34, 0x56, 0x78],
|
||||||
cmd: 0x01,
|
cmd: CtapHidCommand::Ping,
|
||||||
payload: vec![0x00; 0x1DB9]
|
payload: vec![0x00; 0x1DB9]
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -362,12 +362,15 @@ mod test {
|
|||||||
let mut assembler = MessageAssembler::new();
|
let mut assembler = MessageAssembler::new();
|
||||||
for i in 0..10 {
|
for i in 0..10 {
|
||||||
// Introduce some variability in the messages.
|
// Introduce some variability in the messages.
|
||||||
let cmd = 2 * i;
|
let cmd = CtapHidCommand::from(i + 1);
|
||||||
let byte = 3 * i;
|
let byte = 3 * i;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
assembler.parse_packet(
|
assembler.parse_packet(
|
||||||
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x80 | cmd, 0x00, 0x80], byte),
|
&byte_extend(
|
||||||
|
&[0x12, 0x34, 0x56, 0x78, 0x80 | cmd as u8, 0x00, 0x80],
|
||||||
|
byte
|
||||||
|
),
|
||||||
DUMMY_TIMESTAMP
|
DUMMY_TIMESTAMP
|
||||||
),
|
),
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@@ -400,12 +403,12 @@ mod test {
|
|||||||
for i in 0..10 {
|
for i in 0..10 {
|
||||||
// Introduce some variability in the messages.
|
// Introduce some variability in the messages.
|
||||||
let cid = 0x78 + i;
|
let cid = 0x78 + i;
|
||||||
let cmd = 2 * i;
|
let cmd = CtapHidCommand::from(i + 1);
|
||||||
let byte = 3 * i;
|
let byte = 3 * i;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
assembler.parse_packet(
|
assembler.parse_packet(
|
||||||
&byte_extend(&[0x12, 0x34, 0x56, cid, 0x80 | cmd, 0x00, 0x80], byte),
|
&byte_extend(&[0x12, 0x34, 0x56, cid, 0x80 | cmd as u8, 0x00, 0x80], byte),
|
||||||
DUMMY_TIMESTAMP
|
DUMMY_TIMESTAMP
|
||||||
),
|
),
|
||||||
Ok(None)
|
Ok(None)
|
||||||
@@ -443,11 +446,12 @@ mod test {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Check that many sorts of packets on another channel are ignored.
|
// Check that many sorts of packets on another channel are ignored.
|
||||||
for cmd in 0..=0xFF {
|
for i in 0..=0xFF {
|
||||||
|
let cmd = CtapHidCommand::from(i);
|
||||||
for byte in 0..=0xFF {
|
for byte in 0..=0xFF {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
assembler.parse_packet(
|
assembler.parse_packet(
|
||||||
&byte_extend(&[0x12, 0x34, 0x56, 0x9A, cmd, 0x00], byte),
|
&byte_extend(&[0x12, 0x34, 0x56, 0x9A, cmd as u8, 0x00], byte),
|
||||||
DUMMY_TIMESTAMP
|
DUMMY_TIMESTAMP
|
||||||
),
|
),
|
||||||
Err(([0x12, 0x34, 0x56, 0x9A], Error::UnexpectedChannel))
|
Err(([0x12, 0x34, 0x56, 0x9A], Error::UnexpectedChannel))
|
||||||
@@ -462,7 +466,7 @@ mod test {
|
|||||||
),
|
),
|
||||||
Ok(Some(Message {
|
Ok(Some(Message {
|
||||||
cid: [0x12, 0x34, 0x56, 0x78],
|
cid: [0x12, 0x34, 0x56, 0x78],
|
||||||
cmd: 0x01,
|
cmd: CtapHidCommand::Ping,
|
||||||
payload: vec![0x00; 0x40]
|
payload: vec![0x00; 0x40]
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -479,12 +483,12 @@ mod test {
|
|||||||
let byte = 2 * i;
|
let byte = 2 * i;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
assembler.parse_packet(
|
assembler.parse_packet(
|
||||||
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x80, 0x00, 0x10], byte),
|
&byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x00, 0x10], byte),
|
||||||
DUMMY_TIMESTAMP
|
DUMMY_TIMESTAMP
|
||||||
),
|
),
|
||||||
Ok(Some(Message {
|
Ok(Some(Message {
|
||||||
cid: [0x12, 0x34, 0x56, 0x78],
|
cid: [0x12, 0x34, 0x56, 0x78],
|
||||||
cmd: 0x00,
|
cmd: CtapHidCommand::Ping,
|
||||||
payload: vec![byte; 0x10]
|
payload: vec![byte; 0x10]
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -584,7 +588,7 @@ mod test {
|
|||||||
assembler.parse_packet(&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x7F]), timestamp),
|
assembler.parse_packet(&zero_extend(&[0x12, 0x34, 0x56, 0x78, 0x7F]), timestamp),
|
||||||
Ok(Some(Message {
|
Ok(Some(Message {
|
||||||
cid: [0x12, 0x34, 0x56, 0x78],
|
cid: [0x12, 0x34, 0x56, 0x78],
|
||||||
cmd: 0x01,
|
cmd: CtapHidCommand::Ping,
|
||||||
payload: vec![0x00; 0x1DB9]
|
payload: vec![0x00; 0x1DB9]
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
@@ -612,7 +616,7 @@ mod test {
|
|||||||
),
|
),
|
||||||
Ok(Some(Message {
|
Ok(Some(Message {
|
||||||
cid: [0x12, 0x34, 0x56, 0x78],
|
cid: [0x12, 0x34, 0x56, 0x78],
|
||||||
cmd: 0x06,
|
cmd: CtapHidCommand::Init,
|
||||||
payload: vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]
|
payload: vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ impl Iterator for MessageSplitter {
|
|||||||
match self.seq {
|
match self.seq {
|
||||||
None => {
|
None => {
|
||||||
// First, send an initialization packet.
|
// First, send an initialization packet.
|
||||||
self.packet[4] = self.message.cmd | CtapHid::TYPE_INIT_BIT;
|
self.packet[4] = self.message.cmd as u8 | CtapHid::TYPE_INIT_BIT;
|
||||||
self.packet[5] = (payload_len >> 8) as u8;
|
self.packet[5] = (payload_len >> 8) as u8;
|
||||||
self.packet[6] = payload_len as u8;
|
self.packet[6] = payload_len as u8;
|
||||||
|
|
||||||
@@ -128,6 +128,7 @@ impl Iterator for MessageSplitter {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use super::super::CtapHidCommand;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn assert_packet_output_equality(message: Message, expected_packets: Vec<HidPacket>) {
|
fn assert_packet_output_equality(message: Message, expected_packets: Vec<HidPacket>) {
|
||||||
@@ -142,11 +143,11 @@ mod test {
|
|||||||
fn test_hid_packet_iterator_single_packet() {
|
fn test_hid_packet_iterator_single_packet() {
|
||||||
let message = Message {
|
let message = Message {
|
||||||
cid: [0x12, 0x34, 0x56, 0x78],
|
cid: [0x12, 0x34, 0x56, 0x78],
|
||||||
cmd: 0x4C,
|
cmd: CtapHidCommand::Cbor,
|
||||||
payload: vec![0xAA, 0xBB],
|
payload: vec![0xAA, 0xBB],
|
||||||
};
|
};
|
||||||
let expected_packets: Vec<HidPacket> = vec![[
|
let expected_packets: Vec<HidPacket> = vec![[
|
||||||
0x12, 0x34, 0x56, 0x78, 0xCC, 0x00, 0x02, 0xAA, 0xBB, 0x00, 0x00, 0x00, 0x00, 0x00,
|
0x12, 0x34, 0x56, 0x78, 0x90, 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, 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,
|
||||||
@@ -159,11 +160,11 @@ mod test {
|
|||||||
fn test_hid_packet_iterator_big_single_packet() {
|
fn test_hid_packet_iterator_big_single_packet() {
|
||||||
let message = Message {
|
let message = Message {
|
||||||
cid: [0x12, 0x34, 0x56, 0x78],
|
cid: [0x12, 0x34, 0x56, 0x78],
|
||||||
cmd: 0x4C,
|
cmd: CtapHidCommand::Cbor,
|
||||||
payload: vec![0xAA; 64 - 7],
|
payload: vec![0xAA; 64 - 7],
|
||||||
};
|
};
|
||||||
let expected_packets: Vec<HidPacket> = vec![[
|
let expected_packets: Vec<HidPacket> = vec![[
|
||||||
0x12, 0x34, 0x56, 0x78, 0xCC, 0x00, 0x39, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
0x12, 0x34, 0x56, 0x78, 0x90, 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, 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,
|
||||||
@@ -176,12 +177,12 @@ mod test {
|
|||||||
fn test_hid_packet_iterator_two_packets() {
|
fn test_hid_packet_iterator_two_packets() {
|
||||||
let message = Message {
|
let message = Message {
|
||||||
cid: [0x12, 0x34, 0x56, 0x78],
|
cid: [0x12, 0x34, 0x56, 0x78],
|
||||||
cmd: 0x4C,
|
cmd: CtapHidCommand::Cbor,
|
||||||
payload: vec![0xAA; 64 - 7 + 1],
|
payload: vec![0xAA; 64 - 7 + 1],
|
||||||
};
|
};
|
||||||
let expected_packets: Vec<HidPacket> = vec![
|
let expected_packets: Vec<HidPacket> = vec![
|
||||||
[
|
[
|
||||||
0x12, 0x34, 0x56, 0x78, 0xCC, 0x00, 0x3A, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
0x12, 0x34, 0x56, 0x78, 0x90, 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, 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,
|
||||||
@@ -204,12 +205,12 @@ mod test {
|
|||||||
payload.extend(vec![0xBB; 64 - 5]);
|
payload.extend(vec![0xBB; 64 - 5]);
|
||||||
let message = Message {
|
let message = Message {
|
||||||
cid: [0x12, 0x34, 0x56, 0x78],
|
cid: [0x12, 0x34, 0x56, 0x78],
|
||||||
cmd: 0x4C,
|
cmd: CtapHidCommand::Cbor,
|
||||||
payload,
|
payload,
|
||||||
};
|
};
|
||||||
let expected_packets: Vec<HidPacket> = vec![
|
let expected_packets: Vec<HidPacket> = vec![
|
||||||
[
|
[
|
||||||
0x12, 0x34, 0x56, 0x78, 0xCC, 0x00, 0x74, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
|
0x12, 0x34, 0x56, 0x78, 0x90, 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, 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,
|
||||||
@@ -238,12 +239,12 @@ mod test {
|
|||||||
|
|
||||||
let message = Message {
|
let message = Message {
|
||||||
cid: [0x12, 0x34, 0x56, 0x78],
|
cid: [0x12, 0x34, 0x56, 0x78],
|
||||||
cmd: 0xAB,
|
cmd: CtapHidCommand::Msg,
|
||||||
payload,
|
payload,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut expected_packets: Vec<HidPacket> = vec![[
|
let mut expected_packets: Vec<HidPacket> = vec![[
|
||||||
0x12, 0x34, 0x56, 0x78, 0xAB, 0x1D, 0xB9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
0x12, 0x34, 0x56, 0x78, 0x83, 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, 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,
|
||||||
@@ -271,7 +272,7 @@ mod test {
|
|||||||
assert_eq!(payload.len(), 0x1dba);
|
assert_eq!(payload.len(), 0x1dba);
|
||||||
let message = Message {
|
let message = Message {
|
||||||
cid: [0x12, 0x34, 0x56, 0x78],
|
cid: [0x12, 0x34, 0x56, 0x78],
|
||||||
cmd: 0xAB,
|
cmd: CtapHidCommand::Msg,
|
||||||
payload,
|
payload,
|
||||||
};
|
};
|
||||||
assert!(HidPacketIterator::new(message).is_none());
|
assert!(HidPacketIterator::new(message).is_none());
|
||||||
@@ -283,7 +284,7 @@ mod test {
|
|||||||
let payload = vec![0xFF; 0x10000];
|
let payload = vec![0xFF; 0x10000];
|
||||||
let message = Message {
|
let message = Message {
|
||||||
cid: [0x12, 0x34, 0x56, 0x78],
|
cid: [0x12, 0x34, 0x56, 0x78],
|
||||||
cmd: 0xAB,
|
cmd: CtapHidCommand::Msg,
|
||||||
payload,
|
payload,
|
||||||
};
|
};
|
||||||
assert!(HidPacketIterator::new(message).is_none());
|
assert!(HidPacketIterator::new(message).is_none());
|
||||||
|
|||||||
4
src/env/tock.rs
vendored
4
src/env/tock.rs
vendored
@@ -1,4 +1,4 @@
|
|||||||
use crate::ctap::hid::{ChannelID, CtapHid, KeepaliveStatus, ProcessedPacket};
|
use crate::ctap::hid::{ChannelID, CtapHid, CtapHidCommand, KeepaliveStatus, ProcessedPacket};
|
||||||
use crate::ctap::status_code::Ctap2StatusCode;
|
use crate::ctap::status_code::Ctap2StatusCode;
|
||||||
use crate::env::{Env, UserPresence};
|
use crate::env::{Env, UserPresence};
|
||||||
use core::cell::Cell;
|
use core::cell::Cell;
|
||||||
@@ -77,7 +77,7 @@ fn send_keepalive_up_needed(
|
|||||||
}
|
}
|
||||||
match processed_packet {
|
match processed_packet {
|
||||||
ProcessedPacket::InitPacket { cmd, .. } => {
|
ProcessedPacket::InitPacket { cmd, .. } => {
|
||||||
if cmd == CtapHid::COMMAND_CANCEL {
|
if cmd == CtapHidCommand::Cancel as u8 {
|
||||||
// We ignore the payload, we can't answer with an error code anyway.
|
// We ignore the payload, we can't answer with an error code anyway.
|
||||||
#[cfg(feature = "debug_ctap")]
|
#[cfg(feature = "debug_ctap")]
|
||||||
writeln!(Console::new(), "User presence check cancelled").unwrap();
|
writeln!(Console::new(), "User presence check cancelled").unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user