xref: /aosp_15_r20/external/crosvm/devices/src/usb/backend/fido_backend/fido_device.rs (revision bb4ee6a4ae7042d18b07a98463b9c8b875e44b39)
1 // Copyright 2024 The ChromiumOS Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 use std::fs::File;
6 use std::io::Error as IOError;
7 use std::io::ErrorKind;
8 use std::io::Write;
9 use std::sync::Arc;
10 
11 use base::debug;
12 use base::error;
13 use base::warn;
14 use base::AsRawDescriptor;
15 use base::EventType;
16 use base::RawDescriptor;
17 use sync::Mutex;
18 use zerocopy::FromBytes;
19 use zerocopy::FromZeroes;
20 
21 use crate::usb::backend::fido_backend::constants;
22 use crate::usb::backend::fido_backend::error::Error;
23 use crate::usb::backend::fido_backend::error::Result;
24 use crate::usb::backend::fido_backend::fido_guest::FidoGuestKey;
25 use crate::usb::backend::fido_backend::fido_transaction::TransactionManager;
26 use crate::usb::backend::fido_backend::hid_utils::verify_is_fido_device;
27 use crate::usb::backend::fido_backend::poll_thread::PollTimer;
28 use crate::utils::EventLoop;
29 
30 #[derive(FromZeroes, FromBytes, Debug)]
31 #[repr(C)]
32 pub struct InitPacket {
33     cid: u32,
34     cmd: u8,
35     bcnth: u8,
36     bcntl: u8,
37     data: [u8; constants::PACKET_INIT_DATA_SIZE],
38 }
39 
40 impl InitPacket {
extract_cid(bytes: [u8; constants::U2FHID_PACKET_SIZE]) -> Result<u32>41     pub fn extract_cid(bytes: [u8; constants::U2FHID_PACKET_SIZE]) -> Result<u32> {
42         // cid is the first 4 bytes so we don't need to worry about anything else in the bytes
43         // buffer, we can just read from prefix.
44         FromBytes::read_from_prefix(&bytes[..]).ok_or_else(|| Error::CannotExtractCidFromBytes)
45     }
46 
is_valid(bytes: [u8; constants::U2FHID_PACKET_SIZE]) -> bool47     fn is_valid(bytes: [u8; constants::U2FHID_PACKET_SIZE]) -> bool {
48         (bytes[4] & constants::PACKET_INIT_VALID_CMD) != 0
49     }
50 
from_bytes(bytes: [u8; constants::U2FHID_PACKET_SIZE]) -> Result<InitPacket>51     pub fn from_bytes(bytes: [u8; constants::U2FHID_PACKET_SIZE]) -> Result<InitPacket> {
52         if !InitPacket::is_valid(bytes) {
53             return Err(Error::InvalidInitPacket);
54         }
55 
56         InitPacket::read_from(&bytes[..]).ok_or_else(|| Error::CannotConvertInitPacketFromBytes)
57     }
58 
bcnt(&self) -> u1659     pub fn bcnt(&self) -> u16 {
60         (self.bcnth as u16) << 8 | (self.bcntl as u16)
61     }
62 }
63 
64 /// A virtual representation of a FidoDevice emulated on the Host.
65 pub struct FidoDevice {
66     /// Guest representation of the virtual security key device
67     pub guest_key: Arc<Mutex<FidoGuestKey>>,
68     /// The `TransactionManager` which handles starting and stopping u2f transactions
69     pub transaction_manager: Arc<Mutex<TransactionManager>>,
70     /// Marks whether the current device is active in a transaction. If it is not active, the fd
71     /// polling event loop does not handle the device fd monitoring.
72     pub is_active: bool,
73     /// Marks whether the device has been lost. In case the FD stops being responsive we signal
74     /// that the device is lost and any further transaction will return a failure.
75     pub is_device_lost: bool,
76     /// Backend provider event loop to attach/detach the monitored fd.
77     event_loop: Arc<EventLoop>,
78     /// Timer to poll for active USB transfers
79     pub transfer_timer: PollTimer,
80     /// fd of the actual hidraw device
81     pub fd: Arc<Mutex<File>>,
82 }
83 
84 impl AsRawDescriptor for FidoDevice {
as_raw_descriptor(&self) -> RawDescriptor85     fn as_raw_descriptor(&self) -> RawDescriptor {
86         self.fd.lock().as_raw_descriptor()
87     }
88 }
89 
90 impl FidoDevice {
new(hidraw: File, event_loop: Arc<EventLoop>) -> Result<FidoDevice>91     pub fn new(hidraw: File, event_loop: Arc<EventLoop>) -> Result<FidoDevice> {
92         verify_is_fido_device(&hidraw)?;
93         let timer = PollTimer::new(
94             "USB transfer timer".to_string(),
95             std::time::Duration::from_millis(constants::USB_POLL_RATE_MILLIS),
96         )?;
97         Ok(FidoDevice {
98             guest_key: Arc::new(Mutex::new(FidoGuestKey::new()?)),
99             transaction_manager: Arc::new(Mutex::new(TransactionManager::new()?)),
100             is_active: false,
101             is_device_lost: false,
102             event_loop,
103             transfer_timer: timer,
104             fd: Arc::new(Mutex::new(hidraw)),
105         })
106     }
107 
108     /// Sets the device active state. If the device becomes active, it toggles polling on the file
109     /// descriptor for the host hid device. If the devices becomes inactive, it stops polling.
110     /// In case of error, it's not possible to recover so we just log the warning and continue.
set_active(&mut self, active: bool)111     pub fn set_active(&mut self, active: bool) {
112         if self.is_active && !active {
113             if let Err(e) = self.event_loop.pause_event_for_descriptor(self) {
114                 error!("Could not deactivate polling of host device: {}", e);
115             }
116         } else if !self.is_active && active {
117             if let Err(e) = self
118                 .event_loop
119                 .resume_event_for_descriptor(self, EventType::Read)
120             {
121                 error!(
122                     "Could not resume polling of host device, transactions will be lost: {}",
123                     e
124                 );
125             }
126         }
127 
128         self.is_active = active;
129     }
130 
131     /// Starts a new transaction from a given init packet.
start_transaction(&mut self, packet: &InitPacket) -> Result<()>132     pub fn start_transaction(&mut self, packet: &InitPacket) -> Result<()> {
133         let nonce = if packet.cid == constants::BROADCAST_CID {
134             packet.data[..constants::NONCE_SIZE]
135                 .try_into()
136                 .map_err(|_| Error::InvalidNonceSize)?
137         } else {
138             constants::EMPTY_NONCE
139         };
140 
141         // Start a transaction and the expiration timer if necessary
142         if self
143             .transaction_manager
144             .lock()
145             .start_transaction(packet.cid, nonce)
146         {
147             // Enable the timer that polls for transactions to expire
148             self.transaction_manager.lock().transaction_timer.arm()?;
149         }
150 
151         // Transition the low level device to active for a response from the host
152         self.set_active(true);
153         Ok(())
154     }
155 
156     /// Receives a low-level request from the host device. It means we read data from the actual
157     /// key on the host.
recv_from_host(&mut self, packet: [u8; constants::U2FHID_PACKET_SIZE]) -> Result<()>158     pub fn recv_from_host(&mut self, packet: [u8; constants::U2FHID_PACKET_SIZE]) -> Result<()> {
159         let cid = InitPacket::extract_cid(packet)?;
160         let transaction_opt = if cid == constants::BROADCAST_CID {
161             match InitPacket::from_bytes(packet) {
162                 Ok(packet) => {
163                     // This is a special case, in case of an error message we return to the
164                     // latest broadcast transaction without nonce checking.
165                     if packet.cmd == constants::U2FHID_ERROR_CMD {
166                         self.transaction_manager.lock().get_transaction(cid)
167                     // Otherwise we verify that the nonce matches the right transaction.
168                     } else {
169                         let nonce = packet.data[..constants::NONCE_SIZE]
170                             .try_into()
171                             .map_err(|_| Error::InvalidNonceSize)?;
172                         self.transaction_manager
173                             .lock()
174                             .get_transaction_from_nonce(nonce)
175                     }
176                 }
177                 _ => {
178                     // Drop init transaction with bad init packet
179                     return Ok(());
180                 }
181             }
182         } else {
183             self.transaction_manager.lock().get_transaction(cid)
184         };
185 
186         let transaction = match transaction_opt {
187             Some(t) => t,
188             None => {
189                 debug!("Ignoring non-started transaction");
190                 return Ok(());
191             }
192         };
193 
194         match InitPacket::from_bytes(packet) {
195             Ok(packet) => {
196                 if packet.cid == constants::BROADCAST_CID {
197                     let nonce = &packet.data[..constants::NONCE_SIZE];
198                     if transaction.nonce != nonce {
199                         // In case of an error command we can let it through, otherwise we drop the
200                         // response.
201                         if packet.cmd != constants::U2FHID_ERROR_CMD {
202                             warn!(
203                                 "u2f: received a broadcast transaction with mismatched nonce.\
204                                 Ignoring transaction."
205                             );
206                             return Ok(());
207                         }
208                     }
209                 }
210                 self.transaction_manager.lock().update_transaction(
211                     cid,
212                     packet.bcnt(),
213                     constants::PACKET_INIT_DATA_SIZE as u16,
214                 );
215             }
216             // It's not an init packet, it means it's a continuation packet
217             Err(Error::InvalidInitPacket) => {
218                 self.transaction_manager.lock().update_transaction(
219                     cid,
220                     transaction.resp_bcnt,
221                     transaction.resp_size + constants::PACKET_CONT_DATA_SIZE as u16,
222                 );
223             }
224             Err(e) => {
225                 error!(
226                     "u2f: received an invalid transaction state: {:?}. Ignoring transaction.",
227                     e
228                 );
229                 return Ok(());
230             }
231         }
232 
233         // Fetch the transaction again to check if we are done processing it or if we should wait
234         // for more continuation packets.
235         let transaction = match self.transaction_manager.lock().get_transaction(cid) {
236             Some(t) => t,
237             None => {
238                 error!(
239                     "We lost a transaction on the way. This is a bug. (cid: {})",
240                     cid
241                 );
242                 return Ok(());
243             }
244         };
245         // Check for the end of the transaction
246         if transaction.resp_size >= transaction.resp_bcnt {
247             if self
248                 .transaction_manager
249                 .lock()
250                 .close_transaction(transaction.cid)
251             {
252                 // Resets the device as inactive, since we're not waiting for more data to come
253                 // from the host.
254                 self.set_active(false);
255             }
256         }
257 
258         let mut guest_key = self.guest_key.lock();
259         if guest_key.pending_in_packets.is_empty() {
260             // We start polling waiting to send the data back to the guest.
261             if let Err(e) = guest_key.timer.arm() {
262                 error!(
263                     "Unable to start U2F guest key timer. U2F packets may be lost. {}",
264                     e
265                 );
266             }
267         }
268         guest_key.pending_in_packets.push_back(packet);
269 
270         Ok(())
271     }
272 
273     /// Receives a request from the guest device to write into the actual device on the host.
recv_from_guest( &mut self, packet: [u8; constants::U2FHID_PACKET_SIZE], ) -> Result<usize>274     pub fn recv_from_guest(
275         &mut self,
276         packet: [u8; constants::U2FHID_PACKET_SIZE],
277     ) -> Result<usize> {
278         // The first byte in the host packet request is the HID report request ID as required by
279         // the Linux kernel. The real request data starts from the second byte, so we need to
280         // allocate one extra byte in our write buffer.
281         // See: https://docs.kernel.org/hid/hidraw.html#write
282         let mut host_packet = vec![0; constants::U2FHID_PACKET_SIZE + 1];
283 
284         match InitPacket::from_bytes(packet) {
285             Ok(init_packet) => {
286                 self.start_transaction(&init_packet)?;
287             }
288             Err(Error::InvalidInitPacket) => {
289                 // It's not an init packet, so we don't start a transaction.
290             }
291             Err(e) => {
292                 warn!("Received malformed or invalid u2f-hid init packet, request will be dropped");
293                 return Err(e);
294             }
295         }
296 
297         host_packet[1..].copy_from_slice(&packet);
298 
299         let written = self
300             .fd
301             .lock()
302             .write(&host_packet)
303             .map_err(Error::WriteHidrawDevice)?;
304 
305         if written != host_packet.len() {
306             return Err(Error::WriteHidrawDevice(IOError::new(
307                 ErrorKind::Other,
308                 "Wrote too few bytes to hidraw device.",
309             )));
310         }
311 
312         // we subtract 1 because we added 1 extra byte to the host packet
313         Ok(host_packet.len() - 1)
314     }
315 }
316