diff --git a/Cargo.lock b/Cargo.lock index 0626a7b..80f8df2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1921,7 +1921,7 @@ dependencies = [ [[package]] name = "libwebauthn" version = "0.2.2" -source = "git+https://github.com/linux-credentials/libwebauthn.git?rev=80545bff16c4e89a930221e90d3141a76303b84b#80545bff16c4e89a930221e90d3141a76303b84b" +source = "git+https://github.com/msirringhaus/libwebauthn.git?rev=fc41f140f74ea27da1b4c85d3888ae393afcabda#fc41f140f74ea27da1b4c85d3888ae393afcabda" dependencies = [ "aes", "apdu", diff --git a/credentialsd-common/src/client.rs b/credentialsd-common/src/client.rs index 1bff01d..d12163a 100644 --- a/credentialsd-common/src/client.rs +++ b/credentialsd-common/src/client.rs @@ -22,6 +22,7 @@ pub trait FlowController { Output = Result + Send + 'static>>, ()>, > + Send; fn enter_client_pin(&mut self, pin: String) -> impl Future> + Send; + fn set_device_pin(&mut self, pin: String) -> impl Future> + Send; fn select_credential( &self, credential_id: String, diff --git a/credentialsd-common/src/model.rs b/credentialsd-common/src/model.rs index 7c2519b..4b0130e 100644 --- a/credentialsd-common/src/model.rs +++ b/credentialsd-common/src/model.rs @@ -132,9 +132,11 @@ pub enum ViewUpdate { UsbNeedsPin { attempts_left: Option }, UsbNeedsUserVerification { attempts_left: Option }, UsbNeedsUserPresence, + UsbPinNotSet { error: Option }, NfcNeedsPin { attempts_left: Option }, NfcNeedsUserVerification { attempts_left: Option }, + NfcPinNotSet { error: Option }, HybridNeedsQrCode(String), HybridConnecting, @@ -145,6 +147,35 @@ pub enum ViewUpdate { Failed(String), } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum PinNotSetError { + /// PIN too short + PinTooShort, + /// PIN too long + PinTooLong, + /// PIN violates PinPolicy + PinPolicyViolation, +} + +impl PinNotSetError { + pub fn to_string(&self) -> String { + match self { + PinNotSetError::PinTooShort => String::from("Pin too short"), + PinNotSetError::PinTooLong => String::from("Pin too long"), + PinNotSetError::PinPolicyViolation => String::from("Pin policy violation"), + } + } + + pub fn from_string(error: &str) -> Option { + match error { + "Pin too short" => Some(PinNotSetError::PinTooShort), + "Pin too long" => Some(PinNotSetError::PinTooLong), + "Pin policy violation" => Some(PinNotSetError::PinPolicyViolation), + _ => None, + } + } +} + #[derive(Clone, Debug, Default)] pub enum HybridState { /// Default state, not listening for hybrid transport. @@ -192,6 +223,11 @@ pub enum UsbState { attempts_left: Option, }, + /// The device needs the PIN to be entered. + PinNotSet { + error: Option, + }, + /// The device needs on-device user verification. NeedsUserVerification { attempts_left: Option, @@ -231,6 +267,9 @@ pub enum NfcState { /// The device needs the PIN to be entered. NeedsPin { attempts_left: Option }, + /// The device needs the PIN to be entered. + PinNotSet { error: Option }, + /// The device needs on-device user verification. NeedsUserVerification { attempts_left: Option }, @@ -271,8 +310,6 @@ pub enum Error { /// Note that this is different than exhausting the PIN count that fully /// locks out the device. PinAttemptsExhausted, - /// The RP requires user verification, but the device has no PIN/Biometrics set. - PinNotSet, // 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), @@ -284,7 +321,6 @@ impl Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::AuthenticatorError => f.write_str("AuthenticatorError"), - Self::PinNotSet => f.write_str("PinNotSet"), Self::NoCredentials => f.write_str("NoCredentials"), Self::CredentialExcluded => f.write_str("CredentialExcluded"), Self::PinAttemptsExhausted => f.write_str("PinAttemptsExhausted"), diff --git a/credentialsd-common/src/server.rs b/credentialsd-common/src/server.rs index 8096634..4892cc1 100644 --- a/credentialsd-common/src/server.rs +++ b/credentialsd-common/src/server.rs @@ -11,7 +11,7 @@ use zvariant::{ SerializeDict, Signature, Structure, StructureBuilder, Type, Value, signature::Fields, }; -use crate::model::{BackgroundEvent, Device, Operation, RequestingApplication}; +use crate::model::{BackgroundEvent, Operation, PinNotSetError, RequestingApplication}; const TAG_VALUE_SIGNATURE: &Signature = &Signature::Structure(Fields::Static { fields: &[&Signature::U8, &Signature::Variant], @@ -182,7 +182,6 @@ impl TryFrom<&Value<'_>> for crate::model::Error { let err_code: &str = value.downcast_ref()?; let err = match err_code { "AuthenticatorError" => crate::model::Error::AuthenticatorError, - "PinNotSet" => crate::model::Error::PinNotSet, "NoCredentials" => crate::model::Error::NoCredentials, "CredentialExcluded" => crate::model::Error::CredentialExcluded, "PinAttemptsExhausted" => crate::model::Error::PinAttemptsExhausted, @@ -346,6 +345,21 @@ impl From<&crate::model::UsbState> for Structure<'_> { let value = Value::<'_>::from(error.to_string()); (0x0A, Some(value)) } + crate::model::UsbState::PinNotSet { error } => { + let value = error.as_ref().map(|x| { + Value::<'_>::from({ + let this = &x; + match this { + PinNotSetError::PinTooShort => String::from("Pin too short"), + PinNotSetError::PinTooLong => String::from("Pin too long"), + PinNotSetError::PinPolicyViolation => { + String::from("Pin policy violation") + } + } + }) + }); + (0x0B, value) + } }; tag_value_to_struct(tag, value) } @@ -400,7 +414,6 @@ impl TryFrom<&Structure<'_>> for crate::model::UsbState { let err_code: &str = value.downcast_ref()?; let err = match err_code { "AuthenticatorError" => crate::model::Error::AuthenticatorError, - "PinNotSet" => crate::model::Error::PinNotSet, "NoCredentials" => crate::model::Error::NoCredentials, "CredentialExcluded" => crate::model::Error::CredentialExcluded, "PinAttemptsExhausted" => crate::model::Error::PinAttemptsExhausted, @@ -408,6 +421,13 @@ impl TryFrom<&Structure<'_>> for crate::model::UsbState { }; Ok(Self::Failed(err)) } + 0x0B => { + let error = value + .downcast_ref::<&str>() + .ok() + .and_then(PinNotSetError::from_string); + Ok(Self::PinNotSet { error }) + } _ => Err(zvariant::Error::IncorrectType), } } @@ -471,6 +491,21 @@ impl From<&crate::model::NfcState> for Structure<'_> { let value = Value::<'_>::from(error.to_string()); (0x0A, Some(value)) } + crate::model::NfcState::PinNotSet { error } => { + let value = error.as_ref().map(|x| { + Value::<'_>::from({ + let this = &x; + match this { + PinNotSetError::PinTooShort => String::from("Pin too short"), + PinNotSetError::PinTooLong => String::from("Pin too long"), + PinNotSetError::PinPolicyViolation => { + String::from("Pin policy violation") + } + } + }) + }); + (0x0B, value) + } }; tag_value_to_struct(tag, value) } @@ -523,7 +558,6 @@ impl TryFrom<&Structure<'_>> for crate::model::NfcState { let err_code: &str = value.downcast_ref()?; let err = match err_code { "AuthenticatorError" => crate::model::Error::AuthenticatorError, - "PinNotSet" => crate::model::Error::PinNotSet, "NoCredentials" => crate::model::Error::NoCredentials, "CredentialExcluded" => crate::model::Error::CredentialExcluded, "PinAttemptsExhausted" => crate::model::Error::PinAttemptsExhausted, @@ -531,6 +565,13 @@ impl TryFrom<&Structure<'_>> for crate::model::NfcState { }; Ok(Self::Failed(err)) } + 0x0B => { + let error = value + .downcast_ref::<&str>() + .ok() + .and_then(PinNotSetError::from_string); + Ok(Self::PinNotSet { error }) + } _ => Err(zvariant::Error::IncorrectType), } } diff --git a/credentialsd-ui/data/resources/ui/window.ui b/credentialsd-ui/data/resources/ui/window.ui index 2fe6d17..5959785 100644 --- a/credentialsd-ui/data/resources/ui/window.ui +++ b/credentialsd-ui/data/resources/ui/window.ui @@ -208,6 +208,66 @@ + + + set_new_pin + Set a PIN + + + vertical + + + + Please choose a new PIN for your device. + true + + + + + + New PIN + + + + + + + Confirm PIN + + + + + + + end + 6 + + + Close + + + + + + Continue + + + CredentialsUiWindow + + + + + + + + + + + + + completed @@ -244,6 +304,31 @@ Something went wrong while retrieving a credential. Please try again later or use a different authenticator. + + + end + 6 + + + + CredentialsUiWindow + + + + + + Close + + + + + + Set PIN on device + + + + + diff --git a/credentialsd-ui/po/credentialsd-ui.pot b/credentialsd-ui/po/credentialsd-ui.pot index 6f15d5e..ca2c703 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-02-03 10:40+0100\n" +"POT-Creation-Date: 2026-03-20 11:40+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -21,7 +21,7 @@ msgstr "" #: data/xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in:2 #: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:8 -#: src/gui/view_model/gtk/mod.rs:385 +#: src/gui/view_model/gtk/mod.rs:417 msgid "Credential Manager" msgstr "" @@ -57,7 +57,7 @@ msgid "Registering a credential" msgstr "" #. developer_name tag deprecated with Appstream 1.0 -#: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:34 +#: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:35 msgid "Isaiah Inuwa" msgstr "" @@ -105,66 +105,113 @@ msgid "Choose credential" msgstr "" #: data/resources/ui/window.ui:214 +msgid "Set a PIN" +msgstr "" + +#: data/resources/ui/window.ui:221 +msgid "Please choose a new PIN for your device." +msgstr "" + +#: data/resources/ui/window.ui:228 +msgid "New PIN" +msgstr "" + +#: data/resources/ui/window.ui:235 +msgid "Confirm PIN" +msgstr "" + +#: data/resources/ui/window.ui:246 data/resources/ui/window.ui:320 +msgid "Close" +msgstr "" + +#: data/resources/ui/window.ui:252 +msgid "Continue" +msgstr "" + +#: data/resources/ui/window.ui:274 msgid "Complete" msgstr "" -#: data/resources/ui/window.ui:220 +#: data/resources/ui/window.ui:280 msgid "Done!" msgstr "" -#: data/resources/ui/window.ui:231 +#: data/resources/ui/window.ui:291 msgid "Something went wrong." msgstr "" -#: data/resources/ui/window.ui:244 src/gui/view_model/mod.rs:290 +#: data/resources/ui/window.ui:304 src/gui/view_model/mod.rs:305 +#: src/gui/view_model/mod.rs:362 msgid "" "Something went wrong while retrieving a credential. Please try again later " "or use a different authenticator." msgstr "" -#: src/gui/view_model/gtk/mod.rs:147 +#: data/resources/ui/window.ui:326 +msgid "Set PIN on device" +msgstr "" + +#: src/gui/view_model/gtk/mod.rs:156 msgid "Enter your PIN. One attempt remaining." msgid_plural "Enter your PIN. %d attempts remaining." msgstr[0] "" msgstr[1] "" -#: src/gui/view_model/gtk/mod.rs:153 +#: src/gui/view_model/gtk/mod.rs:162 msgid "Enter your PIN." msgstr "" -#: src/gui/view_model/gtk/mod.rs:163 +#: src/gui/view_model/gtk/mod.rs:172 msgid "Touch your device again. One attempt remaining." msgid_plural "Touch your device again. %d attempts remaining." msgstr[0] "" msgstr[1] "" -#: src/gui/view_model/gtk/mod.rs:169 +#: src/gui/view_model/gtk/mod.rs:178 msgid "Touch your device." msgstr "" -#: src/gui/view_model/gtk/mod.rs:174 +#: src/gui/view_model/gtk/mod.rs:183 msgid "Touch your device" msgstr "" -#: src/gui/view_model/gtk/mod.rs:177 +#: src/gui/view_model/gtk/mod.rs:191 +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 "" + +#: src/gui/view_model/gtk/mod.rs:195 +msgid "" +"The entered PIN violates the PIN-policy of this device (likely too short). " +"Please try again." +msgstr "" + +#: src/gui/view_model/gtk/mod.rs:198 +msgid "" +"The entered PIN violates the PIN-policy of this device (PIN too long). " +"Please try again." +msgstr "" + +#: src/gui/view_model/gtk/mod.rs:205 msgid "Scan the QR code with your device to begin authentication." msgstr "" -#: src/gui/view_model/gtk/mod.rs:187 +#: src/gui/view_model/gtk/mod.rs:215 msgid "" "Connecting to your device. Make sure both devices are near each other and " "have Bluetooth enabled." msgstr "" -#: src/gui/view_model/gtk/mod.rs:195 +#: src/gui/view_model/gtk/mod.rs:223 msgid "Device connected. Follow the instructions on your device" msgstr "" -#: src/gui/view_model/gtk/mod.rs:321 +#: src/gui/view_model/gtk/mod.rs:349 msgid "Insert your security key." msgstr "" -#: src/gui/view_model/gtk/mod.rs:340 +#: src/gui/view_model/gtk/mod.rs:368 msgid "Multiple devices found. Please select with which to proceed." msgstr "" @@ -192,17 +239,17 @@ msgstr "" msgid "A security key (USB)" msgstr "" -#: src/gui/view_model/mod.rs:75 +#: src/gui/view_model/mod.rs:70 msgid "unknown application" msgstr "" #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from -#: src/gui/view_model/mod.rs:80 +#: src/gui/view_model/mod.rs:86 msgid "Create a passkey for %s1" msgstr "" #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from -#: src/gui/view_model/mod.rs:84 +#: src/gui/view_model/mod.rs:90 msgid "Use a passkey for %s1" msgstr "" @@ -210,7 +257,7 @@ msgstr "" #. 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 -#: src/gui/view_model/mod.rs:96 +#: src/gui/view_model/mod.rs:102 msgid "" "\"%s2\" (process ID: %i1, binary: %s3) is asking to create a " "credential to register at \"%s1\". Only proceed if you trust this process." @@ -220,36 +267,30 @@ msgstr "" #. 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 -#: src/gui/view_model/mod.rs:103 +#: src/gui/view_model/mod.rs:109 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." msgstr "" -#: src/gui/view_model/mod.rs:227 +#: src/gui/view_model/mod.rs:239 msgid "Failed to select credential from device." msgstr "" -#: src/gui/view_model/mod.rs:281 +#: src/gui/view_model/mod.rs:299 src/gui/view_model/mod.rs:356 msgid "No matching credentials found on this authenticator." msgstr "" -#: src/gui/view_model/mod.rs:284 +#: src/gui/view_model/mod.rs:302 src/gui/view_model/mod.rs:359 msgid "" "No more PIN attempts allowed. Try removing your device and plugging it back " "in." msgstr "" -#: src/gui/view_model/mod.rs:287 -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 "" - -#: src/gui/view_model/mod.rs:293 +#: src/gui/view_model/mod.rs:308 src/gui/view_model/mod.rs:365 msgid "This credential is already registered on this authenticator." msgstr "" -#: src/gui/view_model/mod.rs:395 +#: src/gui/view_model/mod.rs:413 msgid "Something went wrong. Try again later or use a different authenticator." msgstr "" diff --git a/credentialsd-ui/po/de_DE.po b/credentialsd-ui/po/de_DE.po index 1a846e1..c699f30 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-02-03 10:40+0100\n" +"POT-Creation-Date: 2026-03-20 11:40+0100\n" "PO-Revision-Date: 2025-10-10 14:45+0200\n" "Last-Translator: Martin Sirringhaus \n" "Language: de_DE\n" @@ -14,7 +14,7 @@ msgstr "" #: data/xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in:2 #: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:8 -#: src/gui/view_model/gtk/mod.rs:385 +#: src/gui/view_model/gtk/mod.rs:417 msgid "Credential Manager" msgstr "Zugangsdatenmanager" @@ -93,18 +93,43 @@ msgid "Choose credential" msgstr "Wählen Sie Zugangsdaten aus" #: data/resources/ui/window.ui:214 +msgid "Set a PIN" +msgstr "Neue PIN festlegen" + +#: data/resources/ui/window.ui:221 +msgid "Please choose a new PIN for your device." +msgstr "Bitte geben Sie eine neue PIN für das Gerät ein" + +#: data/resources/ui/window.ui:228 +msgid "New PIN" +msgstr "Neue PIN" + +#: data/resources/ui/window.ui:235 +msgid "Confirm PIN" +msgstr "Neue PIN bestätigen" + +#: data/resources/ui/window.ui:246 data/resources/ui/window.ui:320 +msgid "Close" +msgstr "Schließen" + +#: data/resources/ui/window.ui:252 +msgid "Continue" +msgstr " Weiter" + +#: data/resources/ui/window.ui:274 msgid "Complete" msgstr "Abgeschlossen" -#: data/resources/ui/window.ui:220 +#: data/resources/ui/window.ui:280 msgid "Done!" msgstr "Fertig!" -#: data/resources/ui/window.ui:231 +#: data/resources/ui/window.ui:291 msgid "Something went wrong." msgstr "Etwas ist schief gegangen." -#: data/resources/ui/window.ui:244 src/gui/view_model/mod.rs:290 +#: data/resources/ui/window.ui:304 src/gui/view_model/mod.rs:305 +#: src/gui/view_model/mod.rs:362 msgid "" "Something went wrong while retrieving a credential. Please try again later " "or use a different authenticator." @@ -112,37 +137,67 @@ msgstr "" "Beim Abrufen Ihrer Zugangsdaten ist ein Fehler aufgetreten. Versuchen Sie es " "später wieder, oder verwenden Sie einen anderen Security-Token." -#: src/gui/view_model/gtk/mod.rs:147 +#: data/resources/ui/window.ui:326 +msgid "Set PIN on device" +msgstr "Geräte-PIN festlegen" + +#: src/gui/view_model/gtk/mod.rs:156 #, 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." -#: src/gui/view_model/gtk/mod.rs:153 +#: src/gui/view_model/gtk/mod.rs:162 msgid "Enter your PIN." msgstr "Geben Sie Ihren PIN ein." -#: src/gui/view_model/gtk/mod.rs:163 +#: src/gui/view_model/gtk/mod.rs:172 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." -#: src/gui/view_model/gtk/mod.rs:169 +#: src/gui/view_model/gtk/mod.rs:178 msgid "Touch your device." msgstr "Berühren Sie Ihr Gerät." -#: src/gui/view_model/gtk/mod.rs:174 +#: src/gui/view_model/gtk/mod.rs:183 msgid "Touch your device" msgstr "Berühren Sie Ihr Gerät." -#: src/gui/view_model/gtk/mod.rs:177 +#: src/gui/view_model/gtk/mod.rs:191 +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." + +#: src/gui/view_model/gtk/mod.rs:195 +msgid "" +"The entered PIN violates the PIN-policy of this device (likely too short). " +"Please try again." +msgstr "" +"Der eingegebene PIN entspricht nicht den PIN-Bestimmungen des Geräts (z.B. " +"zu kurz). Bitte versuchen Sie es erneut." + +#: src/gui/view_model/gtk/mod.rs:198 +#, fuzzy +msgid "" +"The entered PIN violates the PIN-policy of this device (PIN too long). " +"Please try again." +msgstr "" +"Der eingegebene PIN entspricht nicht den PIN-Bestimmungen des Geräts (z.B. " +"zu lang). Bitte versuchen Sie es erneut." + +#: src/gui/view_model/gtk/mod.rs:205 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." -#: src/gui/view_model/gtk/mod.rs:187 +#: src/gui/view_model/gtk/mod.rs:215 msgid "" "Connecting to your device. Make sure both devices are near each other and " "have Bluetooth enabled." @@ -150,15 +205,15 @@ msgstr "" "Verbindung zu Ihrem Gerät wird aufgebaut. Stellen Sie sicher, dass beide " "Geräte nah beieinander sind und Bluetooth aktiviert haben." -#: src/gui/view_model/gtk/mod.rs:195 +#: src/gui/view_model/gtk/mod.rs:223 msgid "Device connected. Follow the instructions on your device" msgstr "Verbindung hergestellt. Folgen Sie den Anweisungen auf Ihrem Gerät." -#: src/gui/view_model/gtk/mod.rs:321 +#: src/gui/view_model/gtk/mod.rs:349 msgid "Insert your security key." msgstr "Stecken Sie Ihren Security-Token ein." -#: src/gui/view_model/gtk/mod.rs:340 +#: src/gui/view_model/gtk/mod.rs:368 msgid "Multiple devices found. Please select with which to proceed." msgstr "Mehrere Geräte gefunden. Bitte wählen Sie einen aus, um fortzufahren." @@ -186,17 +241,17 @@ msgstr "Ein NFC-Gerät" msgid "A security key (USB)" msgstr "Ein Security-Token" -#: src/gui/view_model/mod.rs:75 +#: src/gui/view_model/mod.rs:70 msgid "unknown application" msgstr "unbekannter Applikation" #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from -#: src/gui/view_model/mod.rs:80 +#: src/gui/view_model/mod.rs:86 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 -#: src/gui/view_model/mod.rs:84 +#: src/gui/view_model/mod.rs:90 msgid "Use a passkey for %s1" msgstr "Passkey für %s1 abrufen" @@ -204,7 +259,7 @@ msgstr "Passkey für %s1 abrufen" #. 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 -#: src/gui/view_model/mod.rs:96 +#: src/gui/view_model/mod.rs:102 msgid "" "\"%s2\" (process ID: %i1, binary: %s3) is asking to create a " "credential to register at \"%s1\". Only proceed if you trust this process." @@ -217,7 +272,7 @@ msgstr "" #. 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 -#: src/gui/view_model/mod.rs:103 +#: src/gui/view_model/mod.rs:109 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." @@ -226,15 +281,15 @@ msgstr "" "abrufen, um Sie bei \"%s1\" anzumelden. Fahren Sie nur fort, wenn Sie diesem " "Prozess vertrauen." -#: src/gui/view_model/mod.rs:227 +#: src/gui/view_model/mod.rs:239 msgid "Failed to select credential from device." msgstr "Zugangsdaten vom Gerät konnten nicht ausgewählt werden." -#: src/gui/view_model/mod.rs:281 +#: src/gui/view_model/mod.rs:299 src/gui/view_model/mod.rs:356 msgid "No matching credentials found on this authenticator." msgstr "Keine passenden Zugangsdaten auf diesem Gerät gefunden." -#: src/gui/view_model/mod.rs:284 +#: src/gui/view_model/mod.rs:302 src/gui/view_model/mod.rs:359 msgid "" "No more PIN attempts allowed. Try removing your device and plugging it back " "in." @@ -242,19 +297,11 @@ msgstr "" "Keine weiteren PIN-Eingaben erlaubt. Versuchen Sie ihr Gerät aus- und wieder " "einzustecken." -#: src/gui/view_model/mod.rs:287 -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." - -#: src/gui/view_model/mod.rs:293 +#: src/gui/view_model/mod.rs:308 src/gui/view_model/mod.rs:365 msgid "This credential is already registered on this authenticator." msgstr "Diese Zugangsdaten sind bereits auf diesem Gerät registriert." -#: src/gui/view_model/mod.rs:395 +#: src/gui/view_model/mod.rs:413 msgid "Something went wrong. Try again later or use a different authenticator." msgstr "" "Es ist ein Fehler aufgetreten. Bitte versuchen Sie es später noch einmal, " diff --git a/credentialsd-ui/po/en_US.po b/credentialsd-ui/po/en_US.po index bdbfce9..c365447 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-02-03 10:40+0100\n" +"POT-Creation-Date: 2026-03-20 11:40+0100\n" "PO-Revision-Date: 2025-10-10 14:45+0200\n" "Last-Translator: Martin Sirringhaus \n" "Language: en_US\n" @@ -13,7 +13,7 @@ msgstr "" #: data/xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in:2 #: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:8 -#: src/gui/view_model/gtk/mod.rs:385 +#: src/gui/view_model/gtk/mod.rs:417 msgid "Credential Manager" msgstr "Credential Manager" @@ -94,18 +94,43 @@ msgid "Choose credential" msgstr "Choose credential" #: data/resources/ui/window.ui:214 +msgid "Set a PIN" +msgstr "Set a PIN" + +#: data/resources/ui/window.ui:221 +msgid "Please choose a new PIN for your device." +msgstr "Please choose a new PIN for your device." + +#: data/resources/ui/window.ui:228 +msgid "New PIN" +msgstr "New PIN" + +#: data/resources/ui/window.ui:235 +msgid "Confirm PIN" +msgstr "Confirm PIN" + +#: data/resources/ui/window.ui:246 data/resources/ui/window.ui:320 +msgid "Close" +msgstr "Close" + +#: data/resources/ui/window.ui:252 +msgid "Continue" +msgstr "Continue" + +#: data/resources/ui/window.ui:274 msgid "Complete" msgstr "Complete" -#: data/resources/ui/window.ui:220 +#: data/resources/ui/window.ui:280 msgid "Done!" msgstr "Done!" -#: data/resources/ui/window.ui:231 +#: data/resources/ui/window.ui:291 msgid "Something went wrong." msgstr "Something went wrong." -#: data/resources/ui/window.ui:244 src/gui/view_model/mod.rs:290 +#: data/resources/ui/window.ui:304 src/gui/view_model/mod.rs:305 +#: src/gui/view_model/mod.rs:362 msgid "" "Something went wrong while retrieving a credential. Please try again later " "or use a different authenticator." @@ -113,35 +138,64 @@ msgstr "" "Something went wrong while retrieving a credential. Please try again later " "or use a different authenticator." -#: src/gui/view_model/gtk/mod.rs:147 +#: data/resources/ui/window.ui:326 +msgid "Set PIN on device" +msgstr "Set PIN on device" + +#: src/gui/view_model/gtk/mod.rs:156 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." -#: src/gui/view_model/gtk/mod.rs:153 +#: src/gui/view_model/gtk/mod.rs:162 msgid "Enter your PIN." msgstr "Enter your PIN." -#: src/gui/view_model/gtk/mod.rs:163 +#: src/gui/view_model/gtk/mod.rs:172 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." -#: src/gui/view_model/gtk/mod.rs:169 +#: src/gui/view_model/gtk/mod.rs:178 msgid "Touch your device." msgstr "Touch your device." -#: src/gui/view_model/gtk/mod.rs:174 +#: src/gui/view_model/gtk/mod.rs:183 msgid "Touch your device" msgstr "Touch your device" -#: src/gui/view_model/gtk/mod.rs:177 +#: src/gui/view_model/gtk/mod.rs:191 +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." + +#: src/gui/view_model/gtk/mod.rs:195 +msgid "" +"The entered PIN violates the PIN-policy of this device (likely too short). " +"Please try again." +msgstr "" +"The entered PIN violates the PIN-policy of this device (likely too short). " +"Please try again." + +#: src/gui/view_model/gtk/mod.rs:198 +#, fuzzy +msgid "" +"The entered PIN violates the PIN-policy of this device (PIN too long). " +"Please try again." +msgstr "" +"The entered PIN violates the PIN-policy of this device (likely too long). " +"Please try again." + +#: src/gui/view_model/gtk/mod.rs:205 msgid "Scan the QR code with your device to begin authentication." msgstr "Scan the QR code with your device to begin authentication." -#: src/gui/view_model/gtk/mod.rs:187 +#: src/gui/view_model/gtk/mod.rs:215 msgid "" "Connecting to your device. Make sure both devices are near each other and " "have Bluetooth enabled." @@ -149,15 +203,15 @@ msgstr "" "Connecting to your device. Make sure both devices are near each other and " "have Bluetooth enabled." -#: src/gui/view_model/gtk/mod.rs:195 +#: src/gui/view_model/gtk/mod.rs:223 msgid "Device connected. Follow the instructions on your device" msgstr "Device connected. Follow the instructions on your device" -#: src/gui/view_model/gtk/mod.rs:321 +#: src/gui/view_model/gtk/mod.rs:349 msgid "Insert your security key." msgstr "Insert your security key." -#: src/gui/view_model/gtk/mod.rs:340 +#: src/gui/view_model/gtk/mod.rs:368 msgid "Multiple devices found. Please select with which to proceed." msgstr "Multiple devices found. Please select with which to proceed." @@ -185,17 +239,17 @@ msgstr "A security key or card (NFC)" msgid "A security key (USB)" msgstr "A security key (USB)" -#: src/gui/view_model/mod.rs:75 +#: src/gui/view_model/mod.rs:70 msgid "unknown application" msgstr "unknown application" #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from -#: src/gui/view_model/mod.rs:80 +#: src/gui/view_model/mod.rs:86 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 -#: src/gui/view_model/mod.rs:84 +#: src/gui/view_model/mod.rs:90 msgid "Use a passkey for %s1" msgstr "Use a passkey for %s1" @@ -203,7 +257,7 @@ msgstr "Use a passkey for %s1" #. 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 -#: src/gui/view_model/mod.rs:96 +#: src/gui/view_model/mod.rs:102 msgid "" "\"%s2\" (process ID: %i1, binary: %s3) is asking to create a " "credential to register at \"%s1\". Only proceed if you trust this process." @@ -215,7 +269,7 @@ msgstr "" #. 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 -#: src/gui/view_model/mod.rs:103 +#: src/gui/view_model/mod.rs:109 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." @@ -223,15 +277,15 @@ 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." -#: src/gui/view_model/mod.rs:227 +#: src/gui/view_model/mod.rs:239 msgid "Failed to select credential from device." msgstr "Failed to select credential from device." -#: src/gui/view_model/mod.rs:281 +#: src/gui/view_model/mod.rs:299 src/gui/view_model/mod.rs:356 msgid "No matching credentials found on this authenticator." msgstr "No matching credentials found on this authenticator." -#: src/gui/view_model/mod.rs:284 +#: src/gui/view_model/mod.rs:302 src/gui/view_model/mod.rs:359 msgid "" "No more PIN attempts allowed. Try removing your device and plugging it back " "in." @@ -239,19 +293,11 @@ msgstr "" "No more PIN attempts allowed. Try removing your device and plugging it back " "in." -#: src/gui/view_model/mod.rs:287 -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." - -#: src/gui/view_model/mod.rs:293 +#: src/gui/view_model/mod.rs:308 src/gui/view_model/mod.rs:365 msgid "This credential is already registered on this authenticator." msgstr "This credential is already registered on this authenticator." -#: src/gui/view_model/mod.rs:395 +#: src/gui/view_model/mod.rs:413 msgid "Something went wrong. Try again later or use a different authenticator." msgstr "" "Something went wrong. Try again later or use a different authenticator." diff --git a/credentialsd-ui/src/client.rs b/credentialsd-ui/src/client.rs index 0f2184d..c7d4c6c 100644 --- a/credentialsd-ui/src/client.rs +++ b/credentialsd-ui/src/client.rs @@ -97,6 +97,14 @@ impl FlowController for DbusCredentialClient { .map_err(|err| tracing::error!("Failed to send PIN to authenticator: {err}")) } + async fn set_device_pin(&mut self, pin: String) -> std::result::Result<(), ()> { + self.proxy() + .await? + .set_device_pin(pin) + .await + .map_err(|err| tracing::error!("Failed to set new PIN for authenticator: {err}")) + } + async fn select_credential(&self, credential_id: String) -> std::result::Result<(), ()> { self.proxy() .await? diff --git a/credentialsd-ui/src/dbus.rs b/credentialsd-ui/src/dbus.rs index 9ab511b..2293761 100644 --- a/credentialsd-ui/src/dbus.rs +++ b/credentialsd-ui/src/dbus.rs @@ -26,6 +26,7 @@ pub trait FlowControlService { async fn select_credential(&self, credential_id: String) -> fdo::Result<()>; async fn cancel_request(&self, request_id: RequestId) -> fdo::Result<()>; + async fn set_device_pin(&self, pin: String) -> fdo::Result<()>; #[zbus(signal)] async fn state_changed(update: BackgroundEvent) -> zbus::Result<()>; } diff --git a/credentialsd-ui/src/gui/view_model/gtk/mod.rs b/credentialsd-ui/src/gui/view_model/gtk/mod.rs index f82afcb..4834b26 100644 --- a/credentialsd-ui/src/gui/view_model/gtk/mod.rs +++ b/credentialsd-ui/src/gui/view_model/gtk/mod.rs @@ -4,6 +4,7 @@ pub mod device; mod window; use async_std::channel::{Receiver, Sender}; +use credentialsd_common::model::PinNotSetError; use credentialsd_common::server::WindowHandle; use gettextrs::{LocaleCategory, gettext, ngettext}; use glib::clone; @@ -77,6 +78,12 @@ mod imp { #[property(get, set)] pub qr_spinner_visible: RefCell, + + #[property(get, set)] + pub start_setting_new_pin_visible: RefCell, + + #[property(get, set)] + pub pin_fields_match: RefCell, } // The central trait for subclassing a GObject @@ -125,6 +132,8 @@ impl ViewModel { Ok(update) => { // 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_failed(false); match update { ViewUpdate::SetTitle((title, subtitle)) => { view_model.set_title(title); @@ -173,6 +182,25 @@ impl ViewModel { ViewUpdate::UsbNeedsUserPresence => { view_model.set_prompt(gettext("Touch your device")); } + ViewUpdate::UsbPinNotSet { error } + | ViewUpdate::NfcPinNotSet { error } => { + view_model.set_failed(true); + view_model.set_start_setting_new_pin_visible(true); + let text = match error { + None => 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.", + ), + Some(PinNotSetError::PinTooShort) + | Some(PinNotSetError::PinPolicyViolation) => gettext( + "The entered PIN violates the PIN-policy of this device (likely too short). Please try again.", + ), + Some(PinNotSetError::PinTooLong) => gettext( + "The entered PIN violates the PIN-policy of this device (PIN too long). Please try again.", + ), + }; + // These are already gettext messages + view_model.set_prompt(text); + } ViewUpdate::HybridNeedsQrCode(qr_code) => { view_model.set_prompt(gettext("Scan the QR code with your device to begin authentication.")); let texture = view_model.draw_qr_code(&qr_code); @@ -200,11 +228,11 @@ impl ViewModel { view_model.set_qr_spinner_visible(false); view_model.set_completed(true); } - ViewUpdate::Failed(error_msg) => { + ViewUpdate::Failed(error) => { view_model.set_qr_spinner_visible(false); view_model.set_failed(true); // These are already gettext messages - view_model.set_prompt(error_msg); + view_model.set_prompt(error); } ViewUpdate::Cancelled => { view_model.set_state(ModelState::Cancelled) @@ -345,6 +373,10 @@ impl ViewModel { self.send_event(ViewEvent::PinEntered(pin)).await; } + pub async fn send_set_new_device_pin(&self, pin: String) { + self.send_event(ViewEvent::SetNewDevicePin(pin)).await; + } + fn draw_qr_code(&self, qr_data: &str) -> Texture { let qr_code = QrCode::new(qr_data).expect("QR code to be valid"); let svg_xml = qr_code.render::().build(); diff --git a/credentialsd-ui/src/gui/view_model/gtk/window.rs b/credentialsd-ui/src/gui/view_model/gtk/window.rs index 642ca73..57feb1a 100644 --- a/credentialsd-ui/src/gui/view_model/gtk/window.rs +++ b/credentialsd-ui/src/gui/view_model/gtk/window.rs @@ -34,6 +34,15 @@ mod imp { #[template_child] pub usb_nfc_pin_entry: TemplateChild, + #[template_child] + pub new_pin_primary_entry: TemplateChild, + + #[template_child] + pub new_pin_confirm_entry: TemplateChild, + + #[template_child] + pub new_pin_btn_continue: TemplateChild, + #[template_child] pub qr_code_pic: TemplateChild, } @@ -53,6 +62,42 @@ mod imp { } )); } + + #[template_callback] + fn handle_start_setting_new_pin(&self) { + let view_model = &self.view_model.borrow(); + let view_model = view_model.as_ref().unwrap(); + // This triggers visibility of the new pin stackpage + view_model.set_pin_fields_match(false); + } + + #[template_callback] + fn handle_setting_pin_change(&self) { + let pin1 = self.new_pin_primary_entry.text(); + let pin2 = self.new_pin_confirm_entry.text(); + let is_valid = !pin1.is_empty() && pin1 == pin2; + // Unlock Button if both entries match (and are non-empty) + self.new_pin_btn_continue.set_sensitive(is_valid); + } + + #[template_callback] + fn handle_close_window(&self) { + self.close_request(); + } + + #[template_callback] + fn handle_commit_new_pin(&self) { + let view_model = &self.view_model.borrow(); + let view_model = view_model.as_ref().unwrap(); + let pin = self.new_pin_primary_entry.text().to_string(); + glib::spawn_future_local(clone!( + #[weak] + view_model, + async move { + view_model.send_set_new_device_pin(pin).await; + } + )); + } } impl Default for CredentialsUiWindow { @@ -64,6 +109,9 @@ mod imp { stack: TemplateChild::default(), usb_nfc_pin_entry: TemplateChild::default(), qr_code_pic: TemplateChild::default(), + new_pin_primary_entry: TemplateChild::default(), + new_pin_confirm_entry: TemplateChild::default(), + new_pin_btn_continue: TemplateChild::default(), } } } @@ -204,6 +252,25 @@ impl CredentialsUiWindow { stack.set_visible_child_name("choose_credential"); } )); + + view_model.connect_usb_nfc_pin_entry_visible_notify(clone!( + #[weak] + stack, + move |vm| { + // If the entry becomes visible, we definitely need to be on the PIN page + if vm.usb_nfc_pin_entry_visible() { + stack.set_visible_child_name("usb_or_nfc"); + } + } + )); + + view_model.connect_pin_fields_match_notify(clone!( + #[weak] + stack, + move |_vm| { + stack.set_visible_child_name("set_new_pin"); + } + )); } fn save_window_size(&self) -> Result<(), glib::BoolError> { diff --git a/credentialsd-ui/src/gui/view_model/mod.rs b/credentialsd-ui/src/gui/view_model/mod.rs index d3113ef..197c5bb 100644 --- a/credentialsd-ui/src/gui/view_model/mod.rs +++ b/credentialsd-ui/src/gui/view_model/mod.rs @@ -213,6 +213,12 @@ impl ViewModel { error!("Failed to send pin to device"); } } + Event::View(ViewEvent::SetNewDevicePin(pin)) => { + let mut cred_service = self.flow_controller.lock().await; + if cred_service.set_device_pin(pin).await.is_err() { + error!("Failed to send new pin to device"); + } + } Event::View(ViewEvent::CredentialSelected(cred_id)) => { println!( "Credential selected: {:?}. Current Device: {:?}", @@ -252,6 +258,12 @@ impl ViewModel { .await .unwrap(); } + UsbState::PinNotSet { error } => { + self.tx_update + .send(ViewUpdate::UsbPinNotSet { error }) + .await + .unwrap(); + } UsbState::NeedsUserVerification { attempts_left } => { self.tx_update .send(ViewUpdate::UsbNeedsUserVerification { attempts_left }) @@ -289,9 +301,6 @@ impl ViewModel { Error::PinAttemptsExhausted => gettext( "No more PIN attempts allowed. Try removing your device and plugging it back in.", ), - Error::PinNotSet => gettext( - "This server requires your device to have additional protection like a PIN, which is not set. Please set a PIN for this device and try again.", - ), Error::AuthenticatorError | Error::Internal(_) => gettext( "Something went wrong while retrieving a credential. Please try again later or use a different authenticator.", ), @@ -324,6 +333,12 @@ impl ViewModel { .await .unwrap(); } + NfcState::PinNotSet { error } => { + self.tx_update + .send(ViewUpdate::NfcPinNotSet { error }) + .await + .unwrap(); + } NfcState::Completed => { self.tx_update.send(ViewUpdate::Completed).await.unwrap(); } @@ -336,23 +351,20 @@ impl ViewModel { } // TODO: Provide more specific error messages using the wrapped Error. NfcState::Failed(err) => { - let error_msg = String::from(match err { + let error_msg = match err { Error::NoCredentials => { - "No matching credentials found on this authenticator." - } - Error::PinAttemptsExhausted => { - "No more PIN attempts allowed. Try removing your device and plugging it back in." - } - Error::PinNotSet => { - "This server requires your device to have additional protection like a PIN, which is not set. Please set a PIN for this device and try again." - } - Error::AuthenticatorError | Error::Internal(_) => { - "Something went wrong while retrieving a credential. Please try again later or use a different authenticator." - } - Error::CredentialExcluded => { - "This credential is already registered on this authenticator." + gettext("No matching credentials found on this authenticator.") } - }); + Error::PinAttemptsExhausted => gettext( + "No more PIN attempts allowed. Try removing your device and plugging it back in.", + ), + Error::AuthenticatorError | Error::Internal(_) => gettext( + "Something went wrong while retrieving a credential. Please try again later or use a different authenticator.", + ), + Error::CredentialExcluded => gettext( + "This credential is already registered on this authenticator.", + ), + }; self.tx_update .send(ViewUpdate::Failed(error_msg)) .await @@ -417,6 +429,7 @@ pub enum ViewEvent { DeviceSelected(String), CredentialSelected(String), PinEntered(String), + SetNewDevicePin(String), UserCancelled, } diff --git a/credentialsd/Cargo.toml b/credentialsd/Cargo.toml index 07b3d04..3c00343 100644 --- a/credentialsd/Cargo.toml +++ b/credentialsd/Cargo.toml @@ -11,7 +11,7 @@ async-trait = "0.1.89" base64 = "0.22.1" credentialsd-common = { path = "../credentialsd-common" } futures-lite.workspace = true -libwebauthn = { git = "https://github.com/linux-credentials/libwebauthn.git", rev="80545bff16c4e89a930221e90d3141a76303b84b", features = ["libnfc","pcsc"] } +libwebauthn = { git = "https://github.com/msirringhaus/libwebauthn.git", rev="fc41f140f74ea27da1b4c85d3888ae393afcabda", features = ["libnfc","pcsc"] } # TODO: split nfc and pcsc into separate features # Also, 0.6.1 fails to build with non-vendored library. # https://github.com/alexrsagen/rs-nfc1/issues/15 diff --git a/credentialsd/src/credential_service/nfc.rs b/credentialsd/src/credential_service/nfc.rs index 51b8a75..d6040b6 100644 --- a/credentialsd/src/credential_service/nfc.rs +++ b/credentialsd/src/credential_service/nfc.rs @@ -5,6 +5,7 @@ use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use futures_lite::Stream; use libwebauthn::{ ops::webauthn::GetAssertionResponse, + pin::PinNotSetReason, proto::CtapError, transport::{nfc::device::NfcDevice, Channel, Device}, webauthn::{Error as WebAuthnError, WebAuthn}, @@ -14,7 +15,7 @@ use tokio::sync::broadcast; use tokio::sync::mpsc::{self, Receiver, Sender, WeakSender}; use tracing::{debug, warn}; -use credentialsd_common::model::{Credential, Error}; +use credentialsd_common::model::{Credential, Error, PinNotSetError}; use crate::model::{CredentialRequest, GetAssertionResponseInternal}; @@ -114,6 +115,9 @@ impl InProcessNfcHandler { attempts_left, pin_tx, }), + Ok(NfcUvMessage::PinNotSet { reason, pin_tx }) => { + Ok(NfcStateInternal::PinNotSet { reason, pin_tx }) + } Ok(NfcUvMessage::NeedsUserVerification { attempts_left }) => { Ok(NfcStateInternal::NeedsUserVerification { attempts_left }) } @@ -174,6 +178,7 @@ impl InProcessNfcHandler { Self::process_user_interaction(&mut signal_rx, &cred_tx).await } NfcStateInternal::NeedsPin { .. } + | NfcStateInternal::PinNotSet { .. } | NfcStateInternal::NeedsUserVerification { .. } => { Self::process_user_interaction(&mut signal_rx, &cred_tx).await } @@ -252,7 +257,6 @@ async fn handle_events( } .map_err(|err| match err { WebAuthnError::Ctap(CtapError::PINAuthBlocked) => Error::PinAttemptsExhausted, - WebAuthnError::Ctap(CtapError::PINNotSet) => Error::PinNotSet, WebAuthnError::Ctap(CtapError::NoCredentials) => Error::NoCredentials, WebAuthnError::Ctap(CtapError::CredentialExcluded) => Error::CredentialExcluded, _ => Error::AuthenticatorError, @@ -311,6 +315,12 @@ pub(super) enum NfcStateInternal { pin_tx: mpsc::Sender, }, + /// The device needs the PIN to be set. + PinNotSet { + reason: PinNotSetReason, + pin_tx: mpsc::Sender, + }, + /// The device needs on-device user verification. NeedsUserVerification { attempts_left: Option }, @@ -349,6 +359,12 @@ pub enum NfcState { pin_tx: mpsc::Sender, }, + /// The device needs the PIN to be set. + PinNotSet { + reason: PinNotSetReason, + pin_tx: mpsc::Sender, + }, + /// The device needs on-device user verification. NeedsUserVerification { attempts_left: Option }, // TODO: implement cancellation @@ -382,6 +398,9 @@ impl From for NfcState { attempts_left, pin_tx, }, + NfcStateInternal::PinNotSet { reason, pin_tx } => { + NfcState::PinNotSet { reason, pin_tx } + } NfcStateInternal::NeedsUserVerification { attempts_left } => { NfcState::NeedsUserVerification { attempts_left } } @@ -440,6 +459,15 @@ impl From<&NfcState> for credentialsd_common::model::NfcState { attempts_left: *attempts_left, } } + NfcState::PinNotSet { reason, .. } => { + let error = match reason { + PinNotSetReason::PinNotSet => None, + PinNotSetReason::PinTooShort => Some(PinNotSetError::PinTooShort), + PinNotSetReason::PinTooLong => Some(PinNotSetError::PinTooLong), + PinNotSetReason::PinPolicyViolation => Some(PinNotSetError::PinPolicyViolation), + }; + credentialsd_common::model::NfcState::PinNotSet { error } + } NfcState::NeedsUserVerification { attempts_left } => { credentialsd_common::model::NfcState::NeedsUserVerification { attempts_left: *attempts_left, @@ -493,6 +521,25 @@ async fn handle_nfc_updates( None => tracing::debug!("Pin channel closed before receiving pin from client."), } } + UvUpdate::PinNotSet(pin_update) => { + let (pin_tx, mut pin_rx) = mpsc::channel(1); + if let Err(err) = signal_tx + .send(Ok(NfcUvMessage::PinNotSet { + pin_tx, + reason: pin_update.reason, + })) + .await + { + tracing::error!("Authenticator requested a PIN from the user, but we cannot relay the message to the credential service: {:?}", err); + } + match pin_rx.recv().await { + Some(pin) => match pin_update.set_pin(&pin) { + Ok(()) => {} + Err(err) => tracing::error!("Error sending pin to device: {:?}", err), + }, + None => tracing::debug!("Pin channel closed before receiving pin from client."), + } + } UvUpdate::PresenceRequired => { tracing::debug!("Authenticator requested user presence, but that makes no sense for NFC. Skipping"); } @@ -507,6 +554,10 @@ enum NfcUvMessage { attempts_left: Option, pin_tx: mpsc::Sender, }, + PinNotSet { + reason: PinNotSetReason, + pin_tx: mpsc::Sender, + }, NeedsUserVerification { attempts_left: Option, }, diff --git a/credentialsd/src/credential_service/usb.rs b/credentialsd/src/credential_service/usb.rs index f64c302..2fe87b8 100644 --- a/credentialsd/src/credential_service/usb.rs +++ b/credentialsd/src/credential_service/usb.rs @@ -5,6 +5,7 @@ use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use futures_lite::Stream; use libwebauthn::{ ops::webauthn::GetAssertionResponse, + pin::PinNotSetReason, proto::CtapError, transport::{ hid::{channel::HidChannelHandle, HidDevice}, @@ -17,7 +18,7 @@ use tokio::sync::broadcast; use tokio::sync::mpsc::{self, Receiver, Sender, WeakSender}; use tracing::{debug, warn}; -use credentialsd_common::model::{Credential, Error}; +use credentialsd_common::model::{Credential, Error, PinNotSetError}; use crate::model::{CredentialRequest, GetAssertionResponseInternal}; @@ -199,6 +200,9 @@ impl InProcessUsbHandler { attempts_left, pin_tx, }), + Ok(UsbUvMessage::PinNotSet { reason, pin_tx }) => { + Ok(UsbStateInternal::PinNotSet { reason, pin_tx }) + } Ok(UsbUvMessage::NeedsUserVerification { attempts_left }) => { Ok(UsbStateInternal::NeedsUserVerification { attempts_left }) } @@ -263,6 +267,7 @@ impl InProcessUsbHandler { Self::process_user_interaction(&mut signal_rx, &cred_tx).await } UsbStateInternal::NeedsPin { .. } + | UsbStateInternal::PinNotSet { .. } | UsbStateInternal::NeedsUserVerification { .. } | UsbStateInternal::NeedsUserPresence => { Self::process_user_interaction(&mut signal_rx, &cred_tx).await @@ -342,7 +347,6 @@ async fn handle_events( } .map_err(|err| match err { WebAuthnError::Ctap(CtapError::PINAuthBlocked) => Error::PinAttemptsExhausted, - WebAuthnError::Ctap(CtapError::PINNotSet) => Error::PinNotSet, WebAuthnError::Ctap(CtapError::NoCredentials) => Error::NoCredentials, WebAuthnError::Ctap(CtapError::CredentialExcluded) => Error::CredentialExcluded, _ => Error::AuthenticatorError, @@ -405,6 +409,12 @@ pub(super) enum UsbStateInternal { pin_tx: mpsc::Sender, }, + /// The device needs the PIN to be entered. + PinNotSet { + reason: PinNotSetReason, + pin_tx: mpsc::Sender, + }, + /// The device needs on-device user verification. NeedsUserVerification { attempts_left: Option }, @@ -450,6 +460,12 @@ pub enum UsbState { pin_tx: mpsc::Sender, }, + /// The device needs the PIN to be set. + PinNotSet { + reason: PinNotSetReason, + pin_tx: mpsc::Sender, + }, + /// The device needs on-device user verification. NeedsUserVerification { attempts_left: Option, @@ -488,6 +504,9 @@ impl From for UsbState { attempts_left, pin_tx, }, + UsbStateInternal::PinNotSet { reason, pin_tx } => { + UsbState::PinNotSet { reason, pin_tx } + } UsbStateInternal::NeedsUserVerification { attempts_left } => { UsbState::NeedsUserVerification { attempts_left } } @@ -549,6 +568,15 @@ impl From<&UsbState> for credentialsd_common::model::UsbState { attempts_left: *attempts_left, } } + UsbState::PinNotSet { reason, .. } => { + let error = match reason { + PinNotSetReason::PinNotSet => None, + PinNotSetReason::PinTooShort => Some(PinNotSetError::PinTooShort), + PinNotSetReason::PinTooLong => Some(PinNotSetError::PinTooLong), + PinNotSetReason::PinPolicyViolation => Some(PinNotSetError::PinPolicyViolation), + }; + credentialsd_common::model::UsbState::PinNotSet { error } + } UsbState::NeedsUserVerification { attempts_left } => { credentialsd_common::model::UsbState::NeedsUserVerification { attempts_left: *attempts_left, @@ -603,6 +631,25 @@ async fn handle_usb_updates( None => tracing::debug!("Pin channel closed before receiving pin from client."), } } + UvUpdate::PinNotSet(pin_update) => { + let (pin_tx, mut pin_rx) = mpsc::channel(1); + if let Err(err) = signal_tx + .send(Ok(UsbUvMessage::PinNotSet { + reason: pin_update.reason, + pin_tx, + })) + .await + { + tracing::error!("Authenticator requested a PIN from the user, but we cannot relay the message to the credential service: {:?}", err); + } + match pin_rx.recv().await { + Some(pin) => match pin_update.set_pin(&pin) { + Ok(()) => {} + Err(err) => tracing::error!("Error sending pin to device: {:?}", err), + }, + None => tracing::debug!("Pin channel closed before receiving pin from client."), + } + } UvUpdate::PresenceRequired => { if let Err(err) = signal_tx.send(Ok(UsbUvMessage::NeedsUserPresence)).await { tracing::error!("Authenticator requested user presence, but we cannot relay the message to the credential service: {:?}", err); @@ -619,6 +666,10 @@ enum UsbUvMessage { attempts_left: Option, pin_tx: mpsc::Sender, }, + PinNotSet { + reason: PinNotSetReason, + pin_tx: mpsc::Sender, + }, NeedsUserVerification { attempts_left: Option, }, diff --git a/credentialsd/src/dbus/flow_control.rs b/credentialsd/src/dbus/flow_control.rs index a6ed555..0ec58fa 100644 --- a/credentialsd/src/dbus/flow_control.rs +++ b/credentialsd/src/dbus/flow_control.rs @@ -63,6 +63,7 @@ pub async fn start_flow_control_service< signal_state: Arc::new(AsyncMutex::new(SignalState::Idle)), svc, pin_tx: Arc::new(AsyncMutex::new(None)), + set_pin_tx: Arc::new(AsyncMutex::new(None)), cred_tx: Arc::new(AsyncMutex::new(None)), usb_event_forwarder_task: Arc::new(AsyncMutex::new(None)), nfc_event_forwarder_task: Arc::new(AsyncMutex::new(None)), @@ -88,6 +89,7 @@ struct FlowControlService>, svc: Arc>>, pin_tx: Arc>>>, + set_pin_tx: Arc>>>, cred_tx: Arc>>>, usb_event_forwarder_task: Arc>>, nfc_event_forwarder_task: Arc>>, @@ -191,6 +193,7 @@ where ) -> fdo::Result<()> { let mut stream = self.svc.lock().await.get_usb_credential(); let usb_pin_tx = self.pin_tx.clone(); + let usb_set_pin_tx = self.set_pin_tx.clone(); let usb_cred_tx = self.cred_tx.clone(); let signal_state = self.signal_state.clone(); let object_server = object_server.clone(); @@ -217,6 +220,10 @@ where let mut usb_pin_tx = usb_pin_tx.lock().await; let _ = usb_pin_tx.insert(pin_tx); } + UsbState::PinNotSet { pin_tx, .. } => { + let mut usb_set_pin_tx = usb_set_pin_tx.lock().await; + let _ = usb_set_pin_tx.insert(pin_tx); + } UsbState::SelectingCredential { cred_tx, .. } => { let mut usb_cred_tx = usb_cred_tx.lock().await; let _ = usb_cred_tx.insert(cred_tx); @@ -292,6 +299,17 @@ where Ok(()) } + async fn set_device_pin( + &self, + pin: String, + #[zbus(object_server)] object_server: &ObjectServer, + ) -> fdo::Result<()> { + if let Some(set_pin_tx) = self.set_pin_tx.lock().await.take() { + set_pin_tx.send(pin).await.unwrap(); + } + Ok(()) + } + async fn select_credential(&self, credential_id: String) -> fdo::Result<()> { if let Some(cred_tx) = self.cred_tx.lock().await.take() { cred_tx.send(credential_id).await.unwrap(); @@ -419,6 +437,7 @@ pub mod test { #[allow(clippy::enum_variant_names)] #[derive(Debug)] pub enum DummyFlowRequest { + SetDevicePin(String), EnterClientPin(String), GetDevices, GetHybridCredential, @@ -431,6 +450,7 @@ pub mod test { // intentional for now. #[allow(clippy::enum_variant_names)] pub enum DummyFlowResponse { + SetNewDevicePin(Result<(), ()>), EnterClientPin(Result<(), ()>), GetDevices(Vec), GetHybridCredential, @@ -442,6 +462,9 @@ pub mod test { impl Debug for DummyFlowResponse { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + Self::SetNewDevicePin(arg0) => { + f.debug_tuple("SetNewDevicePin").field(arg0).finish() + } Self::EnterClientPin(arg0) => f.debug_tuple("EnterClientPin").field(arg0).finish(), Self::GetDevices(arg0) => f.debug_tuple("GetDevices").field(arg0).finish(), Self::GetHybridCredential => f.debug_tuple("GetHybridCredential").finish(), @@ -534,6 +557,16 @@ pub mod test { } } + async fn set_device_pin(&mut self, pin: String) -> Result<(), ()> { + if let Ok(DummyFlowResponse::SetNewDevicePin(Ok(()))) = + self.send(DummyFlowRequest::SetDevicePin(pin)).await + { + Ok(()) + } else { + Err(()) + } + } + async fn select_credential(&self, _credential_id: String) -> Result<(), ()> { todo!(); } @@ -555,6 +588,7 @@ pub mod test { svc: Arc>>, bg_event_tx: Option>, pin_tx: Arc>>>, + set_pin_tx: Arc>>>, usb_event_forwarder_task: Arc>>, nfc_event_forwarder_task: Arc>>, hybrid_event_forwarder_task: Arc>>, @@ -596,6 +630,7 @@ pub mod test { svc, bg_event_tx: None, pin_tx: Arc::new(AsyncMutex::new(None)), + set_pin_tx: Arc::new(AsyncMutex::new(None)), usb_event_forwarder_task: Arc::new(Mutex::new(None)), nfc_event_forwarder_task: Arc::new(Mutex::new(None)), hybrid_event_forwarder_task: Arc::new(Mutex::new(None)), @@ -612,6 +647,10 @@ pub mod test { let rsp = self.enter_client_pin(pin).await; DummyFlowResponse::EnterClientPin(rsp) } + DummyFlowRequest::SetDevicePin(pin) => { + let rsp = self.set_device_pin(pin).await; + DummyFlowResponse::SetNewDevicePin(rsp) + } DummyFlowRequest::GetDevices => { let rsp = self.get_available_public_key_devices().await.unwrap(); DummyFlowResponse::GetDevices(rsp) @@ -697,6 +736,7 @@ pub mod test { let mut stream = self.svc.lock().await.get_usb_credential(); if let Some(tx_weak) = self.bg_event_tx.as_ref().map(|t| t.clone().downgrade()) { let usb_pin_tx = self.pin_tx.clone(); + let usb_set_pin_tx = self.set_pin_tx.clone(); let task = tokio::spawn(async move { while let Some(state) = stream.next().await { if let Some(tx) = tx_weak.upgrade() { @@ -713,6 +753,10 @@ pub mod test { let mut usb_pin_tx = usb_pin_tx.lock().await; let _ = usb_pin_tx.insert(pin_tx); } + UsbState::PinNotSet { pin_tx, .. } => { + let mut usb_set_pin_tx = usb_set_pin_tx.lock().await; + let _ = usb_set_pin_tx.insert(pin_tx); + } UsbState::Completed | UsbState::Failed(_) => { break; } @@ -793,6 +837,13 @@ pub mod test { Ok(()) } + async fn set_device_pin(&self, pin: String) -> Result<(), ()> { + if let Some(set_pin_tx) = self.set_pin_tx.lock().await.take() { + set_pin_tx.send(pin).await.unwrap(); + } + Ok(()) + } + async fn select_credential(&self, _credential_id: String) -> Result<(), ()> { todo!(); } diff --git a/credentialsd/src/webauthn.rs b/credentialsd/src/webauthn.rs index 905fccf..87c3fa3 100644 --- a/credentialsd/src/webauthn.rs +++ b/credentialsd/src/webauthn.rs @@ -682,6 +682,7 @@ pub fn format_client_data_json( let op_str = match op { Operation::Create => "webauthn.create", Operation::Get => "webauthn.get", + _ => unreachable!(), }; let mut client_data_json = format!( r#"{{"type":"{}","challenge":"{}","origin":"{}""#,