devices/usb/backend/fido_backend/
fido_device.rs

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