soft_fido2/
response.rs

1//! Response types for CTAP client operations
2//!
3//! Fully decoded, type-safe response structures for credential management commands.
4
5use crate::error::{Error, Result};
6
7use soft_fido2_ctap::cbor::{MapParser, Value};
8use soft_fido2_ctap::types::{PublicKeyCredentialDescriptor, User};
9
10use alloc::string::String;
11use alloc::vec::Vec;
12
13/// Response from authenticatorCredentialManagement - getCredsMetadata (0x01)
14///
15/// Returns metadata about credential storage on the authenticator.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct CredentialsMetadata {
18    /// Number of existing discoverable credentials on the authenticator
19    pub existing_resident_credentials_count: u32,
20
21    /// Maximum number of additional discoverable credentials that can be created
22    ///
23    /// Note: This is an estimate. Actual space depends on algorithm choice,
24    /// user entity information, etc.
25    pub max_possible_remaining_resident_credentials_count: u32,
26}
27
28impl CredentialsMetadata {
29    /// Parse from CBOR response bytes
30    ///
31    /// Expected format:
32    /// ```cbor
33    /// {
34    ///   0x01: existingResidentCredentialsCount (unsigned int),
35    ///   0x02: maxPossibleRemainingResidentCredentialsCount (unsigned int)
36    /// }
37    /// ```
38    pub fn from_cbor(bytes: &[u8]) -> Result<Self> {
39        let parser = MapParser::from_bytes(bytes).map_err(|_| Error::Other)?;
40
41        let existing: i32 = parser.get(0x01).map_err(|_| Error::Other)?;
42        let remaining: i32 = parser.get(0x02).map_err(|_| Error::Other)?;
43
44        Ok(Self {
45            existing_resident_credentials_count: existing as u32,
46            max_possible_remaining_resident_credentials_count: remaining as u32,
47        })
48    }
49}
50
51/// Relying party information
52///
53/// Note: The RP ID may be truncated to 32 bytes per FIDO 2.2 spec §6.8.7.
54/// Truncated IDs contain "…" (U+2026) ellipsis and preserve protocol prefix if present.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct RpInfo {
57    /// RP ID (may be truncated to 32 bytes with ellipsis)
58    pub id: String,
59
60    /// RP name (optional)
61    pub name: Option<String>,
62
63    /// RP ID SHA-256 hash (32 bytes)
64    pub rp_id_hash: [u8; 32],
65}
66
67impl RpInfo {
68    /// Parse from CBOR response components
69    ///
70    /// # Arguments
71    /// * `rp_value` - CBOR map with "id" (text) and optional "name" (text)
72    /// * `rp_id_hash` - 32-byte SHA-256 hash of RP ID
73    pub fn from_cbor_value(rp_value: &Value, rp_id_hash: [u8; 32]) -> Result<Self> {
74        let Value::Map(map) = rp_value else {
75            return Err(Error::Other);
76        };
77
78        let mut id = None;
79        let mut name = None;
80
81        for (k, v) in map {
82            if let Value::Text(key) = k {
83                match key.as_str() {
84                    "id" => {
85                        if let Value::Text(val) = v {
86                            id = Some(val.clone());
87                        }
88                    }
89                    "name" => {
90                        if let Value::Text(val) = v {
91                            name = Some(val.clone());
92                        }
93                    }
94                    _ => {} // Ignore unknown fields
95                }
96            }
97        }
98
99        let id = id.ok_or(Error::Other)?;
100
101        Ok(Self {
102            id,
103            name,
104            rp_id_hash,
105        })
106    }
107}
108
109/// Response from enumerateRPsBegin (0x02)
110///
111/// Contains first RP information and total count.
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct RpEnumerationBeginResponse {
114    /// First RP information
115    pub rp: RpInfo,
116
117    /// Total number of RPs on the authenticator
118    pub total_rps: u32,
119}
120
121impl RpEnumerationBeginResponse {
122    /// Parse from CBOR response bytes
123    ///
124    /// Expected format:
125    /// ```cbor
126    /// {
127    ///   0x03: rp (PublicKeyCredentialRpEntity),
128    ///   0x04: rpIDHash (byte string, 32 bytes),
129    ///   0x05: totalRPs (unsigned int)
130    /// }
131    /// ```
132    pub fn from_cbor(bytes: &[u8]) -> Result<Self> {
133        let parser = MapParser::from_bytes(bytes).map_err(|_| Error::Other)?;
134
135        let rp_value: Value = parser.get(0x03).map_err(|_| Error::Other)?;
136        let rp_id_hash_vec: Vec<u8> = parser.get_bytes(0x04).map_err(|_| Error::Other)?;
137        let total: i32 = parser.get(0x05).map_err(|_| Error::Other)?;
138
139        let mut rp_id_hash = [0u8; 32];
140        if rp_id_hash_vec.len() != 32 {
141            return Err(Error::Other);
142        }
143        rp_id_hash.copy_from_slice(&rp_id_hash_vec);
144
145        let rp = RpInfo::from_cbor_value(&rp_value, rp_id_hash)?;
146
147        Ok(Self {
148            rp,
149            total_rps: total as u32,
150        })
151    }
152}
153
154/// Response from enumerateRPsGetNextRP (0x03)
155///
156/// Contains next RP in the enumeration sequence.
157pub type RpEnumerationNextResponse = RpInfo;
158
159impl RpEnumerationNextResponse {
160    /// Parse from CBOR response bytes
161    ///
162    /// Expected format:
163    /// ```cbor
164    /// {
165    ///   0x03: rp (PublicKeyCredentialRpEntity),
166    ///   0x04: rpIDHash (byte string, 32 bytes)
167    /// }
168    /// ```
169    pub fn from_cbor(bytes: &[u8]) -> Result<Self> {
170        let parser = MapParser::from_bytes(bytes).map_err(|_| Error::Other)?;
171
172        let rp_value: Value = parser.get(0x03).map_err(|_| Error::Other)?;
173        let rp_id_hash_vec: Vec<u8> = parser.get_bytes(0x04).map_err(|_| Error::Other)?;
174
175        let mut rp_id_hash = [0u8; 32];
176        if rp_id_hash_vec.len() != 32 {
177            return Err(Error::Other);
178        }
179        rp_id_hash.copy_from_slice(&rp_id_hash_vec);
180
181        RpInfo::from_cbor_value(&rp_value, rp_id_hash)
182    }
183}
184
185/// Credential information from enumeration
186///
187/// Returned when enumerating credentials for a specific RP.
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct CredentialInfo {
190    /// User information
191    pub user: User,
192
193    /// Credential ID descriptor
194    pub credential_id: PublicKeyCredentialDescriptor,
195
196    /// Public key in COSE_Key format (raw CBOR bytes)
197    ///
198    /// Only present in first credential from enumerateCredentialsBegin.
199    /// Subsequent calls to enumerateCredentialsGetNextCredential omit this field.
200    pub public_key: Option<Vec<u8>>,
201
202    /// Credential protection policy
203    ///
204    /// Values:
205    /// - 0x01: userVerificationOptional
206    /// - 0x02: userVerificationOptionalWithCredentialIdList
207    /// - 0x03: userVerificationRequired
208    pub cred_protect: Option<u8>,
209
210    /// Large blob encryption key (32 bytes)
211    pub large_blob_key: Option<Vec<u8>>,
212
213    /// Whether credential is third-party payment enabled
214    ///
215    /// Only present if authenticator supports thirdPartyPayment extension.
216    pub third_party_payment: Option<bool>,
217}
218
219impl CredentialInfo {
220    /// Parse from CBOR response components
221    fn from_parser(parser: &MapParser) -> Result<Self> {
222        // 0x06: user (required) - Use soft_fido2_ctap::types::User which has correct serde attributes
223        let user: User = parser.get(0x06).map_err(|_| Error::Other)?;
224
225        // 0x07: credentialID (required)
226        let cred_id_value: Value = parser.get(0x07).map_err(|_| Error::Other)?;
227        let credential_id = parse_credential_descriptor(&cred_id_value)?;
228
229        // 0x08: publicKey (optional)
230        let public_key = parser.get_opt::<Vec<u8>>(0x08).ok().flatten();
231
232        // 0x0A: credProtect (optional)
233        let cred_protect = parser.get_opt::<u8>(0x0A).ok().flatten();
234
235        // 0x0B: largeBlobKey (optional)
236        let large_blob_key = if parser.get_raw(0x0B).is_some() {
237            parser.get_bytes(0x0B).ok()
238        } else {
239            None
240        };
241
242        // 0x0C: thirdPartyPayment (optional)
243        let third_party_payment = parser.get_opt::<bool>(0x0C).ok().flatten();
244
245        Ok(Self {
246            user,
247            credential_id,
248            public_key,
249            cred_protect,
250            large_blob_key,
251            third_party_payment,
252        })
253    }
254}
255
256/// Response from enumerateCredentialsBegin (0x04)
257#[derive(Debug, Clone, PartialEq, Eq)]
258pub struct CredentialEnumerationBeginResponse {
259    /// First credential information
260    pub credential: CredentialInfo,
261
262    /// Total number of credentials for this RP
263    pub total_credentials: u32,
264}
265
266impl CredentialEnumerationBeginResponse {
267    /// Parse from CBOR response bytes
268    ///
269    /// Expected format:
270    /// ```cbor
271    /// {
272    ///   0x06: user,
273    ///   0x07: credentialID,
274    ///   0x08: publicKey (optional),
275    ///   0x09: totalCredentials,
276    ///   0x0A: credProtect (optional),
277    ///   0x0B: largeBlobKey (optional),
278    ///   0x0C: thirdPartyPayment (optional)
279    /// }
280    /// ```
281    pub fn from_cbor(bytes: &[u8]) -> Result<Self> {
282        let parser = MapParser::from_bytes(bytes).map_err(|_| Error::Other)?;
283
284        let credential = CredentialInfo::from_parser(&parser)?;
285        let total: i32 = parser.get(0x09).map_err(|_| Error::Other)?;
286
287        Ok(Self {
288            credential,
289            total_credentials: total as u32,
290        })
291    }
292}
293
294/// Response from enumerateCredentialsGetNextCredential (0x05)
295pub type CredentialEnumerationNextResponse = CredentialInfo;
296
297impl CredentialEnumerationNextResponse {
298    /// Parse from CBOR response bytes
299    ///
300    /// Expected format (same as begin but without totalCredentials):
301    /// ```cbor
302    /// {
303    ///   0x06: user,
304    ///   0x07: credentialID,
305    ///   0x0A: credProtect (optional),
306    ///   0x0B: largeBlobKey (optional),
307    ///   0x0C: thirdPartyPayment (optional)
308    /// }
309    /// ```
310    pub fn from_cbor(bytes: &[u8]) -> Result<Self> {
311        let parser = MapParser::from_bytes(bytes).map_err(|_| Error::Other)?;
312        CredentialInfo::from_parser(&parser)
313    }
314}
315
316/// Helper: Parse PublicKeyCredentialDescriptor from CBOR Value
317///
318/// Expected format:
319/// ```cbor
320/// {
321///   "id": credential_id (bytes),
322///   "type": "public-key" (text)
323/// }
324/// ```
325fn parse_credential_descriptor(value: &Value) -> Result<PublicKeyCredentialDescriptor> {
326    let Value::Map(map) = value else {
327        return Err(Error::Other);
328    };
329
330    let mut id = None;
331    let mut cred_type = None;
332
333    for (k, v) in map {
334        if let Value::Text(key) = k {
335            match key.as_str() {
336                "id" => {
337                    if let Value::Bytes(bytes) = v {
338                        id = Some(bytes.clone());
339                    }
340                }
341                "type" => {
342                    if let Value::Text(t) = v {
343                        cred_type = Some(t.clone());
344                    }
345                }
346                _ => {}
347            }
348        }
349    }
350
351    let id = id.ok_or(Error::Other)?;
352    let r#type = cred_type.ok_or(Error::Other)?;
353
354    Ok(PublicKeyCredentialDescriptor {
355        id,
356        r#type,
357        transports: None,
358    })
359}
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364
365    #[test]
366    fn test_credentials_metadata_parsing() {
367        // CBOR: {0x01: 10, 0x02: 50}
368        let cbor = vec![0xa2, 0x01, 0x0a, 0x02, 0x18, 0x32];
369        let metadata = CredentialsMetadata::from_cbor(&cbor).unwrap();
370        assert_eq!(metadata.existing_resident_credentials_count, 10);
371        assert_eq!(
372            metadata.max_possible_remaining_resident_credentials_count,
373            50
374        );
375    }
376
377    #[test]
378    fn test_rp_info_parsing_with_name() {
379        let rp_value = Value::Map(vec![
380            (
381                Value::Text("id".to_string()),
382                Value::Text("example.com".to_string()),
383            ),
384            (
385                Value::Text("name".to_string()),
386                Value::Text("Example".to_string()),
387            ),
388        ]);
389        let hash = [1u8; 32];
390
391        let rp = RpInfo::from_cbor_value(&rp_value, hash).unwrap();
392        assert_eq!(rp.id, "example.com");
393        assert_eq!(rp.name, Some("Example".to_string()));
394        assert_eq!(rp.rp_id_hash, hash);
395    }
396
397    #[test]
398    fn test_rp_info_parsing_without_name() {
399        let rp_value = Value::Map(vec![(
400            Value::Text("id".to_string()),
401            Value::Text("example.com".to_string()),
402        )]);
403        let hash = [2u8; 32];
404
405        let rp = RpInfo::from_cbor_value(&rp_value, hash).unwrap();
406        assert_eq!(rp.id, "example.com");
407        assert_eq!(rp.name, None);
408    }
409
410    #[test]
411    fn test_parse_credential_descriptor() {
412        let desc_value = Value::Map(vec![
413            (Value::Text("id".to_string()), Value::Bytes(vec![1, 2, 3])),
414            (
415                Value::Text("type".to_string()),
416                Value::Text("public-key".to_string()),
417            ),
418        ]);
419
420        let desc = parse_credential_descriptor(&desc_value).unwrap();
421        assert_eq!(desc.id, vec![1, 2, 3]);
422        assert_eq!(desc.r#type, "public-key");
423    }
424}