diff --git a/CHANGELOG.md b/CHANGELOG.md index c1da53b9..ab92a543 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # [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] ## Improvements diff --git a/credentialsd-common/src/model.rs b/credentialsd-common/src/model.rs index 98a6e888..6c43debe 100644 --- a/credentialsd-common/src/model.rs +++ b/credentialsd-common/src/model.rs @@ -8,43 +8,104 @@ 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)] 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 { + reason: TransportRestartReason, + }, 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 { + reason: TransportRestartReason, + }, 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 { + reason: TransportRestartReason, + }, ErrorInternal, ErrorTimedOut, ErrorCancelled, ErrorAuthenticator, - ErrorNoCredentials, ErrorCredentialExcluded, - ErrorPinAttemptsExhausted, - ErrorPinNotSet, } /// Emitted when a client enters a PIN for the selected authenticator. @@ -146,6 +207,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/data/resources/ui/window.blp b/credentialsd-ui/data/resources/ui/window.blp index 70f62def..c4601aa5 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/po/credentialsd-ui.pot b/credentialsd-ui/po/credentialsd-ui.pot index 5ed55b70..d1a35fca 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 7243f73d..86ca6f63 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 f9eda4a1..f5f12eb9 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 8fcf6764..9ee9abf3 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 "მოწყობილობები" diff --git a/credentialsd-ui/src/dbus.rs b/credentialsd-ui/src/dbus.rs index 8456556f..cce23cef 100644 --- a/credentialsd-ui/src/dbus.rs +++ b/credentialsd-ui/src/dbus.rs @@ -25,14 +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, - NotifyHybridStartedOptions, NotifyNeedsPinOptions, NotifyNeedsUserPresenceOptions, - NotifyNeedsUserVerificationOptions, NotifyNfcConnectedOptions, NotifyPinNotSetOptions, - NotifySelectingCredentialOptions, NotifyUsbConnectedOptions, 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}; @@ -307,6 +307,57 @@ impl CredentialPortalBackend { .await } + async fn notify_hybrid_restarting( + &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 { reason }, + ) + .await + } + + async fn notify_usb_restarting( + &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 { reason }, + ) + .await + } + + async fn notify_nfc_restarting( + &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 { reason }, + ) + .await + } + /// Called when the authentication ceremony completes successfully. async fn notify_ceremony_completed( &self, @@ -333,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 e2acb3fb..b3bc3e3e 100644 --- a/credentialsd-ui/src/gui/mod.rs +++ b/credentialsd-ui/src/gui/mod.rs @@ -95,6 +95,13 @@ 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 and display + /// the provided localized message. + TransportRestarting { + message: String, + }, + 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 109bc89c..c8dd76c2 100644 --- a/credentialsd-ui/src/gui/view_model/gtk/mod.rs +++ b/credentialsd-ui/src/gui/view_model/gtk/mod.rs @@ -84,6 +84,15 @@ mod imp { #[property(get, set)] pub qr_spinner_visible: RefCell, + #[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, @@ -138,6 +147,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 +249,16 @@ impl ViewModel { )); view_model.set_qr_spinner_visible(false); } + 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 => { 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 58d1cac4..dceb7183 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 0a791e1b..c216c3c4 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 @@ -348,6 +324,18 @@ impl ViewModel { .await .unwrap(); } + Event::Background( + 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 { message }) + .await + .unwrap(); + } Event::Background(BackgroundEvent::ErrorCancelled) => { self.hybrid_qr_code_data = None; break; @@ -380,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 369c0b21..96eca9ae 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}; @@ -68,127 +71,90 @@ 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 { + // 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) => { + 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 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); - } - } + 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; + } + + // 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(), + &mut active, + ) + .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 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::RequestCancelled) + break; } - }; - - let terminal_state = match response { - Ok(auth_response) => Some(HybridStateInternal::Completed(auth_response)), - Err(CredentialServiceError::RequestCancelled) => { - // 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) => { + 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. + 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(reason)).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; } - 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 +181,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(TransportRestartReason), } // this is here to prevent making HybridStateInternal public to the whole crate. @@ -241,6 +211,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(TransportRestartReason), } impl From for HybridState { @@ -251,6 +225,7 @@ impl From for HybridState { HybridStateInternal::Connected => HybridState::Connected, HybridStateInternal::Completed(_) => HybridState::Completed, HybridStateInternal::Failed(err) => HybridState::Failed(err), + HybridStateInternal::Restarting(reason) => HybridState::Restarting(reason), } } } @@ -272,29 +247,131 @@ impl From<&HybridState> for BackgroundEvent { HybridState::Connecting => BackgroundEvent::HybridConnecting, HybridState::Connected => BackgroundEvent::HybridConnected, HybridState::Completed => BackgroundEvent::CeremonyCompleted, + 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::RequestCancelled) => { + HybridState::Failed(CredentialServiceError::NonTerminatingCancellation) => { BackgroundEvent::ErrorCancelled } - HybridState::Failed(CredentialServiceError::Internal(_)) => { + HybridState::Failed(CredentialServiceError::Internal(_)) + | HybridState::Failed(CredentialServiceError::NoCredentials) + | HybridState::Failed(CredentialServiceError::PinAttemptsExhausted) => { BackgroundEvent::ErrorInternal } } } } +/// 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, + active: &mut bool, +) -> 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); + } + }; + + // 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 { + 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 c3068a78..f83f0e6d 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 +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 + // 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, @@ -579,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 @@ -739,10 +785,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 +1186,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 +1289,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 +1414,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 +1425,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 +1455,271 @@ 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 + )); + } + + /// 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( + TransportRestartReason::Interrupted, + )); + assert!( + matches!( + hybrid_stream.next().await, + Some(HybridState::Restarting(TransportRestartReason::Interrupted)) + ), + "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( + TransportRestartReason::Interrupted, + )); + assert!( + matches!( + usb_stream.next().await, + Some(UsbState::Restarting(TransportRestartReason::Interrupted)) + ), + "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 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 7ac0edfb..00b835ba 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}; @@ -43,7 +45,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)), @@ -171,6 +173,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"); @@ -206,10 +209,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()), } }; @@ -222,10 +228,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(()); @@ -243,20 +249,84 @@ impl InProcessNfcHandler { std::mem::discriminant(new_state) != std::mem::discriminant(old_state) } }; - 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(), - ) - })?; + // 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(err) => break Err(err), - _ => {} + 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(), + ) + })?; + } + 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 => { + 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(reason)).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(), + ) + })?; + } + } } } } @@ -359,7 +429,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) } }; @@ -440,6 +510,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(TransportRestartReason), } /// Used to share public state between credential service and UI. @@ -482,6 +556,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(TransportRestartReason), } impl From for NfcState { @@ -504,6 +582,7 @@ impl From for NfcState { NfcState::NeedsUserVerification { attempts_left } } NfcStateInternal::Completed(_) => NfcState::Completed, + NfcStateInternal::Restarting(reason) => NfcState::Restarting(reason), NfcStateInternal::SelectCredential { response, cred_tx } => { NfcState::SelectingCredential { creds: response @@ -572,19 +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::RequestCancelled) => { + NfcState::Failed(CredentialServiceError::NonTerminatingCancellation) => { BackgroundEvent::ErrorCancelled } - NfcState::Failed(CredentialServiceError::Internal(_)) => BackgroundEvent::ErrorInternal, + 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 a68837e1..a9a8c3a9 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}; @@ -47,7 +49,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 +149,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 { @@ -275,6 +277,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"); @@ -314,10 +317,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()), } }; @@ -330,12 +336,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(()); @@ -352,20 +358,85 @@ impl InProcessUsbHandler { std::mem::discriminant(new_state) != std::mem::discriminant(old_state) } }; - 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(), - ) - })?; + // 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(err) => break Err(err), - _ => {} + 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(), + ) + })?; + } + 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 => { + 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(reason)).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(), + ) + })?; + } + } } } } @@ -467,7 +538,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) } }; @@ -551,6 +622,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(TransportRestartReason), } /// Used to share public state between credential service and UI. @@ -602,6 +677,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(TransportRestartReason), } impl From for UsbState { @@ -659,6 +738,7 @@ impl From for UsbState { } } UsbStateInternal::Failed(err) => UsbState::Failed(err), + UsbStateInternal::Restarting(reason) => UsbState::Restarting(reason), } } } @@ -696,19 +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::RequestCancelled) => { + UsbState::Failed(CredentialServiceError::NonTerminatingCancellation) => { BackgroundEvent::ErrorCancelled } - UsbState::Failed(CredentialServiceError::Internal(_)) => BackgroundEvent::ErrorInternal, + 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 788ad60d..e3ab8a8a 100644 --- a/credentialsd/src/dbus/ui_control.rs +++ b/credentialsd/src/dbus/ui_control.rs @@ -18,14 +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, - NotifyHybridStartedOptions, NotifyNeedsPinOptions, NotifyNeedsUserPresenceOptions, - NotifyNeedsUserVerificationOptions, NotifyNfcConnectedOptions, NotifyPinNotSetOptions, - NotifySelectingCredentialOptions, NotifyUsbConnectedOptions, 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. @@ -148,6 +148,30 @@ trait UiControlService { _options: NotifyUsbConnectedOptions, ) -> fdo::Result<()>; + #[zbus(no_reply)] + async fn notify_hybrid_restarting( + &self, + session_handle: ObjectPath<'_>, + reason: u8, + _options: NotifyHybridRestartingOptions, + ) -> fdo::Result<()>; + + #[zbus(no_reply)] + async fn notify_usb_restarting( + &self, + session_handle: ObjectPath<'_>, + reason: u8, + _options: NotifyUsbRestartingOptions, + ) -> fdo::Result<()>; + + #[zbus(no_reply)] + async fn notify_nfc_restarting( + &self, + session_handle: ObjectPath<'_>, + reason: u8, + _options: NotifyNfcRestartingOptions, + ) -> fdo::Result<()>; + #[zbus(no_reply)] async fn notify_ceremony_completed(&self, session_handle: ObjectPath<'_>) -> fdo::Result<()>; @@ -299,6 +323,33 @@ impl Ceremony { ) .await } + BackgroundEvent::HybridRestarting { reason } => { + self.proxy + .notify_hybrid_restarting( + self.session_handle.as_ref(), + u8::from(reason), + NotifyHybridRestartingOptions {}, + ) + .await + } + BackgroundEvent::UsbRestarting { reason } => { + self.proxy + .notify_usb_restarting( + self.session_handle.as_ref(), + u8::from(reason), + NotifyUsbRestartingOptions {}, + ) + .await + } + BackgroundEvent::NfcRestarting { reason } => { + self.proxy + .notify_nfc_restarting( + self.session_handle.as_ref(), + u8::from(reason), + NotifyNfcRestartingOptions {}, + ) + .await + } BackgroundEvent::ErrorInternal => { let error = BACKGROUND_EVENT_ERROR_INTERNAL; self.proxy @@ -324,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())