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