fido/
lib.rs

1// SPDX-FileCopyrightText: 2025 Foundation Devices, Inc. <hello@foundation.xyz>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4pub mod api;
5mod attestation_cert;
6mod ctap;
7pub mod error;
8mod implementation;
9pub mod messages;
10mod nav_thread;
11mod u2f;
12
13use ctap::{PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity};
14use error::FidoError;
15
16security::use_api!();
17
18#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
19pub struct RegisteredKeyU2f {
20    pub application_parameter: [u8; 32],
21    pub signature_counter: u32,
22    pub registered_timestamp: u32,
23}
24
25#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
26pub struct RegisteredKeyCtap {
27    pub rp: PublicKeyCredentialRpEntity,
28    pub user: PublicKeyCredentialUserEntity,
29    pub signature_counter: u32,
30    pub registered_timestamp: u32,
31}
32
33#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
34pub enum RegisteredKey {
35    U2f(RegisteredKeyU2f),
36    Ctap(RegisteredKeyCtap),
37}
38
39crypto::use_api!();
40
41impl RegisteredKey {
42    pub(crate) fn signature_counter(&self) -> u32 {
43        match self {
44            RegisteredKey::U2f(key) => key.signature_counter,
45            RegisteredKey::Ctap(key) => key.signature_counter,
46        }
47    }
48
49    pub(crate) fn inc_signature_counter(&mut self) -> u32 {
50        match self {
51            RegisteredKey::U2f(key) => {
52                key.signature_counter += 1;
53                key.signature_counter
54            }
55            RegisteredKey::Ctap(key) => {
56                key.signature_counter += 1;
57                key.signature_counter
58            }
59        }
60    }
61}
62
63#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
64pub struct SecurityKey {
65    pub registered_keys: Vec<RegisteredKey>,
66    pub live: bool,
67    #[serde(default)]
68    pub label: String,
69    #[serde(default)]
70    pub color: u8,
71    #[serde(default)]
72    pub icon: String,
73    #[serde(default)]
74    pub archived: bool,
75    #[serde(default)]
76    pub date: u64,
77}
78
79/// View of a SecurityKey exposed to UI clients via IPC.
80/// Must be serializable with rkyv for archive events/messages.
81#[derive(
82    Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, serde::Serialize, serde::Deserialize,
83)]
84pub struct SecurityKeyView {
85    pub index: usize,
86    pub label: String,
87    pub color: u8,
88    pub icon: String,
89    pub archived: bool,
90    pub live: bool,
91    pub date: u64,
92    pub registered_count: usize,
93}
94
95impl SecurityKey {
96    /// Convert to a view suitable for IPC, given the key's index.
97    pub fn to_view(&self, index: usize) -> SecurityKeyView {
98        SecurityKeyView {
99            index,
100            label: self.label.clone(),
101            color: self.color,
102            icon: self.icon.clone(),
103            archived: self.archived,
104            live: self.live,
105            date: self.date,
106            registered_count: self.registered_keys.len(),
107        }
108    }
109
110    /// Set archived state. Archived keys are automatically not live.
111    pub fn set_archived(&mut self, archived: bool) {
112        self.archived = archived;
113        if archived {
114            self.live = false;
115        } else {
116            self.live = true;
117        }
118    }
119}
120
121impl SecurityKey {
122    fn registered_key(&self, index: usize) -> Result<&RegisteredKey, FidoError> {
123        self.registered_keys.get(index).ok_or(FidoError::InvalidIndex)
124    }
125
126    fn registered_key_mut(&mut self, index: usize) -> Result<&mut RegisteredKey, FidoError> {
127        self.registered_keys.get_mut(index).ok_or(FidoError::InvalidIndex)
128    }
129
130    fn registered_key_indexes(&self, u2f: bool, tag: &[u8]) -> Vec<usize> {
131        self.registered_keys
132            .iter()
133            .enumerate()
134            .filter_map(|(index, key)| match (u2f, key) {
135                (true, RegisteredKey::U2f(key)) if key.application_parameter == tag => Some(index),
136                (false, RegisteredKey::Ctap(key)) if key.rp.id.as_bytes() == tag => Some(index),
137                _ => None,
138            })
139            .collect()
140    }
141}
142
143pub fn listen() {
144    let (security, app_seed) = implementation::wait();
145    server::listen(implementation::FidoServer::new(security, app_seed).unwrap())
146}
147
148#[cfg(test)]
149mod tests {
150    use chrono::Utc;
151
152    use super::*;
153
154    #[test]
155    fn inc_signature_counter() {
156        let mut key = RegisteredKey::U2f(RegisteredKeyU2f {
157            application_parameter: [1; 32],
158            signature_counter: 0,
159            registered_timestamp: Utc::now().timestamp() as u32,
160        });
161        assert_eq!(key.signature_counter(), 0);
162        assert_eq!(key.inc_signature_counter(), 1);
163        assert_eq!(key.signature_counter(), 1);
164    }
165
166    #[test]
167    fn registered_key_indexes() {
168        let security_key = SecurityKey {
169            registered_keys: vec![
170                RegisteredKey::Ctap(RegisteredKeyCtap {
171                    rp: PublicKeyCredentialRpEntity { id: "test.com".to_string(), name: None },
172                    user: PublicKeyCredentialUserEntity {
173                        id: vec![12],
174                        name: None,
175                        display_name: None,
176                        icon: None,
177                    },
178                    signature_counter: 0,
179                    registered_timestamp: Utc::now().timestamp() as u32,
180                }),
181                RegisteredKey::Ctap(RegisteredKeyCtap {
182                    rp: PublicKeyCredentialRpEntity { id: "test2.com".to_string(), name: None },
183                    user: PublicKeyCredentialUserEntity {
184                        id: vec![13],
185                        name: None,
186                        display_name: None,
187                        icon: None,
188                    },
189                    signature_counter: 1,
190                    registered_timestamp: Utc::now().timestamp() as u32,
191                }),
192                RegisteredKey::Ctap(RegisteredKeyCtap {
193                    rp: PublicKeyCredentialRpEntity { id: "test.com".to_string(), name: None },
194                    user: PublicKeyCredentialUserEntity {
195                        id: vec![14],
196                        name: None,
197                        display_name: None,
198                        icon: None,
199                    },
200                    signature_counter: 0,
201                    registered_timestamp: Utc::now().timestamp() as u32,
202                }),
203                RegisteredKey::U2f(RegisteredKeyU2f {
204                    application_parameter: [1; 32],
205                    signature_counter: 0,
206                    registered_timestamp: Utc::now().timestamp() as u32,
207                }),
208                RegisteredKey::U2f(RegisteredKeyU2f {
209                    application_parameter: [2; 32],
210                    signature_counter: 0,
211                    registered_timestamp: Utc::now().timestamp() as u32,
212                }),
213                RegisteredKey::U2f(RegisteredKeyU2f {
214                    application_parameter: [1; 32],
215                    signature_counter: 0,
216                    registered_timestamp: Utc::now().timestamp() as u32,
217                }),
218            ],
219            live: false,
220            ..Default::default()
221        };
222        let registered_key_indexes_ctap = security_key.registered_key_indexes(false, "test.com".as_bytes());
223        println!("{:?}", registered_key_indexes_ctap);
224        assert_eq!(registered_key_indexes_ctap, vec![0, 2]);
225        let registered_key_indexes_u2f = security_key.registered_key_indexes(true, &[1; 32]);
226        println!("{:?}", registered_key_indexes_u2f);
227        assert_eq!(registered_key_indexes_u2f, vec![3, 5]);
228    }
229}