1use crate::crypto;
2use crate::ctap2::client_data::{ClientDataHash, CollectedClientData};
3use crate::ctap2::commands::client_pin::{GetPinToken, GetRetries, Pin, PinAuth, PinError};
4use crate::errors::AuthenticatorError;
5use crate::transport::errors::{ApduErrorStatus, HIDError};
6use crate::transport::FidoDevice;
7use serde_cbor::{error::Error as CborError, Value};
8use serde_json as json;
9use std::error::Error as StdErrorT;
10use std::fmt;
11use std::io::{Read, Write};
12
13pub(crate) mod client_pin;
14pub(crate) mod get_assertion;
15pub(crate) mod get_info;
16pub(crate) mod get_next_assertion;
17pub(crate) mod get_version;
18pub(crate) mod make_credentials;
19pub(crate) mod reset;
20pub(crate) mod selection;
21
22pub trait Request<T>
23where
24 Self: fmt::Debug,
25 Self: RequestCtap1<Output = T>,
26 Self: RequestCtap2<Output = T>,
27{
28 fn is_ctap2_request(&self) -> bool;
29}
30
31#[derive(Debug)]
35pub enum Retryable<T> {
36 Retry,
37 Error(T),
38}
39
40impl<T> Retryable<T> {
41 pub fn is_retry(&self) -> bool {
42 matches!(*self, Retryable::Retry)
43 }
44
45 pub fn is_error(&self) -> bool {
46 !self.is_retry()
47 }
48}
49
50impl<T> From<T> for Retryable<T> {
51 fn from(e: T) -> Self {
52 Retryable::Error(e)
53 }
54}
55
56pub trait RequestCtap1: fmt::Debug {
57 type Output;
58
59 fn apdu_format<Dev>(&self, dev: &mut Dev) -> Result<Vec<u8>, HIDError>
60 where
61 Dev: FidoDevice + Read + Write + fmt::Debug;
62
63 fn handle_response_ctap1(
64 &self,
65 status: Result<(), ApduErrorStatus>,
66 input: &[u8],
67 ) -> Result<Self::Output, Retryable<HIDError>>;
68}
69
70pub trait RequestCtap2: fmt::Debug {
71 type Output;
72
73 fn command() -> Command;
74
75 fn wire_format<Dev>(&self, dev: &mut Dev) -> Result<Vec<u8>, HIDError>
76 where
77 Dev: FidoDevice + Read + Write + fmt::Debug;
78
79 fn handle_response_ctap2<Dev>(
80 &self,
81 dev: &mut Dev,
82 input: &[u8],
83 ) -> Result<Self::Output, HIDError>
84 where
85 Dev: FidoDevice + Read + Write + fmt::Debug;
86}
87
88pub(crate) trait PinAuthCommand {
89 fn pin(&self) -> &Option<Pin>;
90 fn set_pin(&mut self, pin: Option<Pin>);
91 fn pin_auth(&self) -> &Option<PinAuth>;
92 fn set_pin_auth(&mut self, pin_auth: Option<PinAuth>);
93 fn client_data(&self) -> &CollectedClientData;
94 fn unset_uv_option(&mut self);
95 fn determine_pin_auth<D: FidoDevice>(&mut self, dev: &mut D) -> Result<(), AuthenticatorError> {
96 if !dev.supports_ctap2() {
97 self.set_pin_auth(None);
98 return Ok(());
99 }
100
101 let client_data_hash = self
102 .client_data()
103 .hash()
104 .map_err(|e| AuthenticatorError::HIDError(HIDError::Command(CommandError::Json(e))))?;
105 let pin_auth = match calculate_pin_auth(dev, &client_data_hash, &self.pin()) {
106 Ok(pin_auth) => pin_auth,
107 Err(e) => {
108 return Err(repackage_pin_errors(dev, e));
109 }
110 };
111 self.set_pin_auth(pin_auth);
112 Ok(())
113 }
114}
115
116pub(crate) fn repackage_pin_errors<D: FidoDevice>(
117 dev: &mut D,
118 error: AuthenticatorError,
119) -> AuthenticatorError {
120 match error {
121 AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
122 StatusCode::PinInvalid,
123 _,
124 ))) => {
125 let cmd = GetRetries::new();
127 let retries = dev.send_cbor(&cmd).ok(); return AuthenticatorError::PinError(PinError::InvalidPin(retries));
129 }
130 AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
131 StatusCode::PinAuthBlocked,
132 _,
133 ))) => {
134 return AuthenticatorError::PinError(PinError::PinAuthBlocked);
135 }
136 AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
137 StatusCode::PinBlocked,
138 _,
139 ))) => {
140 return AuthenticatorError::PinError(PinError::PinBlocked);
141 }
142 AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode(
143 StatusCode::PinRequired,
144 _,
145 ))) => {
146 return AuthenticatorError::PinError(PinError::PinRequired);
147 }
148 err => {
151 return err;
152 }
153 }
154}
155
156#[repr(u8)]
159#[derive(Debug)]
160pub enum Command {
161 MakeCredentials = 0x01,
162 GetAssertion = 0x02,
163 GetInfo = 0x04,
164 ClientPin = 0x06,
165 Reset = 0x07,
166 GetNextAssertion = 0x08,
167 Selection = 0x0B,
168}
169
170impl Command {
171 #[cfg(test)]
172 pub fn from_u8(v: u8) -> Option<Command> {
173 match v {
174 0x01 => Some(Command::MakeCredentials),
175 0x02 => Some(Command::GetAssertion),
176 0x04 => Some(Command::GetInfo),
177 0x06 => Some(Command::ClientPin),
178 0x07 => Some(Command::Reset),
179 0x08 => Some(Command::GetNextAssertion),
180 _ => None,
181 }
182 }
183}
184
185#[derive(Debug)]
186pub enum StatusCode {
187 OK,
189 InvalidCommand,
191 InvalidParameter,
193 InvalidLength,
195 InvalidSeq,
197 Timeout,
199 ChannelBusy,
201 LockRequired,
203 InvalidChannel,
205 CBORUnexpectedType,
207 InvalidCBOR,
209 MissingParameter,
211 LimitExceeded,
213 UnsupportedExtension,
215 CredentialExcluded,
217 Processing,
219 InvalidCredential,
221 UserActionPending,
223 OperationPending,
225 NoOperations,
227 UnsupportedAlgorithm,
229 OperationDenied,
231 KeyStoreFull,
233 NoOperationPending,
235 UnsupportedOption,
237 InvalidOption,
239 KeepaliveCancel,
241 NoCredentials,
243 UserActionTimeout,
245 NotAllowed,
248 PinInvalid,
250 PinBlocked,
252 PinAuthInvalid,
254 PinAuthBlocked,
256 PinNotSet,
258 PinRequired,
260 PinPolicyViolation,
262 PinTokenExpired,
264 RequestTooLarge,
266 ActionTimeout,
268 UpRequired,
270
271 Unknown(u8),
273}
274
275impl StatusCode {
276 fn is_ok(&self) -> bool {
277 matches!(*self, StatusCode::OK)
278 }
279
280 fn device_busy(&self) -> bool {
281 matches!(*self, StatusCode::ChannelBusy)
282 }
283}
284
285impl From<u8> for StatusCode {
286 fn from(value: u8) -> StatusCode {
287 match value {
288 0x00 => StatusCode::OK,
289 0x01 => StatusCode::InvalidCommand,
290 0x02 => StatusCode::InvalidParameter,
291 0x03 => StatusCode::InvalidLength,
292 0x04 => StatusCode::InvalidSeq,
293 0x05 => StatusCode::Timeout,
294 0x06 => StatusCode::ChannelBusy,
295 0x0A => StatusCode::LockRequired,
296 0x0B => StatusCode::InvalidChannel,
297 0x11 => StatusCode::CBORUnexpectedType,
298 0x12 => StatusCode::InvalidCBOR,
299 0x14 => StatusCode::MissingParameter,
300 0x15 => StatusCode::LimitExceeded,
301 0x16 => StatusCode::UnsupportedExtension,
302 0x19 => StatusCode::CredentialExcluded,
303 0x21 => StatusCode::Processing,
304 0x22 => StatusCode::InvalidCredential,
305 0x23 => StatusCode::UserActionPending,
306 0x24 => StatusCode::OperationPending,
307 0x25 => StatusCode::NoOperations,
308 0x26 => StatusCode::UnsupportedAlgorithm,
309 0x27 => StatusCode::OperationDenied,
310 0x28 => StatusCode::KeyStoreFull,
311 0x2A => StatusCode::NoOperationPending,
312 0x2B => StatusCode::UnsupportedOption,
313 0x2C => StatusCode::InvalidOption,
314 0x2D => StatusCode::KeepaliveCancel,
315 0x2E => StatusCode::NoCredentials,
316 0x2f => StatusCode::UserActionTimeout,
317 0x30 => StatusCode::NotAllowed,
318 0x31 => StatusCode::PinInvalid,
319 0x32 => StatusCode::PinBlocked,
320 0x33 => StatusCode::PinAuthInvalid,
321 0x34 => StatusCode::PinAuthBlocked,
322 0x35 => StatusCode::PinNotSet,
323 0x36 => StatusCode::PinRequired,
324 0x37 => StatusCode::PinPolicyViolation,
325 0x38 => StatusCode::PinTokenExpired,
326 0x39 => StatusCode::RequestTooLarge,
327 0x3A => StatusCode::ActionTimeout,
328 0x3B => StatusCode::UpRequired,
329
330 othr => StatusCode::Unknown(othr),
331 }
332 }
333}
334
335#[cfg(test)]
336impl Into<u8> for StatusCode {
337 fn into(self) -> u8 {
338 match self {
339 StatusCode::OK => 0x00,
340 StatusCode::InvalidCommand => 0x01,
341 StatusCode::InvalidParameter => 0x02,
342 StatusCode::InvalidLength => 0x03,
343 StatusCode::InvalidSeq => 0x04,
344 StatusCode::Timeout => 0x05,
345 StatusCode::ChannelBusy => 0x06,
346 StatusCode::LockRequired => 0x0A,
347 StatusCode::InvalidChannel => 0x0B,
348 StatusCode::CBORUnexpectedType => 0x11,
349 StatusCode::InvalidCBOR => 0x12,
350 StatusCode::MissingParameter => 0x14,
351 StatusCode::LimitExceeded => 0x15,
352 StatusCode::UnsupportedExtension => 0x16,
353 StatusCode::CredentialExcluded => 0x19,
354 StatusCode::Processing => 0x21,
355 StatusCode::InvalidCredential => 0x22,
356 StatusCode::UserActionPending => 0x23,
357 StatusCode::OperationPending => 0x24,
358 StatusCode::NoOperations => 0x25,
359 StatusCode::UnsupportedAlgorithm => 0x26,
360 StatusCode::OperationDenied => 0x27,
361 StatusCode::KeyStoreFull => 0x28,
362 StatusCode::NoOperationPending => 0x2A,
363 StatusCode::UnsupportedOption => 0x2B,
364 StatusCode::InvalidOption => 0x2C,
365 StatusCode::KeepaliveCancel => 0x2D,
366 StatusCode::NoCredentials => 0x2E,
367 StatusCode::UserActionTimeout => 0x2f,
368 StatusCode::NotAllowed => 0x30,
369 StatusCode::PinInvalid => 0x31,
370 StatusCode::PinBlocked => 0x32,
371 StatusCode::PinAuthInvalid => 0x33,
372 StatusCode::PinAuthBlocked => 0x34,
373 StatusCode::PinNotSet => 0x35,
374 StatusCode::PinRequired => 0x36,
375 StatusCode::PinPolicyViolation => 0x37,
376 StatusCode::PinTokenExpired => 0x38,
377 StatusCode::RequestTooLarge => 0x39,
378 StatusCode::ActionTimeout => 0x3A,
379 StatusCode::UpRequired => 0x3B,
380
381 StatusCode::Unknown(othr) => othr,
382 }
383 }
384}
385
386#[derive(Debug)]
387pub enum CommandError {
388 InputTooSmall,
389 MissingRequiredField(&'static str),
390 Deserializing(CborError),
391 Serializing(CborError),
392 StatusCode(StatusCode, Option<Value>),
393 Json(json::Error),
394 Crypto(crypto::CryptoError),
395 UnsupportedPinProtocol,
396}
397
398impl fmt::Display for CommandError {
399 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
400 match *self {
401 CommandError::InputTooSmall => write!(f, "CommandError: Input is too small"),
402 CommandError::MissingRequiredField(field) => {
403 write!(f, "CommandError: Missing required field {}", field)
404 }
405 CommandError::Deserializing(ref e) => {
406 write!(f, "CommandError: Error while parsing: {}", e)
407 }
408 CommandError::Serializing(ref e) => {
409 write!(f, "CommandError: Error while serializing: {}", e)
410 }
411 CommandError::StatusCode(ref code, ref value) => {
412 write!(f, "CommandError: Unexpected code: {:?} ({:?})", code, value)
413 }
414 CommandError::Json(ref e) => write!(f, "CommandError: Json serializing error: {}", e),
415 CommandError::Crypto(ref e) => write!(f, "CommandError: Crypto error: {:?}", e),
416 CommandError::UnsupportedPinProtocol => {
417 write!(f, "CommandError: Pin protocol is not supported")
418 }
419 }
420 }
421}
422
423impl StdErrorT for CommandError {}
424
425pub(crate) fn calculate_pin_auth<Dev>(
426 dev: &mut Dev,
427 client_data_hash: &ClientDataHash,
428 pin: &Option<Pin>,
429) -> Result<Option<PinAuth>, AuthenticatorError>
430where
431 Dev: FidoDevice,
432{
433 let (shared_secret, info) = dev.establish_shared_secret()?;
436
437 let pin_auth = if info.options.client_pin == Some(true) {
440 let pin = pin
441 .as_ref()
442 .ok_or(HIDError::Command(CommandError::StatusCode(
443 StatusCode::PinRequired,
444 None,
445 )))?;
446
447 let pin_command = GetPinToken::new(&info, &shared_secret, &pin)?;
448 let pin_token = dev.send_cbor(&pin_command)?;
449
450 Some(
451 pin_token
452 .auth(client_data_hash.as_ref())
453 .map_err(CommandError::Crypto)?,
454 )
455 } else {
456 None
457 };
458
459 Ok(pin_auth)
460}