From addf1e5562f69ae1608287c13411f5059d121c64 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus Date: Tue, 8 Sep 2026 13:29:06 +0200 Subject: [PATCH 1/5] Introduce function to determine if error code should terminate the ceremony (plus renaming of the cancellation variant) --- credentialsd/src/credential_service/hybrid.rs | 6 +- credentialsd/src/credential_service/mod.rs | 241 ++++++++++++++---- credentialsd/src/credential_service/nfc.rs | 10 +- credentialsd/src/credential_service/usb.rs | 16 +- 4 files changed, 203 insertions(+), 70 deletions(-) diff --git a/credentialsd/src/credential_service/hybrid.rs b/credentialsd/src/credential_service/hybrid.rs index 369c0b2..56e9f94 100644 --- a/credentialsd/src/credential_service/hybrid.rs +++ b/credentialsd/src/credential_service/hybrid.rs @@ -166,13 +166,13 @@ impl HybridHandler for InternalHybridHandler { Some(resp) => resp, None => { tracing::debug!("Hybrid handler cancelled, stopping processing"); - Err(CredentialServiceError::RequestCancelled) + Err(CredentialServiceError::NonTerminatingCancellation) } }; let terminal_state = match response { Ok(auth_response) => Some(HybridStateInternal::Completed(auth_response)), - Err(CredentialServiceError::RequestCancelled) => { + Err(CredentialServiceError::NonTerminatingCancellation) => { // Cancelled by another transport winning or an explicit user cancel. // Do not emit a Failed state — complete_request was already called // by the winning path, and emitting Failed here would produce a @@ -285,7 +285,7 @@ impl From<&HybridState> for BackgroundEvent { BackgroundEvent::ErrorAuthenticator } // This should currently never be reached, but we'll likely use it in future refactoring - HybridState::Failed(CredentialServiceError::RequestCancelled) => { + HybridState::Failed(CredentialServiceError::NonTerminatingCancellation) => { BackgroundEvent::ErrorCancelled } HybridState::Failed(CredentialServiceError::Internal(_)) => { diff --git a/credentialsd/src/credential_service/mod.rs b/credentialsd/src/credential_service/mod.rs index c3068a7..01a04fe 100644 --- a/credentialsd/src/credential_service/mod.rs +++ b/credentialsd/src/credential_service/mod.rs @@ -49,7 +49,7 @@ async fn cancellable_sleep( tokio::select! { _ = tokio::time::sleep(duration) => Ok(()), _ = cancellation.cancelled() => { - Err(CredentialServiceError::RequestCancelled) + Err(CredentialServiceError::NonTerminatingCancellation) } } } @@ -76,11 +76,14 @@ pub enum CredentialServiceError { /// Note that this is different than exhausting the PIN count that fully /// locks out the device. PinAttemptsExhausted, - /// The request was cancelled — either because another transport completed the - /// ceremony first, or because the user or client explicitly cancelled it. - /// This is an expected, non-error termination and should not be treated as an - /// authenticator failure. - RequestCancelled, + /// Internal cancellation — the ceremony was cancelled because another transport + /// completed first (superseded), or because a code-path cancellation propagated. + /// This is distinct from user- or client-issued cancellation: the response channel + /// has already been consumed elsewhere, so `complete_request` must NOT be called. + /// + /// A future `TerminatingCancellation` variant will be added for user-initiated + /// cancellation from the trusted UI, which *is* ceremony-terminating. + NonTerminatingCancellation, // TODO: We may want to hide the details on this variant from the public API. /// Something went wrong with the credential service itself, not the authenticator. Internal(String), @@ -95,7 +98,7 @@ impl Display for CredentialServiceError { Self::NoCredentials => f.write_str("NoCredentials"), Self::CredentialExcluded => f.write_str("CredentialExcluded"), Self::PinAttemptsExhausted => f.write_str("PinAttemptsExhausted"), - Self::RequestCancelled => f.write_str("RequestCancelled"), + Self::NonTerminatingCancellation => f.write_str("NonTerminatingCancellation"), Self::Internal(s) => write!(f, "InternalError: {s}"), } } @@ -111,7 +114,7 @@ impl TryFrom<&Value<'_>> for CredentialServiceError { "NoCredentials" => Self::NoCredentials, "CredentialExcluded" => Self::CredentialExcluded, "PinAttemptsExhausted" => Self::PinAttemptsExhausted, - "RequestCancelled" => Self::RequestCancelled, + "NonTerminatingCancellation" => Self::NonTerminatingCancellation, s => Self::Internal(String::from(s)), }; Ok(err) @@ -403,13 +406,15 @@ where HybridStateInternal::Completed(response) => { complete_request(ctx, Ok(response.clone())); } - // RequestCancelled (another transport won or user cancelled) - // should not call complete_request — it was already called - // by the winning transport or cancel_request(). - HybridStateInternal::Failed(CredentialServiceError::RequestCancelled) => {} - HybridStateInternal::Failed(err) => { + HybridStateInternal::Failed(err) if is_ceremony_terminating(err) => { complete_request(ctx, Err(err.clone())); } + HybridStateInternal::Failed(_) => { + // Non-terminating: forward the Failed state to the UI + // without calling complete_request. The ceremony stays alive + // for other transports. The transport is expected to restart + // itself. + } _ => {} } Poll::Ready(Some(state.into())) @@ -447,12 +452,14 @@ where UsbStateInternal::Completed(response) => { complete_request(ctx, Ok(response.clone())); } - // RequestCancelled (another transport won or user cancelled) - // should not call complete_request — it was already called - // by the winning transport or cancel_request(). - UsbStateInternal::Failed(CredentialServiceError::RequestCancelled) => {} - UsbStateInternal::Failed(error) => { - complete_request(ctx, Err(error.clone())); + UsbStateInternal::Failed(err) if is_ceremony_terminating(err) => { + complete_request(ctx, Err(err.clone())); + } + UsbStateInternal::Failed(_) => { + // Non-terminating: forward the Failed state to the UI + // without calling complete_request. The ceremony stays alive + // for other transports. The transport is expected to restart + // itself. } _ => {} } @@ -493,12 +500,14 @@ where NfcStateInternal::Completed(response) => { complete_request(ctx, Ok(response.clone())); } - // RequestCancelled (another transport won or user cancelled) - // should not call complete_request — it was already called - // by the winning transport or cancel_request(). - NfcStateInternal::Failed(CredentialServiceError::RequestCancelled) => {} - NfcStateInternal::Failed(error) => { - complete_request(ctx, Err(error.clone())); + NfcStateInternal::Failed(err) if is_ceremony_terminating(err) => { + complete_request(ctx, Err(err.clone())); + } + NfcStateInternal::Failed(_) => { + // Non-terminating: forward the Failed state to the UI + // without calling complete_request. The ceremony stays alive + // for other transports. The transport is expected to restart + // itself. } _ => {} } @@ -543,6 +552,42 @@ impl From for DeviceStateUpdate { } } +/// Returns `true` if this arm of `poll_next` must call `complete_request()`, +/// terminating the entire ceremony. Returns `false` if the error is +/// per-authenticator or already-handled: the ceremony continues on other +/// transports, and this transport is expected to restart itself. +/// +/// The `match` has no wildcard arm so any new `Error` variant forces a +/// deliberate decision here. The mapping follows the WebAuthn specification: +/// - https://www.w3.org/TR/webauthn-3/#sctn-create-request-exceptions +/// - https://www.w3.org/TR/webauthn-3/#sctn-get-request-exceptions +fn is_ceremony_terminating(err: &CredentialServiceError) -> bool { + match err { + // WebAuthn spec requires CredentialExcluded be remapped to InvalidStateError + // and returned to the RP. The credential is already registered on this + // authenticator. + CredentialServiceError::CredentialExcluded => true, + + // NonTerminatingCancellation is emitted by transports whose ceremony was + // cancelled by *another* path — the winning transports `complete_request()`, + // or `cancel_request()` sending its own response. The response channel is + // already consumed, so this arm must NOT invoke `complete_request()` again. + // A future `TerminatingCancellation` variant will handle user-initiated + // cancellation from the trusted UI, which is ceremony-terminating. + CredentialServiceError::NonTerminatingCancellation => false, + + // Per-authenticator errors: keep the ceremony alive. The user may succeed + // on another transport, or the same transport may recover and retry + // (the latter is not yet implemented). + CredentialServiceError::AuthenticatorError => false, + CredentialServiceError::NoCredentials => false, + CredentialServiceError::PinAttemptsExhausted => false, + + // Transient internal errors: do not kill the ceremony. + CredentialServiceError::Internal(_) => false, + } +} + fn complete_request( ctx: &Mutex>, response: Result, @@ -739,10 +784,13 @@ mod tests { let start = tokio::time::Instant::now(); let result = cancellable_sleep(Duration::from_secs(5), &token).await; - // Must return RequestCancelled, not a generic Internal error + // Must return NonTerminatingCancellation, not a generic Internal error assert!( - matches!(result, Err(CredentialServiceError::RequestCancelled)), - "cancellable_sleep must return RequestCancelled when the token is cancelled" + matches!( + result, + Err(CredentialServiceError::NonTerminatingCancellation) + ), + "cancellable_sleep must return NonTerminatingCancellation when the token is cancelled" ); // Should return immediately, not after 5 seconds assert!(start.elapsed() < Duration::from_millis(100)); @@ -1137,32 +1185,80 @@ mod tests { } #[tokio::test] - async fn test_failed_request_triggers_cancellation() { + async fn test_terminating_failure_ends_ceremony() { let usb_handler = CancellationTrackingHandler::::new(); let usb_ref = usb_handler.get_handler_ref(); let service = CredentialService::new(MockHybridHandler, MockNfcHandler, usb_handler); let request = create_test_request().await; - let (tx, _rx) = oneshot::channel(); + let (tx, rx) = oneshot::channel(); - let (_request_id, cancellation_token) = service.init_request(&request, tx).await.unwrap(); + let (_id, token) = service.init_request(&request, tx).await.unwrap(); let mut usb_stream = service.get_usb_credential().await; - assert!(!cancellation_token.is_cancelled()); + assert!(!token.is_cancelled()); - usb_ref.shift_state(UsbStateInternal::Waiting); - assert!(matches!(usb_stream.next().await, Some(UsbState::Waiting))); + // CredentialExcluded is ceremony-terminating per the WebAuthn spec + usb_ref.shift_state(UsbStateInternal::Failed( + CredentialServiceError::CredentialExcluded, + )); + assert!(matches!( + usb_stream.next().await, + Some(UsbState::Failed(CredentialServiceError::CredentialExcluded)) + )); - usb_ref.shift_state(UsbStateInternal::Failed(CredentialServiceError::Internal( - "test failure".to_string(), - ))); - assert!(matches!(usb_stream.next().await, Some(UsbState::Failed(_)))); + // complete_request was called: token cancelled, response delivered to caller + assert!( + token.is_cancelled(), + "ceremony must end on terminating error" + ); + assert!( + matches!( + rx.await, + Ok(Err(CredentialServiceError::CredentialExcluded)) + ), + "caller must receive the terminating error" + ); + } - // UsbStateStream calls complete_request on Failed, which cancels the token + #[tokio::test] + async fn test_non_terminating_failure_keeps_ceremony_alive() { + let usb_handler = CancellationTrackingHandler::::new(); + let hybrid_handler = CancellationTrackingHandler::::new(); + let usb_ref = usb_handler.get_handler_ref(); + let hybrid_ref = hybrid_handler.get_handler_ref(); + + let service = CredentialService::new(hybrid_handler, MockNfcHandler, usb_handler); + let request = create_test_request().await; + let (tx, _rx) = oneshot::channel(); + let (id, token) = service.init_request(&request, tx).await.unwrap(); + + let mut usb_stream = service.get_usb_credential().await; + let mut hybrid_stream = service.get_hybrid_credential().await; + + // USB fails with a non-terminating error — forwarded to UI but ceremony lives + usb_ref.shift_state(UsbStateInternal::Failed( + CredentialServiceError::AuthenticatorError, + )); + assert!(matches!( + usb_stream.next().await, + Some(UsbState::Failed(CredentialServiceError::AuthenticatorError)) + )); + + // Token must still be live — is_ceremony_terminating(AuthenticatorError) == false assert!( - cancellation_token.is_cancelled(), - "Cancellation token should be triggered when request fails" + !token.is_cancelled(), + "ceremony must stay alive on non-terminating error" ); + + // Other transports must still be operational + hybrid_ref.shift_state(HybridStateInternal::Connecting); + assert!(matches!( + hybrid_stream.next().await, + Some(HybridState::Connecting) + )); + + service.cancel_request(id).await; } #[tokio::test] @@ -1192,13 +1288,16 @@ mod tests { // It sits in the channel when complete_request() cancels the token. hybrid_ref.shift_state(HybridStateInternal::Connecting); - // USB fails — UsbStateStream calls complete_request → token cancelled + // USB fails with a ceremony-terminating error → complete_request → token cancelled usb_ref.shift_state(UsbStateInternal::Waiting); - usb_ref.shift_state(UsbStateInternal::Failed(CredentialServiceError::Internal( - "test".to_string(), - ))); + usb_ref.shift_state(UsbStateInternal::Failed( + CredentialServiceError::CredentialExcluded, + )); assert!(matches!(usb_stream.next().await, Some(UsbState::Waiting))); - assert!(matches!(usb_stream.next().await, Some(UsbState::Failed(_)))); + assert!(matches!( + usb_stream.next().await, + Some(UsbState::Failed(CredentialServiceError::CredentialExcluded)) + )); assert!( cancellation_token.is_cancelled(), @@ -1314,10 +1413,10 @@ mod tests { usb_ref.shift_state(UsbStateInternal::Waiting); assert!(matches!(usb_stream.next().await, Some(UsbState::Waiting))); - // Queue a RequestCancelled — simulates what process() emits when the - // cancellation token fires internally before the outer branch catches it. + // Queue a NonTerminatingCancellation — simulates what process() emits when + // the cancellation token fires internally before the outer branch catches it. usb_ref.shift_state(UsbStateInternal::Failed( - CredentialServiceError::RequestCancelled, + CredentialServiceError::NonTerminatingCancellation, )); // Cancel the request synchronously so the token is already cancelled @@ -1325,12 +1424,12 @@ mod tests { service.cancel_request(request_id).await; assert!(token.is_cancelled()); - // The stream must not yield the Failed(RequestCancelled) state. + // The stream must not yield the Failed(NonTerminatingCancellation) state. // biased select! polls cancellation first; the queued state is discarded. let remaining: Vec<_> = usb_stream.collect().await; assert!( remaining.is_empty(), - "cancelled USB stream must emit no further states, including Failed(RequestCancelled)" + "cancelled USB stream must emit no further states, including Failed(NonTerminatingCancellation)" ); } @@ -1355,20 +1454,54 @@ mod tests { Some(HybridState::Init(_)) )); - // Queue a RequestCancelled — what the real handler would emit when + // Queue a NonTerminatingCancellation — what the real handler would emit when // run_until_cancelled returns None hybrid_ref.shift_state(HybridStateInternal::Failed( - CredentialServiceError::RequestCancelled, + CredentialServiceError::NonTerminatingCancellation, )); service.cancel_request(request_id).await; assert!(token.is_cancelled()); - // Stream must stop without emitting the Failed(RequestCancelled) state. + // Stream must stop without emitting the Failed(NonTerminatingCancellation) state. let remaining: Vec<_> = hybrid_stream.collect().await; assert!( remaining.is_empty(), "cancelled hybrid stream must emit no further states" ); } + + // The following tests are stupid, but try to prevent regressions regarding changes around + // `is_ceremony_terminating()` + #[test] + fn test_classifier_ceremony_terminating_errors() { + // These return true — this arm must call complete_request and end the ceremony. + assert!(is_ceremony_terminating( + &CredentialServiceError::CredentialExcluded + )); + } + + #[test] + fn test_classifier_per_authenticator_errors() { + // Per-authenticator: ceremony stays alive; the same or another transport can retry. + let errors = [ + CredentialServiceError::AuthenticatorError, + CredentialServiceError::NoCredentials, + CredentialServiceError::PinAttemptsExhausted, + CredentialServiceError::Internal("x".into()), + ]; + for err in errors { + assert!(!is_ceremony_terminating(&err)); + } + } + + #[test] + fn test_classifier_non_terminating_cancellation() { + // NonTerminatingCancellation: the response channel has already been consumed + // by the winning transport's complete_request or by cancel_request directly. + // This arm must NOT invoke complete_request again — hence false. + assert!(!is_ceremony_terminating( + &CredentialServiceError::NonTerminatingCancellation + )); + } } diff --git a/credentialsd/src/credential_service/nfc.rs b/credentialsd/src/credential_service/nfc.rs index 7ac0edf..2dbba68 100644 --- a/credentialsd/src/credential_service/nfc.rs +++ b/credentialsd/src/credential_service/nfc.rs @@ -43,7 +43,7 @@ impl InProcessNfcHandler { let list_device_fut = libwebauthn::transport::nfc::get_nfc_device(); let Some(result) = cancellation.run_until_cancelled(list_device_fut).await else { tracing::debug!("NFC idle polling cancelled"); - return Err(CredentialServiceError::RequestCancelled); + return Err(CredentialServiceError::NonTerminatingCancellation); }; match result { Ok(Some(nfc_device)) => Ok(NfcStateInternal::Connected(nfc_device)), @@ -222,10 +222,10 @@ impl InProcessNfcHandler { }; // Guard: inner future may have raced the cancellation token and returned - // RequestCancelled. Break cleanly without emitting a spurious Failed state. + // NonTerminatingCancellation. Break cleanly without emitting a spurious Failed state. if matches!( next_nfc_state, - Err(CredentialServiceError::RequestCancelled) + Err(CredentialServiceError::NonTerminatingCancellation) ) { tracing::debug!("NFC handler cancelled (inner path), stopping processing"); break Ok(()); @@ -359,7 +359,7 @@ async fn handle_events( // because libwebauthn drops _handle_rx in NfcChannel::new(). Cancellation // takes effect at the next inter-APDU .await point when the future is // dropped; NFC exchanges are short so the latency is acceptable. - Err(CredentialServiceError::RequestCancelled) + Err(CredentialServiceError::NonTerminatingCancellation) } }; @@ -581,7 +581,7 @@ impl From<&NfcState> for BackgroundEvent { NfcState::Failed(CredentialServiceError::PinAttemptsExhausted) => { BackgroundEvent::ErrorAuthenticator } - NfcState::Failed(CredentialServiceError::RequestCancelled) => { + NfcState::Failed(CredentialServiceError::NonTerminatingCancellation) => { BackgroundEvent::ErrorCancelled } NfcState::Failed(CredentialServiceError::Internal(_)) => BackgroundEvent::ErrorInternal, diff --git a/credentialsd/src/credential_service/usb.rs b/credentialsd/src/credential_service/usb.rs index a68837e..13ab453 100644 --- a/credentialsd/src/credential_service/usb.rs +++ b/credentialsd/src/credential_service/usb.rs @@ -47,7 +47,7 @@ impl InProcessUsbHandler { let list_device_fut = libwebauthn::transport::hid::list_devices(); let Some(result) = cancellation.run_until_cancelled(list_device_fut).await else { tracing::debug!("USB idle polling cancelled"); - return Err(CredentialServiceError::RequestCancelled); + return Err(CredentialServiceError::NonTerminatingCancellation); }; match result { @@ -147,7 +147,7 @@ impl InProcessUsbHandler { tracing::info!("Cancelling blinking device {device:?}."); handle.cancel_ongoing_operation().await; } - return Err(CredentialServiceError::RequestCancelled); + return Err(CredentialServiceError::NonTerminatingCancellation); }; let Some(msg) = maybe_msg else { @@ -330,12 +330,12 @@ impl InProcessUsbHandler { }; // Guard: an inner future may have raced the cancellation token and - // returned RequestCancelled as a value rather than the outer branch - // firing. Treat it the same way — break cleanly without emitting a - // spurious Failed state to the UI. + // returned NonTerminatingCancellation as a value rather than the outer + // branch firing. Treat it the same way — break cleanly without emitting + // a spurious Failed state to the UI. if matches!( next_usb_state, - Err(CredentialServiceError::RequestCancelled) + Err(CredentialServiceError::NonTerminatingCancellation) ) { tracing::debug!("USB handler cancelled (inner path), stopping processing"); break Ok(()); @@ -467,7 +467,7 @@ async fn handle_events( None => { tracing::debug!("USB ceremony cancelled, interrupting authenticator operation"); cancel_handle.cancel_ongoing_operation().await; - Err(CredentialServiceError::RequestCancelled) + Err(CredentialServiceError::NonTerminatingCancellation) } }; @@ -705,7 +705,7 @@ impl From<&UsbState> for BackgroundEvent { UsbState::Failed(CredentialServiceError::PinAttemptsExhausted) => { BackgroundEvent::ErrorAuthenticator } - UsbState::Failed(CredentialServiceError::RequestCancelled) => { + UsbState::Failed(CredentialServiceError::NonTerminatingCancellation) => { BackgroundEvent::ErrorCancelled } UsbState::Failed(CredentialServiceError::Internal(_)) => BackgroundEvent::ErrorInternal, From 427f449bde3709555db33a86edf6be26ad536d58 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus Date: Tue, 8 Sep 2026 14:55:12 +0200 Subject: [PATCH 2/5] Add new TransportRestarted signal that lets the UI recover from a non-terminal error --- CHANGELOG.md | 2 + credentialsd-common/src/model.rs | 40 ++- credentialsd-ui/src/dbus.rs | 49 +++- credentialsd-ui/src/gui/mod.rs | 4 + credentialsd-ui/src/gui/view_model/gtk/mod.rs | 12 + .../src/gui/view_model/gtk/window.rs | 14 + credentialsd-ui/src/gui/view_model/mod.rs | 11 + credentialsd/src/credential_service/hybrid.rs | 269 ++++++++++-------- credentialsd/src/credential_service/mod.rs | 129 ++++++++- credentialsd/src/credential_service/nfc.rs | 40 ++- credentialsd/src/credential_service/usb.rs | 40 ++- credentialsd/src/dbus/ui_control.rs | 52 +++- 12 files changed, 527 insertions(+), 135 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1da53b..6b95367 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # [unreleased] +- ui: Basic recovery from failed attempts to use devices (e.g. flaky bluetooth) + # 0.3.1 [2026-09-05] ## Improvements diff --git a/credentialsd-common/src/model.rs b/credentialsd-common/src/model.rs index 98a6e88..1c74184 100644 --- a/credentialsd-common/src/model.rs +++ b/credentialsd-common/src/model.rs @@ -17,25 +17,45 @@ pub const BACKGROUND_EVENT_ERROR_PIN_NOT_SET: u32 = 0x80000008; #[derive(Debug, PartialEq)] pub enum BackgroundEvent { CeremonyCompleted, - NeedsPin { attempts_left: Option }, - PinNotSet { error: PinNotSetError }, - NeedsUserVerification { attempts_left: Option }, + NeedsPin { + attempts_left: Option, + }, + PinNotSet { + error: PinNotSetError, + }, + NeedsUserVerification { + attempts_left: Option, + }, NeedsUserPresence, - SelectingCredential { creds: Vec }, + SelectingCredential { + creds: Vec, + }, HybridIdle, HybridStarted(OwnedFd), HybridConnecting, HybridConnected, + /// The hybrid ceremony was interrupted by a non-terminating error and a new + /// QR code is about to be issued. The UI should navigate back to the start + /// page so the new QR becomes visible. + HybridRestarting, NfcIdle, NfcWaiting, NfcConnected, + /// The NFC ceremony was interrupted by a non-terminating error and the + /// transport is polling for a new device tap. The UI should navigate back + /// to the start page. + NfcRestarting, UsbIdle, UsbWaiting, UsbSelectingDevice, UsbConnected, + /// The USB ceremony was interrupted by a non-terminating error and the + /// transport is polling for a device. The UI should navigate back to the + /// start page. + UsbRestarting, ErrorInternal, ErrorTimedOut, @@ -146,6 +166,18 @@ pub struct NotifyNfcConnectedOptions {} #[zvariant(signature = "dict")] pub struct NotifyUsbConnectedOptions {} +#[derive(Debug, SerializeDict, DeserializeDict, Type)] +#[zvariant(signature = "dict")] +pub struct NotifyHybridRestartingOptions {} + +#[derive(Debug, SerializeDict, DeserializeDict, Type)] +#[zvariant(signature = "dict")] +pub struct NotifyUsbRestartingOptions {} + +#[derive(Debug, SerializeDict, DeserializeDict, Type)] +#[zvariant(signature = "dict")] +pub struct NotifyNfcRestartingOptions {} + #[derive(Clone, Debug, Serialize, Deserialize, Type)] pub enum Operation { PublicKeyCreate, diff --git a/credentialsd-ui/src/dbus.rs b/credentialsd-ui/src/dbus.rs index 8456556..7f10823 100644 --- a/credentialsd-ui/src/dbus.rs +++ b/credentialsd-ui/src/dbus.rs @@ -29,9 +29,10 @@ use credentialsd_common::model::{ BACKGROUND_EVENT_ERROR_PIN_NOT_SET, BACKGROUND_EVENT_ERROR_TIMED_OUT, BackgroundEvent, ClientPinEnteredOptions, Credential, CredentialSelectedOptions, Device, DiscoveryRequestedOptions, NotifyHybridConnectedOptions, NotifyHybridConnectingOptions, - NotifyHybridStartedOptions, NotifyNeedsPinOptions, NotifyNeedsUserPresenceOptions, - NotifyNeedsUserVerificationOptions, NotifyNfcConnectedOptions, NotifyPinNotSetOptions, - NotifySelectingCredentialOptions, NotifyUsbConnectedOptions, Operation, PinNotSetError, + NotifyHybridRestartingOptions, NotifyHybridStartedOptions, NotifyNeedsPinOptions, + NotifyNeedsUserPresenceOptions, NotifyNeedsUserVerificationOptions, NotifyNfcConnectedOptions, + NotifyNfcRestartingOptions, NotifyPinNotSetOptions, NotifySelectingCredentialOptions, + NotifyUsbConnectedOptions, NotifyUsbRestartingOptions, Operation, PinNotSetError, PortalBackendOptions, SetDevicePinOptions, UserInteractedEvent, WindowHandle, }; @@ -307,6 +308,48 @@ impl CredentialPortalBackend { .await } + async fn notify_hybrid_restarting( + &self, + #[zbus(object_server)] object_server: &ObjectServer, + session_handle: ObjectPath<'_>, + _options: NotifyHybridRestartingOptions, + ) -> fdo::Result<()> { + self.notify_state_changed( + object_server, + session_handle, + BackgroundEvent::HybridRestarting, + ) + .await + } + + async fn notify_usb_restarting( + &self, + #[zbus(object_server)] object_server: &ObjectServer, + session_handle: ObjectPath<'_>, + _options: NotifyUsbRestartingOptions, + ) -> fdo::Result<()> { + self.notify_state_changed( + object_server, + session_handle, + BackgroundEvent::UsbRestarting, + ) + .await + } + + async fn notify_nfc_restarting( + &self, + #[zbus(object_server)] object_server: &ObjectServer, + session_handle: ObjectPath<'_>, + _options: NotifyNfcRestartingOptions, + ) -> fdo::Result<()> { + self.notify_state_changed( + object_server, + session_handle, + BackgroundEvent::NfcRestarting, + ) + .await + } + /// Called when the authentication ceremony completes successfully. async fn notify_ceremony_completed( &self, diff --git a/credentialsd-ui/src/gui/mod.rs b/credentialsd-ui/src/gui/mod.rs index e2acb3f..e55ada9 100644 --- a/credentialsd-ui/src/gui/mod.rs +++ b/credentialsd-ui/src/gui/mod.rs @@ -95,6 +95,10 @@ pub enum ViewUpdate { HybridConnecting, HybridConnected, + /// A transport ceremony was interrupted by a non-terminating error and + /// is restarting. The UI should navigate back to the start page. + TransportRestarting, + Completed, Cancelled, Failed(String), diff --git a/credentialsd-ui/src/gui/view_model/gtk/mod.rs b/credentialsd-ui/src/gui/view_model/gtk/mod.rs index 109bc89..76afc9e 100644 --- a/credentialsd-ui/src/gui/view_model/gtk/mod.rs +++ b/credentialsd-ui/src/gui/view_model/gtk/mod.rs @@ -84,6 +84,9 @@ mod imp { #[property(get, set)] pub qr_spinner_visible: RefCell, + #[property(get, set)] + pub transport_restarting: RefCell, + #[property(get, set)] pub start_setting_new_pin_visible: RefCell, @@ -138,6 +141,7 @@ impl ViewModel { // TODO: hack so I don't have to unset this in every event manually. view_model.set_usb_nfc_pin_entry_visible(false); view_model.set_start_setting_new_pin_visible(false); + view_model.set_transport_restarting(false); view_model.set_failed(false); match update { ViewUpdate::SetTitle { @@ -239,6 +243,14 @@ impl ViewModel { )); view_model.set_qr_spinner_visible(false); } + ViewUpdate::TransportRestarting => { + // Signal the window to navigate back to start_page. + // The transport will emit a fresh Init/Connected state + // next, which will update the prompt and show the new + // QR code or device-waiting UI from start_page. + view_model.set_qr_spinner_visible(false); + view_model.set_transport_restarting(true); + } ViewUpdate::Completed => { view_model.set_qr_spinner_visible(false); view_model.set_completed(true); diff --git a/credentialsd-ui/src/gui/view_model/gtk/window.rs b/credentialsd-ui/src/gui/view_model/gtk/window.rs index 58d1cac..dceb718 100644 --- a/credentialsd-ui/src/gui/view_model/gtk/window.rs +++ b/credentialsd-ui/src/gui/view_model/gtk/window.rs @@ -226,6 +226,20 @@ impl CredentialsUiWindow { } )); + // When any transport restarts after a non-terminating error, navigate back to + // start_page. For hybrid this ensures the new QR code (which lives on start_page) + // is visible; for USB/NFC it clears stale prompts and lets the user re-plug or + // choose a different transport. + view_model.connect_transport_restarting_notify(clone!( + #[weak] + stack, + move |vm| { + if vm.transport_restarting() { + stack.set_visible_child_name("start_page"); + } + } + )); + view_model.connect_completed_notify(clone!( #[weak] stack, diff --git a/credentialsd-ui/src/gui/view_model/mod.rs b/credentialsd-ui/src/gui/view_model/mod.rs index 0a791e1..6cf331b 100644 --- a/credentialsd-ui/src/gui/view_model/mod.rs +++ b/credentialsd-ui/src/gui/view_model/mod.rs @@ -348,6 +348,17 @@ impl ViewModel { .await .unwrap(); } + Event::Background( + BackgroundEvent::HybridRestarting + | BackgroundEvent::UsbRestarting + | BackgroundEvent::NfcRestarting, + ) => { + self.hybrid_qr_code_data = None; + self.tx_update + .send(ViewUpdate::TransportRestarting) + .await + .unwrap(); + } Event::Background(BackgroundEvent::ErrorCancelled) => { self.hybrid_qr_code_data = None; break; diff --git a/credentialsd/src/credential_service/hybrid.rs b/credentialsd/src/credential_service/hybrid.rs index 56e9f94..c5094e9 100644 --- a/credentialsd/src/credential_service/hybrid.rs +++ b/credentialsd/src/credential_service/hybrid.rs @@ -68,127 +68,65 @@ impl HybridHandler for InternalHybridHandler { } else { CableTransports::CloudAssistedOnly }; - let mut device = match CableQrCodeDevice::new_transient(hint, hybrid_transports) { - Ok(device) => device, - Err(err) => { - tracing::error!("Failed to create caBLE QR code device: {:?}", err); - return; - } - }; - let qr_code = device.qr_code.to_string(); - if let Err(err) = tx.send(HybridStateInternal::Init(qr_code)).await { - tracing::error!("Failed to send caBLE update: {:?}", err); - return; - }; - tokio::spawn(async move { - let mut channel = match device.channel(ChannelSettings::default()).await { - Ok(channel) => channel, - Err(e) => { - tracing::error!("Failed to open hybrid channel: {:?}", e); - panic!(); + + // Outer retry loop: re-issues QR on non-terminating failures. + // Each iteration creates a fresh CableQrCodeDevice (the previous one + // is consumed by channel()), so the old QR secret is discarded. + loop { + let mut device = match CableQrCodeDevice::new_transient(hint, hybrid_transports) { + Ok(device) => device, + Err(err) => { + tracing::error!("Failed to create caBLE QR code device: {:?}", err); + // Device creation failure cannot be retried meaningfully — + // break to avoid a tight error loop. + let _ = tx + .send(HybridStateInternal::Failed( + CredentialServiceError::Internal(format!( + "Failed to create caBLE device: {err:?}" + )), + )) + .await; + break; } }; - let state_sender_clone = tx.clone(); - let ux_updates_rx = channel.get_ux_update_receiver(); - tokio::spawn(async move { - handle_hybrid_updates(&state_sender_clone, ux_updates_rx).await; - debug!("Reached end of Hybrid updates stream."); - }); + let qr_code = device.qr_code.to_string(); + if let Err(err) = tx.send(HybridStateInternal::Init(qr_code)).await { + tracing::error!("Failed to send caBLE update: {:?}", err); + break; + } - let wait_for_response_fut = async { - loop { - let response: Result = match &request { - CredentialRequest::CreatePublicKeyCredentialRequest(make_request) => { - channel.webauthn_make_credential(make_request).await.map( - |make_credential_response| { - CredentialResponse::from_make_credential( - &make_credential_response, - &["hybrid"], - "cross-platform", - ) - }, - ) - } - CredentialRequest::GetPublicKeyCredentialRequest(get_request) => { - channel.webauthn_get_assertion(get_request).await.map( - |get_assertion_response| { - CredentialResponse::from_get_assertion( - // When doing hybrid, the authenticator is capable of displaying it's own UI. - // So we assume here, it only ever returns one assertion. - // In case this doesn't hold true, we have to implement credential selection here, - // like USB, for example. - &get_assertion_response.assertions[0], - "cross-platform", - ) - }, - ) - } - }; - match response { - Ok(response) => { - tracing::debug!("Received credential from hybrid authenticator"); - break Ok(response); - } - Err(WebAuthnError::Ctap(ctap_error)) - if ctap_error.is_retryable_user_error() => - { - tracing::debug!(%ctap_error, "Retrying WebAuthn operation"); - continue; - } - Err(err) => { - tracing::error!(%err, - "Failed to make/get credential with hybrid authenticator" - ); - break Err(err); - } - } + // Run the ceremony awaited directly (not in a nested spawn) so that + // the retry loop is sequential and no orphaned tasks can arise. + let response = + run_hybrid_ceremony(&mut device, &request, &tx, cancellation.clone()).await; + + match response { + Ok(auth_response) => { + let _ = tx.send(HybridStateInternal::Completed(auth_response)).await; + break; } - .map_err(|err| match err { - WebAuthnError::Ctap(CtapError::PINAuthBlocked) => { - CredentialServiceError::PinAttemptsExhausted - } - WebAuthnError::Ctap(CtapError::NoCredentials) => { - CredentialServiceError::NoCredentials + Err(err) if super::is_ceremony_terminating(&err) => { + // Terminating errors (CredentialExcluded, NonTerminatingCancellation + // from a winning transport, etc.) stop the loop. + // NonTerminatingCancellation exits silently; others surface as Failed. + if !matches!(err, CredentialServiceError::NonTerminatingCancellation) { + let _ = tx.send(HybridStateInternal::Failed(err)).await; + } else { + tracing::debug!("Hybrid handler cancelled, exiting silently"); } - WebAuthnError::Ctap(CtapError::CredentialExcluded) => { - CredentialServiceError::CredentialExcluded - } - _ => CredentialServiceError::AuthenticatorError, - }) - }; - - tracing::debug!("Polling hybrid channel for updates."); - let response = match cancellation - .run_until_cancelled(wait_for_response_fut) - .await - { - Some(resp) => resp, - None => { - tracing::debug!("Hybrid handler cancelled, stopping processing"); - Err(CredentialServiceError::NonTerminatingCancellation) + break; } - }; - - let terminal_state = match response { - Ok(auth_response) => Some(HybridStateInternal::Completed(auth_response)), - Err(CredentialServiceError::NonTerminatingCancellation) => { - // Cancelled by another transport winning or an explicit user cancel. - // Do not emit a Failed state — complete_request was already called - // by the winning path, and emitting Failed here would produce a - // spurious ErrorAuthenticator in the UI and a redundant - // complete_request invocation. - tracing::debug!("Hybrid handler cancelled, exiting silently"); - None + Err(err) => { + // Non-terminating: notify the UI that a restart is in progress + // so it can navigate back to the start page, then reissue a + // fresh QR on the next iteration. + tracing::warn!(?err, "Hybrid error, reissuing QR"); + let _ = tx.send(HybridStateInternal::Restarting).await; + continue; } - Err(err) => Some(HybridStateInternal::Failed(err)), - }; - if let Some(state) = terminal_state - && let Err(err) = tx.send(state).await - { - tracing::error!("Failed to send caBLE update: {:?}", err) } - }); + } }); Box::pin(stream! { while let Some(state) = rx.recv().await { @@ -215,6 +153,10 @@ pub(super) enum HybridStateInternal { Completed(CredentialResponse), Failed(CredentialServiceError), + + /// The ceremony was interrupted by a non-terminating error. A fresh QR code + /// is about to be issued on the next iteration. + Restarting, } // this is here to prevent making HybridStateInternal public to the whole crate. @@ -241,6 +183,10 @@ pub enum HybridState { /// Hybrid operation failed. Failed(CredentialServiceError), + + /// The ceremony was interrupted by a non-terminating error and a new QR + /// code is being issued. The UI should navigate back to the start page. + Restarting, } impl From for HybridState { @@ -251,6 +197,7 @@ impl From for HybridState { HybridStateInternal::Connected => HybridState::Connected, HybridStateInternal::Completed(_) => HybridState::Completed, HybridStateInternal::Failed(err) => HybridState::Failed(err), + HybridStateInternal::Restarting => HybridState::Restarting, } } } @@ -272,6 +219,7 @@ impl From<&HybridState> for BackgroundEvent { HybridState::Connecting => BackgroundEvent::HybridConnecting, HybridState::Connected => BackgroundEvent::HybridConnected, HybridState::Completed => BackgroundEvent::CeremonyCompleted, + HybridState::Restarting => BackgroundEvent::HybridRestarting, HybridState::Failed(CredentialServiceError::AuthenticatorError) => { BackgroundEvent::ErrorAuthenticator } @@ -295,6 +243,103 @@ impl From<&HybridState> for BackgroundEvent { } } +/// Runs a single hybrid ceremony attempt: opens the caBLE channel, spawns the UX +/// update forwarder, and drives the `webauthn_make_credential` / `webauthn_get_assertion` +/// retry loop until a terminal result or cancellation. +/// +/// Returns `Ok(CredentialResponse)` on success, or `Err(CredentialServiceError)` on +/// failure. `NonTerminatingCancellation` is returned when the cancellation token fires. +async fn run_hybrid_ceremony( + device: &mut CableQrCodeDevice, + request: &CredentialRequest, + tx: &Sender, + cancellation: CancellationToken, +) -> Result { + let mut channel = match device.channel(ChannelSettings::default()).await { + Ok(channel) => channel, + Err(e) => { + tracing::error!("Failed to open hybrid channel: {:?}", e); + return Err(CredentialServiceError::AuthenticatorError); + } + }; + + let state_sender_clone = tx.clone(); + let ux_updates_rx = channel.get_ux_update_receiver(); + tokio::spawn(async move { + handle_hybrid_updates(&state_sender_clone, ux_updates_rx).await; + debug!("Reached end of Hybrid updates stream."); + }); + + let wait_for_response_fut = async { + loop { + let response: Result = match request { + CredentialRequest::CreatePublicKeyCredentialRequest(make_request) => { + channel.webauthn_make_credential(make_request).await.map( + |make_credential_response| { + CredentialResponse::from_make_credential( + &make_credential_response, + &["hybrid"], + "cross-platform", + ) + }, + ) + } + CredentialRequest::GetPublicKeyCredentialRequest(get_request) => { + channel.webauthn_get_assertion(get_request).await.map( + |get_assertion_response| { + CredentialResponse::from_get_assertion( + // When doing hybrid, the authenticator is capable of + // displaying its own UI, so we assume it only ever + // returns one assertion. If this doesn't hold true, + // credential selection must be implemented here, as + // done for USB. + &get_assertion_response.assertions[0], + "cross-platform", + ) + }, + ) + } + }; + match response { + Ok(response) => { + tracing::debug!("Received credential from hybrid authenticator"); + break Ok(response); + } + Err(WebAuthnError::Ctap(ctap_error)) if ctap_error.is_retryable_user_error() => { + tracing::debug!(%ctap_error, "Retrying WebAuthn operation"); + continue; + } + Err(err) => { + tracing::error!(%err, "Failed to make/get credential with hybrid authenticator"); + break Err(err); + } + } + } + .map_err(|err| match err { + WebAuthnError::Ctap(CtapError::PINAuthBlocked) => { + CredentialServiceError::PinAttemptsExhausted + } + WebAuthnError::Ctap(CtapError::NoCredentials) => CredentialServiceError::NoCredentials, + WebAuthnError::Ctap(CtapError::CredentialExcluded) => { + CredentialServiceError::CredentialExcluded + } + _ => CredentialServiceError::AuthenticatorError, + }) + }; + + tracing::debug!("Polling hybrid channel for updates."); + match cancellation + .run_until_cancelled(wait_for_response_fut) + .await + { + Some(resp) => resp, + None => { + tracing::debug!("Hybrid handler cancelled, stopping processing"); + Err(CredentialServiceError::NonTerminatingCancellation) + } + } +} + async fn handle_hybrid_updates( state_sender: &Sender, mut ux_update_receiver: broadcast::Receiver, diff --git a/credentialsd/src/credential_service/mod.rs b/credentialsd/src/credential_service/mod.rs index 01a04fe..459361c 100644 --- a/credentialsd/src/credential_service/mod.rs +++ b/credentialsd/src/credential_service/mod.rs @@ -561,7 +561,7 @@ impl From for DeviceStateUpdate { /// deliberate decision here. The mapping follows the WebAuthn specification: /// - https://www.w3.org/TR/webauthn-3/#sctn-create-request-exceptions /// - https://www.w3.org/TR/webauthn-3/#sctn-get-request-exceptions -fn is_ceremony_terminating(err: &CredentialServiceError) -> bool { +pub(super) fn is_ceremony_terminating(err: &CredentialServiceError) -> bool { match err { // WebAuthn spec requires CredentialExcluded be remapped to InvalidStateError // and returned to the RP. The credential is already registered on this @@ -1504,4 +1504,131 @@ mod tests { &CredentialServiceError::NonTerminatingCancellation )); } + + /// After a non-terminating USB failure, the mock stream remains live and + /// continues to emit subsequent states. + #[tokio::test] + async fn test_usb_non_terminating_error_transport_continues() { + let usb_handler = CancellationTrackingHandler::::new(); + let usb_ref = usb_handler.get_handler_ref(); + + let service = CredentialService::new(MockHybridHandler, MockNfcHandler, usb_handler); + let request = create_test_request().await; + let (tx, _rx) = oneshot::channel(); + let (id, token) = service.init_request(&request, tx).await.unwrap(); + let mut usb_stream = service.get_usb_credential().await; + + // Non-terminating error: forwarded to UI, ceremony stays alive + usb_ref.shift_state(UsbStateInternal::Failed( + CredentialServiceError::AuthenticatorError, + )); + assert!(matches!( + usb_stream.next().await, + Some(UsbState::Failed(CredentialServiceError::AuthenticatorError)) + )); + assert!( + !token.is_cancelled(), + "ceremony must stay alive on non-terminating error" + ); + + // Transport is still live — subsequent states arrive + usb_ref.shift_state(UsbStateInternal::Waiting); + assert!(matches!(usb_stream.next().await, Some(UsbState::Waiting))); + + service.cancel_request(id).await; + } + + /// After a non-terminating hybrid failure, a new QR Init is emitted — simulating + /// the retry loop in run_hybrid_ceremony / start() re-issuing a fresh QR code. + #[tokio::test] + async fn test_hybrid_qr_reissued_after_non_terminating_error() { + let hybrid_handler = CancellationTrackingHandler::::new(); + let hybrid_ref = hybrid_handler.get_handler_ref(); + + let service = CredentialService::new(hybrid_handler, MockNfcHandler, MockUsbHandler); + let request = create_test_request().await; + let (tx, _rx) = oneshot::channel(); + let (id, token) = service.init_request(&request, tx).await.unwrap(); + let mut hybrid_stream = service.get_hybrid_credential().await; + + // First QR issued + hybrid_ref.shift_state(HybridStateInternal::Init("qr-code-1".to_string())); + assert!(matches!( + hybrid_stream.next().await, + Some(HybridState::Init(_)) + )); + + // Tunnel fails with a non-terminating error — forwarded to UI + hybrid_ref.shift_state(HybridStateInternal::Failed( + CredentialServiceError::AuthenticatorError, + )); + assert!(matches!( + hybrid_stream.next().await, + Some(HybridState::Failed( + CredentialServiceError::AuthenticatorError + )) + )); + assert!(!token.is_cancelled(), "ceremony must stay alive"); + + // Real retry loop re-issues a new QR; simulated here via shift_state + hybrid_ref.shift_state(HybridStateInternal::Init("qr-code-2".to_string())); + assert!(matches!( + hybrid_stream.next().await, + Some(HybridState::Init(_)) + )); + + service.cancel_request(id).await; + } + + /// A Restarting state from the hybrid handler is forwarded to the UI stream + /// and does not call complete_request or cancel the ceremony token. + #[tokio::test] + async fn test_hybrid_restarting_forwarded_ceremony_alive() { + let hybrid_handler = CancellationTrackingHandler::::new(); + let hybrid_ref = hybrid_handler.get_handler_ref(); + + let service = CredentialService::new(hybrid_handler, MockNfcHandler, MockUsbHandler); + let request = create_test_request().await; + let (tx, _rx) = oneshot::channel(); + let (id, token) = service.init_request(&request, tx).await.unwrap(); + let mut hybrid_stream = service.get_hybrid_credential().await; + + hybrid_ref.shift_state(HybridStateInternal::Restarting); + assert!( + matches!(hybrid_stream.next().await, Some(HybridState::Restarting)), + "Restarting state must be forwarded to the UI stream" + ); + assert!( + !token.is_cancelled(), + "ceremony must stay alive on Restarting" + ); + + service.cancel_request(id).await; + } + + /// A Restarting state from the USB handler is forwarded to the UI stream + /// and does not call complete_request or cancel the ceremony token. + #[tokio::test] + async fn test_usb_restarting_forwarded_ceremony_alive() { + let usb_handler = CancellationTrackingHandler::::new(); + let usb_ref = usb_handler.get_handler_ref(); + + let service = CredentialService::new(MockHybridHandler, MockNfcHandler, usb_handler); + let request = create_test_request().await; + let (tx, _rx) = oneshot::channel(); + let (id, token) = service.init_request(&request, tx).await.unwrap(); + let mut usb_stream = service.get_usb_credential().await; + + usb_ref.shift_state(UsbStateInternal::Restarting); + assert!( + matches!(usb_stream.next().await, Some(UsbState::Restarting)), + "Restarting state must be forwarded to the UI stream" + ); + assert!( + !token.is_cancelled(), + "ceremony must stay alive on Restarting" + ); + + service.cancel_request(id).await; + } } diff --git a/credentialsd/src/credential_service/nfc.rs b/credentialsd/src/credential_service/nfc.rs index 2dbba68..f3ae821 100644 --- a/credentialsd/src/credential_service/nfc.rs +++ b/credentialsd/src/credential_service/nfc.rs @@ -206,10 +206,13 @@ impl InProcessNfcHandler { ref response, cred_tx: _, } => Self::process_select_credential(response, &mut cred_rx).await, - // Terminal states - preserve state unchanged, will break loop after sending - NfcStateInternal::Completed(_) | NfcStateInternal::Failed(_) => { - Ok(prev_nfc_state.clone()) - } + // Terminal states - preserve state unchanged, will break loop after sending. + // Restarting is a transient signal state only; it is immediately replaced + // by Idle in the non-terminating branch so it should never be the prev + // state, but we cover it here for exhaustiveness. + NfcStateInternal::Completed(_) + | NfcStateInternal::Failed(_) + | NfcStateInternal::Restarting => Ok(prev_nfc_state.clone()), } }; @@ -243,7 +246,13 @@ impl InProcessNfcHandler { std::mem::discriminant(new_state) != std::mem::discriminant(old_state) } }; - if state_changed { + // Suppress forwarding a non-terminating Failed state to the UI directly: + // the Restarting state emitted below takes its place with cleaner semantics. + let is_non_terminating_failure = matches!( + &state, + NfcStateInternal::Failed(err) if !super::is_ceremony_terminating(err) + ); + if state_changed && !is_non_terminating_failure { tracing::debug!("NFC current state: {state:?}"); tx.send(state.clone()).await.map_err(|_| { CredentialServiceError::Internal( @@ -255,7 +264,16 @@ impl InProcessNfcHandler { // Check for terminal states AFTER sending match state { NfcStateInternal::Completed(_) => break Ok(()), - NfcStateInternal::Failed(err) => break Err(err), + NfcStateInternal::Failed(ref err) => { + if super::is_ceremony_terminating(err) { + break Err(err.clone()); + } + // Non-terminating: notify the UI that a restart is in progress so + // it can navigate back to the start page, then restart polling. + tracing::warn!(?err, "NFC authenticator error, restarting transport"); + let _ = tx.send(NfcStateInternal::Restarting).await; + state = NfcStateInternal::Idle; + } _ => {} } } @@ -440,6 +458,10 @@ pub(super) enum NfcStateInternal { /// There was an error while interacting with the authenticator. Failed(CredentialServiceError), + + /// The ceremony was interrupted by a non-terminating error and the transport + /// is restarting. The UI should navigate back to the start page. + Restarting, } /// Used to share public state between credential service and UI. @@ -482,6 +504,10 @@ pub enum NfcState { /// Interaction with the authenticator failed. Failed(CredentialServiceError), + + /// The ceremony was interrupted by a non-terminating error and the transport + /// is restarting. The UI should navigate back to the start page. + Restarting, } impl From for NfcState { @@ -504,6 +530,7 @@ impl From for NfcState { NfcState::NeedsUserVerification { attempts_left } } NfcStateInternal::Completed(_) => NfcState::Completed, + NfcStateInternal::Restarting => NfcState::Restarting, NfcStateInternal::SelectCredential { response, cred_tx } => { NfcState::SelectingCredential { creds: response @@ -585,6 +612,7 @@ impl From<&NfcState> for BackgroundEvent { BackgroundEvent::ErrorCancelled } NfcState::Failed(CredentialServiceError::Internal(_)) => BackgroundEvent::ErrorInternal, + NfcState::Restarting => BackgroundEvent::NfcRestarting, } } } diff --git a/credentialsd/src/credential_service/usb.rs b/credentialsd/src/credential_service/usb.rs index 13ab453..1207833 100644 --- a/credentialsd/src/credential_service/usb.rs +++ b/credentialsd/src/credential_service/usb.rs @@ -314,10 +314,13 @@ impl InProcessUsbHandler { ref response, cred_tx: _, } => Self::process_select_credential(response, &mut cred_rx).await, - // Terminal states - preserve state unchanged, will break loop after sending - UsbStateInternal::Completed(_) | UsbStateInternal::Failed(_) => { - Ok(prev_usb_state.clone()) - } + // Terminal states - preserve state unchanged, will break loop after sending. + // Restarting is a transient signal state only; it is immediately replaced + // by Idle in the non-terminating branch so it should never be the prev + // state, but we cover it here for exhaustiveness. + UsbStateInternal::Completed(_) + | UsbStateInternal::Failed(_) + | UsbStateInternal::Restarting => Ok(prev_usb_state.clone()), } }; @@ -352,7 +355,13 @@ impl InProcessUsbHandler { std::mem::discriminant(new_state) != std::mem::discriminant(old_state) } }; - if state_changed { + // Suppress forwarding a non-terminating Failed state to the UI directly: + // the Restarting state emitted below takes its place with cleaner semantics. + let is_non_terminating_failure = matches!( + &state, + UsbStateInternal::Failed(err) if !super::is_ceremony_terminating(err) + ); + if state_changed && !is_non_terminating_failure { tracing::debug!("USB current state: {state:?}"); tx.send(state.clone()).await.map_err(|_| { CredentialServiceError::Internal( @@ -364,7 +373,16 @@ impl InProcessUsbHandler { // Check for terminal states AFTER sending match state { UsbStateInternal::Completed(_) => break Ok(()), - UsbStateInternal::Failed(err) => break Err(err), + UsbStateInternal::Failed(ref err) => { + if super::is_ceremony_terminating(err) { + break Err(err.clone()); + } + // Non-terminating: notify the UI that a restart is in progress so + // it can navigate back to the start page, then restart polling. + tracing::warn!(?err, "USB authenticator error, restarting transport"); + let _ = tx.send(UsbStateInternal::Restarting).await; + state = UsbStateInternal::Idle; + } _ => {} } } @@ -551,6 +569,10 @@ pub(super) enum UsbStateInternal { /// There was an error while interacting with the authenticator. Failed(CredentialServiceError), + + /// The ceremony was interrupted by a non-terminating error and the transport + /// is restarting. The UI should navigate back to the start page. + Restarting, } /// Used to share public state between credential service and UI. @@ -602,6 +624,10 @@ pub enum UsbState { /// Interaction with the authenticator failed. Failed(CredentialServiceError), + + /// The ceremony was interrupted by a non-terminating error and the transport + /// is restarting. The UI should navigate back to the start page. + Restarting, } impl From for UsbState { @@ -659,6 +685,7 @@ impl From for UsbState { } } UsbStateInternal::Failed(err) => UsbState::Failed(err), + UsbStateInternal::Restarting => UsbState::Restarting, } } } @@ -709,6 +736,7 @@ impl From<&UsbState> for BackgroundEvent { BackgroundEvent::ErrorCancelled } UsbState::Failed(CredentialServiceError::Internal(_)) => BackgroundEvent::ErrorInternal, + UsbState::Restarting => BackgroundEvent::UsbRestarting, } } } diff --git a/credentialsd/src/dbus/ui_control.rs b/credentialsd/src/dbus/ui_control.rs index 788ad60..65a9090 100644 --- a/credentialsd/src/dbus/ui_control.rs +++ b/credentialsd/src/dbus/ui_control.rs @@ -22,9 +22,10 @@ use credentialsd_common::model::{ BACKGROUND_EVENT_ERROR_PIN_NOT_SET, BACKGROUND_EVENT_ERROR_TIMED_OUT, BackgroundEvent, ClientPinEnteredOptions, Credential, CredentialSelectedOptions, Device, DiscoveryRequestedOptions, NotifyHybridConnectedOptions, NotifyHybridConnectingOptions, - NotifyHybridStartedOptions, NotifyNeedsPinOptions, NotifyNeedsUserPresenceOptions, - NotifyNeedsUserVerificationOptions, NotifyNfcConnectedOptions, NotifyPinNotSetOptions, - NotifySelectingCredentialOptions, NotifyUsbConnectedOptions, Operation, PinNotSetError, + NotifyHybridRestartingOptions, NotifyHybridStartedOptions, NotifyNeedsPinOptions, + NotifyNeedsUserPresenceOptions, NotifyNeedsUserVerificationOptions, NotifyNfcConnectedOptions, + NotifyNfcRestartingOptions, NotifyPinNotSetOptions, NotifySelectingCredentialOptions, + NotifyUsbConnectedOptions, NotifyUsbRestartingOptions, Operation, PinNotSetError, PortalBackendOptions, SetDevicePinOptions, UserInteractedEvent, WindowHandle, }; @@ -148,6 +149,27 @@ trait UiControlService { _options: NotifyUsbConnectedOptions, ) -> fdo::Result<()>; + #[zbus(no_reply)] + async fn notify_hybrid_restarting( + &self, + session_handle: ObjectPath<'_>, + _options: NotifyHybridRestartingOptions, + ) -> fdo::Result<()>; + + #[zbus(no_reply)] + async fn notify_usb_restarting( + &self, + session_handle: ObjectPath<'_>, + _options: NotifyUsbRestartingOptions, + ) -> fdo::Result<()>; + + #[zbus(no_reply)] + async fn notify_nfc_restarting( + &self, + session_handle: ObjectPath<'_>, + _options: NotifyNfcRestartingOptions, + ) -> fdo::Result<()>; + #[zbus(no_reply)] async fn notify_ceremony_completed(&self, session_handle: ObjectPath<'_>) -> fdo::Result<()>; @@ -299,6 +321,30 @@ impl Ceremony { ) .await } + BackgroundEvent::HybridRestarting => { + self.proxy + .notify_hybrid_restarting( + self.session_handle.as_ref(), + NotifyHybridRestartingOptions {}, + ) + .await + } + BackgroundEvent::UsbRestarting => { + self.proxy + .notify_usb_restarting( + self.session_handle.as_ref(), + NotifyUsbRestartingOptions {}, + ) + .await + } + BackgroundEvent::NfcRestarting => { + self.proxy + .notify_nfc_restarting( + self.session_handle.as_ref(), + NotifyNfcRestartingOptions {}, + ) + .await + } BackgroundEvent::ErrorInternal => { let error = BACKGROUND_EVENT_ERROR_INTERNAL; self.proxy From 5063401eeb87f56b14f651b4dd1b9675aabd9435 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus Date: Wed, 9 Sep 2026 10:26:22 +0200 Subject: [PATCH 3/5] Only send restart-signals to the UI, if the device was actively used --- credentialsd/src/credential_service/hybrid.rs | 40 +++++++-- credentialsd/src/credential_service/mod.rs | 80 ++++++++++++++++++ credentialsd/src/credential_service/nfc.rs | 83 +++++++++++++----- credentialsd/src/credential_service/usb.rs | 84 ++++++++++++++----- 4 files changed, 236 insertions(+), 51 deletions(-) diff --git a/credentialsd/src/credential_service/hybrid.rs b/credentialsd/src/credential_service/hybrid.rs index c5094e9..f5b1536 100644 --- a/credentialsd/src/credential_service/hybrid.rs +++ b/credentialsd/src/credential_service/hybrid.rs @@ -73,6 +73,11 @@ impl HybridHandler for InternalHybridHandler { // Each iteration creates a fresh CableQrCodeDevice (the previous one // is consumed by channel()), so the old QR secret is discarded. loop { + // Reset the active flag for each ceremony attempt. It will be set to + // true inside run_hybrid_ceremony once the phone establishes a channel + // (i.e. the QR has been consumed and the BLE handshake succeeded). + let mut active = false; + let mut device = match CableQrCodeDevice::new_transient(hint, hybrid_transports) { Ok(device) => device, Err(err) => { @@ -98,8 +103,14 @@ impl HybridHandler for InternalHybridHandler { // Run the ceremony awaited directly (not in a nested spawn) so that // the retry loop is sequential and no orphaned tasks can arise. - let response = - run_hybrid_ceremony(&mut device, &request, &tx, cancellation.clone()).await; + let response = run_hybrid_ceremony( + &mut device, + &request, + &tx, + cancellation.clone(), + &mut active, + ) + .await; match response { Ok(auth_response) => { @@ -107,8 +118,7 @@ impl HybridHandler for InternalHybridHandler { break; } Err(err) if super::is_ceremony_terminating(&err) => { - // Terminating errors (CredentialExcluded, NonTerminatingCancellation - // from a winning transport, etc.) stop the loop. + // Terminating errors stop the loop. // NonTerminatingCancellation exits silently; others surface as Failed. if !matches!(err, CredentialServiceError::NonTerminatingCancellation) { let _ = tx.send(HybridStateInternal::Failed(err)).await; @@ -118,11 +128,17 @@ impl HybridHandler for InternalHybridHandler { break; } Err(err) => { - // Non-terminating: notify the UI that a restart is in progress - // so it can navigate back to the start page, then reissue a - // fresh QR on the next iteration. - tracing::warn!(?err, "Hybrid error, reissuing QR"); - let _ = tx.send(HybridStateInternal::Restarting).await; + if active { + // Post-active: the phone was engaged — surface the error + // via the Restarting signal so the UI navigates back to + // start_page, then reissue a fresh QR. + tracing::warn!(?err, "Hybrid post-active error, reissuing QR"); + let _ = tx.send(HybridStateInternal::Restarting).await; + } else { + // Pre-active: the QR was never consumed or the BLE channel + // failed before the phone responded. Reissue silently. + tracing::debug!(?err, "Hybrid pre-active error, reissuing QR silently"); + } continue; } } @@ -254,6 +270,7 @@ async fn run_hybrid_ceremony( request: &CredentialRequest, tx: &Sender, cancellation: CancellationToken, + active: &mut bool, ) -> Result { let mut channel = match device.channel(ChannelSettings::default()).await { Ok(channel) => channel, @@ -263,6 +280,11 @@ async fn run_hybrid_ceremony( } }; + // The BLE channel is open, which means the phone has consumed the QR code + // and completed the handshake. Mark this attempt as active so that any + // subsequent error is surfaced to the user rather than silenced. + *active = true; + let state_sender_clone = tx.clone(); let ux_updates_rx = channel.get_ux_update_receiver(); tokio::spawn(async move { diff --git a/credentialsd/src/credential_service/mod.rs b/credentialsd/src/credential_service/mod.rs index 459361c..73983f5 100644 --- a/credentialsd/src/credential_service/mod.rs +++ b/credentialsd/src/credential_service/mod.rs @@ -1631,4 +1631,84 @@ mod tests { service.cancel_request(id).await; } + + /// A non-terminating USB failure after a user-input state (post-active) + /// must surface to the UI. + #[tokio::test] + async fn test_usb_post_active_error_surfaces() { + let usb_handler = CancellationTrackingHandler::::new(); + let usb_ref = usb_handler.get_handler_ref(); + + let service = CredentialService::new(MockHybridHandler, MockNfcHandler, usb_handler); + let request = create_test_request().await; + let (tx, _rx) = oneshot::channel(); + let (id, token) = service.init_request(&request, tx).await.unwrap(); + let mut usb_stream = service.get_usb_credential().await; + + // Activate: user-input required + usb_ref.shift_state(UsbStateInternal::NeedsUserPresence); + assert!(matches!( + usb_stream.next().await, + Some(UsbState::NeedsUserPresence) + )); + + // Non-terminating error after activation — must surface + usb_ref.shift_state(UsbStateInternal::Failed( + CredentialServiceError::AuthenticatorError, + )); + assert!( + matches!( + usb_stream.next().await, + Some(UsbState::Failed(CredentialServiceError::AuthenticatorError)) + ), + "post-active Failed must be forwarded to UI" + ); + assert!(!token.is_cancelled()); + + service.cancel_request(id).await; + } + + /// A non-terminating hybrid failure after Connecting (post-active, phone has + /// consumed the QR) must surface to the UI. + #[tokio::test] + async fn test_hybrid_post_active_error_surfaces() { + let hybrid_handler = CancellationTrackingHandler::::new(); + let hybrid_ref = hybrid_handler.get_handler_ref(); + + let service = CredentialService::new(hybrid_handler, MockNfcHandler, MockUsbHandler); + let request = create_test_request().await; + let (tx, _rx) = oneshot::channel(); + let (id, token) = service.init_request(&request, tx).await.unwrap(); + let mut hybrid_stream = service.get_hybrid_credential().await; + + hybrid_ref.shift_state(HybridStateInternal::Init("qr".to_string())); + assert!(matches!( + hybrid_stream.next().await, + Some(HybridState::Init(_)) + )); + + // Connecting = phone consumed the QR = active + hybrid_ref.shift_state(HybridStateInternal::Connecting); + assert!(matches!( + hybrid_stream.next().await, + Some(HybridState::Connecting) + )); + + // Post-active failure must surface + hybrid_ref.shift_state(HybridStateInternal::Failed( + CredentialServiceError::AuthenticatorError, + )); + assert!( + matches!( + hybrid_stream.next().await, + Some(HybridState::Failed( + CredentialServiceError::AuthenticatorError + )) + ), + "post-active Failed must be forwarded to UI" + ); + assert!(!token.is_cancelled()); + + service.cancel_request(id).await; + } } diff --git a/credentialsd/src/credential_service/nfc.rs b/credentialsd/src/credential_service/nfc.rs index f3ae821..a9a6e42 100644 --- a/credentialsd/src/credential_service/nfc.rs +++ b/credentialsd/src/credential_service/nfc.rs @@ -171,6 +171,7 @@ impl InProcessNfcHandler { cancellation: CancellationToken, ) -> Result<(), CredentialServiceError> { let mut state = NfcStateInternal::Idle; + let mut active = false; let (signal_tx, mut signal_rx) = mpsc::channel(256); let (cred_tx, mut cred_rx) = mpsc::channel(1); debug!("polling for NFC status"); @@ -246,35 +247,75 @@ impl InProcessNfcHandler { std::mem::discriminant(new_state) != std::mem::discriminant(old_state) } }; - // Suppress forwarding a non-terminating Failed state to the UI directly: - // the Restarting state emitted below takes its place with cleaner semantics. - let is_non_terminating_failure = matches!( - &state, - NfcStateInternal::Failed(err) if !super::is_ceremony_terminating(err) - ); - if state_changed && !is_non_terminating_failure { - tracing::debug!("NFC current state: {state:?}"); - tx.send(state.clone()).await.map_err(|_| { - CredentialServiceError::Internal( - "NFC state channel receiver closed prematurely".to_string(), - ) - })?; + // Activate when libwebauthn signals it is waiting for user input. + // The flag is monotonic within a single ceremony attempt; it is reset + // to false when the transport restarts below. + match &state { + NfcStateInternal::NeedsPin { .. } + | NfcStateInternal::PinNotSet { .. } + | NfcStateInternal::NeedsUserVerification { .. } => { + active = true; + } + _ => {} } - // Check for terminal states AFTER sending match state { - NfcStateInternal::Completed(_) => break Ok(()), - NfcStateInternal::Failed(ref err) => { - if super::is_ceremony_terminating(err) { - break Err(err.clone()); + NfcStateInternal::Completed(_) => { + tracing::debug!("NFC current state: {state:?}"); + tx.send(state.clone()).await.map_err(|_| { + CredentialServiceError::Internal( + "NFC state channel receiver closed prematurely".to_string(), + ) + })?; + break Ok(()); + } + + // Catch terminating errors first. Doesn't matter if the user was already engaged + // or not. We have to terminate either way. + NfcStateInternal::Failed(ref err) if super::is_ceremony_terminating(err) => { + if state_changed { + tracing::debug!("NFC current state: {state:?}"); + tx.send(NfcStateInternal::Failed(err.clone())) + .await + .map_err(|_| { + CredentialServiceError::Internal( + "NFC state channel receiver closed prematurely".to_string(), + ) + })?; } - // Non-terminating: notify the UI that a restart is in progress so - // it can navigate back to the start page, then restart polling. + break Err(err.clone()); + } + + // Active non-terminating failure: the user was already engaged, surface + // the error via the Restarting signal so the UI navigates back to + // start_page. + // Reset active here only: the other Failed arms either break + // (so active is moot) or reach this arm with active already false. + NfcStateInternal::Failed(err) if active => { tracing::warn!(?err, "NFC authenticator error, restarting transport"); let _ = tx.send(NfcStateInternal::Restarting).await; + active = false; state = NfcStateInternal::Idle; } - _ => {} + + // Pre-active non-terminating failure: the device errored before the user + // tapped it. Restart silently without any UI notification. + NfcStateInternal::Failed(err) => { + tracing::debug!(?err, "NFC pre-active error, restarting silently"); + state = NfcStateInternal::Idle; + } + + // All other state changes are sent, if they are 'new' + _ => { + if state_changed { + tracing::debug!("NFC current state: {state:?}"); + tx.send(state.clone()).await.map_err(|_| { + CredentialServiceError::Internal( + "NFC state channel receiver closed prematurely".to_string(), + ) + })?; + } + } } } } diff --git a/credentialsd/src/credential_service/usb.rs b/credentialsd/src/credential_service/usb.rs index 1207833..e9f0da1 100644 --- a/credentialsd/src/credential_service/usb.rs +++ b/credentialsd/src/credential_service/usb.rs @@ -275,6 +275,7 @@ impl InProcessUsbHandler { cancellation: CancellationToken, ) -> Result<(), CredentialServiceError> { let mut state = UsbStateInternal::Idle; + let mut active = false; let (signal_tx, mut signal_rx) = mpsc::channel(256); let (cred_tx, mut cred_rx) = mpsc::channel(1); debug!("polling for USB status"); @@ -355,35 +356,76 @@ impl InProcessUsbHandler { std::mem::discriminant(new_state) != std::mem::discriminant(old_state) } }; - // Suppress forwarding a non-terminating Failed state to the UI directly: - // the Restarting state emitted below takes its place with cleaner semantics. - let is_non_terminating_failure = matches!( - &state, - UsbStateInternal::Failed(err) if !super::is_ceremony_terminating(err) - ); - if state_changed && !is_non_terminating_failure { - tracing::debug!("USB current state: {state:?}"); - tx.send(state.clone()).await.map_err(|_| { - CredentialServiceError::Internal( - "USB state channel receiver closed prematurely".to_string(), - ) - })?; + // Activate when libwebauthn signals it is waiting for user input. + // The flag is monotonic within a single ceremony attempt; it is reset + // to false when the transport restarts below. + match &state { + UsbStateInternal::NeedsPin { .. } + | UsbStateInternal::PinNotSet { .. } + | UsbStateInternal::NeedsUserVerification { .. } + | UsbStateInternal::NeedsUserPresence => { + active = true; + } + _ => {} } - // Check for terminal states AFTER sending match state { - UsbStateInternal::Completed(_) => break Ok(()), - UsbStateInternal::Failed(ref err) => { - if super::is_ceremony_terminating(err) { - break Err(err.clone()); + UsbStateInternal::Completed(_) => { + tracing::debug!("USB current state: {state:?}"); + tx.send(state.clone()).await.map_err(|_| { + CredentialServiceError::Internal( + "USB state channel receiver closed prematurely".to_string(), + ) + })?; + break Ok(()); + } + + // Catch terminating errors first. Doesn't matter if the user was already engaged + // or not. We have to terminate either way. + UsbStateInternal::Failed(ref err) if super::is_ceremony_terminating(err) => { + if state_changed { + tracing::debug!("USB current state: {state:?}"); + tx.send(UsbStateInternal::Failed(err.clone())) + .await + .map_err(|_| { + CredentialServiceError::Internal( + "USB state channel receiver closed prematurely".to_string(), + ) + })?; } - // Non-terminating: notify the UI that a restart is in progress so - // it can navigate back to the start page, then restart polling. + break Err(err.clone()); + } + + // Active non-terminating failure: the user was already engaged, surface + // the error via the Restarting signal so the UI navigates back to + // start_page. + // Reset active here only: the other Failed arms either break + // (so active is moot) or reach this arm with active already false. + UsbStateInternal::Failed(err) if active => { tracing::warn!(?err, "USB authenticator error, restarting transport"); let _ = tx.send(UsbStateInternal::Restarting).await; + active = false; state = UsbStateInternal::Idle; } - _ => {} + + // Pre-active non-terminating failure: the device errored before the user + // touched it. Restart silently without any UI notification. + UsbStateInternal::Failed(err) => { + tracing::debug!(?err, "USB pre-active error, restarting silently"); + state = UsbStateInternal::Idle; + } + + // All other state changes are sent, if they are 'new' + _ => { + if state_changed { + tracing::debug!("USB current state: {state:?}"); + tx.send(state.clone()).await.map_err(|_| { + CredentialServiceError::Internal( + "USB state channel receiver closed prematurely".to_string(), + ) + })?; + } + } } } } From 607aa64a6d2d3f77871184306473ad234ec9b431 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus Date: Wed, 9 Sep 2026 16:13:47 +0200 Subject: [PATCH 4/5] Display restart reason as an error message to the user --- CHANGELOG.md | 1 + credentialsd-common/src/model.rs | 59 ++++++++++++++++--- credentialsd-ui/data/resources/ui/window.blp | 11 ++++ credentialsd-ui/src/dbus.rs | 37 ++++++------ credentialsd-ui/src/gui/mod.rs | 7 ++- credentialsd-ui/src/gui/view_model/gtk/mod.rs | 10 +++- credentialsd-ui/src/gui/view_model/mod.rs | 51 +++++++--------- credentialsd/src/credential_service/hybrid.rs | 36 +++++++---- credentialsd/src/credential_service/mod.rs | 19 ++++-- credentialsd/src/credential_service/nfc.rs | 37 +++++++----- credentialsd/src/credential_service/usb.rs | 37 +++++++----- credentialsd/src/dbus/ui_control.rs | 47 ++++++--------- 12 files changed, 218 insertions(+), 134 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b95367..ab92a54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # [unreleased] - ui: Basic recovery from failed attempts to use devices (e.g. flaky bluetooth) +- ui: Display restart reason in case of premature failure # 0.3.1 [2026-09-05] diff --git a/credentialsd-common/src/model.rs b/credentialsd-common/src/model.rs index 1c74184..6c43deb 100644 --- a/credentialsd-common/src/model.rs +++ b/credentialsd-common/src/model.rs @@ -8,10 +8,48 @@ pub const BACKGROUND_EVENT_ERROR_INTERNAL: u32 = 0x80000001; pub const BACKGROUND_EVENT_ERROR_TIMED_OUT: u32 = 0x80000002; pub const BACKGROUND_EVENT_ERROR_CANCELLED: u32 = 0x80000003; pub const BACKGROUND_EVENT_ERROR_AUTHENTICATOR: u32 = 0x80000004; -pub const BACKGROUND_EVENT_ERROR_NO_CREDENTIALS: u32 = 0x80000005; pub const BACKGROUND_EVENT_ERROR_CREDENTIAL_EXCLUDED: u32 = 0x80000006; -pub const BACKGROUND_EVENT_ERROR_PIN_ATTEMPTS_EXHAUSTED: u32 = 0x80000007; -pub const BACKGROUND_EVENT_ERROR_PIN_NOT_SET: u32 = 0x80000008; + +/// Machine-readable reason for a transport restart. Shared across all transports. +/// Human-readable strings are built by the UI (gettext). +/// +/// The `#[repr(u8)]` layout doubles as the D-Bus wire format via `From for u8` +/// and `TryFrom for Self`. Discriminants must remain stable across releases. +/// Discriminant 0 is deliberately unused so that an all-zeroed message is recognisable +/// as invalid by `TryFrom`. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TransportRestartReason { + /// The ceremony was interrupted before completing — transport error, + /// user cancelled on their phone/device, or an unrecoverable authenticator + /// error. The user should follow the new prompts to retry. + Interrupted = 1, + /// No matching credentials were found on the device. The user should try + /// a different authenticator. + NoCredentials = 2, + /// Too many incorrect PIN attempts on this device — it is now locked. The + /// user should remove and reinsert the device, or use a different authenticator. + PinAttemptsExhausted = 3, +} + +impl From for u8 { + fn from(value: TransportRestartReason) -> Self { + value as u8 + } +} + +impl TryFrom for TransportRestartReason { + type Error = u8; + + fn try_from(value: u8) -> Result { + match value { + 1 => Ok(Self::Interrupted), + 2 => Ok(Self::NoCredentials), + 3 => Ok(Self::PinAttemptsExhausted), + other => Err(other), + } + } +} /// Credential service events intended to inform the UI. #[derive(Debug, PartialEq)] @@ -38,7 +76,9 @@ pub enum BackgroundEvent { /// The hybrid ceremony was interrupted by a non-terminating error and a new /// QR code is about to be issued. The UI should navigate back to the start /// page so the new QR becomes visible. - HybridRestarting, + HybridRestarting { + reason: TransportRestartReason, + }, NfcIdle, NfcWaiting, @@ -46,7 +86,9 @@ pub enum BackgroundEvent { /// The NFC ceremony was interrupted by a non-terminating error and the /// transport is polling for a new device tap. The UI should navigate back /// to the start page. - NfcRestarting, + NfcRestarting { + reason: TransportRestartReason, + }, UsbIdle, UsbWaiting, @@ -55,16 +97,15 @@ pub enum BackgroundEvent { /// The USB ceremony was interrupted by a non-terminating error and the /// transport is polling for a device. The UI should navigate back to the /// start page. - UsbRestarting, + UsbRestarting { + reason: TransportRestartReason, + }, ErrorInternal, ErrorTimedOut, ErrorCancelled, ErrorAuthenticator, - ErrorNoCredentials, ErrorCredentialExcluded, - ErrorPinAttemptsExhausted, - ErrorPinNotSet, } /// Emitted when a client enters a PIN for the selected authenticator. diff --git a/credentialsd-ui/data/resources/ui/window.blp b/credentialsd-ui/data/resources/ui/window.blp index 70f62de..c4601aa 100644 --- a/credentialsd-ui/data/resources/ui/window.blp +++ b/credentialsd-ui/data/resources/ui/window.blp @@ -154,6 +154,17 @@ template $CredentialsUiWindow: ApplicationWindow { wrap: true; } } + + Label { + label: bind template.view-model as <$CredentialManagerViewModel>.restart_message; + visible: bind template.view-model as <$CredentialManagerViewModel>.restart_message_visible; + margin-top: 16; + margin-start: 24; + margin-end: 24; + margin-bottom: 8; + wrap: true; + styles ["error"] + } }; } diff --git a/credentialsd-ui/src/dbus.rs b/credentialsd-ui/src/dbus.rs index 7f10823..cce23ce 100644 --- a/credentialsd-ui/src/dbus.rs +++ b/credentialsd-ui/src/dbus.rs @@ -25,15 +25,14 @@ use zbus::{ use credentialsd_common::model::{ BACKGROUND_EVENT_ERROR_AUTHENTICATOR, BACKGROUND_EVENT_ERROR_CANCELLED, BACKGROUND_EVENT_ERROR_CREDENTIAL_EXCLUDED, BACKGROUND_EVENT_ERROR_INTERNAL, - BACKGROUND_EVENT_ERROR_NO_CREDENTIALS, BACKGROUND_EVENT_ERROR_PIN_ATTEMPTS_EXHAUSTED, - BACKGROUND_EVENT_ERROR_PIN_NOT_SET, BACKGROUND_EVENT_ERROR_TIMED_OUT, BackgroundEvent, - ClientPinEnteredOptions, Credential, CredentialSelectedOptions, Device, - DiscoveryRequestedOptions, NotifyHybridConnectedOptions, NotifyHybridConnectingOptions, - NotifyHybridRestartingOptions, NotifyHybridStartedOptions, NotifyNeedsPinOptions, - NotifyNeedsUserPresenceOptions, NotifyNeedsUserVerificationOptions, NotifyNfcConnectedOptions, - NotifyNfcRestartingOptions, NotifyPinNotSetOptions, NotifySelectingCredentialOptions, - NotifyUsbConnectedOptions, NotifyUsbRestartingOptions, Operation, PinNotSetError, - PortalBackendOptions, SetDevicePinOptions, UserInteractedEvent, WindowHandle, + BACKGROUND_EVENT_ERROR_TIMED_OUT, BackgroundEvent, ClientPinEnteredOptions, Credential, + CredentialSelectedOptions, Device, DiscoveryRequestedOptions, NotifyHybridConnectedOptions, + NotifyHybridConnectingOptions, NotifyHybridRestartingOptions, NotifyHybridStartedOptions, + NotifyNeedsPinOptions, NotifyNeedsUserPresenceOptions, NotifyNeedsUserVerificationOptions, + NotifyNfcConnectedOptions, NotifyNfcRestartingOptions, NotifyPinNotSetOptions, + NotifySelectingCredentialOptions, NotifyUsbConnectedOptions, NotifyUsbRestartingOptions, + Operation, PinNotSetError, PortalBackendOptions, SetDevicePinOptions, TransportRestartReason, + UserInteractedEvent, WindowHandle, }; use crate::{RequestingApplication, ViewRequest, client::FlowControlClient}; @@ -312,12 +311,15 @@ impl CredentialPortalBackend { &self, #[zbus(object_server)] object_server: &ObjectServer, session_handle: ObjectPath<'_>, + reason: u8, _options: NotifyHybridRestartingOptions, ) -> fdo::Result<()> { + let reason = + TransportRestartReason::try_from(reason).unwrap_or(TransportRestartReason::Interrupted); self.notify_state_changed( object_server, session_handle, - BackgroundEvent::HybridRestarting, + BackgroundEvent::HybridRestarting { reason }, ) .await } @@ -326,12 +328,15 @@ impl CredentialPortalBackend { &self, #[zbus(object_server)] object_server: &ObjectServer, session_handle: ObjectPath<'_>, + reason: u8, _options: NotifyUsbRestartingOptions, ) -> fdo::Result<()> { + let reason = + TransportRestartReason::try_from(reason).unwrap_or(TransportRestartReason::Interrupted); self.notify_state_changed( object_server, session_handle, - BackgroundEvent::UsbRestarting, + BackgroundEvent::UsbRestarting { reason }, ) .await } @@ -340,12 +345,15 @@ impl CredentialPortalBackend { &self, #[zbus(object_server)] object_server: &ObjectServer, session_handle: ObjectPath<'_>, + reason: u8, _options: NotifyNfcRestartingOptions, ) -> fdo::Result<()> { + let reason = + TransportRestartReason::try_from(reason).unwrap_or(TransportRestartReason::Interrupted); self.notify_state_changed( object_server, session_handle, - BackgroundEvent::NfcRestarting, + BackgroundEvent::NfcRestarting { reason }, ) .await } @@ -376,14 +384,9 @@ impl CredentialPortalBackend { BACKGROUND_EVENT_ERROR_TIMED_OUT => Ok(BackgroundEvent::ErrorTimedOut), BACKGROUND_EVENT_ERROR_CANCELLED => Ok(BackgroundEvent::ErrorCancelled), BACKGROUND_EVENT_ERROR_AUTHENTICATOR => Ok(BackgroundEvent::ErrorAuthenticator), - BACKGROUND_EVENT_ERROR_NO_CREDENTIALS => Ok(BackgroundEvent::ErrorNoCredentials), BACKGROUND_EVENT_ERROR_CREDENTIAL_EXCLUDED => { Ok(BackgroundEvent::ErrorCredentialExcluded) } - BACKGROUND_EVENT_ERROR_PIN_ATTEMPTS_EXHAUSTED => { - Ok(BackgroundEvent::ErrorPinAttemptsExhausted) - } - BACKGROUND_EVENT_ERROR_PIN_NOT_SET => Ok(BackgroundEvent::ErrorPinNotSet), _ => Err(fdo::Error::Failed("Unknown error code".to_string())), }?; self.notify_state_changed(object_server, session_handle, error_event) diff --git a/credentialsd-ui/src/gui/mod.rs b/credentialsd-ui/src/gui/mod.rs index e55ada9..b3bc3e3 100644 --- a/credentialsd-ui/src/gui/mod.rs +++ b/credentialsd-ui/src/gui/mod.rs @@ -96,8 +96,11 @@ pub enum ViewUpdate { HybridConnected, /// A transport ceremony was interrupted by a non-terminating error and - /// is restarting. The UI should navigate back to the start page. - TransportRestarting, + /// is restarting. The UI should navigate back to the start page and display + /// the provided localized message. + TransportRestarting { + message: String, + }, Completed, Cancelled, diff --git a/credentialsd-ui/src/gui/view_model/gtk/mod.rs b/credentialsd-ui/src/gui/view_model/gtk/mod.rs index 76afc9e..c8dd76c 100644 --- a/credentialsd-ui/src/gui/view_model/gtk/mod.rs +++ b/credentialsd-ui/src/gui/view_model/gtk/mod.rs @@ -87,6 +87,12 @@ mod imp { #[property(get, set)] pub transport_restarting: RefCell, + #[property(get, set)] + pub restart_message: RefCell, + + #[property(get, set)] + pub restart_message_visible: RefCell, + #[property(get, set)] pub start_setting_new_pin_visible: RefCell, @@ -243,12 +249,14 @@ impl ViewModel { )); view_model.set_qr_spinner_visible(false); } - ViewUpdate::TransportRestarting => { + ViewUpdate::TransportRestarting { message } => { // Signal the window to navigate back to start_page. // The transport will emit a fresh Init/Connected state // next, which will update the prompt and show the new // QR code or device-waiting UI from start_page. view_model.set_qr_spinner_visible(false); + view_model.set_restart_message(message); + view_model.set_restart_message_visible(true); view_model.set_transport_restarting(true); } ViewUpdate::Completed => { diff --git a/credentialsd-ui/src/gui/view_model/mod.rs b/credentialsd-ui/src/gui/view_model/mod.rs index 6cf331b..c216c3c 100644 --- a/credentialsd-ui/src/gui/view_model/mod.rs +++ b/credentialsd-ui/src/gui/view_model/mod.rs @@ -8,7 +8,7 @@ use async_std::{ sync::Mutex as AsyncMutex, }; use credentialsd_common::memfd::read_secret; -use credentialsd_common::model::{BackgroundEvent, Credential}; +use credentialsd_common::model::{BackgroundEvent, Credential, TransportRestartReason}; use gettextrs::gettext; use serde::{Deserialize, Serialize}; use tracing::{error, info}; @@ -252,31 +252,7 @@ impl ViewModel { .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 @@ -349,13 +325,14 @@ impl ViewModel { .unwrap(); } Event::Background( - BackgroundEvent::HybridRestarting - | BackgroundEvent::UsbRestarting - | BackgroundEvent::NfcRestarting, + BackgroundEvent::HybridRestarting { reason } + | BackgroundEvent::UsbRestarting { reason } + | BackgroundEvent::NfcRestarting { reason }, ) => { self.hybrid_qr_code_data = None; + let message = localized_transport_restart_reason(&reason); self.tx_update - .send(ViewUpdate::TransportRestarting) + .send(ViewUpdate::TransportRestarting { message }) .await .unwrap(); } @@ -391,6 +368,20 @@ impl Debug for ViewEvent { } } +fn localized_transport_restart_reason(reason: &TransportRestartReason) -> String { + match reason { + TransportRestartReason::Interrupted => gettext( + "The previous attempt was interrupted. Please follow the new prompts to try again.", + ), + TransportRestartReason::NoCredentials => { + gettext("No matching credentials on this authenticator. Please try a different one.") + } + TransportRestartReason::PinAttemptsExhausted => gettext( + "No more PIN attempts allowed. Remove and reinsert your device, or use a different authenticator.", + ), + } +} + #[derive(Debug)] pub enum Event { Background(BackgroundEvent), diff --git a/credentialsd/src/credential_service/hybrid.rs b/credentialsd/src/credential_service/hybrid.rs index f5b1536..96eca9a 100644 --- a/credentialsd/src/credential_service/hybrid.rs +++ b/credentialsd/src/credential_service/hybrid.rs @@ -22,7 +22,10 @@ use tokio_util::sync::CancellationToken; use tracing::{debug, error}; use super::CredentialServiceError; -use credentialsd_common::{memfd::write_secret, model::BackgroundEvent}; +use credentialsd_common::{ + memfd::write_secret, + model::{BackgroundEvent, TransportRestartReason}, +}; use crate::model::{CredentialRequest, CredentialResponse}; @@ -132,8 +135,17 @@ impl HybridHandler for InternalHybridHandler { // Post-active: the phone was engaged — surface the error // via the Restarting signal so the UI navigates back to // start_page, then reissue a fresh QR. + let reason = match err { + CredentialServiceError::NoCredentials => { + TransportRestartReason::NoCredentials + } + CredentialServiceError::PinAttemptsExhausted => { + TransportRestartReason::PinAttemptsExhausted + } + _ => TransportRestartReason::Interrupted, + }; tracing::warn!(?err, "Hybrid post-active error, reissuing QR"); - let _ = tx.send(HybridStateInternal::Restarting).await; + let _ = tx.send(HybridStateInternal::Restarting(reason)).await; } else { // Pre-active: the QR was never consumed or the BLE channel // failed before the phone responded. Reissue silently. @@ -172,7 +184,7 @@ pub(super) enum HybridStateInternal { /// The ceremony was interrupted by a non-terminating error. A fresh QR code /// is about to be issued on the next iteration. - Restarting, + Restarting(TransportRestartReason), } // this is here to prevent making HybridStateInternal public to the whole crate. @@ -202,7 +214,7 @@ pub enum HybridState { /// The ceremony was interrupted by a non-terminating error and a new QR /// code is being issued. The UI should navigate back to the start page. - Restarting, + Restarting(TransportRestartReason), } impl From for HybridState { @@ -213,7 +225,7 @@ impl From for HybridState { HybridStateInternal::Connected => HybridState::Connected, HybridStateInternal::Completed(_) => HybridState::Completed, HybridStateInternal::Failed(err) => HybridState::Failed(err), - HybridStateInternal::Restarting => HybridState::Restarting, + HybridStateInternal::Restarting(reason) => HybridState::Restarting(reason), } } } @@ -235,24 +247,22 @@ impl From<&HybridState> for BackgroundEvent { HybridState::Connecting => BackgroundEvent::HybridConnecting, HybridState::Connected => BackgroundEvent::HybridConnected, HybridState::Completed => BackgroundEvent::CeremonyCompleted, - HybridState::Restarting => BackgroundEvent::HybridRestarting, + HybridState::Restarting(reason) => { + BackgroundEvent::HybridRestarting { reason: *reason } + } HybridState::Failed(CredentialServiceError::AuthenticatorError) => { BackgroundEvent::ErrorAuthenticator } - HybridState::Failed(CredentialServiceError::NoCredentials) => { - BackgroundEvent::ErrorNoCredentials - } HybridState::Failed(CredentialServiceError::CredentialExcluded) => { BackgroundEvent::ErrorCredentialExcluded } - HybridState::Failed(CredentialServiceError::PinAttemptsExhausted) => { - BackgroundEvent::ErrorAuthenticator - } // This should currently never be reached, but we'll likely use it in future refactoring HybridState::Failed(CredentialServiceError::NonTerminatingCancellation) => { BackgroundEvent::ErrorCancelled } - HybridState::Failed(CredentialServiceError::Internal(_)) => { + HybridState::Failed(CredentialServiceError::Internal(_)) + | HybridState::Failed(CredentialServiceError::NoCredentials) + | HybridState::Failed(CredentialServiceError::PinAttemptsExhausted) => { BackgroundEvent::ErrorInternal } } diff --git a/credentialsd/src/credential_service/mod.rs b/credentialsd/src/credential_service/mod.rs index 73983f5..f83f0e6 100644 --- a/credentialsd/src/credential_service/mod.rs +++ b/credentialsd/src/credential_service/mod.rs @@ -624,6 +624,7 @@ impl From for AuthenticatorResponse { #[cfg(test)] mod tests { use super::*; + use credentialsd_common::model::TransportRestartReason; use std::time::Duration; // Mock handlers for testing @@ -1593,9 +1594,14 @@ mod tests { let (id, token) = service.init_request(&request, tx).await.unwrap(); let mut hybrid_stream = service.get_hybrid_credential().await; - hybrid_ref.shift_state(HybridStateInternal::Restarting); + hybrid_ref.shift_state(HybridStateInternal::Restarting( + TransportRestartReason::Interrupted, + )); assert!( - matches!(hybrid_stream.next().await, Some(HybridState::Restarting)), + matches!( + hybrid_stream.next().await, + Some(HybridState::Restarting(TransportRestartReason::Interrupted)) + ), "Restarting state must be forwarded to the UI stream" ); assert!( @@ -1619,9 +1625,14 @@ mod tests { let (id, token) = service.init_request(&request, tx).await.unwrap(); let mut usb_stream = service.get_usb_credential().await; - usb_ref.shift_state(UsbStateInternal::Restarting); + usb_ref.shift_state(UsbStateInternal::Restarting( + TransportRestartReason::Interrupted, + )); assert!( - matches!(usb_stream.next().await, Some(UsbState::Restarting)), + matches!( + usb_stream.next().await, + Some(UsbState::Restarting(TransportRestartReason::Interrupted)) + ), "Restarting state must be forwarded to the UI stream" ); assert!( diff --git a/credentialsd/src/credential_service/nfc.rs b/credentialsd/src/credential_service/nfc.rs index a9a6e42..00b835b 100644 --- a/credentialsd/src/credential_service/nfc.rs +++ b/credentialsd/src/credential_service/nfc.rs @@ -16,7 +16,9 @@ use tokio::sync::mpsc::{self, Receiver, Sender, WeakSender}; use tokio_util::sync::CancellationToken; use tracing::{debug, warn}; -use credentialsd_common::model::{BackgroundEvent, Credential, PinNotSetError}; +use credentialsd_common::model::{ + BackgroundEvent, Credential, PinNotSetError, TransportRestartReason, +}; use crate::model::{CredentialRequest, GetAssertionResponseInternal}; @@ -213,7 +215,7 @@ impl InProcessNfcHandler { // state, but we cover it here for exhaustiveness. NfcStateInternal::Completed(_) | NfcStateInternal::Failed(_) - | NfcStateInternal::Restarting => Ok(prev_nfc_state.clone()), + | NfcStateInternal::Restarting(_) => Ok(prev_nfc_state.clone()), } }; @@ -292,8 +294,17 @@ impl InProcessNfcHandler { // Reset active here only: the other Failed arms either break // (so active is moot) or reach this arm with active already false. NfcStateInternal::Failed(err) if active => { + let reason = match err { + CredentialServiceError::NoCredentials => { + TransportRestartReason::NoCredentials + } + CredentialServiceError::PinAttemptsExhausted => { + TransportRestartReason::PinAttemptsExhausted + } + _ => TransportRestartReason::Interrupted, + }; tracing::warn!(?err, "NFC authenticator error, restarting transport"); - let _ = tx.send(NfcStateInternal::Restarting).await; + let _ = tx.send(NfcStateInternal::Restarting(reason)).await; active = false; state = NfcStateInternal::Idle; } @@ -502,7 +513,7 @@ pub(super) enum NfcStateInternal { /// The ceremony was interrupted by a non-terminating error and the transport /// is restarting. The UI should navigate back to the start page. - Restarting, + Restarting(TransportRestartReason), } /// Used to share public state between credential service and UI. @@ -548,7 +559,7 @@ pub enum NfcState { /// The ceremony was interrupted by a non-terminating error and the transport /// is restarting. The UI should navigate back to the start page. - Restarting, + Restarting(TransportRestartReason), } impl From for NfcState { @@ -571,7 +582,7 @@ impl From for NfcState { NfcState::NeedsUserVerification { attempts_left } } NfcStateInternal::Completed(_) => NfcState::Completed, - NfcStateInternal::Restarting => NfcState::Restarting, + NfcStateInternal::Restarting(reason) => NfcState::Restarting(reason), NfcStateInternal::SelectCredential { response, cred_tx } => { NfcState::SelectingCredential { creds: response @@ -640,20 +651,18 @@ impl From<&NfcState> for BackgroundEvent { NfcState::Failed(CredentialServiceError::AuthenticatorError) => { BackgroundEvent::ErrorAuthenticator } - NfcState::Failed(CredentialServiceError::NoCredentials) => { - BackgroundEvent::ErrorNoCredentials - } NfcState::Failed(CredentialServiceError::CredentialExcluded) => { BackgroundEvent::ErrorCredentialExcluded } - NfcState::Failed(CredentialServiceError::PinAttemptsExhausted) => { - BackgroundEvent::ErrorAuthenticator - } NfcState::Failed(CredentialServiceError::NonTerminatingCancellation) => { BackgroundEvent::ErrorCancelled } - NfcState::Failed(CredentialServiceError::Internal(_)) => BackgroundEvent::ErrorInternal, - NfcState::Restarting => BackgroundEvent::NfcRestarting, + NfcState::Failed(CredentialServiceError::Internal(_)) + | NfcState::Failed(CredentialServiceError::NoCredentials) + | NfcState::Failed(CredentialServiceError::PinAttemptsExhausted) => { + BackgroundEvent::ErrorInternal + } + NfcState::Restarting(reason) => BackgroundEvent::NfcRestarting { reason: *reason }, } } } diff --git a/credentialsd/src/credential_service/usb.rs b/credentialsd/src/credential_service/usb.rs index e9f0da1..a9a8c3a 100644 --- a/credentialsd/src/credential_service/usb.rs +++ b/credentialsd/src/credential_service/usb.rs @@ -21,7 +21,9 @@ use tokio::sync::{ use tokio_util::sync::CancellationToken; use tracing::{debug, warn}; -use credentialsd_common::model::{BackgroundEvent, Credential, PinNotSetError}; +use credentialsd_common::model::{ + BackgroundEvent, Credential, PinNotSetError, TransportRestartReason, +}; use crate::model::{CredentialRequest, GetAssertionResponseInternal}; @@ -321,7 +323,7 @@ impl InProcessUsbHandler { // state, but we cover it here for exhaustiveness. UsbStateInternal::Completed(_) | UsbStateInternal::Failed(_) - | UsbStateInternal::Restarting => Ok(prev_usb_state.clone()), + | UsbStateInternal::Restarting(_) => Ok(prev_usb_state.clone()), } }; @@ -402,8 +404,17 @@ impl InProcessUsbHandler { // Reset active here only: the other Failed arms either break // (so active is moot) or reach this arm with active already false. UsbStateInternal::Failed(err) if active => { + let reason = match err { + CredentialServiceError::NoCredentials => { + TransportRestartReason::NoCredentials + } + CredentialServiceError::PinAttemptsExhausted => { + TransportRestartReason::PinAttemptsExhausted + } + _ => TransportRestartReason::Interrupted, + }; tracing::warn!(?err, "USB authenticator error, restarting transport"); - let _ = tx.send(UsbStateInternal::Restarting).await; + let _ = tx.send(UsbStateInternal::Restarting(reason)).await; active = false; state = UsbStateInternal::Idle; } @@ -614,7 +625,7 @@ pub(super) enum UsbStateInternal { /// The ceremony was interrupted by a non-terminating error and the transport /// is restarting. The UI should navigate back to the start page. - Restarting, + Restarting(TransportRestartReason), } /// Used to share public state between credential service and UI. @@ -669,7 +680,7 @@ pub enum UsbState { /// The ceremony was interrupted by a non-terminating error and the transport /// is restarting. The UI should navigate back to the start page. - Restarting, + Restarting(TransportRestartReason), } impl From for UsbState { @@ -727,7 +738,7 @@ impl From for UsbState { } } UsbStateInternal::Failed(err) => UsbState::Failed(err), - UsbStateInternal::Restarting => UsbState::Restarting, + UsbStateInternal::Restarting(reason) => UsbState::Restarting(reason), } } } @@ -765,20 +776,18 @@ impl From<&UsbState> for BackgroundEvent { UsbState::Failed(CredentialServiceError::AuthenticatorError) => { BackgroundEvent::ErrorAuthenticator } - UsbState::Failed(CredentialServiceError::NoCredentials) => { - BackgroundEvent::ErrorNoCredentials - } UsbState::Failed(CredentialServiceError::CredentialExcluded) => { BackgroundEvent::ErrorCredentialExcluded } - UsbState::Failed(CredentialServiceError::PinAttemptsExhausted) => { - BackgroundEvent::ErrorAuthenticator - } UsbState::Failed(CredentialServiceError::NonTerminatingCancellation) => { BackgroundEvent::ErrorCancelled } - UsbState::Failed(CredentialServiceError::Internal(_)) => BackgroundEvent::ErrorInternal, - UsbState::Restarting => BackgroundEvent::UsbRestarting, + UsbState::Failed(CredentialServiceError::Internal(_)) + | UsbState::Failed(CredentialServiceError::NoCredentials) + | UsbState::Failed(CredentialServiceError::PinAttemptsExhausted) => { + BackgroundEvent::ErrorInternal + } + UsbState::Restarting(reason) => BackgroundEvent::UsbRestarting { reason: *reason }, } } } diff --git a/credentialsd/src/dbus/ui_control.rs b/credentialsd/src/dbus/ui_control.rs index 65a9090..e3ab8a8 100644 --- a/credentialsd/src/dbus/ui_control.rs +++ b/credentialsd/src/dbus/ui_control.rs @@ -18,15 +18,14 @@ use zbus::{ use credentialsd_common::model::{ BACKGROUND_EVENT_ERROR_AUTHENTICATOR, BACKGROUND_EVENT_ERROR_CANCELLED, BACKGROUND_EVENT_ERROR_CREDENTIAL_EXCLUDED, BACKGROUND_EVENT_ERROR_INTERNAL, - BACKGROUND_EVENT_ERROR_NO_CREDENTIALS, BACKGROUND_EVENT_ERROR_PIN_ATTEMPTS_EXHAUSTED, - BACKGROUND_EVENT_ERROR_PIN_NOT_SET, BACKGROUND_EVENT_ERROR_TIMED_OUT, BackgroundEvent, - ClientPinEnteredOptions, Credential, CredentialSelectedOptions, Device, - DiscoveryRequestedOptions, NotifyHybridConnectedOptions, NotifyHybridConnectingOptions, - NotifyHybridRestartingOptions, NotifyHybridStartedOptions, NotifyNeedsPinOptions, - NotifyNeedsUserPresenceOptions, NotifyNeedsUserVerificationOptions, NotifyNfcConnectedOptions, - NotifyNfcRestartingOptions, NotifyPinNotSetOptions, NotifySelectingCredentialOptions, - NotifyUsbConnectedOptions, NotifyUsbRestartingOptions, Operation, PinNotSetError, - PortalBackendOptions, SetDevicePinOptions, UserInteractedEvent, WindowHandle, + BACKGROUND_EVENT_ERROR_TIMED_OUT, BackgroundEvent, ClientPinEnteredOptions, Credential, + CredentialSelectedOptions, Device, DiscoveryRequestedOptions, NotifyHybridConnectedOptions, + NotifyHybridConnectingOptions, NotifyHybridRestartingOptions, NotifyHybridStartedOptions, + NotifyNeedsPinOptions, NotifyNeedsUserPresenceOptions, NotifyNeedsUserVerificationOptions, + NotifyNfcConnectedOptions, NotifyNfcRestartingOptions, NotifyPinNotSetOptions, + NotifySelectingCredentialOptions, NotifyUsbConnectedOptions, NotifyUsbRestartingOptions, + Operation, PinNotSetError, PortalBackendOptions, SetDevicePinOptions, UserInteractedEvent, + WindowHandle, }; /// Used by the credential service to control the UI. @@ -153,6 +152,7 @@ trait UiControlService { async fn notify_hybrid_restarting( &self, session_handle: ObjectPath<'_>, + reason: u8, _options: NotifyHybridRestartingOptions, ) -> fdo::Result<()>; @@ -160,6 +160,7 @@ trait UiControlService { async fn notify_usb_restarting( &self, session_handle: ObjectPath<'_>, + reason: u8, _options: NotifyUsbRestartingOptions, ) -> fdo::Result<()>; @@ -167,6 +168,7 @@ trait UiControlService { async fn notify_nfc_restarting( &self, session_handle: ObjectPath<'_>, + reason: u8, _options: NotifyNfcRestartingOptions, ) -> fdo::Result<()>; @@ -321,26 +323,29 @@ impl Ceremony { ) .await } - BackgroundEvent::HybridRestarting => { + BackgroundEvent::HybridRestarting { reason } => { self.proxy .notify_hybrid_restarting( self.session_handle.as_ref(), + u8::from(reason), NotifyHybridRestartingOptions {}, ) .await } - BackgroundEvent::UsbRestarting => { + BackgroundEvent::UsbRestarting { reason } => { self.proxy .notify_usb_restarting( self.session_handle.as_ref(), + u8::from(reason), NotifyUsbRestartingOptions {}, ) .await } - BackgroundEvent::NfcRestarting => { + BackgroundEvent::NfcRestarting { reason } => { self.proxy .notify_nfc_restarting( self.session_handle.as_ref(), + u8::from(reason), NotifyNfcRestartingOptions {}, ) .await @@ -370,30 +375,12 @@ impl Ceremony { .notify_error_occurred(self.session_handle.as_ref(), error) .await } - BackgroundEvent::ErrorNoCredentials => { - let error = BACKGROUND_EVENT_ERROR_NO_CREDENTIALS; - self.proxy - .notify_error_occurred(self.session_handle.as_ref(), error) - .await - } BackgroundEvent::ErrorCredentialExcluded => { let error = BACKGROUND_EVENT_ERROR_CREDENTIAL_EXCLUDED; self.proxy .notify_error_occurred(self.session_handle.as_ref(), error) .await } - BackgroundEvent::ErrorPinAttemptsExhausted => { - let error = BACKGROUND_EVENT_ERROR_PIN_ATTEMPTS_EXHAUSTED; - self.proxy - .notify_error_occurred(self.session_handle.as_ref(), error) - .await - } - BackgroundEvent::ErrorPinNotSet => { - let error = BACKGROUND_EVENT_ERROR_PIN_NOT_SET; - self.proxy - .notify_error_occurred(self.session_handle.as_ref(), error) - .await - } BackgroundEvent::CeremonyCompleted => { self.proxy .notify_ceremony_completed(self.session_handle.as_ref()) From b2209abbed3b8a7c57a71eec62f42dfc72dc51ca Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus Date: Wed, 9 Sep 2026 16:36:11 +0200 Subject: [PATCH 5/5] Update language files --- credentialsd-ui/po/credentialsd-ui.pot | 157 +++++++++++++-------- credentialsd-ui/po/de_DE.po | 180 ++++++++++++++++--------- credentialsd-ui/po/en_US.po | 177 +++++++++++++++--------- credentialsd-ui/po/ka_GE.po | 178 +++++++++++++++--------- 4 files changed, 453 insertions(+), 239 deletions(-) diff --git a/credentialsd-ui/po/credentialsd-ui.pot b/credentialsd-ui/po/credentialsd-ui.pot index 5ed55b7..d1a35fc 100644 --- a/credentialsd-ui/po/credentialsd-ui.pot +++ b/credentialsd-ui/po/credentialsd-ui.pot @@ -9,7 +9,7 @@ msgstr "" "Project-Id-Version: credentialsd-ui\n" "Report-Msgid-Bugs-To: \"https://github.com/linux-credentials/credentialsd/" "issues\"\n" -"POT-Creation-Date: 2026-06-18 07:18-0500\n" +"POT-Creation-Date: 2026-09-09 15:53+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -37,8 +37,7 @@ msgid "Registering a credential" msgstr "" #. developer_name tag deprecated with Appstream 1.0 -#: credentialsd-ui/data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:36 -#: credentialsd-ui/data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:39 +#: credentialsd-ui/data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:38 msgid "Isaiah Inuwa" msgstr "" @@ -54,82 +53,133 @@ msgstr "" msgid "Use your security key" msgstr "" -#: credentialsd-ui/data/resources/ui/window.blp:162 +#: credentialsd-ui/data/resources/ui/window.blp:173 msgid "Connect a security key" msgstr "" -#: credentialsd-ui/data/resources/ui/window.blp:197 +#: credentialsd-ui/data/resources/ui/window.blp:208 msgid "Enter your device PIN" msgstr "" -#: credentialsd-ui/data/resources/ui/window.blp:205 +#: credentialsd-ui/data/resources/ui/window.blp:217 msgid "Scan the QR code to connect your device" msgstr "" -#: credentialsd-ui/data/resources/ui/window.blp:247 -#: credentialsd-ui/data/resources/ui/window.blp:253 +#: credentialsd-ui/data/resources/ui/window.blp:259 +#: credentialsd-ui/data/resources/ui/window.blp:265 msgid "Choose credential" msgstr "" -#: credentialsd-ui/data/resources/ui/window.blp:266 +#: credentialsd-ui/data/resources/ui/window.blp:278 +msgid "Set a PIN" +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:301 +msgid "Please choose a new PIN for your device." +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:314 +msgid "New PIN" +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:321 +msgid "Confirm PIN" +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:333 +#: credentialsd-ui/data/resources/ui/window.blp:404 +msgid "Close" +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:338 +msgid "Continue" +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:353 msgid "Complete" msgstr "" -#: credentialsd-ui/data/resources/ui/window.blp:288 +#: credentialsd-ui/data/resources/ui/window.blp:375 msgid "Done!" msgstr "" -#: credentialsd-ui/data/resources/ui/window.blp:299 +#: credentialsd-ui/data/resources/ui/window.blp:386 msgid "Something went wrong." msgstr "" -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:159 +#: credentialsd-ui/data/resources/ui/window.blp:409 +msgid "Set PIN on device" +msgstr "" + +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:177 msgid "Enter your PIN. One attempt remaining." msgid_plural "Enter your PIN. %d attempts remaining." msgstr[0] "" msgstr[1] "" -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:165 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:183 msgid "Enter your PIN." msgstr "" -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:174 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:192 msgid "Touch your device again. One attempt remaining." msgid_plural "Touch your device again. %d attempts remaining." msgstr[0] "" msgstr[1] "" -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:180 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:198 msgid "Touch your device." msgstr "" -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:185 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:203 msgid "Touch your device" msgstr "" -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:188 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:210 +msgid "" +"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." +msgstr "" + +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:214 +msgid "" +"The entered PIN violates the PIN-policy of this device (likely too short). " +"Please try again." +msgstr "" + +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:217 +msgid "" +"The entered PIN violates the PIN-policy of this device (PIN too long). " +"Please try again." +msgstr "" + +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:220 +msgid "A PIN change is required by your device to continue." +msgstr "" + +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:227 msgid "Scan the QR code with your device to begin authentication." msgstr "" -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:201 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:240 msgid "" "Connecting to your device. Make sure both devices are near each other and " "have Bluetooth enabled." msgstr "" -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:209 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:248 msgid "Device connected. Follow the instructions on your device" msgstr "" -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:298 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:347 msgid "Insert your security key." msgstr "" -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:317 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:366 msgid "Multiple devices found. Please select with which to proceed." msgstr "" -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:362 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:415 msgid "Credential Manager" msgstr "" @@ -157,81 +207,80 @@ msgstr "" msgid "A security key (USB)" msgstr "" -#: credentialsd-ui/src/gui/view_model/mod.rs:64 +#: credentialsd-ui/src/gui/view_model/mod.rs:62 msgid "unknown application" msgstr "" #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from -#: credentialsd-ui/src/gui/view_model/mod.rs:80 +#: credentialsd-ui/src/gui/view_model/mod.rs:76 msgid "Create a passkey for %s1" msgstr "" #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from -#: credentialsd-ui/src/gui/view_model/mod.rs:84 +#: credentialsd-ui/src/gui/view_model/mod.rs:80 msgid "Use a passkey for %s1" msgstr "" #. TRANSLATORS: %s1 is the "relying party" (e.g.: domain name) where the request is coming from #. TRANSLATORS: %s2 is the application name (e.g.: firefox) where the request is coming from, must be left untouched to make the name bold -#. TRANSLATORS: %i1 is the process ID of the requesting application -#. TRANSLATORS: %s3 is the absolute path (think: /usr/bin/firefox) of the requesting application -#: credentialsd-ui/src/gui/view_model/mod.rs:96 +#. TRANSLATORS: %s3 is the app ID (think: org.mozilla.firefox) of the requesting application +#: credentialsd-ui/src/gui/view_model/mod.rs:91 msgid "" -"\"%s2\" (process ID: %i1, binary: %s3) is asking to create a " -"credential to register at \"%s1\". Only proceed if you trust this process." +"\"%s2\" (%s3) is asking to create a credential to register at " +"\"%s1\". Only proceed if you trust this process." msgstr "" #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from #. TRANSLATORS: %s2 is the application name (e.g.: firefox) where the request is coming from, must be left untouched to make the name bold -#. TRANSLATORS: %i1 is the process ID of the requesting application -#. TRANSLATORS: %s3 is the absolute path (think: /usr/bin/firefox) of the requesting application -#: credentialsd-ui/src/gui/view_model/mod.rs:103 +#. TRANSLATORS: %s3 is the app ID (think: org.mozilla.firefox) of the requesting application +#: credentialsd-ui/src/gui/view_model/mod.rs:97 msgid "" -"\"%s2\" (process ID: %i1, binary: %s3) is asking to use a credential " -"to sign in to \"%s1\". Only proceed if you trust this process." +"\"%s2\" (%s3) is asking to use a credential to sign in to \"%s1\". " +"Only proceed if you trust this process." msgstr "" #. TRANSLATORS: %s1 is the relying party (think: domain name) where the request is coming from -#: credentialsd-ui/src/gui/view_model/mod.rs:115 +#: credentialsd-ui/src/gui/view_model/mod.rs:108 msgid "" "Scan the QR code using the camera on the device that has the passkey for %s1" msgstr "" #. TRANSLATORS: %s1 is the relying party (think: domain name) where the request is coming from -#: credentialsd-ui/src/gui/view_model/mod.rs:118 +#: credentialsd-ui/src/gui/view_model/mod.rs:111 msgid "Insert and activate your security key to use it for %s1" msgstr "" -#: credentialsd-ui/src/gui/view_model/mod.rs:181 +#: credentialsd-ui/src/gui/view_model/mod.rs:193 msgid "Failed to select credential from device." msgstr "" -#: credentialsd-ui/src/gui/view_model/mod.rs:237 -msgid "No matching credentials found on this authenticator." +#: credentialsd-ui/src/gui/view_model/mod.rs:257 +msgid "The credential request timed out. Please try again." msgstr "" -#: credentialsd-ui/src/gui/view_model/mod.rs:245 +#: credentialsd-ui/src/gui/view_model/mod.rs:267 msgid "" -"No more PIN attempts allowed. Try removing your device and plugging it back " -"in." +"Something went wrong while retrieving a credential. Please try again later " +"or use a different authenticator." msgstr "" -#: credentialsd-ui/src/gui/view_model/mod.rs:254 -msgid "" -"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." +#: credentialsd-ui/src/gui/view_model/mod.rs:276 +msgid "This credential is already registered on this authenticator." msgstr "" -#: credentialsd-ui/src/gui/view_model/mod.rs:262 -msgid "The credential request timed out. Please try again." +#: credentialsd-ui/src/gui/view_model/mod.rs:374 +msgid "" +"The previous attempt was interrupted. Please follow the new prompts to try " +"again." msgstr "" -#: credentialsd-ui/src/gui/view_model/mod.rs:272 +#: credentialsd-ui/src/gui/view_model/mod.rs:377 msgid "" -"Something went wrong while retrieving a credential. Please try again later " -"or use a different authenticator." +"No matching credentials on this authenticator. Please try a different one." msgstr "" -#: credentialsd-ui/src/gui/view_model/mod.rs:281 -msgid "This credential is already registered on this authenticator." +#: credentialsd-ui/src/gui/view_model/mod.rs:380 +msgid "" +"No more PIN attempts allowed. Remove and reinsert your device, or use a " +"different authenticator." msgstr "" diff --git a/credentialsd-ui/po/de_DE.po b/credentialsd-ui/po/de_DE.po index 7243f73..86ca6f6 100644 --- a/credentialsd-ui/po/de_DE.po +++ b/credentialsd-ui/po/de_DE.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \"https://github.com/linux-credentials/credentialsd/" "issues\"\n" -"POT-Creation-Date: 2026-06-18 07:18-0500\n" +"POT-Creation-Date: 2026-09-09 15:53+0200\n" "PO-Revision-Date: 2025-10-10 14:45+0200\n" "Last-Translator: Martin Sirringhaus \n" "Language: de_DE\n" @@ -32,8 +32,7 @@ msgid "Registering a credential" msgstr "Zugangsdaten registrieren" #. developer_name tag deprecated with Appstream 1.0 -#: credentialsd-ui/data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:36 -#: credentialsd-ui/data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:39 +#: credentialsd-ui/data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:38 msgid "Isaiah Inuwa" msgstr "" @@ -51,66 +50,122 @@ msgstr "Ein mobiles Gerät" msgid "Use your security key" msgstr "Stecken Sie Ihren Security-Token ein." -#: credentialsd-ui/data/resources/ui/window.blp:162 +#: credentialsd-ui/data/resources/ui/window.blp:173 msgid "Connect a security key" msgstr "Stecken Sie Ihren Security-Token ein." -#: credentialsd-ui/data/resources/ui/window.blp:197 +#: credentialsd-ui/data/resources/ui/window.blp:208 #, fuzzy msgid "Enter your device PIN" msgstr "Geben Sie Ihren PIN ein." -#: credentialsd-ui/data/resources/ui/window.blp:205 +#: credentialsd-ui/data/resources/ui/window.blp:217 msgid "Scan the QR code to connect your device" msgstr "Scannen Sie den QR-Code, um Ihr Gerät zu verbinden" -#: credentialsd-ui/data/resources/ui/window.blp:247 -#: credentialsd-ui/data/resources/ui/window.blp:253 +#: credentialsd-ui/data/resources/ui/window.blp:259 +#: credentialsd-ui/data/resources/ui/window.blp:265 msgid "Choose credential" msgstr "Wählen Sie Zugangsdaten aus" -#: credentialsd-ui/data/resources/ui/window.blp:266 + +#: credentialsd-ui/data/resources/ui/window.blp:278 +msgid "Set a PIN" +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:301 +msgid "Please choose a new PIN for your device." +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:314 +msgid "New PIN" +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:321 +msgid "Confirm PIN" +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:333 +#: credentialsd-ui/data/resources/ui/window.blp:404 +msgid "Close" +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:338 +msgid "Continue" +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:353 msgid "Complete" msgstr "Abgeschlossen" -#: credentialsd-ui/data/resources/ui/window.blp:288 +#: credentialsd-ui/data/resources/ui/window.blp:375 msgid "Done!" msgstr "Fertig!" -#: credentialsd-ui/data/resources/ui/window.blp:299 +#: credentialsd-ui/data/resources/ui/window.blp:386 msgid "Something went wrong." msgstr "Etwas ist schief gegangen." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:159 +#: credentialsd-ui/data/resources/ui/window.blp:409 +#, fuzzy +msgid "Set PIN on device" +msgstr "Ein mobiles Gerät" + +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:177 #, fuzzy msgid "Enter your PIN. One attempt remaining." msgid_plural "Enter your PIN. %d attempts remaining." msgstr[0] "Geben Sie Ihren PIN ein. Sie haben nur noch einen Versuch." msgstr[1] "Geben Sie Ihren PIN ein. Sie haben noch %d Versuche." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:165 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:183 msgid "Enter your PIN." msgstr "Geben Sie Ihren PIN ein." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:174 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:192 msgid "Touch your device again. One attempt remaining." msgid_plural "Touch your device again. %d attempts remaining." msgstr[0] "Berühren Sie Ihr Gerät. Sie haben nur noch einen Versuch." msgstr[1] "Berühren Sie nochmal Ihr Gerät. Sie haben nur noch %d Versuche." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:180 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:198 msgid "Touch your device." msgstr "Berühren Sie Ihr Gerät." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:185 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:203 msgid "Touch your device" msgstr "Berühren Sie Ihr Gerät." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:188 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:210 +msgid "" +"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." +msgstr "" +"Für diesen Server benötigt ihr Gerät eine zusätzliche Absicherung, z.B. " +"einen PIN. Bitte setzen Sie einen PIN für ihr Gerät und versuchen Sie es " +"erneut." + +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:214 +msgid "" +"The entered PIN violates the PIN-policy of this device (likely too short). " +"Please try again." +msgstr "" + +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:217 +msgid "" +"The entered PIN violates the PIN-policy of this device (PIN too long). " +"Please try again." +msgstr "" + +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:220 +msgid "A PIN change is required by your device to continue." +msgstr "" + +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:227 msgid "Scan the QR code with your device to begin authentication." msgstr "" "Scannen Sie den QR code mit ihrem Gerät um die Authentifizierung zu beginnen." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:201 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:240 msgid "" "Connecting to your device. Make sure both devices are near each other and " "have Bluetooth enabled." @@ -118,19 +173,19 @@ msgstr "" "Verbindung zu Ihrem Gerät wird aufgebaut. Stellen Sie sicher, dass beide " "Geräte nah beieinander sind und Bluetooth aktiviert haben." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:209 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:248 msgid "Device connected. Follow the instructions on your device" msgstr "Verbindung hergestellt. Folgen Sie den Anweisungen auf Ihrem Gerät." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:298 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:347 msgid "Insert your security key." msgstr "Stecken Sie Ihren Security-Token ein." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:317 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:366 msgid "Multiple devices found. Please select with which to proceed." msgstr "Mehrere Geräte gefunden. Bitte wählen Sie einen aus, um fortzufahren." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:362 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:415 msgid "Credential Manager" msgstr "Zugangsdatenmanager" @@ -158,28 +213,28 @@ msgstr "Ein NFC-Gerät" msgid "A security key (USB)" msgstr "Ein Security-Token" -#: credentialsd-ui/src/gui/view_model/mod.rs:64 +#: credentialsd-ui/src/gui/view_model/mod.rs:62 msgid "unknown application" msgstr "unbekannter Applikation" #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from -#: credentialsd-ui/src/gui/view_model/mod.rs:80 +#: credentialsd-ui/src/gui/view_model/mod.rs:76 msgid "Create a passkey for %s1" msgstr "Neuen Passkey für %s1 erstellen" #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from -#: credentialsd-ui/src/gui/view_model/mod.rs:84 +#: credentialsd-ui/src/gui/view_model/mod.rs:80 msgid "Use a passkey for %s1" msgstr "Passkey für %s1 abrufen" #. TRANSLATORS: %s1 is the "relying party" (e.g.: domain name) where the request is coming from #. TRANSLATORS: %s2 is the application name (e.g.: firefox) where the request is coming from, must be left untouched to make the name bold -#. TRANSLATORS: %i1 is the process ID of the requesting application -#. TRANSLATORS: %s3 is the absolute path (think: /usr/bin/firefox) of the requesting application -#: credentialsd-ui/src/gui/view_model/mod.rs:96 +#. TRANSLATORS: %s3 is the app ID (think: org.mozilla.firefox) of the requesting application +#: credentialsd-ui/src/gui/view_model/mod.rs:91 +#, fuzzy msgid "" -"\"%s2\" (process ID: %i1, binary: %s3) is asking to create a " -"credential to register at \"%s1\". Only proceed if you trust this process." +"\"%s2\" (%s3) is asking to create a credential to register at " +"\"%s1\". Only proceed if you trust this process." msgstr "" "\"%s2\" (Prozess-ID: %i1, ausführbare Datei: %s3) möchte neue " "Zugangsdaten erstellen, um Sie bei \"%s1\" zu registrieren. Fahren Sie nur " @@ -187,59 +242,38 @@ msgstr "" #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from #. TRANSLATORS: %s2 is the application name (e.g.: firefox) where the request is coming from, must be left untouched to make the name bold -#. TRANSLATORS: %i1 is the process ID of the requesting application -#. TRANSLATORS: %s3 is the absolute path (think: /usr/bin/firefox) of the requesting application -#: credentialsd-ui/src/gui/view_model/mod.rs:103 +#. TRANSLATORS: %s3 is the app ID (think: org.mozilla.firefox) of the requesting application +#: credentialsd-ui/src/gui/view_model/mod.rs:97 +#, fuzzy msgid "" -"\"%s2\" (process ID: %i1, binary: %s3) is asking to use a credential " -"to sign in to \"%s1\". Only proceed if you trust this process." +"\"%s2\" (%s3) is asking to use a credential to sign in to \"%s1\". " +"Only proceed if you trust this process." msgstr "" "\"%s2\" (Prozess-ID: %i1, ausführbare Datei: %s3) möchte Zugangsdaten " "abrufen, um Sie bei \"%s1\" anzumelden. Fahren Sie nur fort, wenn Sie diesem " "Prozess vertrauen." #. TRANSLATORS: %s1 is the relying party (think: domain name) where the request is coming from -#: credentialsd-ui/src/gui/view_model/mod.rs:115 +#: credentialsd-ui/src/gui/view_model/mod.rs:108 msgid "" "Scan the QR code using the camera on the device that has the passkey for %s1" msgstr "" #. TRANSLATORS: %s1 is the relying party (think: domain name) where the request is coming from -#: credentialsd-ui/src/gui/view_model/mod.rs:118 +#: credentialsd-ui/src/gui/view_model/mod.rs:111 #, fuzzy msgid "Insert and activate your security key to use it for %s1" msgstr "Stecken Sie Ihren Security-Token ein." -#: credentialsd-ui/src/gui/view_model/mod.rs:181 +#: credentialsd-ui/src/gui/view_model/mod.rs:193 msgid "Failed to select credential from device." msgstr "Zugangsdaten vom Gerät konnten nicht ausgewählt werden." -#: credentialsd-ui/src/gui/view_model/mod.rs:237 -msgid "No matching credentials found on this authenticator." -msgstr "Keine passenden Zugangsdaten auf diesem Gerät gefunden." - -#: credentialsd-ui/src/gui/view_model/mod.rs:245 -msgid "" -"No more PIN attempts allowed. Try removing your device and plugging it back " -"in." -msgstr "" -"Keine weiteren PIN-Eingaben erlaubt. Versuchen Sie ihr Gerät aus- und wieder " -"einzustecken." - -#: credentialsd-ui/src/gui/view_model/mod.rs:254 -msgid "" -"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." -msgstr "" -"Für diesen Server benötigt ihr Gerät eine zusätzliche Absicherung, z.B. " -"einen PIN. Bitte setzen Sie einen PIN für ihr Gerät und versuchen Sie es " -"erneut." - -#: credentialsd-ui/src/gui/view_model/mod.rs:262 +#: credentialsd-ui/src/gui/view_model/mod.rs:257 msgid "The credential request timed out. Please try again." msgstr "" -#: credentialsd-ui/src/gui/view_model/mod.rs:272 +#: credentialsd-ui/src/gui/view_model/mod.rs:267 msgid "" "Something went wrong while retrieving a credential. Please try again later " "or use a different authenticator." @@ -247,10 +281,32 @@ msgstr "" "Beim Abrufen Ihrer Zugangsdaten ist ein Fehler aufgetreten. Versuchen Sie es " "später wieder, oder verwenden Sie einen anderen Security-Token." -#: credentialsd-ui/src/gui/view_model/mod.rs:281 +#: credentialsd-ui/src/gui/view_model/mod.rs:276 msgid "This credential is already registered on this authenticator." msgstr "Diese Zugangsdaten sind bereits auf diesem Gerät registriert." +#: credentialsd-ui/src/gui/view_model/mod.rs:374 +msgid "" +"The previous attempt was interrupted. Please follow the new prompts to try " +"again." +msgstr "" +"Der Versuch wurde abgebrochen. Bitte versuchen Sie es erneut." + +#: credentialsd-ui/src/gui/view_model/mod.rs:377 +msgid "" +"No matching credentials on this authenticator. Please try a different one." +msgstr "" +"Keine passenden Zugangsdaten auf diesem Gerät. Bitte versuchen Sie es mit " +"einem anderen." + +#: credentialsd-ui/src/gui/view_model/mod.rs:380 +msgid "" +"No more PIN attempts allowed. Remove and reinsert your device, or use a " +"different authenticator." +msgstr "" +"Keine weiteren PIN-Eingaben erlaubt. Versuchen Sie ihr Gerät aus- und wieder " +"einzustecken, oder verwenden Sie ein anderes Gerät." + #~ msgid "Devices" #~ msgstr "Geräte" diff --git a/credentialsd-ui/po/en_US.po b/credentialsd-ui/po/en_US.po index f9eda4a..f5f12eb 100644 --- a/credentialsd-ui/po/en_US.po +++ b/credentialsd-ui/po/en_US.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \"https://github.com/linux-credentials/credentialsd/" "issues\"\n" -"POT-Creation-Date: 2026-06-18 07:18-0500\n" +"POT-Creation-Date: 2026-09-09 15:53+0200\n" "PO-Revision-Date: 2025-10-10 14:45+0200\n" "Last-Translator: Martin Sirringhaus \n" "Language: en_US\n" @@ -31,8 +31,7 @@ msgid "Registering a credential" msgstr "Registering a credential" #. developer_name tag deprecated with Appstream 1.0 -#: credentialsd-ui/data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:36 -#: credentialsd-ui/data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:39 +#: credentialsd-ui/data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:38 msgid "Isaiah Inuwa" msgstr "" @@ -50,65 +49,119 @@ msgstr "A mobile device" msgid "Use your security key" msgstr "Insert your security key." -#: credentialsd-ui/data/resources/ui/window.blp:162 +#: credentialsd-ui/data/resources/ui/window.blp:173 msgid "Connect a security key" msgstr "Connect a security key" -#: credentialsd-ui/data/resources/ui/window.blp:197 +#: credentialsd-ui/data/resources/ui/window.blp:208 #, fuzzy msgid "Enter your device PIN" msgstr "Enter your PIN." -#: credentialsd-ui/data/resources/ui/window.blp:205 +#: credentialsd-ui/data/resources/ui/window.blp:217 msgid "Scan the QR code to connect your device" msgstr "Scan the QR code to connect your device" -#: credentialsd-ui/data/resources/ui/window.blp:247 -#: credentialsd-ui/data/resources/ui/window.blp:253 +#: credentialsd-ui/data/resources/ui/window.blp:259 +#: credentialsd-ui/data/resources/ui/window.blp:265 msgid "Choose credential" msgstr "Choose credential" -#: credentialsd-ui/data/resources/ui/window.blp:266 +#: credentialsd-ui/data/resources/ui/window.blp:278 +msgid "Set a PIN" +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:301 +msgid "Please choose a new PIN for your device." +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:314 +msgid "New PIN" +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:321 +msgid "Confirm PIN" +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:333 +#: credentialsd-ui/data/resources/ui/window.blp:404 +msgid "Close" +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:338 +msgid "Continue" +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:353 msgid "Complete" msgstr "Complete" -#: credentialsd-ui/data/resources/ui/window.blp:288 +#: credentialsd-ui/data/resources/ui/window.blp:375 msgid "Done!" msgstr "Done!" -#: credentialsd-ui/data/resources/ui/window.blp:299 +#: credentialsd-ui/data/resources/ui/window.blp:386 msgid "Something went wrong." msgstr "Something went wrong." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:159 +#: credentialsd-ui/data/resources/ui/window.blp:409 +#, fuzzy +msgid "Set PIN on device" +msgstr "A mobile device" + +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:177 msgid "Enter your PIN. One attempt remaining." msgid_plural "Enter your PIN. %d attempts remaining." msgstr[0] "Enter your PIN. One attempt remaining." msgstr[1] "Enter your PIN. %d attempts remaining." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:165 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:183 msgid "Enter your PIN." msgstr "Enter your PIN." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:174 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:192 msgid "Touch your device again. One attempt remaining." msgid_plural "Touch your device again. %d attempts remaining." msgstr[0] "Touch your device again. One attempt remaining." msgstr[1] "Touch your device again. %d attempts remaining." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:180 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:198 msgid "Touch your device." msgstr "Touch your device." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:185 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:203 msgid "Touch your device" msgstr "Touch your device" -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:188 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:210 +msgid "" +"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." +msgstr "" +"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." + +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:214 +msgid "" +"The entered PIN violates the PIN-policy of this device (likely too short). " +"Please try again." +msgstr "" + +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:217 +msgid "" +"The entered PIN violates the PIN-policy of this device (PIN too long). " +"Please try again." +msgstr "" + +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:220 +msgid "A PIN change is required by your device to continue." +msgstr "" + +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:227 msgid "Scan the QR code with your device to begin authentication." msgstr "Scan the QR code with your device to begin authentication." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:201 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:240 msgid "" "Connecting to your device. Make sure both devices are near each other and " "have Bluetooth enabled." @@ -116,19 +169,19 @@ msgstr "" "Connecting to your device. Make sure both devices are near each other and " "have Bluetooth enabled." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:209 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:248 msgid "Device connected. Follow the instructions on your device" msgstr "Device connected. Follow the instructions on your device" -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:298 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:347 msgid "Insert your security key." msgstr "Insert your security key." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:317 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:366 msgid "Multiple devices found. Please select with which to proceed." msgstr "Multiple devices found. Please select with which to proceed." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:362 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:415 msgid "Credential Manager" msgstr "Credential Manager" @@ -156,85 +209,65 @@ msgstr "A security key or card (NFC)" msgid "A security key (USB)" msgstr "A security key (USB)" -#: credentialsd-ui/src/gui/view_model/mod.rs:64 +#: credentialsd-ui/src/gui/view_model/mod.rs:62 msgid "unknown application" msgstr "unknown application" #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from -#: credentialsd-ui/src/gui/view_model/mod.rs:80 +#: credentialsd-ui/src/gui/view_model/mod.rs:76 msgid "Create a passkey for %s1" msgstr "Create a passkey for %s1" #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from -#: credentialsd-ui/src/gui/view_model/mod.rs:84 +#: credentialsd-ui/src/gui/view_model/mod.rs:80 msgid "Use a passkey for %s1" msgstr "Use a passkey for %s1" #. TRANSLATORS: %s1 is the "relying party" (e.g.: domain name) where the request is coming from #. TRANSLATORS: %s2 is the application name (e.g.: firefox) where the request is coming from, must be left untouched to make the name bold -#. TRANSLATORS: %i1 is the process ID of the requesting application -#. TRANSLATORS: %s3 is the absolute path (think: /usr/bin/firefox) of the requesting application -#: credentialsd-ui/src/gui/view_model/mod.rs:96 +#. TRANSLATORS: %s3 is the app ID (think: org.mozilla.firefox) of the requesting application +#: credentialsd-ui/src/gui/view_model/mod.rs:91 +#, fuzzy msgid "" -"\"%s2\" (process ID: %i1, binary: %s3) is asking to create a " -"credential to register at \"%s1\". Only proceed if you trust this process." +"\"%s2\" (%s3) is asking to create a credential to register at " +"\"%s1\". Only proceed if you trust this process." msgstr "" "\"%s2\" (process ID: %i1, binary: %s3) is asking to create a " "credential to register at \"%s1\". Only proceed if you trust this process." #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from #. TRANSLATORS: %s2 is the application name (e.g.: firefox) where the request is coming from, must be left untouched to make the name bold -#. TRANSLATORS: %i1 is the process ID of the requesting application -#. TRANSLATORS: %s3 is the absolute path (think: /usr/bin/firefox) of the requesting application -#: credentialsd-ui/src/gui/view_model/mod.rs:103 +#. TRANSLATORS: %s3 is the app ID (think: org.mozilla.firefox) of the requesting application +#: credentialsd-ui/src/gui/view_model/mod.rs:97 +#, fuzzy msgid "" -"\"%s2\" (process ID: %i1, binary: %s3) is asking to use a credential " -"to sign in to \"%s1\". Only proceed if you trust this process." +"\"%s2\" (%s3) is asking to use a credential to sign in to \"%s1\". " +"Only proceed if you trust this process." msgstr "" "\"%s2\" (process ID: %i1, binary: %s3) is asking to use a credential " "to sign in to \"%s1\". Only proceed if you trust this process." #. TRANSLATORS: %s1 is the relying party (think: domain name) where the request is coming from -#: credentialsd-ui/src/gui/view_model/mod.rs:115 +#: credentialsd-ui/src/gui/view_model/mod.rs:108 msgid "" "Scan the QR code using the camera on the device that has the passkey for %s1" msgstr "" #. TRANSLATORS: %s1 is the relying party (think: domain name) where the request is coming from -#: credentialsd-ui/src/gui/view_model/mod.rs:118 +#: credentialsd-ui/src/gui/view_model/mod.rs:111 #, fuzzy msgid "Insert and activate your security key to use it for %s1" msgstr "Insert your security key." -#: credentialsd-ui/src/gui/view_model/mod.rs:181 +#: credentialsd-ui/src/gui/view_model/mod.rs:193 msgid "Failed to select credential from device." msgstr "Failed to select credential from device." -#: credentialsd-ui/src/gui/view_model/mod.rs:237 -msgid "No matching credentials found on this authenticator." -msgstr "No matching credentials found on this authenticator." - -#: credentialsd-ui/src/gui/view_model/mod.rs:245 -msgid "" -"No more PIN attempts allowed. Try removing your device and plugging it back " -"in." -msgstr "" -"No more PIN attempts allowed. Try removing your device and plugging it back " -"in." - -#: credentialsd-ui/src/gui/view_model/mod.rs:254 -msgid "" -"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." -msgstr "" -"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." - -#: credentialsd-ui/src/gui/view_model/mod.rs:262 +#: credentialsd-ui/src/gui/view_model/mod.rs:257 msgid "The credential request timed out. Please try again." msgstr "" -#: credentialsd-ui/src/gui/view_model/mod.rs:272 +#: credentialsd-ui/src/gui/view_model/mod.rs:267 msgid "" "Something went wrong while retrieving a credential. Please try again later " "or use a different authenticator." @@ -242,10 +275,32 @@ msgstr "" "Something went wrong while retrieving a credential. Please try again later " "or use a different authenticator." -#: credentialsd-ui/src/gui/view_model/mod.rs:281 +#: credentialsd-ui/src/gui/view_model/mod.rs:276 msgid "This credential is already registered on this authenticator." msgstr "This credential is already registered on this authenticator." +#: credentialsd-ui/src/gui/view_model/mod.rs:374 +msgid "" +"The previous attempt was interrupted. Please follow the new prompts to try " +"again." +msgstr "" +"The previous attempt was interrupted. Please follow the new prompts to try " +"again." + +#: credentialsd-ui/src/gui/view_model/mod.rs:377 +msgid "" +"No matching credentials on this authenticator. Please try a different one." +msgstr "" +"No matching credentials on this authenticator. Please try a different one." + +#: credentialsd-ui/src/gui/view_model/mod.rs:380 +msgid "" +"No more PIN attempts allowed. Remove and reinsert your device, or use a " +"different authenticator." +msgstr "" +"No more PIN attempts allowed. Remove and reinsert your device, or use a " +"different authenticator." + #~ msgid "Devices" #~ msgstr "Devices" diff --git a/credentialsd-ui/po/ka_GE.po b/credentialsd-ui/po/ka_GE.po index 8fcf676..9ee9abf 100644 --- a/credentialsd-ui/po/ka_GE.po +++ b/credentialsd-ui/po/ka_GE.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: credentialsd-ui\n" "Report-Msgid-Bugs-To: \"https://github.com/linux-credentials/credentialsd/" "issues\"\n" -"POT-Creation-Date: 2026-06-18 07:18-0500\n" +"POT-Creation-Date: 2026-09-09 15:53+0200\n" "PO-Revision-Date: 2026-04-20 07:16+0200\n" "Last-Translator: Ekaterine Papava \n" "Language-Team: \n" @@ -39,8 +39,7 @@ msgid "Registering a credential" msgstr "ავტორიზაციის დეტალების რეგისტრაცია" #. developer_name tag deprecated with Appstream 1.0 -#: credentialsd-ui/data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:36 -#: credentialsd-ui/data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:39 +#: credentialsd-ui/data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:38 msgid "Isaiah Inuwa" msgstr "Isaiah Inuwa" @@ -58,65 +57,120 @@ msgstr "მობილური მოწყობილობა" msgid "Use your security key" msgstr "შეერთეთ თქვენი უსაფრთხოების გასაღები." -#: credentialsd-ui/data/resources/ui/window.blp:162 +#: credentialsd-ui/data/resources/ui/window.blp:173 msgid "Connect a security key" msgstr "უსაფრთხოების გასაღების დაკავშირება" -#: credentialsd-ui/data/resources/ui/window.blp:197 +#: credentialsd-ui/data/resources/ui/window.blp:208 #, fuzzy msgid "Enter your device PIN" msgstr "შეიყვანეთ PIN-კოდი." -#: credentialsd-ui/data/resources/ui/window.blp:205 +#: credentialsd-ui/data/resources/ui/window.blp:217 msgid "Scan the QR code to connect your device" msgstr "დაასკანირეთ QR კოდი თქვენი მოწყობილობის დასაკავშირებლად" -#: credentialsd-ui/data/resources/ui/window.blp:247 -#: credentialsd-ui/data/resources/ui/window.blp:253 +#: credentialsd-ui/data/resources/ui/window.blp:259 +#: credentialsd-ui/data/resources/ui/window.blp:265 msgid "Choose credential" msgstr "ავტორიზაციის დეტალების არჩევა" -#: credentialsd-ui/data/resources/ui/window.blp:266 +#: credentialsd-ui/data/resources/ui/window.blp:278 +msgid "Set a PIN" +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:301 +msgid "Please choose a new PIN for your device." +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:314 +msgid "New PIN" +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:321 +msgid "Confirm PIN" +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:333 +#: credentialsd-ui/data/resources/ui/window.blp:404 +msgid "Close" +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:338 +msgid "Continue" +msgstr "" + +#: credentialsd-ui/data/resources/ui/window.blp:353 msgid "Complete" msgstr "დასრულება" -#: credentialsd-ui/data/resources/ui/window.blp:288 +#: credentialsd-ui/data/resources/ui/window.blp:375 msgid "Done!" msgstr "მზადაა!" -#: credentialsd-ui/data/resources/ui/window.blp:299 +#: credentialsd-ui/data/resources/ui/window.blp:386 msgid "Something went wrong." msgstr "რაღაც მოხდა." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:159 +#: credentialsd-ui/data/resources/ui/window.blp:409 +#, fuzzy +msgid "Set PIN on device" +msgstr "მობილური მოწყობილობა" + +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:177 msgid "Enter your PIN. One attempt remaining." msgid_plural "Enter your PIN. %d attempts remaining." msgstr[0] "შეიყვანეთ თქვენი PIN-კოდი. დარჩენილია ერთი მცდელობა." msgstr[1] "შეიყვანეთ თქვენი PIN-კოდი. დარჩენილია %d მცდელობა." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:165 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:183 msgid "Enter your PIN." msgstr "შეიყვანეთ PIN-კოდი." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:174 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:192 msgid "Touch your device again. One attempt remaining." msgid_plural "Touch your device again. %d attempts remaining." msgstr[0] "შეეხეთ თქვენს მოწყობილობას კიდევ ერთხელ. დარჩენილია ერთი მცდელობა." msgstr[1] "შეეხეთ თქვენს მოწყობილობას კიდევ ერთხელ. დარჩენილია %d მცდელობა." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:180 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:198 msgid "Touch your device." msgstr "შეეხეთ თქვენს მოწყობილობას." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:185 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:203 msgid "Touch your device" msgstr "შეეხეთ თქვენს მოწყობილობას" -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:188 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:210 +msgid "" +"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." +msgstr "" +"ეს სერვერი ითხოვს, რომ თქვენს მოწყობილობას ჰქონდეს დამატებითი დაცვა, " +"როგორიცაა PIN-კოდი, რომელიც დაყენებული არაა. დააყენეთ PIN-კოდი ამ " +"მოწყობილობისთვის და თავიდან სცადეთ." + +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:214 +msgid "" +"The entered PIN violates the PIN-policy of this device (likely too short). " +"Please try again." +msgstr "" + +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:217 +msgid "" +"The entered PIN violates the PIN-policy of this device (PIN too long). " +"Please try again." +msgstr "" + +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:220 +msgid "A PIN change is required by your device to continue." +msgstr "" + +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:227 msgid "Scan the QR code with your device to begin authentication." msgstr "ავთენტიკაციის დასაწყებად დაასკანერეთ QR კოდი თქვენი მოწყობილობით." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:201 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:240 msgid "" "Connecting to your device. Make sure both devices are near each other and " "have Bluetooth enabled." @@ -124,20 +178,20 @@ msgstr "" "მიმდინარეობს თქვენს მოწყობილობასთან დაკავშირება. დარწმუნდით, რომ ორივე " "მოწყობილობა ახლოსაა ერთმანეთთან და რომ ბლუთუზი ჩართულია." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:209 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:248 msgid "Device connected. Follow the instructions on your device" msgstr "მოწყობილობა დაკავშირებულია. მიჰყევით ინსტრუქციებს თქვენს მოწყობილობაზე" -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:298 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:347 msgid "Insert your security key." msgstr "შეერთეთ თქვენი უსაფრთხოების გასაღები." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:317 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:366 msgid "Multiple devices found. Please select with which to proceed." msgstr "" "აღმოჩენილია ერთზე მეტი მოწყობილობა. აირჩიეთ, რომელი გნებავთ, გამოიყენოთ." -#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:362 +#: credentialsd-ui/src/gui/view_model/gtk/mod.rs:415 msgid "Credential Manager" msgstr "ავტორიზაციის დეტალების მმართველი" @@ -165,28 +219,28 @@ msgstr "უსაფრთხოების გასაღები, ან msgid "A security key (USB)" msgstr "უსაფრთხოების გასაღები (USB)" -#: credentialsd-ui/src/gui/view_model/mod.rs:64 +#: credentialsd-ui/src/gui/view_model/mod.rs:62 msgid "unknown application" msgstr "უცნობი აპლიკაცია" #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from -#: credentialsd-ui/src/gui/view_model/mod.rs:80 +#: credentialsd-ui/src/gui/view_model/mod.rs:76 msgid "Create a passkey for %s1" msgstr "საკვანძო გასაღების შექმნა %1-ისთვის" #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from -#: credentialsd-ui/src/gui/view_model/mod.rs:84 +#: credentialsd-ui/src/gui/view_model/mod.rs:80 msgid "Use a passkey for %s1" msgstr "საკვანძო გასაღების გამოყენება %1-ისთვის" #. TRANSLATORS: %s1 is the "relying party" (e.g.: domain name) where the request is coming from #. TRANSLATORS: %s2 is the application name (e.g.: firefox) where the request is coming from, must be left untouched to make the name bold -#. TRANSLATORS: %i1 is the process ID of the requesting application -#. TRANSLATORS: %s3 is the absolute path (think: /usr/bin/firefox) of the requesting application -#: credentialsd-ui/src/gui/view_model/mod.rs:96 +#. TRANSLATORS: %s3 is the app ID (think: org.mozilla.firefox) of the requesting application +#: credentialsd-ui/src/gui/view_model/mod.rs:91 +#, fuzzy msgid "" -"\"%s2\" (process ID: %i1, binary: %s3) is asking to create a " -"credential to register at \"%s1\". Only proceed if you trust this process." +"\"%s2\" (%s3) is asking to create a credential to register at " +"\"%s1\". Only proceed if you trust this process." msgstr "" "\"%s2\" (პროცესის ID: %i1, გამშვები ფაილი: %s3) ითხოვს ავტორიზაციის " "დეტალების შექმნას \"%s1\"-ზე რეგისტრაციისთვის. გააგრძელეთ, მხოლოდ, მაშინ, თუ " @@ -194,59 +248,38 @@ msgstr "" #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from #. TRANSLATORS: %s2 is the application name (e.g.: firefox) where the request is coming from, must be left untouched to make the name bold -#. TRANSLATORS: %i1 is the process ID of the requesting application -#. TRANSLATORS: %s3 is the absolute path (think: /usr/bin/firefox) of the requesting application -#: credentialsd-ui/src/gui/view_model/mod.rs:103 +#. TRANSLATORS: %s3 is the app ID (think: org.mozilla.firefox) of the requesting application +#: credentialsd-ui/src/gui/view_model/mod.rs:97 +#, fuzzy msgid "" -"\"%s2\" (process ID: %i1, binary: %s3) is asking to use a credential " -"to sign in to \"%s1\". Only proceed if you trust this process." +"\"%s2\" (%s3) is asking to use a credential to sign in to \"%s1\". " +"Only proceed if you trust this process." msgstr "" "\"%s2\" (პროცესის ID: %i1, გამშვები ფაილი: %s3) ითხოვს ავტორიზაციის " "დეტალების გამოყენებას \"%s1\"-ზე შესასვლელად. გააგრძელეთ, მხოლოდ, მაშინ, თუ " "ენდობით ამ პროცესს." #. TRANSLATORS: %s1 is the relying party (think: domain name) where the request is coming from -#: credentialsd-ui/src/gui/view_model/mod.rs:115 +#: credentialsd-ui/src/gui/view_model/mod.rs:108 msgid "" "Scan the QR code using the camera on the device that has the passkey for %s1" msgstr "" #. TRANSLATORS: %s1 is the relying party (think: domain name) where the request is coming from -#: credentialsd-ui/src/gui/view_model/mod.rs:118 +#: credentialsd-ui/src/gui/view_model/mod.rs:111 #, fuzzy msgid "Insert and activate your security key to use it for %s1" msgstr "შეერთეთ თქვენი უსაფრთხოების გასაღები." -#: credentialsd-ui/src/gui/view_model/mod.rs:181 +#: credentialsd-ui/src/gui/view_model/mod.rs:193 msgid "Failed to select credential from device." msgstr "ავტორიზაციის დეტალების არჩევა მოწყობილობიდან ჩავარდა." -#: credentialsd-ui/src/gui/view_model/mod.rs:237 -msgid "No matching credentials found on this authenticator." -msgstr "ამ ავთენტიკატორში შესაბამისი ავტორიზაციის დეტალები აღმოჩენილი არაა." - -#: credentialsd-ui/src/gui/view_model/mod.rs:245 -msgid "" -"No more PIN attempts allowed. Try removing your device and plugging it back " -"in." -msgstr "" -"მეტი PIN-კოდი დაშვებული აღარაა. სცადეთ, გამოაძროთ თქვენი მოწყობილობა და ისევ " -"შეაერთოთ." - -#: credentialsd-ui/src/gui/view_model/mod.rs:254 -msgid "" -"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." -msgstr "" -"ეს სერვერი ითხოვს, რომ თქვენს მოწყობილობას ჰქონდეს დამატებითი დაცვა, " -"როგორიცაა PIN-კოდი, რომელიც დაყენებული არაა. დააყენეთ PIN-კოდი ამ " -"მოწყობილობისთვის და თავიდან სცადეთ." - -#: credentialsd-ui/src/gui/view_model/mod.rs:262 +#: credentialsd-ui/src/gui/view_model/mod.rs:257 msgid "The credential request timed out. Please try again." msgstr "" -#: credentialsd-ui/src/gui/view_model/mod.rs:272 +#: credentialsd-ui/src/gui/view_model/mod.rs:267 msgid "" "Something went wrong while retrieving a credential. Please try again later " "or use a different authenticator." @@ -254,10 +287,31 @@ msgstr "" "ავტორიზაციის დეტალების მიღებისას რაღაც არასწორად წავიდა. სცადეთ თავიდან " "მოგვიანებით, ან გამოიყენეთ სხვა ავთენტიკატორი." -#: credentialsd-ui/src/gui/view_model/mod.rs:281 +#: credentialsd-ui/src/gui/view_model/mod.rs:276 msgid "This credential is already registered on this authenticator." msgstr "ეს ავტორიზაციის დეტალი უკვე რეგისტრირებულია ამ ავთენტიკატორზე." +#: credentialsd-ui/src/gui/view_model/mod.rs:374 +msgid "" +"The previous attempt was interrupted. Please follow the new prompts to try " +"again." +msgstr "" + +#: credentialsd-ui/src/gui/view_model/mod.rs:377 +#, fuzzy +msgid "" +"No matching credentials on this authenticator. Please try a different one." +msgstr "ამ ავთენტიკატორში შესაბამისი ავტორიზაციის დეტალები აღმოჩენილი არაა." + +#: credentialsd-ui/src/gui/view_model/mod.rs:380 +#, fuzzy +msgid "" +"No more PIN attempts allowed. Remove and reinsert your device, or use a " +"different authenticator." +msgstr "" +"მეტი PIN-კოდი დაშვებული აღარაა. სცადეთ, გამოაძროთ თქვენი მოწყობილობა და ისევ " +"შეაერთოთ." + #~ msgid "Devices" #~ msgstr "მოწყობილობები"