fido/
messages.rs

1// SPDX-FileCopyrightText: 2025 Foundation Devices, Inc. <hello@foundation.xyz>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4use iso7816::command::{CommandView, FromSliceError};
5use server::{AsScalar, FromScalar};
6
7use crate::error::FidoError;
8use crate::SecurityKeyView;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
11#[rkyv(derive(Debug))]
12pub enum Transport {
13    Usb,
14    Nfc,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum U2fApduParseError {
19    WrongLength,
20    ClassNotSupported,
21}
22
23impl U2fApduParseError {
24    pub fn to_u2f_response(self) -> Vec<u8> {
25        match self {
26            Self::WrongLength => vec![0x67, 0x00],
27            Self::ClassNotSupported => vec![0x6e, 0x00],
28        }
29    }
30}
31
32impl From<FromSliceError> for U2fApduParseError {
33    fn from(error: FromSliceError) -> Self {
34        match error {
35            FromSliceError::InvalidClass => Self::ClassNotSupported,
36            FromSliceError::TooShort
37            | FromSliceError::TooLong
38            | FromSliceError::InvalidFirstBodyByteForExtended
39            | FromSliceError::InvalidSliceLength => Self::WrongLength,
40        }
41    }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
45pub struct U2fApduCommand {
46    pub class: u8,
47    pub instruction: u8,
48    pub p1: u8,
49    pub p2: u8,
50    pub data: Vec<u8>,
51    pub expected: u32,
52    pub extended: bool,
53}
54
55impl U2fApduCommand {
56    pub fn parse(apdu: &[u8]) -> Result<Self, U2fApduParseError> {
57        let command = CommandView::try_from(apdu)?;
58        Ok(Self::from_command_view(command))
59    }
60
61    pub fn from_command_view(command: CommandView<'_>) -> Self {
62        Self {
63            class: command.class().into_inner(),
64            instruction: command.instruction().into(),
65            p1: command.p1,
66            p2: command.p2,
67            data: command.data().to_vec(),
68            expected: command.expected() as u32,
69            extended: command.extended,
70        }
71    }
72
73    pub fn data(&self) -> &[u8] { &self.data }
74}
75
76// === Key change event ===
77
78/// Wrapper for the key list published via events.
79#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
80pub struct KeysChangedEvent {
81    pub keys: Vec<SecurityKeyView>,
82}
83
84// === Key management messages ===
85
86/// Subscribe to key changes. Returns current key list immediately,
87/// then pushes updates whenever keys are modified.
88#[derive(server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
89#[event(KeysChangedEvent)]
90pub struct SubscribeKeyChanges;
91
92// === Presence keep-alive event ===
93
94/// Heartbeat published by the FIDO server every time it replies to an RP with a "retry me"
95/// status (`ConditionNotSatisfied` for U2F, `UserActionPending` for CTAP2). Subscribers — the
96/// Security Keys app while its user-presence modal is up — use these to distinguish an active
97/// RP from an abandoned one and auto-dismiss the modal when the heartbeat stops.
98///
99/// The fingerprint is the SHA-256 of the current in-flight request, exposed so the UI can
100/// ignore heartbeats for a different fingerprint if a newer request has taken over.
101#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
102pub struct PresenceKeepAliveEvent {
103    pub fingerprint: [u8; 32],
104}
105
106/// Subscribe to presence keep-alive events.
107#[derive(server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
108#[event(PresenceKeepAliveEvent)]
109pub struct SubscribePresenceKeepAlive;
110
111// === Operation outcome event ===
112
113/// Whether a completed FIDO operation was a registration or an authentication.
114#[derive(Debug, Clone, Copy, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
115pub enum OperationType {
116    Registration,
117    Authentication,
118}
119
120/// Event published by the FIDO server after a U2F/CTAP operation completes (success or
121/// failure). The Security Keys app is the only subscriber: it shows the success/failure
122/// modal in response. At outcome time the app is guaranteed running (it just handled the
123/// presence prompt), so a subscription is sufficient.
124#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
125pub struct OperationOutcomeEvent {
126    pub security_key_index: usize,
127    pub operation: OperationType,
128    pub success: bool,
129}
130
131/// Subscribe to operation outcome events.
132#[derive(server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
133#[event(OperationOutcomeEvent)]
134pub struct SubscribeOperationOutcomes;
135
136/// Create a new security key with UI metadata. Returns the new key's index, or an error if
137/// creation failed before any state was mutated. Note: a `save_and_notify` failure after a
138/// successful in-memory create is intentionally still returned as `Ok(index)` and only logged
139/// — the new key is usable in this session and worst case is lost on reboot, which is a softer
140/// failure mode than refusing the create over a transient FS or subscriber hiccup.
141#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
142#[response(Result<usize, FidoError>)]
143pub struct CreateSecurityKey {
144    pub label: String,
145    pub color: u8,
146    pub icon: String,
147}
148
149/// Edit metadata of an existing security key. Returns the validation outcome so the GUI
150/// can surface `EmptyLabel`/`DuplicateLabel` without a separate `validate_label` round-trip.
151#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
152#[response(Result<(), FidoError>)]
153pub struct EditSecurityKey {
154    pub index: usize,
155    pub label: String,
156    pub color: u8,
157    pub icon: String,
158    pub date: u64,
159}
160
161/// Set the archived state of a security key.
162/// Archived keys are automatically set to live=false.
163/// Restoring from archive sets live=true.
164#[derive(Debug, server::Message)]
165#[response(Result<(), FidoError>)]
166pub struct SetArchived {
167    pub index: usize,
168    pub archived: bool,
169}
170
171impl AsScalar<2> for SetArchived {
172    fn as_scalar(&self) -> [u32; 2] { [self.index as u32, self.archived as u32] }
173}
174impl FromScalar<2> for SetArchived {
175    fn from_scalar([a, b]: [u32; 2]) -> Self { Self { index: a as usize, archived: b != 0 } }
176}
177
178/// Blocking synchronous snapshot of all security keys. Used at app startup to populate
179/// local state before the async `SubscribeKeyChanges` stream has had a chance to run —
180/// avoids a race where the app (launched by a presence check) concludes "no keys" because
181/// the initial subscribed event hasn't been drained yet.
182#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
183#[response(Vec<SecurityKeyView>)]
184pub struct ListSecurityKeys;
185
186// === Selection messages ===
187
188#[derive(Debug, server::Message)]
189#[response(Option<usize>)]
190pub struct GetSelectedSecurityKey;
191
192/// Fire-and-forget message for selecting a security key.
193#[derive(Debug, server::Message)]
194pub struct SelectSecurityKey(pub Option<usize>);
195
196// === Protocol messages ===
197
198#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
199#[response(Vec<u8>)]
200pub struct U2fProcessApdu {
201    pub command: U2fApduCommand,
202    pub transport: Transport,
203}
204
205#[derive(Debug, Clone, server::Message, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
206#[response(Vec<u8>)]
207pub struct CtapProcessCbor {
208    pub cmd: u8,
209    pub raw: Vec<u8>,
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn u2f_apdu_command_rejects_short_buffers() {
218        for len in 0..4 {
219            let data = vec![0; len];
220            assert_eq!(U2fApduCommand::parse(&data), Err(U2fApduParseError::WrongLength));
221        }
222    }
223
224    #[test]
225    fn u2f_apdu_command_parses_short_apdu() {
226        let command = U2fApduCommand::parse(&[0x00, 0x01, 0x02, 0x00, 0x02, 0xaa, 0xbb, 0x00]).unwrap();
227
228        assert_eq!(command.class, 0x00);
229        assert_eq!(command.instruction, 0x01);
230        assert_eq!(command.p1, 0x02);
231        assert_eq!(command.p2, 0x00);
232        assert_eq!(command.data(), &[0xaa, 0xbb]);
233        assert_eq!(command.expected, 256);
234        assert!(!command.extended);
235    }
236
237    #[test]
238    fn u2f_apdu_command_parses_extended_apdu() {
239        let command =
240            U2fApduCommand::parse(&[0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x02, 0xaa, 0xbb, 0x00, 0x00])
241                .unwrap();
242
243        assert_eq!(command.data(), &[0xaa, 0xbb]);
244        assert_eq!(command.expected, 65_536);
245        assert!(command.extended);
246    }
247}
248
249// === Test messages ===
250
251#[cfg(feature = "test-app")]
252#[derive(Debug, server::Message)]
253#[response(Result<(), FidoError>)]
254pub struct ResetState;