From 37da72046580d3ac163b05bdd40863301f025d26 Mon Sep 17 00:00:00 2001 From: Isaiah Inuwa Date: Sat, 25 Apr 2026 07:42:22 -0500 Subject: [PATCH] Simplify BackgroundEvent - Change BackgroundEvent tag to u32 from u8 - Flatten BackgroundEvent D-Bus struct --- .vscode/launch.json | 4 +- credentialsd-common/src/client.rs | 4 +- credentialsd-common/src/model.rs | 11 +- credentialsd-common/src/server.rs | 763 ++++++------------ credentialsd-ui/src/client.rs | 2 +- credentialsd-ui/src/dbus.rs | 4 +- credentialsd-ui/src/gui/view_model/gtk/mod.rs | 4 +- credentialsd-ui/src/gui/view_model/mod.rs | 283 +++---- credentialsd/src/credential_service/hybrid.rs | 14 + credentialsd/src/credential_service/nfc.rs | 39 +- credentialsd/src/credential_service/usb.rs | 35 +- credentialsd/src/dbus/flow_control.rs | 47 +- credentialsd/src/dbus/ui_control.rs | 3 +- doc/api.md | 302 +++---- 14 files changed, 601 insertions(+), 914 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 42702bf..9e3de3f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,8 +12,8 @@ "args": [], "env": { "RUST_LOG": "credentialsd=debug,libwebauthn=debug,libwebauthn::webauthn=debug,libwebauthn=warn,libwebauthn::proto::ctap2::preflight=debug,libwebauthn::transport::channel=debug,zbus::object_server::debug,zbus=debug", - "CREDSD_TRUSTED_CALLERS": "/usr/bin/python3.14", - "CREDSD_TRUSTED_APP_IDS": "app:xyz.iinuwa.credentialsd.DemoCredentialsUi", + "CREDSD_TRUSTED_CALLERS": "/usr/bin/python3.14,${workspaceFolder}/../../portal/xdg-desktop-portal/build/src/xdg-desktop-portal", + "CREDSD_TRUSTED_APP_IDS": "xyz.iinuwa.credentialsd.DemoCredentialsUi", }, "sourceLanguages": [ "rust" diff --git a/credentialsd-common/src/client.rs b/credentialsd-common/src/client.rs index 1bff01d..e165c93 100644 --- a/credentialsd-common/src/client.rs +++ b/credentialsd-common/src/client.rs @@ -3,8 +3,8 @@ use std::pin::Pin; use futures_lite::Stream; use crate::{ - model::{BackgroundEvent, Device}, - server::RequestId, + model::Device, + server::{BackgroundEvent, RequestId}, }; /// Used for communication from trusted UI to credential service diff --git a/credentialsd-common/src/model.rs b/credentialsd-common/src/model.rs index dd6fb7c..e32f048 100644 --- a/credentialsd-common/src/model.rs +++ b/credentialsd-common/src/model.rs @@ -122,11 +122,13 @@ pub struct RequestingParty { pub origin: String, } +// TODO: Move to credentialsd-ui #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ViewUpdate { SetTitle((String, String)), SetDevices(Vec), - SetCredentials(Vec), + // TODO: Fix this + SetCredentials(Vec), WaitingForDevice(Device), SelectingDevice, @@ -252,13 +254,6 @@ pub enum NfcState { Failed(Error), } -#[derive(Clone, Debug)] -pub enum BackgroundEvent { - UsbStateChanged(UsbState), - HybridQrStateChanged(HybridState), - NfcStateChanged(NfcState), -} - #[derive(Debug, Clone)] pub enum Error { /// Some unknown error with the authenticator occurred. diff --git a/credentialsd-common/src/server.rs b/credentialsd-common/src/server.rs index b318ffe..dcdd105 100644 --- a/credentialsd-common/src/server.rs +++ b/credentialsd-common/src/server.rs @@ -11,29 +11,152 @@ use zvariant::{ SerializeDict, Signature, Structure, StructureBuilder, Type, Value, signature::Fields, }; -use crate::model::{BackgroundEvent, Device, Operation, RequestingApplication}; +use crate::model::{Device, Operation, RequestingApplication}; const TAG_VALUE_SIGNATURE: &Signature = &Signature::Structure(Fields::Static { - fields: &[&Signature::U8, &Signature::Variant], + fields: &[&Signature::U32, &Signature::Variant], }); +/// Ceremony completed successfully +const BACKGROUND_EVENT_CEREMONY_COMPLETED: u32 = 0x01; +/// Device needs the client PIN to be entered. The backend should collect the +/// PIN and send it back with `EnterClientPin` event of `UserInteracted` signal. +const BACKGROUND_EVENT_NEEDS_PIN: u32 = 0x10; +const BACKGROUND_EVENT_NEEDS_USER_VERIFICATION: u32 = 0x11; +const BACKGROUND_EVENT_NEEDS_USER_PRESENCE: u32 = 0x12; +const BACKGROUND_EVENT_SELECTING_CREDENTIAL: u32 = 0x13; + +const BACKGROUND_EVENT_HYBRID_IDLE: u32 = 0x20; +const BACKGROUND_EVENT_HYBRID_STARTED: u32 = 0x21; +const BACKGROUND_EVENT_HYBRID_CONNECTING: u32 = 0x22; +const BACKGROUND_EVENT_HYBRID_CONNECTED: u32 = 0x23; + +const BACKGROUND_EVENT_NFC_IDLE: u32 = 0x30; +const BACKGROUND_EVENT_NFC_WAITING: u32 = 0x31; +const BACKGROUND_EVENT_NFC_CONNECTED: u32 = 0x32; + +const BACKGROUND_EVENT_USB_IDLE: u32 = 0x40; +const BACKGROUND_EVENT_USB_WAITING: u32 = 0x41; +const BACKGROUND_EVENT_USB_SELECTING_DEVICE: u32 = 0x42; +const BACKGROUND_EVENT_USB_CONNECTED: u32 = 0x43; + +const BACKGROUND_EVENT_ERROR_INTERNAL: u32 = 0x80000001; +const BACKGROUND_EVENT_ERROR_TIMED_OUT: u32 = 0x80000002; +const BACKGROUND_EVENT_ERROR_CANCELLED: u32 = 0x80000003; +const BACKGROUND_EVENT_ERROR_AUTHENTICATOR: u32 = 0x80000004; +const BACKGROUND_EVENT_ERROR_NO_CREDENTIALS: u32 = 0x80000005; +const BACKGROUND_EVENT_ERROR_CREDENTIAL_EXCLUDED: u32 = 0x80000006; +const BACKGROUND_EVENT_ERROR_PIN_ATTEMPTS_EXHAUSTED: u32 = 0x80000007; +const BACKGROUND_EVENT_ERROR_PIN_NOT_SET: u32 = 0x80000008; + +/// Flattened enum BackgroundEvent for sending across D-Bus. +#[derive(Debug, Clone, PartialEq)] +pub enum BackgroundEvent { + CeremonyCompleted, + NeedsPin { attempts_left: Option }, + NeedsUserVerification { attempts_left: Option }, + NeedsUserPresence, + SelectingCredential { creds: Vec }, + + HybridIdle, + HybridStarted(String), + HybridConnecting, + HybridConnected, + + NfcIdle, + NfcWaiting, + NfcConnected, + + UsbIdle, + UsbWaiting, + UsbSelectingDevice, + UsbConnected, + + ErrorInternal, + ErrorTimedOut, + ErrorCancelled, + ErrorAuthenticator, + ErrorNoCredentials, + ErrorCredentialExcluded, + ErrorPinAttemptsExhausted, + ErrorPinNotSet, +} + +impl BackgroundEvent { + fn tag(&self) -> u32 { + match self { + Self::CeremonyCompleted => BACKGROUND_EVENT_CEREMONY_COMPLETED, + Self::NeedsPin { .. } => BACKGROUND_EVENT_NEEDS_PIN, + Self::NeedsUserVerification { .. } => BACKGROUND_EVENT_NEEDS_USER_VERIFICATION, + Self::NeedsUserPresence => BACKGROUND_EVENT_NEEDS_USER_PRESENCE, + Self::SelectingCredential { .. } => BACKGROUND_EVENT_SELECTING_CREDENTIAL, + + Self::HybridIdle => BACKGROUND_EVENT_HYBRID_IDLE, + Self::HybridStarted(_) => BACKGROUND_EVENT_HYBRID_STARTED, + Self::HybridConnecting => BACKGROUND_EVENT_HYBRID_CONNECTING, + Self::HybridConnected => BACKGROUND_EVENT_HYBRID_CONNECTED, + + Self::NfcIdle => BACKGROUND_EVENT_NFC_IDLE, + Self::NfcWaiting => BACKGROUND_EVENT_NFC_WAITING, + Self::NfcConnected => BACKGROUND_EVENT_NFC_CONNECTED, + + Self::UsbIdle => BACKGROUND_EVENT_USB_IDLE, + Self::UsbWaiting => BACKGROUND_EVENT_USB_WAITING, + Self::UsbSelectingDevice => BACKGROUND_EVENT_USB_SELECTING_DEVICE, + Self::UsbConnected => BACKGROUND_EVENT_USB_CONNECTED, + + Self::ErrorInternal => BACKGROUND_EVENT_ERROR_INTERNAL, + Self::ErrorTimedOut => BACKGROUND_EVENT_ERROR_TIMED_OUT, + Self::ErrorCancelled => BACKGROUND_EVENT_ERROR_CANCELLED, + Self::ErrorAuthenticator => BACKGROUND_EVENT_ERROR_AUTHENTICATOR, + Self::ErrorNoCredentials => BACKGROUND_EVENT_ERROR_NO_CREDENTIALS, + Self::ErrorCredentialExcluded => BACKGROUND_EVENT_ERROR_CREDENTIAL_EXCLUDED, + Self::ErrorPinAttemptsExhausted => BACKGROUND_EVENT_ERROR_PIN_ATTEMPTS_EXHAUSTED, + Self::ErrorPinNotSet => BACKGROUND_EVENT_ERROR_PIN_NOT_SET, + } + } +} + impl Type for BackgroundEvent { const SIGNATURE: &'static Signature = TAG_VALUE_SIGNATURE; } impl From<&BackgroundEvent> for Structure<'_> { fn from(value: &BackgroundEvent) -> Self { - match value { - BackgroundEvent::UsbStateChanged(state) => { - tag_value_to_struct(0x01, Some(Value::Structure(state.into()))) - } - BackgroundEvent::HybridQrStateChanged(state) => { - tag_value_to_struct(0x02, Some(Value::Structure(state.into()))) + let tag = value.tag(); + let payload = match value { + // States with payloads + BackgroundEvent::NeedsPin { attempts_left } => { + Some(Value::U32(attempts_left.map(u32::from).unwrap_or(u32::MAX))) } - BackgroundEvent::NfcStateChanged(state) => { - tag_value_to_struct(0x03, Some(Value::Structure(state.into()))) + BackgroundEvent::NeedsUserVerification { attempts_left } => { + Some(Value::U32(attempts_left.map(u32::from).unwrap_or(u32::MAX))) } - } + BackgroundEvent::SelectingCredential { creds } => Some(Value::Array(creds.into())), + BackgroundEvent::HybridStarted(qr_data) => Some(Value::Str(qr_data.into())), + // Empty + BackgroundEvent::CeremonyCompleted => None, + BackgroundEvent::NeedsUserPresence => None, + BackgroundEvent::HybridIdle => None, + BackgroundEvent::HybridConnecting => None, + BackgroundEvent::HybridConnected => None, + BackgroundEvent::NfcIdle => None, + BackgroundEvent::NfcWaiting => None, + BackgroundEvent::NfcConnected => None, + BackgroundEvent::UsbIdle => None, + BackgroundEvent::UsbWaiting => None, + BackgroundEvent::UsbSelectingDevice => None, + BackgroundEvent::UsbConnected => None, + BackgroundEvent::ErrorInternal => None, + BackgroundEvent::ErrorTimedOut => None, + BackgroundEvent::ErrorCancelled => None, + BackgroundEvent::ErrorAuthenticator => None, + BackgroundEvent::ErrorNoCredentials => None, + BackgroundEvent::ErrorCredentialExcluded => None, + BackgroundEvent::ErrorPinAttemptsExhausted => None, + BackgroundEvent::ErrorPinNotSet => None, + }; + tag_value_to_struct(tag, payload) } } @@ -44,20 +167,70 @@ impl TryFrom<&Structure<'_>> for BackgroundEvent { let (tag, value) = parse_tag_value_struct(value)?; match tag { - 0x01 => { - let structure: Structure = value.downcast_ref()?; - Ok(BackgroundEvent::UsbStateChanged((&structure).try_into()?)) + BACKGROUND_EVENT_CEREMONY_COMPLETED => Ok(Self::CeremonyCompleted), + BACKGROUND_EVENT_NEEDS_PIN => value.downcast::().map(|attempts_left| { + if attempts_left == u32::MAX { + Self::NeedsPin { + attempts_left: None, + } + } else { + Self::NeedsPin { + attempts_left: Some(attempts_left), + } + } + }), + BACKGROUND_EVENT_NEEDS_USER_VERIFICATION => { + value.downcast::().map(|attempts_left| { + if attempts_left == u32::MAX { + Self::NeedsUserVerification { + attempts_left: None, + } + } else { + Self::NeedsUserVerification { + attempts_left: Some(attempts_left), + } + } + }) } - 0x02 => { - let structure: Structure = value.downcast_ref()?; - Ok(BackgroundEvent::HybridQrStateChanged( - (&structure).try_into()?, - )) + BACKGROUND_EVENT_NEEDS_USER_PRESENCE => Ok(Self::NeedsUserPresence), + BACKGROUND_EVENT_SELECTING_CREDENTIAL => { + let creds: Array = value.downcast_ref()?; + let creds: Result, zvariant::Error> = creds + .iter() + .map(|v| v.try_to_owned().unwrap()) + .map(|v| { + let cred: Result = Value::from(v) + .downcast::() + .map(Credential::from); + cred + }) + .collect(); + Ok(Self::SelectingCredential { creds: creds? }) } - 0x03 => { - let structure: Structure = value.downcast_ref()?; - Ok(BackgroundEvent::NfcStateChanged((&structure).try_into()?)) + + BACKGROUND_EVENT_HYBRID_IDLE => Ok(Self::HybridIdle), + BACKGROUND_EVENT_HYBRID_STARTED => { + let qr_data = value.downcast_ref::<&str>()?; + Ok(Self::HybridStarted(qr_data.to_string())) } + BACKGROUND_EVENT_HYBRID_CONNECTING => Ok(Self::HybridConnecting), + BACKGROUND_EVENT_HYBRID_CONNECTED => Ok(Self::HybridConnected), + + BACKGROUND_EVENT_NFC_IDLE => Ok(Self::NfcIdle), + BACKGROUND_EVENT_NFC_WAITING => Ok(Self::NfcWaiting), + BACKGROUND_EVENT_NFC_CONNECTED => Ok(Self::NfcConnected), + + BACKGROUND_EVENT_USB_IDLE => Ok(Self::UsbIdle), + BACKGROUND_EVENT_USB_WAITING => Ok(Self::UsbWaiting), + BACKGROUND_EVENT_USB_SELECTING_DEVICE => Ok(Self::UsbSelectingDevice), + BACKGROUND_EVENT_USB_CONNECTED => Ok(Self::UsbConnected), + + BACKGROUND_EVENT_ERROR_AUTHENTICATOR => Ok(Self::ErrorAuthenticator), + BACKGROUND_EVENT_ERROR_NO_CREDENTIALS => Ok(Self::ErrorNoCredentials), + BACKGROUND_EVENT_ERROR_PIN_ATTEMPTS_EXHAUSTED => Ok(Self::ErrorPinAttemptsExhausted), + BACKGROUND_EVENT_ERROR_INTERNAL => Ok(Self::ErrorInternal), + BACKGROUND_EVENT_ERROR_TIMED_OUT => Ok(Self::ErrorTimedOut), + BACKGROUND_EVENT_ERROR_CANCELLED => Ok(Self::ErrorCancelled), _ => Err(zvariant::Error::Message(format!( "Unknown BackgroundEvent tag : {tag}" ))), @@ -143,12 +316,12 @@ impl From for CreateCredentialResponse { } } -#[derive(SerializeDict, DeserializeDict, Type, Value)] +#[derive(Debug, Clone, SerializeDict, DeserializeDict, PartialEq, Type, Value)] #[zvariant(signature = "dict")] pub struct Credential { - id: String, - name: String, - username: Optional, + pub id: String, + pub name: String, + pub username: Option, } impl From<&Credential> for crate::model::Credential { @@ -239,354 +412,9 @@ impl From for GetCredentialResponse { } } -impl Serialize for crate::model::HybridState { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let structure: Structure = self.into(); - structure.serialize(serializer) - } -} - -impl From<&crate::model::HybridState> for Structure<'_> { - fn from(value: &crate::model::HybridState) -> Self { - let (tag, value): (u8, Option) = match value { - crate::model::HybridState::Idle => (0x01, None), - crate::model::HybridState::Started(value) => (0x02, Some(Value::Str(value.into()))), - crate::model::HybridState::Connecting => (0x03, None), - crate::model::HybridState::Connected => (0x04, None), - crate::model::HybridState::Completed => (0x05, None), - crate::model::HybridState::UserCancelled => (0x06, None), - crate::model::HybridState::Failed => (0x07, None), - }; - tag_value_to_struct(tag, value) - } -} - -impl TryFrom<&Structure<'_>> for crate::model::HybridState { - type Error = zvariant::Error; - - fn try_from(structure: &Structure<'_>) -> Result { - let (tag, value) = parse_tag_value_struct(structure)?; - match tag { - 0x01 => Ok(Self::Idle), - 0x02 => { - let qr_code: &str = value.downcast_ref()?; - Ok(Self::Started(qr_code.to_string())) - } - 0x03 => Ok(Self::Connecting), - 0x04 => Ok(Self::Connected), - 0x05 => Ok(Self::Completed), - 0x06 => Ok(Self::UserCancelled), - 0x07 => Ok(Self::Failed), - _ => Err(zvariant::Error::Message(format!( - "Invalid HybridState type passed: {tag}" - ))), - } - } -} - -impl TryFrom> for crate::model::HybridState { - type Error = zvariant::Error; - - fn try_from(structure: Structure<'_>) -> Result { - Self::try_from(&structure) - } -} - -impl<'de> Deserialize<'de> for crate::model::HybridState { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserialize_tag_value(deserializer) - } -} - -impl Type for crate::model::HybridState { - const SIGNATURE: &'static Signature = TAG_VALUE_SIGNATURE; -} - /// Identifier for a request to be used for cancellation. pub type RequestId = u32; -impl Type for crate::model::UsbState { - const SIGNATURE: &'static Signature = TAG_VALUE_SIGNATURE; -} - -impl Type for crate::model::NfcState { - const SIGNATURE: &'static Signature = TAG_VALUE_SIGNATURE; -} - -impl From<&crate::model::UsbState> for Structure<'_> { - fn from(value: &crate::model::UsbState) -> Self { - let (tag, value): (u8, Option) = match value { - crate::model::UsbState::Idle => (0x01, None), - crate::model::UsbState::Waiting => (0x02, None), - crate::model::UsbState::SelectingDevice => (0x03, None), - crate::model::UsbState::Connected => (0x04, None), - // TODO: Add pin request reason to this struct - crate::model::UsbState::NeedsPin { attempts_left } => { - let num = match attempts_left { - Some(num) => *num as i32, - None => -1, - }; - (0x05, Some(Value::I32(num))) - } - crate::model::UsbState::NeedsUserVerification { attempts_left } => { - let num = match attempts_left { - Some(num) => *num as i32, - None => -1, - }; - (0x06, Some(Value::I32(num))) - } - crate::model::UsbState::NeedsUserPresence => (0x07, None), - crate::model::UsbState::SelectingCredential { creds } => { - let creds: Vec = creds.iter().map(Credential::from).collect(); - let value = Value::new(creds); - (0x08, Some(value)) - } - crate::model::UsbState::Completed => (0x09, None), - crate::model::UsbState::Failed(error) => { - let value = Value::<'_>::from(error.to_string()); - (0x0A, Some(value)) - } - }; - tag_value_to_struct(tag, value) - } -} - -impl TryFrom<&Structure<'_>> for crate::model::UsbState { - type Error = zvariant::Error; - - fn try_from(structure: &Structure<'_>) -> Result { - let (tag, value) = parse_tag_value_struct(structure)?; - match tag { - 0x01 => Ok(Self::Idle), - 0x02 => Ok(Self::Waiting), - 0x03 => Ok(Self::SelectingDevice), - 0x04 => Ok(Self::Connected), - 0x05 => { - let attempts_left: i32 = value.downcast_ref()?; - let attempts_left = if attempts_left == -1 { - None - } else { - Some(attempts_left as u32) - }; - Ok(Self::NeedsPin { attempts_left }) - } - 0x06 => { - let attempts_left: i32 = value.downcast_ref()?; - let attempts_left = if attempts_left == -1 { - None - } else { - Some(attempts_left as u32) - }; - Ok(Self::NeedsUserVerification { attempts_left }) - } - 0x07 => Ok(Self::NeedsUserPresence), - 0x08 => { - let creds: Array = value.downcast_ref()?; - let creds: Result, zvariant::Error> = creds - .iter() - .map(|v| v.try_to_owned().unwrap()) - .map(|v| { - let cred: Result = - Value::from(v) - .downcast::() - .map(crate::model::Credential::from); - cred - }) - .collect(); - Ok(Self::SelectingCredential { creds: creds? }) - } - 0x09 => Ok(Self::Completed), - 0x0A => { - let err_code: &str = value.downcast_ref()?; - let err = match err_code { - "AuthenticatorError" => crate::model::Error::AuthenticatorError, - "PinNotSet" => crate::model::Error::PinNotSet, - "NoCredentials" => crate::model::Error::NoCredentials, - "CredentialExcluded" => crate::model::Error::CredentialExcluded, - "PinAttemptsExhausted" => crate::model::Error::PinAttemptsExhausted, - s => crate::model::Error::Internal(String::from(s)), - }; - Ok(Self::Failed(err)) - } - _ => Err(zvariant::Error::IncorrectType), - } - } -} - -impl TryFrom> for crate::model::UsbState { - type Error = zvariant::Error; - - fn try_from(structure: Structure<'_>) -> Result { - Self::try_from(&structure) - } -} - -impl Serialize for crate::model::UsbState { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let structure: Structure = self.into(); - structure.serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for crate::model::UsbState { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserialize_tag_value(deserializer) - } -} - -impl From<&crate::model::NfcState> for Structure<'_> { - fn from(value: &crate::model::NfcState) -> Self { - let (tag, value): (u8, Option) = match value { - crate::model::NfcState::Idle => (0x01, None), - crate::model::NfcState::Waiting => (0x02, None), - crate::model::NfcState::Connected => (0x04, None), - // TODO: Add pin request reason to this struct - crate::model::NfcState::NeedsPin { attempts_left } => { - let num = match attempts_left { - Some(num) => *num as i32, - None => -1, - }; - (0x05, Some(Value::I32(num))) - } - crate::model::NfcState::NeedsUserVerification { attempts_left } => { - let num = match attempts_left { - Some(num) => *num as i32, - None => -1, - }; - (0x06, Some(Value::I32(num))) - } - crate::model::NfcState::SelectingCredential { creds } => { - let creds: Vec = creds.iter().map(Credential::from).collect(); - let value = Value::new(creds); - (0x08, Some(value)) - } - crate::model::NfcState::Completed => (0x09, None), - crate::model::NfcState::Failed(error) => { - let value = Value::<'_>::from(error.to_string()); - (0x0A, Some(value)) - } - }; - tag_value_to_struct(tag, value) - } -} - -impl TryFrom<&Structure<'_>> for crate::model::NfcState { - type Error = zvariant::Error; - - fn try_from(structure: &Structure<'_>) -> Result { - let (tag, value) = parse_tag_value_struct(structure)?; - match tag { - 0x01 => Ok(Self::Idle), - 0x02 => Ok(Self::Waiting), - 0x04 => Ok(Self::Connected), - 0x05 => { - let attempts_left: i32 = value.downcast_ref()?; - let attempts_left = if attempts_left == -1 { - None - } else { - Some(attempts_left as u32) - }; - Ok(Self::NeedsPin { attempts_left }) - } - 0x06 => { - let attempts_left: i32 = value.downcast_ref()?; - let attempts_left = if attempts_left == -1 { - None - } else { - Some(attempts_left as u32) - }; - Ok(Self::NeedsUserVerification { attempts_left }) - } - 0x08 => { - let creds: Array = value.downcast_ref()?; - let creds: Result, zvariant::Error> = creds - .iter() - .map(|v| v.try_to_owned().unwrap()) - .map(|v| { - let cred: Result = - Value::from(v) - .downcast::() - .map(crate::model::Credential::from); - cred - }) - .collect(); - Ok(Self::SelectingCredential { creds: creds? }) - } - 0x09 => Ok(Self::Completed), - 0x0A => { - let err_code: &str = value.downcast_ref()?; - let err = match err_code { - "AuthenticatorError" => crate::model::Error::AuthenticatorError, - "PinNotSet" => crate::model::Error::PinNotSet, - "NoCredentials" => crate::model::Error::NoCredentials, - "CredentialExcluded" => crate::model::Error::CredentialExcluded, - "PinAttemptsExhausted" => crate::model::Error::PinAttemptsExhausted, - s => crate::model::Error::Internal(String::from(s)), - }; - Ok(Self::Failed(err)) - } - _ => Err(zvariant::Error::IncorrectType), - } - } -} - -impl TryFrom> for crate::model::NfcState { - type Error = zvariant::Error; - - fn try_from(structure: Structure<'_>) -> Result { - Self::try_from(&structure) - } -} - -impl Serialize for crate::model::NfcState { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let structure: Structure = self.into(); - structure.serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for crate::model::NfcState { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserialize_tag_value(deserializer) - } -} - -fn deserialize_tag_value<'a, 'de, T, D>(deserializer: D) -> Result -where - T: TryFrom>, - >>::Error: std::fmt::Display, - D: serde::Deserializer<'de>, - 'de: 'a, -{ - let d = Structure::deserializer_for_signature(TAG_VALUE_SIGNATURE).map_err(|err| { - D::Error::custom(format!( - "could not create deserializer for structure: {err}", - )) - })?; - let structure = d.deserialize(deserializer)?; - structure - .try_into() - .map_err(|err| D::Error::custom(format!("could not deserialize from structure: {err}"))) -} - #[derive(Serialize, Deserialize, Type)] pub struct ViewRequest { pub operation: Operation, @@ -696,21 +524,18 @@ fn value_to_owned(value: &Value<'_>) -> OwnedValue { .expect("non-file descriptor values to succeed") } -fn parse_tag_value_struct<'a>(s: &'a Structure) -> Result<(u8, Value<'a>), zvariant::Error> { +fn parse_tag_value_struct<'a>(s: &'a Structure) -> Result<(u32, Value<'a>), zvariant::Error> { if s.signature() != TAG_VALUE_SIGNATURE { return Err(zvariant::Error::SignatureMismatch( s.signature().clone(), TAG_VALUE_SIGNATURE.to_string(), )); } - let tag: u8 = s + let tag: u32 = s .fields() .first() .ok_or_else(|| { - zvariant::Error::SignatureMismatch( - Signature::U8, - "expected a single-byte tag".to_string(), - ) + zvariant::Error::SignatureMismatch(Signature::U32, "expected a u32 tag".to_string()) }) .and_then(|f| f.downcast_ref())?; let value = s @@ -726,7 +551,7 @@ fn parse_tag_value_struct<'a>(s: &'a Structure) -> Result<(u8, Value<'a>), zvari Ok((tag, value)) } -fn tag_value_to_struct(tag: u8, value: Option>) -> Structure<'static> { +fn tag_value_to_struct(tag: u32, value: Option>) -> Structure<'static> { StructureBuilder::new() .add_field(tag) .append_field(Value::new(value_to_owned( @@ -743,112 +568,66 @@ mod test { serialized::{Context, Data, Format}, }; - use crate::model::{BackgroundEvent, HybridState, UsbState}; + use super::{BackgroundEvent, Credential}; #[test] - fn test_serialize_hybrid_state() { - let state = HybridState::Completed; + fn test_round_trip_completed_event() { + let event1 = BackgroundEvent::CeremonyCompleted; let ctx = zvariant::serialized::Context::new_dbus(zvariant::BE, 0); - let data = zvariant::to_bytes(ctx, &state).unwrap(); - assert_eq!("(yv)", HybridState::SIGNATURE.to_string()); - assert_eq!(&[5, 1, b'y', 0, 0], data.bytes()); + let data = zvariant::to_bytes(ctx, &event1).unwrap(); + assert_eq!("(uv)", BackgroundEvent::SIGNATURE.to_string()); + assert_eq!(&[0, 0, 0, 1, 1, b'y', 0, 0], data.bytes()); + let event2 = data.deserialize().unwrap().0; + assert_eq!(event1, event2); } #[test] - fn test_serialize_background_hybrid_event() { - let state = HybridState::Started("FIDO:/1234".to_string()); - let event = BackgroundEvent::HybridQrStateChanged(state); + fn test_round_trip_background_hybrid_event() { + let event1 = BackgroundEvent::HybridStarted("FIDO:/1234".to_string()); let ctx = zvariant::serialized::Context::new_dbus(zvariant::BE, 0); - assert_eq!("(yv)", BackgroundEvent::SIGNATURE.to_string()); - let data = zvariant::to_bytes(ctx, &event).unwrap(); - let expected = b"\x02\x04(yv)\0\0\x02\x01s\0\0\0\0\x0aFIDO:/1234\0"; + assert_eq!("(uv)", BackgroundEvent::SIGNATURE.to_string()); + let data = zvariant::to_bytes(ctx, &event1).unwrap(); + let expected = b"\x00\x00\x00\x21\x01s\0\0\0\0\0\x0aFIDO:/1234\0"; assert_eq!(expected, data.bytes()); + let event2 = data.deserialize().unwrap().0; + assert_eq!(event1, event2); } #[test] fn test_deserialize_background_hybrid_event() { - let data = Data::new( - b"\x02\x04(yv)\0\0\x05\x01y\0\0", - Context::new(Format::DBus, zvariant::BE, 0), - ); + let bytes = b"\x00\x00\x00\x21\x01s\0\0\0\0\0\x0aFIDO:/1234\0"; + let data = Data::new(bytes, Context::new(Format::DBus, zvariant::BE, 0)); let event: BackgroundEvent = data.deserialize().unwrap().0; assert!(matches!( event, - BackgroundEvent::HybridQrStateChanged(crate::model::HybridState::Completed) - )); - } - - #[test] - fn test_round_trip_background_hybrid_event() { - let event = - BackgroundEvent::HybridQrStateChanged(HybridState::Started(String::from("FIDO:/1234"))); - let ctx = zvariant::serialized::Context::new_dbus(zvariant::BE, 0); - let data = zvariant::to_bytes(ctx, &event).unwrap(); - let bytes = data.bytes(); - let data2 = Data::new(bytes, Context::new(Format::DBus, zvariant::BE, 0)); - let event_2: BackgroundEvent = data2.deserialize().unwrap().0; - assert!(matches!( - event_2, - BackgroundEvent::HybridQrStateChanged(HybridState::Started(ref f)) if f == "FIDO:/1234" + BackgroundEvent::HybridStarted(ref s) if s == "FIDO:/1234" )); } #[test] - fn test_serialize_usb_state() { + fn test_round_trip_selecting_credential_state() { let creds = vec![ - crate::model::Credential { + Credential { id: "a1b2c3".to_string(), name: "user 1".to_string(), username: Some("u1@example.com".to_string()), }, - crate::model::Credential { + Credential { id: "321".to_string(), name: "User 2".to_string(), username: None, }, ]; - let state = UsbState::SelectingCredential { creds }; + let event1 = BackgroundEvent::SelectingCredential { creds }; let ctx = zvariant::serialized::Context::new_dbus(zvariant::BE, 0); - let data = zvariant::to_bytes(ctx, &state).unwrap(); - assert_eq!("(yv)", UsbState::SIGNATURE.to_string()); + let data = zvariant::to_bytes(ctx, &event1).unwrap(); + assert_eq!("(uv)", BackgroundEvent::SIGNATURE.to_string()); #[rustfmt::skip] let expected = [ - 8, // UsbState::SelectingCredential - 6, 97, 97, 123, 115, 118, 125, 0, 0, 0, 0, // Signature aa{sv} + padding - 0, 0, 0, 165, // array(struct) data length - 0, 0, 0, 83, 0, 0, 0, 0, // element 1(struct) length, + padding(4) - 0, 0, 0, 2, 105, 100, 0, // string[2] "id" - 1, 115, 0, 0, 0, // Signature s + padding - 0, 0, 0, 6, 97, 49, 98, 50, 99, 51, 0, 0, // String, len 6, "a1b2c3" + padding(1) - 0, 0, 0, 4, 110, 97, 109, 101, 0, // String, len 4, "name" - 1, 115, 0, // Signature s + padding - 0, 0, 0, 6, 117, 115, 101, 114, 32, 49, 0, 0, // String, len 6, "user 1" + padding(1) - 0, 0, 0, 8, 117, 115, 101, 114, 110, 97, 109, 101, 0, // String, len 8, "username" - 1, 115, 0, // Signature s - 0, 0, 0, 14, 117, 49, 64, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 0, 0, // String, len 14, "u1@example.com" + padding(1) - - 0, 0, 0, 69, // element 2, length 69 - 0, 0, 0, 2, 105, 100, 0, // string, len 2, "id" - 1, 115, 0, 0, 0, // Signature s + padding(2) - 0, 0, 0, 3, 51, 50, 49, 0, 0, 0, 0, 0, // string, len 3, "321" + padding(4) - 0, 0, 0, 4, 110, 97, 109, 101, 0, // String, len 4, "name" - 1, 115, 0, // Signature s - 0, 0, 0, 6, 85, 115, 101, 114, 32, 50, 0, 0, // String, len 6, "User 2" + padding(1) - 0, 0, 0, 8, 117, 115, 101, 114, 110, 97, 109, 101, 0, // string, len 8, "username" - 1, 115, 0, 0, // Signature s + padding(1) - 0, 0, 0, 0, // string, len 0, "" - ]; - assert_eq!(expected, data.bytes()); - } - - #[test] - fn test_deserialize_usb_state() { - #[rustfmt::skip] - let input = [ - 8, // UsbState::SelectingCredential - 6, 97, 97, 123, 115, 118, 125, 0, 0, 0, 0, // Signature aa{sv} + padding - 0, 0, 0, 165, // array(struct) data length + 0, 0, 0, 0x13, // BACKGROUND_EVENT_SELECTING_CREDENTIAL + 6, b'a', b'a', b'{', b's', b'v', b'}', 0, // Signature aa{sv} + padding(1) + 0, 0, 0, 143, // array(struct) data length 0, 0, 0, 83, 0, 0, 0, 0, // element 1(struct) length, + padding(4) 0, 0, 0, 2, 105, 100, 0, // string[2] "id" 1, 115, 0, 0, 0, // Signature s + padding @@ -860,89 +639,17 @@ mod test { 1, 115, 0, // Signature s 0, 0, 0, 14, 117, 49, 64, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 0, 0, // String, len 14, "u1@example.com" + padding(1) - 0, 0, 0, 69, // element 2, length 69 + 0, 0, 0, 47, // element 2, length 69 0, 0, 0, 2, 105, 100, 0, // string, len 2, "id" 1, 115, 0, 0, 0, // Signature s + padding(2) 0, 0, 0, 3, 51, 50, 49, 0, 0, 0, 0, 0, // string, len 3, "321" + padding(4) 0, 0, 0, 4, 110, 97, 109, 101, 0, // String, len 4, "name" 1, 115, 0, // Signature s - 0, 0, 0, 6, 85, 115, 101, 114, 32, 50, 0, 0, // String, len 6, "User 2" + padding(1) - 0, 0, 0, 8, 117, 115, 101, 114, 110, 97, 109, 101, 0, // string, len 8, "username" - 1, 115, 0, 0, // Signature s + padding(1) - 0, 0, 0, 0, // string, len 0, "" + 0, 0, 0, 6, 85, 115, 101, 114, 32, 50, 0, // String, len 6, "User 2" + padding(1) + // username omitted ]; - let ctx = Context::new(Format::DBus, zvariant::BE, 0); - let data = Data::new(&input, ctx); - let state: UsbState = data.deserialize().unwrap().0; - match state { - UsbState::SelectingCredential { creds } => { - assert_eq!(2, creds.len()); - assert_eq!("a1b2c3", creds[0].id,); - assert_eq!("user 1", creds[0].name,); - assert_eq!("u1@example.com", creds[0].username.as_ref().unwrap()); - assert_eq!("321", creds[1].id,); - assert_eq!("User 2", creds[1].name,); - assert_eq!(None, creds[1].username,); - } - _ => panic!(""), - } - } - - #[test] - fn test_serialize_background_usb_event() { - let state = UsbState::NeedsPin { - attempts_left: Some(254), - }; - let event = BackgroundEvent::UsbStateChanged(state); - let ctx = zvariant::serialized::Context::new_dbus(zvariant::BE, 0); - assert_eq!("(yv)", BackgroundEvent::SIGNATURE.to_string()); - let data = zvariant::to_bytes(ctx, &event).unwrap(); - let expected = b"\x01\x04(yv)\0\0\x05\x01i\0\0\0\0\xfe"; assert_eq!(expected, data.bytes()); - } - - #[test] - fn test_round_trip_background_usb_event() { - let event = BackgroundEvent::UsbStateChanged(UsbState::NeedsUserVerification { - attempts_left: None, - }); - let ctx = zvariant::serialized::Context::new_dbus(zvariant::BE, 0); - let data = zvariant::to_bytes(ctx, &event).unwrap(); - let bytes = data.bytes(); - let data2 = Data::new(bytes, Context::new(Format::DBus, zvariant::BE, 0)); - let event_2: BackgroundEvent = data2.deserialize().unwrap().0; - assert!(matches!( - event_2, - BackgroundEvent::UsbStateChanged(UsbState::NeedsUserVerification{ ref attempts_left }) if attempts_left.is_none() - )); - } - - #[test] - fn test_zvariant() { - let input = b"\x01y\0\xdd"; - let ctx = Context::new(Format::DBus, zvariant::BE, 0); - let data = Data::new(input, ctx); - let value: zvariant::Value = data.deserialize().unwrap().0; - assert!(matches!(value, zvariant::Value::U8(b) if b == b'\xdd')) - } - - #[test] - fn test_zvariant_array() { - #[rustfmt::skip] - let input = [ - 2, b'a', b's', 0, // Signature aa{sv} - 0, 0, 0, 7, // array(string) data length - 0, 0, 0, 2, b'y', b'o', 0 // string, len 2, 'yo' - ]; - let ctx = Context::new(Format::DBus, zvariant::BE, 0); - let data = Data::new(&input, ctx); - let value: zvariant::Value = data.deserialize().unwrap().0; - match value { - zvariant::Value::Array(arr) => { - let s = arr.get::(0).unwrap().unwrap(); - assert_eq!("yo", s.as_str()); - } - _ => panic!(), - }; + let event2: BackgroundEvent = data.deserialize().unwrap().0; + assert_eq!(event1, event2); } } diff --git a/credentialsd-ui/src/client.rs b/credentialsd-ui/src/client.rs index 0f2184d..fe8b1cc 100644 --- a/credentialsd-ui/src/client.rs +++ b/credentialsd-ui/src/client.rs @@ -64,7 +64,7 @@ impl FlowController for DbusCredentialClient { &mut self, ) -> std::result::Result< std::pin::Pin< - Box + Send + 'static>, + Box + Send + 'static>, >, (), > { diff --git a/credentialsd-ui/src/dbus.rs b/credentialsd-ui/src/dbus.rs index 9ab511b..b1bdf26 100644 --- a/credentialsd-ui/src/dbus.rs +++ b/credentialsd-ui/src/dbus.rs @@ -1,7 +1,7 @@ use async_std::channel::Sender; use credentialsd_common::{ - model::{BackgroundEvent, Device}, - server::{RequestId, ViewRequest}, + model::Device, + server::{BackgroundEvent, RequestId, ViewRequest}, }; use zbus::{fdo, interface, proxy}; diff --git a/credentialsd-ui/src/gui/view_model/gtk/mod.rs b/credentialsd-ui/src/gui/view_model/gtk/mod.rs index f82afcb..cff1d34 100644 --- a/credentialsd-ui/src/gui/view_model/gtk/mod.rs +++ b/credentialsd-ui/src/gui/view_model/gtk/mod.rs @@ -146,7 +146,7 @@ impl ViewModel { let localized = ngettext( "Enter your PIN. One attempt remaining.", "Enter your PIN. %d attempts remaining.", - left, + left.into(), ); localized.replace("%d", &format!("{}", left)) } else { @@ -162,7 +162,7 @@ impl ViewModel { let localized = ngettext( "Touch your device again. One attempt remaining.", "Touch your device again. %d attempts remaining.", - left, + left.into(), ); localized.replace("%d", &format!("{}", left)) } diff --git a/credentialsd-ui/src/gui/view_model/mod.rs b/credentialsd-ui/src/gui/view_model/mod.rs index 4472afa..6ca166f 100644 --- a/credentialsd-ui/src/gui/view_model/mod.rs +++ b/credentialsd-ui/src/gui/view_model/mod.rs @@ -8,17 +8,14 @@ use async_std::{ sync::Mutex as AsyncMutex, }; use credentialsd_common::model::RequestingApplication; -use credentialsd_common::server::ViewRequest; +use credentialsd_common::server::{BackgroundEvent, Credential, ViewRequest}; use gettextrs::gettext; use serde::{Deserialize, Serialize}; use tracing::{error, info}; use credentialsd_common::{ client::FlowController, - model::{ - BackgroundEvent, Credential, Device, Error, HybridState, NfcState, Operation, Transport, - UsbState, ViewUpdate, - }, + model::{Device, Error, HybridState, NfcState, Operation, Transport, UsbState, ViewUpdate}, }; #[derive(Debug)] @@ -234,167 +231,129 @@ impl ViewModel { break; } - Event::Background(BackgroundEvent::UsbStateChanged(state)) => { - match state { - UsbState::Connected => { - info!("Found USB device") - } + Event::Background(BackgroundEvent::UsbConnected) => { + info!("Found USB device") + } - UsbState::NeedsPin { attempts_left } => { - self.tx_update - .send(ViewUpdate::UsbNeedsPin { attempts_left }) - .await - .unwrap(); - } - UsbState::NeedsUserVerification { attempts_left } => { - self.tx_update - .send(ViewUpdate::UsbNeedsUserVerification { attempts_left }) - .await - .unwrap(); - } - UsbState::NeedsUserPresence => { - self.tx_update - .send(ViewUpdate::UsbNeedsUserPresence) - .await - .unwrap(); - } - UsbState::Completed => { - self.tx_update.send(ViewUpdate::Completed).await.unwrap(); - } - UsbState::SelectingDevice => { - self.tx_update - .send(ViewUpdate::SelectingDevice) - .await - .unwrap(); - } - UsbState::Idle | UsbState::Waiting => {} - UsbState::SelectingCredential { creds } => { - self.tx_update - .send(ViewUpdate::SetCredentials(creds)) - .await - .unwrap(); - } - // TODO: Provide more specific error messages using the wrapped Error. - UsbState::Failed(err) => { - let error_msg = match err { - Error::NoCredentials => { - gettext("No matching credentials found on this authenticator.") - } - Error::PinAttemptsExhausted => gettext( - "No more PIN attempts allowed. Try removing your device and plugging it back in.", - ), - Error::PinNotSet => gettext( - "This server requires your device to have additional protection like a PIN, which is not set. Please set a PIN for this device and try again.", - ), - Error::AuthenticatorError | Error::Internal(_) => gettext( - "Something went wrong while retrieving a credential. Please try again later or use a different authenticator.", - ), - Error::CredentialExcluded => gettext( - "This credential is already registered on this authenticator.", - ), - }; - self.tx_update - .send(ViewUpdate::Failed(error_msg)) - .await - .unwrap() - } - } + Event::Background(BackgroundEvent::NeedsPin { attempts_left }) => { + // TODO: UsbNeedsPin just needs to be NeedsPing + self.tx_update + .send(ViewUpdate::UsbNeedsPin { attempts_left }) + .await + .unwrap(); + } + Event::Background(BackgroundEvent::NeedsUserVerification { attempts_left }) => { + self.tx_update + .send(ViewUpdate::UsbNeedsUserVerification { attempts_left }) + .await + .unwrap(); + } + Event::Background(BackgroundEvent::NeedsUserPresence) => { + self.tx_update + .send(ViewUpdate::UsbNeedsUserPresence) + .await + .unwrap(); + } + Event::Background(BackgroundEvent::CeremonyCompleted) => { + self.tx_update.send(ViewUpdate::Completed).await.unwrap(); + } + Event::Background(BackgroundEvent::UsbSelectingDevice) => { + self.tx_update + .send(ViewUpdate::SelectingDevice) + .await + .unwrap(); + } + Event::Background(BackgroundEvent::UsbIdle) + | Event::Background(BackgroundEvent::UsbWaiting) => {} + Event::Background(BackgroundEvent::SelectingCredential { creds }) => { + self.tx_update + .send(ViewUpdate::SetCredentials(creds)) + .await + .unwrap(); + } + Event::Background(BackgroundEvent::ErrorNoCredentials) => { + let error_msg = gettext("No matching credentials found on this authenticator."); + self.tx_update + .send(ViewUpdate::Failed(error_msg)) + .await + .unwrap() + } + Event::Background(BackgroundEvent::ErrorPinAttemptsExhausted) => { + let error_msg = gettext( + "No more PIN attempts allowed. Try removing your device and plugging it back in.", + ); + self.tx_update + .send(ViewUpdate::Failed(error_msg)) + .await + .unwrap() + } + Event::Background(BackgroundEvent::ErrorPinNotSet) => { + let error_msg = gettext( + "This server requires your device to have additional protection like a PIN, which is not set. Please set a PIN for this device and try again.", + ); + self.tx_update + .send(ViewUpdate::Failed(error_msg)) + .await + .unwrap() + } + Event::Background(BackgroundEvent::ErrorTimedOut) => { + let error_msg = gettext("The credential request timed out. Please try again."); + self.tx_update + .send(ViewUpdate::Failed(error_msg)) + .await + .unwrap() + } + Event::Background( + BackgroundEvent::ErrorAuthenticator | BackgroundEvent::ErrorInternal, + ) => { + let error_msg = gettext( + "Something went wrong while retrieving a credential. Please try again later or use a different authenticator.", + ); + self.tx_update + .send(ViewUpdate::Failed(error_msg)) + .await + .unwrap() + } + Event::Background(BackgroundEvent::ErrorCredentialExcluded) => { + let error_msg = + gettext("This credential is already registered on this authenticator."); + self.tx_update + .send(ViewUpdate::Failed(error_msg)) + .await + .unwrap() + } + Event::Background(BackgroundEvent::NfcConnected) => { + info!("Found NFC device") } - Event::Background(BackgroundEvent::NfcStateChanged(state)) => { - match state { - NfcState::Connected => { - info!("Found NFC device") - } - NfcState::NeedsPin { attempts_left } => { - self.tx_update - .send(ViewUpdate::NfcNeedsPin { attempts_left }) - .await - .unwrap(); - } - NfcState::NeedsUserVerification { attempts_left } => { - self.tx_update - .send(ViewUpdate::NfcNeedsUserVerification { attempts_left }) - .await - .unwrap(); - } - NfcState::Completed => { - self.tx_update.send(ViewUpdate::Completed).await.unwrap(); - } - NfcState::Idle | NfcState::Waiting => {} - NfcState::SelectingCredential { creds } => { - self.tx_update - .send(ViewUpdate::SetCredentials(creds)) - .await - .unwrap(); - } - // TODO: Provide more specific error messages using the wrapped Error. - NfcState::Failed(err) => { - let error_msg = String::from(match err { - Error::NoCredentials => { - "No matching credentials found on this authenticator." - } - Error::PinAttemptsExhausted => { - "No more PIN attempts allowed. Try removing your device and plugging it back in." - } - Error::PinNotSet => { - "This server requires your device to have additional protection like a PIN, which is not set. Please set a PIN for this device and try again." - } - Error::AuthenticatorError | Error::Internal(_) => { - "Something went wrong while retrieving a credential. Please try again later or use a different authenticator." - } - Error::CredentialExcluded => { - "This credential is already registered on this authenticator." - } - }); - self.tx_update - .send(ViewUpdate::Failed(error_msg)) - .await - .unwrap() - } - } + Event::Background(BackgroundEvent::NfcIdle | BackgroundEvent::NfcWaiting) => {} + Event::Background(BackgroundEvent::HybridIdle) => { + self.hybrid_qr_code_data = None; } - Event::Background(BackgroundEvent::HybridQrStateChanged(state)) => { - self.hybrid_qr_state = state.clone(); - tracing::debug!("Received HybridQrState::{:?}", &state); - match state { - HybridState::Idle => { - self.hybrid_qr_code_data = None; - } - HybridState::Started(qr_code) => { - self.hybrid_qr_code_data = Some(qr_code.clone().into_bytes()); - self.tx_update - .send(ViewUpdate::HybridNeedsQrCode(qr_code)) - .await - .unwrap(); - } - HybridState::Connecting => { - self.hybrid_qr_code_data = None; - self.tx_update - .send(ViewUpdate::HybridConnecting) - .await - .unwrap(); - } - HybridState::Connected => { - self.hybrid_qr_code_data = None; - self.tx_update - .send(ViewUpdate::HybridConnected) - .await - .unwrap(); - } - HybridState::Completed => { - self.hybrid_qr_code_data = None; - self.tx_update.send(ViewUpdate::Completed).await.unwrap(); - } - HybridState::UserCancelled => { - self.hybrid_qr_code_data = None; - break; - } - HybridState::Failed => { - self.hybrid_qr_code_data = None; - self.tx_update.send(ViewUpdate::Failed(gettext("Something went wrong. Try again later or use a different authenticator."))).await.unwrap(); - } - }; + Event::Background(BackgroundEvent::HybridStarted(qr_code)) => { + self.hybrid_qr_code_data = Some(qr_code.clone().into_bytes()); + self.tx_update + .send(ViewUpdate::HybridNeedsQrCode(qr_code)) + .await + .unwrap(); + } + Event::Background(BackgroundEvent::HybridConnecting) => { + self.hybrid_qr_code_data = None; + self.tx_update + .send(ViewUpdate::HybridConnecting) + .await + .unwrap(); + } + Event::Background(BackgroundEvent::HybridConnected) => { + self.hybrid_qr_code_data = None; + self.tx_update + .send(ViewUpdate::HybridConnected) + .await + .unwrap(); + } + Event::Background(BackgroundEvent::ErrorCancelled) => { + self.hybrid_qr_code_data = None; + break; } /* Event::Background(BackgroundEvent::RequestCancelled(request_id)) => { break; diff --git a/credentialsd/src/credential_service/hybrid.rs b/credentialsd/src/credential_service/hybrid.rs index 85cb6d5..fba0c2d 100644 --- a/credentialsd/src/credential_service/hybrid.rs +++ b/credentialsd/src/credential_service/hybrid.rs @@ -2,6 +2,7 @@ use core::panic; use std::fmt::Debug; use async_stream::stream; +use credentialsd_common::server::BackgroundEvent; use futures_lite::Stream; use tokio::sync::broadcast; use tokio::sync::mpsc::{self, Sender}; @@ -220,6 +221,19 @@ impl From for credentialsd_common::model::HybridState { } } +impl From<&HybridState> for BackgroundEvent { + fn from(value: &HybridState) -> Self { + match value { + HybridState::Init(qr_code) => BackgroundEvent::HybridStarted(qr_code.to_string()), + HybridState::Connecting => BackgroundEvent::HybridConnecting, + HybridState::Connected => BackgroundEvent::HybridConnected, + HybridState::Completed => BackgroundEvent::CeremonyCompleted, + HybridState::UserCancelled => BackgroundEvent::ErrorCancelled, + HybridState::Failed => BackgroundEvent::ErrorAuthenticator, + } + } +} + async fn handle_hybrid_updates( state_sender: &Sender, mut ux_update_receiver: broadcast::Receiver, diff --git a/credentialsd/src/credential_service/nfc.rs b/credentialsd/src/credential_service/nfc.rs index 51b8a75..9291985 100644 --- a/credentialsd/src/credential_service/nfc.rs +++ b/credentialsd/src/credential_service/nfc.rs @@ -14,7 +14,10 @@ use tokio::sync::broadcast; use tokio::sync::mpsc::{self, Receiver, Sender, WeakSender}; use tracing::{debug, warn}; -use credentialsd_common::model::{Credential, Error}; +use credentialsd_common::{ + model::{Credential, Error}, + server::BackgroundEvent, +}; use crate::model::{CredentialRequest, GetAssertionResponseInternal}; @@ -357,7 +360,7 @@ pub enum NfcState { // Multiple credentials have been found and the user has to select which to use // List of user-identities to decide which to use. - SelectCredential { + SelectingCredential { creds: Vec, cred_tx: mpsc::Sender, }, @@ -388,7 +391,7 @@ impl From for NfcState { NfcStateInternal::Completed(_) => NfcState::Completed, // NfcStateInternal::UserCancelled => NfcState:://UserCancelled, NfcStateInternal::SelectCredential { response, cred_tx } => { - NfcState::SelectCredential { + NfcState::SelectingCredential { creds: response .assertions .iter() @@ -445,7 +448,7 @@ impl From<&NfcState> for credentialsd_common::model::NfcState { attempts_left: *attempts_left, } } - NfcState::SelectCredential { creds, .. } => { + NfcState::SelectingCredential { creds, .. } => { credentialsd_common::model::NfcState::SelectingCredential { creds: creds.to_owned(), } @@ -456,6 +459,34 @@ impl From<&NfcState> for credentialsd_common::model::NfcState { } } +impl From<&NfcState> for BackgroundEvent { + fn from(value: &NfcState) -> Self { + match value { + NfcState::Idle => BackgroundEvent::NfcIdle, + NfcState::Waiting => BackgroundEvent::NfcWaiting, + NfcState::Connected => BackgroundEvent::NfcConnected, + NfcState::NeedsPin { attempts_left, .. } => BackgroundEvent::NeedsPin { + attempts_left: *attempts_left, + }, + NfcState::NeedsUserVerification { attempts_left } => { + BackgroundEvent::NeedsUserVerification { + attempts_left: *attempts_left, + } + } + NfcState::SelectingCredential { creds, .. } => BackgroundEvent::SelectingCredential { + creds: creds.to_owned().into_iter().map(|c| c.into()).collect(), + }, + NfcState::Completed => BackgroundEvent::CeremonyCompleted, + NfcState::Failed(Error::AuthenticatorError) => BackgroundEvent::ErrorAuthenticator, + NfcState::Failed(Error::NoCredentials) => BackgroundEvent::ErrorNoCredentials, + NfcState::Failed(Error::CredentialExcluded) => BackgroundEvent::ErrorAuthenticator, + NfcState::Failed(Error::PinNotSet) => BackgroundEvent::ErrorPinNotSet, + NfcState::Failed(Error::PinAttemptsExhausted) => BackgroundEvent::ErrorAuthenticator, + NfcState::Failed(Error::Internal(_)) => BackgroundEvent::ErrorInternal, + } + } +} + async fn handle_nfc_updates( signal_tx: &WeakSender>, mut state_rx: broadcast::Receiver, diff --git a/credentialsd/src/credential_service/usb.rs b/credentialsd/src/credential_service/usb.rs index f64c302..8fc8dc9 100644 --- a/credentialsd/src/credential_service/usb.rs +++ b/credentialsd/src/credential_service/usb.rs @@ -17,7 +17,10 @@ use tokio::sync::broadcast; use tokio::sync::mpsc::{self, Receiver, Sender, WeakSender}; use tracing::{debug, warn}; -use credentialsd_common::model::{Credential, Error}; +use credentialsd_common::{ + model::{Credential, Error}, + server::BackgroundEvent, +}; use crate::model::{CredentialRequest, GetAssertionResponseInternal}; @@ -566,6 +569,36 @@ impl From<&UsbState> for credentialsd_common::model::UsbState { } } +impl From<&UsbState> for BackgroundEvent { + fn from(value: &UsbState) -> Self { + match value { + UsbState::Idle => BackgroundEvent::UsbIdle, + UsbState::Waiting => BackgroundEvent::UsbWaiting, + UsbState::SelectingDevice => BackgroundEvent::UsbSelectingDevice, + UsbState::Connected => BackgroundEvent::UsbConnected, + UsbState::NeedsPin { attempts_left, .. } => BackgroundEvent::NeedsPin { + attempts_left: *attempts_left, + }, + UsbState::NeedsUserVerification { attempts_left } => { + BackgroundEvent::NeedsUserVerification { + attempts_left: *attempts_left, + } + } + UsbState::NeedsUserPresence => BackgroundEvent::NeedsUserPresence, + UsbState::SelectingCredential { creds, .. } => BackgroundEvent::SelectingCredential { + creds: creds.to_owned().into_iter().map(|c| c.into()).collect(), + }, + UsbState::Completed => BackgroundEvent::CeremonyCompleted, + UsbState::Failed(Error::AuthenticatorError) => BackgroundEvent::ErrorAuthenticator, + UsbState::Failed(Error::NoCredentials) => BackgroundEvent::ErrorNoCredentials, + UsbState::Failed(Error::CredentialExcluded) => BackgroundEvent::ErrorAuthenticator, + UsbState::Failed(Error::PinNotSet) => BackgroundEvent::ErrorPinNotSet, + UsbState::Failed(Error::PinAttemptsExhausted) => BackgroundEvent::ErrorAuthenticator, + UsbState::Failed(Error::Internal(_)) => BackgroundEvent::ErrorInternal, + } + } +} + async fn handle_usb_updates( signal_tx: &WeakSender>, mut state_rx: broadcast::Receiver, diff --git a/credentialsd/src/dbus/flow_control.rs b/credentialsd/src/dbus/flow_control.rs index a6ed555..11fa85b 100644 --- a/credentialsd/src/dbus/flow_control.rs +++ b/credentialsd/src/dbus/flow_control.rs @@ -5,9 +5,9 @@ use std::{collections::VecDeque, fmt::Debug, sync::Arc}; use async_trait::async_trait; use credentialsd_common::model::{ - BackgroundEvent, Device, Error as CredentialServiceError, RequestingApplication, WebAuthnError, + Device, Error as CredentialServiceError, RequestingApplication, WebAuthnError, }; -use credentialsd_common::server::{RequestId, WindowHandle}; +use credentialsd_common::server::{BackgroundEvent, RequestId, WindowHandle}; use futures_lite::StreamExt; use tokio::sync::oneshot; use tokio::{ @@ -163,9 +163,7 @@ where } }; while let Some(state) = stream.next().await { - let event = credentialsd_common::model::BackgroundEvent::HybridQrStateChanged( - state.clone().into(), - ); + let event = (&state).into(); if let Err(err) = send_state_update(emitter, &signal_state, event).await { tracing::error!("Failed to send state update to UI: {err}"); break; @@ -206,8 +204,7 @@ where } }; while let Some(state) = stream.next().await { - let event = - credentialsd_common::model::BackgroundEvent::UsbStateChanged((&state).into()); + let event = (&state).into(); if let Err(err) = send_state_update(emitter, &signal_state, event).await { tracing::error!("Failed to send state update to UI: {err}"); break; @@ -256,8 +253,7 @@ where } }; while let Some(state) = stream.next().await { - let event = - credentialsd_common::model::BackgroundEvent::NfcStateChanged((&state).into()); + let event = (&state).into(); if let Err(err) = send_state_update(emitter, &signal_state, event).await { tracing::error!("Failed to send state update to UI: {err}"); break; @@ -267,7 +263,7 @@ where let mut nfc_pin_tx = nfc_pin_tx.lock().await; let _ = nfc_pin_tx.insert(pin_tx); } - NfcState::SelectCredential { cred_tx, .. } => { + NfcState::SelectingCredential { cred_tx, .. } => { let mut nfc_cred_tx = nfc_cred_tx.lock().await; let _ = nfc_cred_tx.insert(cred_tx); } @@ -403,8 +399,8 @@ pub mod test { use credentialsd_common::{ client::FlowController, - model::{BackgroundEvent, Device}, - server::RequestId, + model::Device, + server::{BackgroundEvent, RequestId}, }; use futures_lite::{Stream, StreamExt}; use tokio::sync::{mpsc, oneshot, Mutex as AsyncMutex}; @@ -659,21 +655,14 @@ pub mod test { while let Some(hybrid_state) = stream.next().await { tracing::debug!(target: "DummyFlowServer", "Received hybrid state change: {hybrid_state:?}"); if let Some(tx) = tx_weak.upgrade() { + tx.send((&hybrid_state).into()) + .await + .unwrap(); match hybrid_state { HybridState::Completed | HybridState::Failed => { - tx.send(BackgroundEvent::HybridQrStateChanged( - hybrid_state.into(), - )) - .await - .unwrap(); break; } - _ => tx - .send(BackgroundEvent::HybridQrStateChanged( - hybrid_state.into(), - )) - .await - .unwrap(), + _ => {}, }; } } @@ -700,11 +689,7 @@ pub mod test { let task = tokio::spawn(async move { while let Some(state) = stream.next().await { if let Some(tx) = tx_weak.upgrade() { - if tx - .send(BackgroundEvent::UsbStateChanged(state.clone().into())) - .await - .is_err() - { + if tx.send((&state).into()).await.is_err() { tracing::debug!("Closing USB background event forwarder"); break; } @@ -739,11 +724,7 @@ pub mod test { let task = tokio::spawn(async move { while let Some(state) = stream.next().await { if let Some(tx) = tx_weak.upgrade() { - if tx - .send(BackgroundEvent::NfcStateChanged(state.clone().into())) - .await - .is_err() - { + if tx.send((&state).into()).await.is_err() { tracing::debug!("Closing NFC background event forwarder"); break; } diff --git a/credentialsd/src/dbus/ui_control.rs b/credentialsd/src/dbus/ui_control.rs index c3eb35c..b94ff04 100644 --- a/credentialsd/src/dbus/ui_control.rs +++ b/credentialsd/src/dbus/ui_control.rs @@ -54,7 +54,8 @@ pub mod test { }; use credentialsd_common::{ - client::FlowController, model::BackgroundEvent, server::ViewRequest, + client::FlowController, + server::{BackgroundEvent, ViewRequest}, }; use futures_lite::StreamExt; use tokio::sync::{ diff --git a/doc/api.md b/doc/api.md index 115f420..94aa867 100644 --- a/doc/api.md +++ b/doc/api.md @@ -49,7 +49,7 @@ sequenceDiagram ### Breaking Changes - (UI Controller): Renamed `InitiateEventStream()` to `Subscribe()` -- (UI Controller): Serialize enums (including BackgroundEvent, HybridState and UsbState) as (yv) structs instead for a{sv} dicts +- (UI Controller): Serialize enums (including BackgroundEvent, HybridState and UsbState) as (uv) structs instead for a{sv} dicts - (Gateway): Flatten `request` parameters into options. - (Gateway): Make `origin` and `type` a required method parameter. - (Gateway): Flatten nested D-Bus struct with `request_json` on CreateCredential and GetCredential @@ -82,7 +82,7 @@ sequenceDiagram ## Enum values Generally, enums are serialized as a tag-value structure with a single-byte tag -and a variant as the value (`(yv)`, in D-Bus terms). The documentation for each +and a variant as the value (`(uv)`, in D-Bus terms). The documentation for each specific enum variant describes how to parse the values. A single null byte (`\0`) is sent for unused enum values. @@ -435,123 +435,89 @@ to the UI until it calls this method. Notification of authenticator state change. ``` -BackgroundEvent[(yv)] [ - (0x01) UsbStateChanged: UsbState, - (0x02) HybridStateChanged: HybridState, +BackgroundEvent[(uv)] [ + /// Ceremony completed successfully + (0x01) CeremonyCompleted + /// Device needs the client PIN to be entered. The backend should collect the + /// PIN and send it back with `EnterClientPin` event of `UserInteracted` signal. + (0x10) NeedsPin: u + (0x11) NeedsUserVerification: u + (0x12) NeedsUserPresence + (0x13) SelectingCredential: aa{sv} u32 = 0x13; + + (0x20) HybridIdle + (0x21) HybridStarted: s + (0x22) HybridConnecting + (0x23) HybridConnected + + (0x30) NfcIdle + (0x31) NfcWaiting + (0x32) NfcConnected + + (0x40) UsbIdle + (0x41) UsbWaiting + (0x42) UsbSelectingDevice: aa{sv} + (0x43) UsbConnected + + (0x80000001) ErrorInternal + (0x80000002) ErrorTimedOut + (0x80000003) ErrorCancelled + (0x80000004) ErrorAuthenticator + (0x80000005) ErrorNoCredentials + (0x80000006) ErrorCredentialExcluded + (0x80000007) ErrorPinAttemptsExhausted + (0x80000008) ErrorPinNotSet ] ``` +### BackgroundEvent::CeremonyCompleted -``` -UsbState[(yv)] { - (0x01) "IDLE", - (0x02) "WAITING" , - (0x03) "SELECTING_DEVICE", - (0x04) "CONNECTED", - (0x05) "NEEDS_PIN", - (0x06) "NEEDS_USER_VERIFICATION", - (0x07) "NEEDS_USER_PRESENCE", - (0x08) "SELECT_CREDENTIAL", - (0x09) "COMPLETED", - (0x0a) "FAILED", -] -``` - -#### UsbState::IDLE - -Not polling for FIDO USB device. - -`name`: "IDLE"` +Authenticator has released the credential, and the ceremony is complete. `tag`: `0x01` `value`: No associated value. -#### UsbState::WAITING - -Awaiting FIDO USB device to be plugged in. - -`name`: `"WAITING"` - -`tag`: `0x02` - -`value`: No associated value. - -#### UsbState::SELECTING_DEVICE - -Multiple USB devices have been detected and are blinking, prompt the user to -tap one to select it. - -`name`: `"SELECTING_DEVICE"` - -`tag`: `0x02` - -`value`: No associated value. - -#### UsbState::CONNECTED - -USB device connected, prompt user to tap. The device may require additional -user verification, but that might not be known until after the user taps the -device. -`name`: `"CONNECTED"` +### BackgroundEvent::NeedsPin -`tag`: `0x04` - -`value`: No associated value. - -#### UsbState::NEEDS_PIN - -> TODO: is attempts_left attempts to permanent lockout or until power cycle? > TODO: Implement cancellation of USB flow The device needs PIN user verification: prompt the user to enter the pin. Send the pin to the flow controller using the enter_client_pin() method. -`name`: `"NEEDS_PIN"` -`tag`: `0x05` +`tag`: `0x10` -`value`: `[i]`, a signed integer indicating the number of PIN attempts remaining -before the device is locked out. If the value is less than 0, the number of attempts +`value`: `[i]`, an integer indicating the number of PIN attempts remaining +before the device is locked out. If the value is `0xffffffff`, the number of attempts left is unknown. -#### UsbState::NEEDS_USER_VERIFICATION - -> TODO: is attempts_left attempts to permanent lockout or until power cycle? +### BackgroundEvent::NeedsUserVerification The device needs on-device user verification (likely biometrics, or can be on-device PIN entry). Prompt the user to interact with the device. -`name`: `"NEEDS_USER_VERIFICATION"` - -`tag`: `0x06` +`tag`: `0x11` -`value`: `[i]`, a signed integer indicating the number of user verification -attempts remaining before the device is locked out. If the value is less than -0, the number of attempts left is unknown. +`value`: `[i]`, am integer indicating the number of user verification +attempts remaining before the user verification is disabled. Once disabled, only the client PIN can be used as a user verification method. If the value is 0xffffffff, the number of attempts left is unknown. -#### UsbState::NEEDS_USER_PRESENCE +### BackgroundEvent::NeedsUserPresence The device needs evidence of user presence (e.g. touch) to release the credential. -`name`: `"NEEDS_USER_PRESENCE"` - -`tag`: `0x07` +`tag`: `0x12` `value`: No associated value. -#### UsbState::SELECT_CREDENTIAL - -> TODO: Change tense of verb to match other states -> SELECTING_CREDENTIAL +### BackgroundEvent::SelectingCredential > TODO: field names of Credential type are confusing: "name" is an ID, and > "username" is a name. We should flip them. Multiple credentials have been found and the user has to select which to use -`name`: `"SELECT_CREDENTIAL"` - -`tag`: `0x08` +`tag`: `0x13` `value`: `[aa{sv}]`: A list of `Credential` objects. @@ -567,157 +533,157 @@ To prevent CTAP credential IDs leaking to the UI, servers SHOULD make `id` an opaque value known only to the implementation, for example, by hashing the actual CTAP credential ID before sending it to the UI. -#### UsbState::COMPLETED - -User tapped USB tapped, flow controller has received credential. +### BackgroundEvent::HybridIdle -`name`: `"COMPLETED"` +Default state, not listening for hybrid transport. -`tag`: `0x09` +`tag`: `0x20` `value`: No associated value. -#### UsbState::FAILED +### BackgroundEvent::HybridStarted, -> TODO: determine how ServiceError is serialized, force to string? +QR code flow is starting, awaiting QR code scan and BLE advert from phone. -Interaction with the authenticator failed. +`tag`: `0x21` -`name`: `"FAILED"` +`value`: `[s]`. String to be encoded as a QR code and displayed to the user to scan. -`tag`: `0x0a` +### BackgroundEvent::HybridConnecting, -`value`: `ServiceError` +BLE advertisement received, connecting to caBLE tunnel with shared secret. -> TODO: Change serialization of ServiceError +`tag`: `0x22` -``` -ServiceError[?] [ - AUTHENTICATOR_ERROR, - NO_CREDENTIALS, - PIN_ATTEMPTS_EXHAUSTED, - INTERNAL, -] -``` +`value`: No associated value -#### ServiceError::AUTHENTICATOR_ERROR +### BackgroundEvent::HybridConnected, -Some unknown error with the authenticator occurred. +Connected to device via caBLE tunnel, waiting for user to release the +credential from their remote device. -`type`: `"AUTHENTICATOR_ERR"` +`tag`: `0x23` -#### ServiceError::NO_CREDENTIALS +`value`: No associated value -No matching credentials were found on the device. +### BackgroundEvent::NfcIdle -`type`: `"NO_CREDENTIALS"` +Not polling for FIDO NFC device. -#### ServiceError::PIN_ATTEMPTS_EXHAUSTED, +`tag`: `0x30` -Too many incorrect PIN attempts, and authenticator must be removed and -reinserted to continue any more PIN attempts. +`value`: No associated value. -Note that this is different than exhausting the PIN count that fully -locks out the device. +### BackgroundEvent::NfcWaiting -`type`: `"PIN_ATTEMPTS_EXHAUSTED"` +Awaiting FIDO NFC device to be detected. -#### ServiceError::INTERNAL, +`tag`: `0x31` -Something went wrong with the credential service itself, not the authenticator. +`value`: No associated value. -`type`: `"INTERNAL"` +### BackgroundEvent::NfcConnected -### HybridState +NFC device connected, prompt user to tap. The device may require additional +user verification, but that might not be known until after the user taps the +device. -> TODO: Failed has no reason +`tag`: `0x32` -``` -HybridState[(yv)] [ - (0x01) "IDLE", - (0x02) "STARTED", - (0x03) "CONNECTING", - (0x04) "CONNECTED", - (0x05) "COMPLETED", - (0x06) "USER_CANCELLED", - (0x07) "FAILED", -] -``` +`value`: No associated value. -`HybridState` represents the state of hybrid authenticator flow. +### BackgroundEvent::UsbIdle -In D-Bus this is represented as a dictionary `[a{sv}]` with two keys `type`, -which is a `HybridStateType`, and `value` whose value depends on -`HybridStateType` and is described below. +Not polling for FIDO USB device. -#### HybridState::Idle +`tag`: `0x41` -Default state, not listening for hybrid transport. +`value`: No associated value. -`name`: `"IDLE"` +### BackgroundEvent::UsbWaiting -`tag`: `0x04` +Awaiting FIDO USB device to be plugged in. + +`tag`: `0x42` `value`: No associated value. -#### HybridState::Started, +### BackgroundEvent::UsbSelectingDevice -QR code flow is starting, awaiting QR code scan and BLE advert from phone. +Multiple USB devices have been detected and are blinking, prompt the user to +tap one to select it. -`name`: `"STARTED"` +`tag`: `0x43` -`tag`: `0x04` +`value`: No associated value. -`value`: `[s]`. String to be encoded as a QR code and displayed to the user to scan. +### BackgroundEvent::UsbConnected -#### HybridState::Connecting, +USB device connected, prompt user to tap. The device may require additional +user verification, but that might not be known until after the user taps the +device. -BLE advert received, connecting to caBLE tunnel with shared secret. +`tag`: `0x44` -`name`: `"CONNECTING"` +`value`: No associated value. -`tag`: `0x03` +### BackgroundEvent::ErrorInternal -`value`: No associated value +Something went wrong with the credential service itself, not the authenticator. -#### HybridState::Connected, +`tag`: `0x80000001` -Connected to device via caBLE tunnel, waiting for user to release the -credential from their remote device. +`value`: No associated value. -`name`: `"CONNECTED"` +### BackgroundEvent::ErrorTimedOut -`tag`: `0x04` +Request timed out. -`value`: No associated value +`tag`: `0x80000002` -#### HybridState::Completed, +`value`: No associated value. -Credential received over tunnel. +### BackgroundEvent::ErrorCancelled -`name`: `"COMPLETED"` +User cancelled the request -`tag`: `0x05` +`tag`: `0x80000003` -`value`: No associated value +`value`: No associated value. -#### HybridState::UserCancelled, +### BackgroundEvent::ErrorAuthenticator -Authenticator operation was cancelled. +Some unknown error with the authenticator occurred. -`name`: `"USER_CANCELLED"` +`tag`: `0x80000004` -`tag`: `0x06` +`value`: No associated value. -`value`: No associated value +### BackgroundEvent::NoCredentials -#### HybridState::Failed, +No matching credentials were found on the device. -Failed to receive a credential from the hybrid authenticator. +`tag`: `0x80000005` -`name`: `"FAILED"` +`value`: No associated value. + +### BackgroundEvent::CredentialExcluded, + +A credential matching the credential request already exists on the authenticator. + +`tag`: `0x80000006` + +`value`: No associated value. + +### BackgroundEvent::PinAttemptsExhausted, + +Too many incorrect PIN attempts, and authenticator must be removed and +reinserted to continue any more PIN attempts. + +Note that this is different than exhausting the PIN count that fully +locks out the device. -`tag`: `0x07` +`tag`: `0x80000007` `value`: No associated value.