Skip to main content

soft_fido2/
error.rs

1//! Error types for CTAP operations
2
3#[cfg(feature = "std")]
4use std::fmt;
5
6#[cfg(not(feature = "std"))]
7use core::fmt;
8
9use alloc::string::String;
10
11/// Error type for CTAP operations
12#[non_exhaustive]
13#[derive(Debug, Clone, PartialEq)]
14pub enum Error {
15    /// The given operation was successful
16    Success,
17    /// The given value already exists
18    DoesAlreadyExist,
19    /// The requested value doesn't exist
20    DoesNotExist,
21    /// Credentials can't be inserted into the key-store
22    KeyStoreFull,
23    /// The client ran out of memory
24    OutOfMemory,
25    /// The operation timed out
26    Timeout,
27    /// Unspecified operation
28    Other,
29    /// Initialization failed
30    InitializationFailed,
31    /// Invalid callback result
32    InvalidCallbackResult,
33    /// CBOR command failed
34    CborCommandFailed(i32),
35    /// Invalid client data hash (must be 32 bytes)
36    InvalidClientDataHash,
37    /// No credentials exist for the requested operation
38    ///
39    /// Returned when:
40    /// - Attempting to enumerate credentials for an RP with no credentials
41    /// - Attempting to delete a non-existent credential
42    NoCredentials,
43    /// PIN/UV authentication required but not provided
44    PinAuthRequired,
45    /// PIN/UV auth token has insufficient permissions
46    ///
47    /// The token may not have the required permission bit set,
48    /// or may have the wrong permissions RP ID.
49    UnauthorizedPermission,
50    /// Invalid RP ID hash
51    ///
52    /// RP ID hash must be exactly 32 bytes (SHA-256 output).
53    InvalidRpIdHash,
54    /// PIN/UV auth token has expired
55    PinTokenExpired,
56    /// Invalid subcommand for credential management
57    InvalidSubcommand,
58    /// CTAP error with status code
59    CtapError(u8),
60    /// IO error (from transport operations)
61    IoError(String),
62    /// Invalid PIN length (must be 4-63 characters)
63    InvalidPinLength,
64}
65
66impl fmt::Display for Error {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        match self {
69            Error::Success => write!(f, "Success"),
70            Error::DoesAlreadyExist => write!(f, "Value already exists"),
71            Error::DoesNotExist => write!(f, "Value does not exist"),
72            Error::KeyStoreFull => write!(f, "Key store is full"),
73            Error::OutOfMemory => write!(f, "Out of memory"),
74            Error::Timeout => write!(f, "Operation timed out"),
75            Error::Other => write!(f, "Unspecified error"),
76            Error::InitializationFailed => write!(f, "Initialization failed"),
77            Error::InvalidCallbackResult => write!(f, "Invalid callback result"),
78            Error::CborCommandFailed(code) => {
79                write!(f, "CBOR command failed with code {}", code)
80            }
81            Error::InvalidClientDataHash => {
82                write!(f, "Invalid client data hash (must be 32 bytes)")
83            }
84            Error::NoCredentials => write!(f, "No credentials found"),
85            Error::PinAuthRequired => write!(f, "PIN/UV authentication required"),
86            Error::UnauthorizedPermission => write!(f, "Insufficient permissions"),
87            Error::InvalidRpIdHash => write!(f, "Invalid RP ID hash (must be 32 bytes)"),
88            Error::PinTokenExpired => write!(f, "PIN/UV auth token expired"),
89            Error::InvalidSubcommand => write!(f, "Invalid subcommand"),
90            Error::CtapError(code) => write!(f, "CTAP error: 0x{:02X}", code),
91            Error::IoError(msg) => write!(f, "IO error: {}", msg),
92            Error::InvalidPinLength => write!(f, "Invalid PIN length (must be 4-63 characters)"),
93        }
94    }
95}
96
97#[cfg(feature = "std")]
98impl std::error::Error for Error {}
99
100impl From<i32> for Error {
101    fn from(value: i32) -> Self {
102        match value {
103            0 => Error::Success,
104            -1 => Error::DoesAlreadyExist,
105            -2 => Error::DoesNotExist,
106            -3 => Error::KeyStoreFull,
107            -4 => Error::OutOfMemory,
108            -5 => Error::Timeout,
109            -6 => Error::Other,
110            _ => Error::CborCommandFailed(value),
111        }
112    }
113}
114
115impl From<soft_fido2_ctap::StatusCode> for Error {
116    fn from(status: soft_fido2_ctap::StatusCode) -> Self {
117        use soft_fido2_ctap::StatusCode;
118
119        match status {
120            StatusCode::Success => Error::Success,
121            StatusCode::Timeout | StatusCode::UserActionTimeout | StatusCode::ActionTimeout => {
122                Error::Timeout
123            }
124            StatusCode::KeyStoreFull => Error::KeyStoreFull,
125            StatusCode::NoCredentials => Error::NoCredentials,
126            StatusCode::Other => Error::Other,
127            _ => Error::CtapError(status.to_u8()),
128        }
129    }
130}
131
132impl From<Error> for soft_fido2_ctap::StatusCode {
133    fn from(error: Error) -> Self {
134        use soft_fido2_ctap::StatusCode;
135
136        match error {
137            Error::Success => StatusCode::Success,
138            Error::DoesNotExist | Error::NoCredentials => StatusCode::NoCredentials,
139            Error::KeyStoreFull => StatusCode::KeyStoreFull,
140            Error::Timeout => StatusCode::Timeout,
141            Error::Other => StatusCode::Other,
142            Error::CtapError(code) => StatusCode::from_u8(code),
143            Error::InvalidPinLength => StatusCode::PinPolicyViolation,
144            Error::PinAuthRequired => StatusCode::PuatRequired,
145            Error::UnauthorizedPermission => StatusCode::UnauthorizedPermission,
146            Error::InvalidRpIdHash => StatusCode::InvalidParameter,
147            Error::PinTokenExpired => StatusCode::PinAuthInvalid,
148            Error::InvalidSubcommand => StatusCode::InvalidSubcommand,
149            _ => StatusCode::Other,
150        }
151    }
152}
153
154// Conversion from IO errors
155#[cfg(feature = "std")]
156impl From<std::io::Error> for Error {
157    fn from(error: std::io::Error) -> Self {
158        Error::IoError(error.to_string())
159    }
160}
161
162impl Error {
163    /// Parse CTAP response and extract CBOR data
164    ///
165    /// CTAP responses follow the format: `[status_byte, ...cbor_data]`
166    /// - `0x00` = success, returns the CBOR data
167    /// - `!0x00` = error, converts status byte to Error
168    ///
169    /// This is the single source of truth for CTAP status code handling.
170    pub fn parse_ctap_response(data: &[u8]) -> Result<&[u8]> {
171        if data.is_empty() {
172            return Err(Error::Other);
173        }
174
175        let status_byte = data[0];
176        if status_byte == 0x00 {
177            // Success - return CBOR data (skip status byte)
178            Ok(&data[1..])
179        } else {
180            // Error - convert status byte to StatusCode, then to Error
181            Err(soft_fido2_ctap::StatusCode::from(status_byte).into())
182        }
183    }
184}
185
186/// Result type alias for common operations
187#[cfg(feature = "std")]
188pub type Result<T> = std::result::Result<T, Error>;
189
190#[cfg(not(feature = "std"))]
191pub type Result<T> = core::result::Result<T, Error>;
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use soft_fido2_ctap::StatusCode;
197
198    #[test]
199    fn status_to_error_uses_the_status_registry() {
200        assert_eq!(
201            Error::from(StatusCode::PuatRequired),
202            Error::CtapError(0x36)
203        );
204        assert_eq!(Error::from(StatusCode::UpRequired), Error::CtapError(0x3b));
205        assert_eq!(
206            Error::from(StatusCode::UnauthorizedPermission),
207            Error::CtapError(0x40)
208        );
209    }
210
211    #[test]
212    fn ctap_error_round_trips_through_the_status_registry() {
213        assert_eq!(
214            StatusCode::from(Error::CtapError(0x36)),
215            StatusCode::PuatRequired
216        );
217        assert_eq!(
218            StatusCode::from(Error::CtapError(0x3b)),
219            StatusCode::UpRequired
220        );
221        assert_eq!(StatusCode::from(Error::CtapError(0x38)), StatusCode::Other);
222        assert_eq!(StatusCode::from(Error::CtapError(0x41)), StatusCode::Other);
223    }
224}