diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ca8ab14..2a25e89 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -192,3 +192,25 @@ formatting and linting tools [mentioned above](#code-formatting-and-linting). You should also follow the install instructions in [`BUILDING.md`](/BUILDING.md) and execute authentication flows in a browser to ensure that everything still works as it should. + +# Translations + +credentialsd-ui is using [gettext-rs](https://github.com/gettext-rs/gettext-rs) to translate user-facing strings. + +Please wrap all user-facing messages in `gettext("my string")`-calls and add the files you add them to, to `credentialsd-ui/po/POTFILES`. + +If you introduce a new language, also add them to `credentialsd-ui/po/LINGUAS`. + +Then `cd` into your build-directory (e.g. `build/`) and run + +``` + # To update the POT template file, in case new strings have been added in the sources + meson compile credentialsd-ui-pot + # and to update the individual language files + meson compile credentialsd-ui-update-po +``` +to update the template, so it contains all messages to be translated. + +Meson should take care of building the translations. + +When using the development-profile to build, meson will use the locally built translations. diff --git a/credentialsd-common/src/model.rs b/credentialsd-common/src/model.rs index b3cdd20..d345131 100644 --- a/credentialsd-common/src/model.rs +++ b/credentialsd-common/src/model.rs @@ -1,4 +1,4 @@ -use std::fmt::Display; +use std::{fmt::Display, path::PathBuf}; use serde::{Deserialize, Serialize}; use zvariant::{SerializeDict, Type}; @@ -170,9 +170,22 @@ impl Transport { } } +#[derive(Debug, Default, Clone, Serialize, Deserialize, Type)] +pub struct RequestingApplication { + pub name: String, + pub path: PathBuf, + pub pid: u32, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, Type)] +pub struct RequestingParty { + pub rp_id: String, + pub origin: String, +} + #[derive(Serialize, Deserialize)] pub enum ViewUpdate { - SetTitle(String), + SetTitle((String, String)), SetDevices(Vec), SetCredentials(Vec), diff --git a/credentialsd-common/src/server.rs b/credentialsd-common/src/server.rs index eaa9436..b6b3b73 100644 --- a/credentialsd-common/src/server.rs +++ b/credentialsd-common/src/server.rs @@ -9,7 +9,7 @@ use zvariant::{ Signature, Structure, StructureBuilder, Type, Value, signature::Fields, }; -use crate::model::{BackgroundEvent, Operation}; +use crate::model::{BackgroundEvent, Operation, RequestingApplication}; const TAG_VALUE_SIGNATURE: &Signature = &Signature::Structure(Fields::Static { fields: &[&Signature::U8, &Signature::Variant], @@ -484,6 +484,8 @@ where pub struct ViewRequest { pub operation: Operation, pub id: RequestId, + pub rp_id: String, + pub requesting_app: RequestingApplication, } fn value_to_owned(value: &Value<'_>) -> OwnedValue { diff --git a/credentialsd-ui/data/resources/style.css b/credentialsd-ui/data/resources/style.css index 3c4bd47..a117719 100644 --- a/credentialsd-ui/data/resources/style.css +++ b/credentialsd-ui/data/resources/style.css @@ -1,4 +1,4 @@ .title-header{ - font-size: 36px; + font-size: 24px; font-weight: bold; } diff --git a/credentialsd-ui/data/resources/ui/window.ui b/credentialsd-ui/data/resources/ui/window.ui index c3c1639..5539cba 100644 --- a/credentialsd-ui/data/resources/ui/window.ui +++ b/credentialsd-ui/data/resources/ui/window.ui @@ -20,6 +20,7 @@ vertical + 10 @@ -34,24 +35,49 @@ + + + + + + true + true + + + + CredentialsUiWindow + + + + + + + + + + + choose_device - Choose device + Choose device vertical - Devices + Devices - 256 + true + 256 @@ -69,7 +95,7 @@ usb - Plug in security key + Plug in security key vertical @@ -110,7 +136,7 @@ hybrid_qr - Scan the QR code to connect your device + Scan the QR code to connect your device vertical @@ -155,18 +181,19 @@ choose_credential - Choose credential + Choose credential vertical - Choose credential + Choose credential - 256 + true + 256 @@ -184,13 +211,13 @@ completed - Complete + Complete vertical - Done! + Done! @@ -201,7 +228,7 @@ failed - Something went wrong. + Something went wrong. vertical @@ -214,7 +241,7 @@ - Something went wrong while retrieving a credential. Please try again later or use a different authenticator. + Something went wrong while retrieving a credential. Please try again later or use a different authenticator. diff --git a/credentialsd-ui/meson.build b/credentialsd-ui/meson.build index d2e2cb3..b8c9f7b 100644 --- a/credentialsd-ui/meson.build +++ b/credentialsd-ui/meson.build @@ -31,8 +31,6 @@ localedir = prefix / get_option('localedir') datadir = prefix / get_option('datadir') pkgdatadir = datadir / gui_executable_name iconsdir = datadir / 'icons' -podir = meson.project_source_root() / meson.current_source_dir() / 'po' -gettext_package = gui_executable_name if get_option('profile') == 'development' profile = 'Devel' @@ -71,6 +69,10 @@ if get_option('cargo_offline') == true cargo_options += ['--offline'] endif +# Localization setup +podir = meson.project_source_root() / meson.current_source_dir() / 'po' +gettext_package = gui_executable_name + subdir('data') subdir('po') subdir('src') @@ -79,4 +81,4 @@ gnome.post_install( gtk_update_icon_cache: true, glib_compile_schemas: true, update_desktop_database: true, -) \ No newline at end of file +) diff --git a/credentialsd-ui/po/LINGUAS b/credentialsd-ui/po/LINGUAS index e69de29..845d78b 100644 --- a/credentialsd-ui/po/LINGUAS +++ b/credentialsd-ui/po/LINGUAS @@ -0,0 +1,2 @@ +en_US +de_DE diff --git a/credentialsd-ui/po/POTFILES.in b/credentialsd-ui/po/POTFILES.in index 3417f90..c342f04 100644 --- a/credentialsd-ui/po/POTFILES.in +++ b/credentialsd-ui/po/POTFILES.in @@ -1,6 +1,8 @@ -data/xyz.iinuwa.CredentialManager.desktop.in.in -data/xyz.iinuwa.CredentialManager.gschema.xml.in -data/xyz.iinuwa.CredentialManager.metainfo.xml.in.in +data/xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in +data/xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in +data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in data/resources/ui/shortcuts.ui data/resources/ui/window.ui -src/application.rs +src/gui/view_model/gtk/mod.rs +src/gui/view_model/gtk/device.rs +src/gui/view_model/mod.rs diff --git a/credentialsd-ui/po/credentialsd-ui.pot b/credentialsd-ui/po/credentialsd-ui.pot new file mode 100644 index 0000000..4d43622 --- /dev/null +++ b/credentialsd-ui/po/credentialsd-ui.pot @@ -0,0 +1,244 @@ +msgid "" +msgstr "" +"Project-Id-Version: credentialsd-ui\n" +"Report-Msgid-Bugs-To: \"https://github.com/linux-credentials/credentialsd/" +"issues\"\n" +"POT-Creation-Date: 2025-10-30 14:43+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#. Insert your license of choice here +#. LGPL-3.0-only +#: 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:378 +msgid "Credential Manager" +msgstr "" + +#: data/xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in:3 +#: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:9 +msgid "Write a GTK + Rust application" +msgstr "" + +#: data/xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in:9 +msgid "Gnome;GTK;" +msgstr "" + +#: data/xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in:6 +msgid "Window width" +msgstr "" + +#: data/xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in:10 +msgid "Window height" +msgstr "" + +#: data/xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in:14 +msgid "Window maximized state" +msgstr "" + +#: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:11 +msgid "" +"A boilerplate template for GTK + Rust. It uses Meson as a build system and " +"has flatpak support by default." +msgstr "" + +#: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:16 +msgid "Registering a credential" +msgstr "" + +#. developer_name tag deprecated with Appstream 1.0 +#: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:34 +msgid "Isaiah Inuwa" +msgstr "" + +#: data/resources/ui/shortcuts.ui:11 +msgctxt "shortcut window" +msgid "General" +msgstr "" + +#: data/resources/ui/shortcuts.ui:14 +msgctxt "shortcut window" +msgid "Show Shortcuts" +msgstr "" + +#: data/resources/ui/shortcuts.ui:20 +msgctxt "shortcut window" +msgid "Quit" +msgstr "" + +#: data/resources/ui/window.ui:6 +msgid "_Preferences" +msgstr "" + +#: data/resources/ui/window.ui:10 +msgid "_Keyboard Shortcuts" +msgstr "" + +#: data/resources/ui/window.ui:68 +msgid "Choose device" +msgstr "" + +#: data/resources/ui/window.ui:74 +msgid "Devices" +msgstr "" + +#: data/resources/ui/window.ui:97 +msgid "Plug in security key" +msgstr "" + +#: data/resources/ui/window.ui:138 +msgid "Scan the QR code to connect your device" +msgstr "" + +#: data/resources/ui/window.ui:183 data/resources/ui/window.ui:189 +msgid "Choose credential" +msgstr "" + +#: data/resources/ui/window.ui:212 +msgid "Complete" +msgstr "" + +#: data/resources/ui/window.ui:218 +msgid "Done!" +msgstr "" + +#: data/resources/ui/window.ui:229 +msgid "Something went wrong." +msgstr "" + +#: data/resources/ui/window.ui:242 src/gui/view_model/mod.rs:280 +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:145 +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:151 +msgid "Enter your PIN." +msgstr "" + +#: src/gui/view_model/gtk/mod.rs:160 +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:166 +msgid "Touch your device." +msgstr "" + +#: src/gui/view_model/gtk/mod.rs:171 +msgid "Touch your device" +msgstr "" + +#: src/gui/view_model/gtk/mod.rs:174 +msgid "Scan the QR code with your device to begin authentication." +msgstr "" + +#: src/gui/view_model/gtk/mod.rs:184 +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:192 +msgid "Device connected. Follow the instructions on your device" +msgstr "" + +#: src/gui/view_model/gtk/mod.rs:318 +msgid "Insert your security key." +msgstr "" + +#: src/gui/view_model/gtk/mod.rs:334 +msgid "Multiple devices found. Please select with which to proceed." +msgstr "" + +#: src/gui/view_model/gtk/device.rs:57 +msgid "A Bluetooth device" +msgstr "" + +#: src/gui/view_model/gtk/device.rs:58 +msgid "This device" +msgstr "" + +#: src/gui/view_model/gtk/device.rs:59 +msgid "A mobile device" +msgstr "" + +#: src/gui/view_model/gtk/device.rs:60 +msgid "Linked Device" +msgstr "" + +#: src/gui/view_model/gtk/device.rs:61 +msgid "An NFC device" +msgstr "" + +#: src/gui/view_model/gtk/device.rs:62 +msgid "A security key" +msgstr "" + +#: src/gui/view_model/mod.rs:75 +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 +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 +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 +#: src/gui/view_model/mod.rs:96 +msgid "" +"\"%s2\" (process ID: %i1, binary: %s3) is asking to create 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 +#. 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 +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:220 +msgid "Failed to select credential from device." +msgstr "" + +#: src/gui/view_model/mod.rs:274 +msgid "No matching credentials found on this authenticator." +msgstr "" + +#: src/gui/view_model/mod.rs:277 +msgid "" +"No more PIN attempts allowed. Try removing your device and plugging it back " +"in." +msgstr "" + +#: src/gui/view_model/mod.rs:283 +msgid "This credential is already registered on this authenticator." +msgstr "" + +#: src/gui/view_model/mod.rs:331 +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 new file mode 100644 index 0000000..4c11cbb --- /dev/null +++ b/credentialsd-ui/po/de_DE.po @@ -0,0 +1,253 @@ +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \"https://github.com/linux-credentials/credentialsd/" +"issues\"\n" +"POT-Creation-Date: 2025-10-30 14:43+0100\n" +"PO-Revision-Date: 2025-10-10 14:45+0200\n" +"Last-Translator: Martin Sirringhaus \n" +"Language: de_DE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#. Insert your license of choice here +#. LGPL-3.0-only +#: 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:378 +msgid "Credential Manager" +msgstr "Zugangsdatenmanager" + +#: data/xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in:3 +#: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:9 +msgid "Write a GTK + Rust application" +msgstr "" + +#: data/xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in:9 +msgid "Gnome;GTK;" +msgstr "Gnome;GTK;" + +#: data/xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in:6 +msgid "Window width" +msgstr "Fensterbreite" + +#: data/xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in:10 +msgid "Window height" +msgstr "Fensterhöhe" + +#: data/xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in:14 +msgid "Window maximized state" +msgstr "Fenster maximiert" + +#: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:11 +msgid "" +"A boilerplate template for GTK + Rust. It uses Meson as a build system and " +"has flatpak support by default." +msgstr "Eine Vorlage für eine GTK + Rust Anwendung." + +#: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:16 +msgid "Registering a credential" +msgstr "Zugangsdaten registrieren" + +#: data/resources/ui/shortcuts.ui:11 +msgctxt "shortcut window" +msgid "General" +msgstr "Allgemein" + +#: data/resources/ui/shortcuts.ui:14 +msgctxt "shortcut window" +msgid "Show Shortcuts" +msgstr "Zeige Kürzel" + +#: data/resources/ui/shortcuts.ui:20 +msgctxt "shortcut window" +msgid "Quit" +msgstr "Beenden" + +#: data/resources/ui/window.ui:6 +msgid "_Preferences" +msgstr "Einstellungen" + +#: data/resources/ui/window.ui:10 +msgid "_Keyboard Shortcuts" +msgstr "Tastaturkürzel" + +#: data/resources/ui/window.ui:68 +msgid "Choose device" +msgstr "Gerät auswählen" + +#: data/resources/ui/window.ui:74 +msgid "Devices" +msgstr "Geräte" + +#: data/resources/ui/window.ui:97 +msgid "Plug in security key" +msgstr "Stecken Sie Ihren Security-Token ein" + +#: data/resources/ui/window.ui:138 +msgid "Scan the QR code to connect your device" +msgstr "Scannen Sie den QR-Code, um Ihr Gerät zu verbinden" + +#: data/resources/ui/window.ui:183 data/resources/ui/window.ui:189 +msgid "Choose credential" +msgstr "Wählen Sie Zugangsdaten aus" + +#: data/resources/ui/window.ui:212 +msgid "Complete" +msgstr "Abgeschlossen" + +#: data/resources/ui/window.ui:218 +msgid "Done!" +msgstr "Fertig!" + +#: data/resources/ui/window.ui:229 +msgid "Something went wrong." +msgstr "Etwas ist schief gegangen." + +#: data/resources/ui/window.ui:242 src/gui/view_model/mod.rs:280 +msgid "" +"Something went wrong while retrieving a credential. Please try again later " +"or use a different authenticator." +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:145 +#, 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:151 +msgid "Enter your PIN." +msgstr "Geben Sie Ihren PIN ein." + +#: src/gui/view_model/gtk/mod.rs:160 +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:166 +msgid "Touch your device." +msgstr "Berühren Sie Ihr Gerät." + +#: src/gui/view_model/gtk/mod.rs:171 +msgid "Touch your device" +msgstr "Berühren Sie Ihr Gerät." + +#: src/gui/view_model/gtk/mod.rs:174 +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:184 +msgid "" +"Connecting to your device. Make sure both devices are near each other and " +"have Bluetooth enabled." +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:192 +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:318 +msgid "Insert your security key." +msgstr "Stecken Sie Ihren Security-Token ein." + +#: src/gui/view_model/gtk/mod.rs:334 +msgid "Multiple devices found. Please select with which to proceed." +msgstr "Mehrere Geräte gefunden. Bitte wählen Sie einen aus, um fortzufahren." + +#: src/gui/view_model/gtk/device.rs:57 +msgid "A Bluetooth device" +msgstr "Ein Bluetooth-Gerät" + +#: src/gui/view_model/gtk/device.rs:58 +msgid "This device" +msgstr "Dieses Gerät" + +#: src/gui/view_model/gtk/device.rs:59 +msgid "A mobile device" +msgstr "Ein mobiles Gerät" + +#: src/gui/view_model/gtk/device.rs:60 +msgid "Linked Device" +msgstr "Verbundenes Gerät" + +#: src/gui/view_model/gtk/device.rs:61 +msgid "An NFC device" +msgstr "Ein NFC-Gerät" + +#: src/gui/view_model/gtk/device.rs:62 +msgid "A security key" +msgstr "Ein Security-Token" + +#: src/gui/view_model/mod.rs:75 +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 +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 +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 +#: src/gui/view_model/mod.rs:96 +msgid "" +"\"%s2\" (process ID: %i1, binary: %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 fort, wenn Sie diesem Prozess vertrauen." + +#. 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 +#: src/gui/view_model/mod.rs:103 +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 "" +"\"%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." + +#: src/gui/view_model/mod.rs:220 +msgid "Failed to select credential from device." +msgstr "Zugangsdaten vom Gerät konnten nicht ausgewählt werden." + +#: src/gui/view_model/mod.rs:274 +msgid "No matching credentials found on this authenticator." +msgstr "Keine passenden Zugangsdaten auf diesem Gerät gefunden." + +#: src/gui/view_model/mod.rs:277 +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." + +#: src/gui/view_model/mod.rs:283 +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:331 +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, " +"oder benutzen Sie ein anderes Gerät." diff --git a/credentialsd-ui/po/en_US.po b/credentialsd-ui/po/en_US.po new file mode 100644 index 0000000..34fd54d --- /dev/null +++ b/credentialsd-ui/po/en_US.po @@ -0,0 +1,252 @@ +msgid "" +msgstr "" +"Report-Msgid-Bugs-To: \"https://github.com/linux-credentials/credentialsd/" +"issues\"\n" +"POT-Creation-Date: 2025-10-30 14:43+0100\n" +"PO-Revision-Date: 2025-10-10 14:45+0200\n" +"Last-Translator: Martin Sirringhaus \n" +"Language: en_US\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#. Insert your license of choice here +#. LGPL-3.0-only +#: 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:378 +msgid "Credential Manager" +msgstr "Credential Manager" + +#: data/xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in:3 +#: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:9 +msgid "Write a GTK + Rust application" +msgstr "Write a GTK + Rust application" + +#: data/xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in:9 +msgid "Gnome;GTK;" +msgstr "Gnome;GTK;" + +#: data/xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in:6 +msgid "Window width" +msgstr "Window width" + +#: data/xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in:10 +msgid "Window height" +msgstr "Window height" + +#: data/xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in:14 +msgid "Window maximized state" +msgstr "Window maximized state" + +#: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:11 +msgid "" +"A boilerplate template for GTK + Rust. It uses Meson as a build system and " +"has flatpak support by default." +msgstr "" +"A boilerplate template for GTK + Rust. It uses Meson as a build system and " +"has flatpak support by default." + +#: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:16 +msgid "Registering a credential" +msgstr "Registering a credential" + +#: data/resources/ui/shortcuts.ui:11 +msgctxt "shortcut window" +msgid "General" +msgstr "General" + +#: data/resources/ui/shortcuts.ui:14 +msgctxt "shortcut window" +msgid "Show Shortcuts" +msgstr "Show Shortcuts" + +#: data/resources/ui/shortcuts.ui:20 +msgctxt "shortcut window" +msgid "Quit" +msgstr "Quit" + +#: data/resources/ui/window.ui:6 +msgid "_Preferences" +msgstr "_Preferences" + +#: data/resources/ui/window.ui:10 +msgid "_Keyboard Shortcuts" +msgstr "Keyboard Shortcuts" + +#: data/resources/ui/window.ui:68 +msgid "Choose device" +msgstr "Choose device" + +#: data/resources/ui/window.ui:74 +msgid "Devices" +msgstr "Devices" + +#: data/resources/ui/window.ui:97 +msgid "Plug in security key" +msgstr "Plug in security key" + +#: data/resources/ui/window.ui:138 +msgid "Scan the QR code to connect your device" +msgstr "Scan the QR code to connect your device" + +#: data/resources/ui/window.ui:183 data/resources/ui/window.ui:189 +msgid "Choose credential" +msgstr "Choose credential" + +#: data/resources/ui/window.ui:212 +msgid "Complete" +msgstr "Complete" + +#: data/resources/ui/window.ui:218 +msgid "Done!" +msgstr "Done!" + +#: data/resources/ui/window.ui:229 +msgid "Something went wrong." +msgstr "Something went wrong." + +#: data/resources/ui/window.ui:242 src/gui/view_model/mod.rs:280 +msgid "" +"Something went wrong while retrieving a credential. Please try again later " +"or use a different authenticator." +msgstr "" +"Something went wrong while retrieving a credential. Please try again later " +"or use a different authenticator." + +#: src/gui/view_model/gtk/mod.rs:145 +#, fuzzy +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:151 +msgid "Enter your PIN." +msgstr "Enter your PIN." + +#: src/gui/view_model/gtk/mod.rs:160 +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:166 +msgid "Touch your device." +msgstr "Touch your device." + +#: src/gui/view_model/gtk/mod.rs:171 +msgid "Touch your device" +msgstr "Touch your device" + +#: src/gui/view_model/gtk/mod.rs:174 +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:184 +msgid "" +"Connecting to your device. Make sure both devices are near each other and " +"have Bluetooth enabled." +msgstr "" +"Connecting to your device. Make sure both devices are near each other and " +"have Bluetooth enabled." + +#: src/gui/view_model/gtk/mod.rs:192 +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:318 +msgid "Insert your security key." +msgstr "Insert your security key." + +#: src/gui/view_model/gtk/mod.rs:334 +msgid "Multiple devices found. Please select with which to proceed." +msgstr "Multiple devices found. Please select with which to proceed." + +#: src/gui/view_model/gtk/device.rs:57 +msgid "A Bluetooth device" +msgstr "A Bluetooth device" + +#: src/gui/view_model/gtk/device.rs:58 +msgid "This device" +msgstr "This device" + +#: src/gui/view_model/gtk/device.rs:59 +msgid "A mobile device" +msgstr "A mobile device" + +#: src/gui/view_model/gtk/device.rs:60 +msgid "Linked Device" +msgstr "Linked Device" + +#: src/gui/view_model/gtk/device.rs:61 +msgid "An NFC device" +msgstr "An NFC device" + +#: src/gui/view_model/gtk/device.rs:62 +msgid "A security key" +msgstr "A security key" + +#: src/gui/view_model/mod.rs:75 +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 +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 +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 +#: src/gui/view_model/mod.rs:96 +msgid "" +"\"%s2\" (process ID: %i1, binary: %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 +#: src/gui/view_model/mod.rs:103 +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 "" +"\"%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:220 +msgid "Failed to select credential from device." +msgstr "Failed to select credential from device." + +#: src/gui/view_model/mod.rs:274 +msgid "No matching credentials found on this authenticator." +msgstr "No matching credentials found on this authenticator." + +#: src/gui/view_model/mod.rs:277 +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." + +#: src/gui/view_model/mod.rs:283 +msgid "This credential is already registered on this authenticator." +msgstr "This credential is already registered on this authenticator." + +#: src/gui/view_model/mod.rs:331 +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/po/meson.build b/credentialsd-ui/po/meson.build index 57d1266..75cb319 100644 --- a/credentialsd-ui/po/meson.build +++ b/credentialsd-ui/po/meson.build @@ -1 +1,11 @@ -i18n.gettext(gettext_package, preset: 'glib') +i18n = import('i18n') + +# This creates build targets: 'credentialsd-ui-pot', 'credentialsd-ui-update-po', etc. +i18n.gettext(gettext_package, + args: ['--directory=' + meson.project_source_root() / 'credentialsd-ui', + '--from-code=UTF-8', + '--copyright-holder="The Credentials for Linux Project"', + '--msgid-bugs-address="https://github.com/linux-credentials/credentialsd/issues"', + '--add-comments=TRANSLATORS:' + ], +) diff --git a/credentialsd-ui/src/gui/mod.rs b/credentialsd-ui/src/gui/mod.rs index 08ba897..d9870b1 100644 --- a/credentialsd-ui/src/gui/mod.rs +++ b/credentialsd-ui/src/gui/mod.rs @@ -27,19 +27,19 @@ fn run_gui( flow_controller: Arc>, request: ViewRequest, ) { - let operation = request.operation; let (tx_update, rx_update) = async_std::channel::unbounded::(); let (tx_event, rx_event) = async_std::channel::unbounded::(); let event_loop = async_std::task::spawn(async move { + let request_id = request.id; let mut vm = - view_model::ViewModel::new(operation, flow_controller.clone(), rx_event, tx_update); + view_model::ViewModel::new(request, flow_controller.clone(), rx_event, tx_update); vm.start_event_loop().await; tracing::debug!("Finishing user request."); // If cancellation fails, that's fine. let _ = flow_controller .lock() .await - .cancel_request(request.id) + .cancel_request(request_id) .await; }); diff --git a/credentialsd-ui/src/gui/view_model/gtk/application.rs b/credentialsd-ui/src/gui/view_model/gtk/application.rs index 82216cd..7ffa2b6 100644 --- a/credentialsd-ui/src/gui/view_model/gtk/application.rs +++ b/credentialsd-ui/src/gui/view_model/gtk/application.rs @@ -6,7 +6,7 @@ use gtk::subclass::prelude::*; use gtk::{gdk, gio, glib}; use super::{ViewModel, window::CredentialsUiWindow}; -use crate::config::{APP_ID, PKGDATADIR, PROFILE, VERSION}; +use crate::config::{APP_ID, LOCALEDIR, PKGDATADIR, PROFILE, VERSION}; use crate::gui::view_model::{ViewEvent, ViewUpdate}; mod imp { @@ -152,6 +152,7 @@ impl CredentialsUi { info!("Credentials UI ({})", APP_ID); info!("Version: {} ({})", VERSION, PROFILE); info!("Datadir: {}", PKGDATADIR); + info!("Localedir: {}", LOCALEDIR); ApplicationExtManual::run(self) } diff --git a/credentialsd-ui/src/gui/view_model/gtk/device.rs b/credentialsd-ui/src/gui/view_model/gtk/device.rs index ad859ef..7992f56 100644 --- a/credentialsd-ui/src/gui/view_model/gtk/device.rs +++ b/credentialsd-ui/src/gui/view_model/gtk/device.rs @@ -1,5 +1,6 @@ use std::cell::RefCell; +use gettextrs::gettext; use glib::Object; use gtk::glib; use gtk::prelude::*; @@ -51,28 +52,28 @@ impl DeviceObject { } } -fn transport_name(transport: &Transport) -> &'static str { +fn transport_name(transport: &Transport) -> String { match transport { - Transport::Ble => "A Bluetooth device", - Transport::Internal => "This device", - Transport::HybridQr => "A mobile device", - Transport::HybridLinked => "TODO: Linked Device", - Transport::Nfc => "An NFC device", - Transport::Usb => "A security key", + Transport::Ble => gettext("A Bluetooth device"), + Transport::Internal => gettext("This device"), + Transport::HybridQr => gettext("A mobile device"), + Transport::HybridLinked => gettext("Linked Device"), + Transport::Nfc => gettext("An NFC device"), + Transport::Usb => gettext("A security key"), // Transport::PasskeyProvider => ("symbolic-link-symbolic", "ACME Password Manager"), } } impl From for DeviceObject { fn from(value: crate::gui::view_model::Device) -> Self { let name = transport_name(&value.transport); - Self::new(&value.id, &value.transport, name) + Self::new(&value.id, &value.transport, &name) } } impl From<&crate::gui::view_model::Device> for DeviceObject { fn from(value: &crate::gui::view_model::Device) -> Self { let name = transport_name(&value.transport); - Self::new(&value.id, &value.transport, name) + Self::new(&value.id, &value.transport, &name) } } diff --git a/credentialsd-ui/src/gui/view_model/gtk/mod.rs b/credentialsd-ui/src/gui/view_model/gtk/mod.rs index 966c5f6..d39e125 100644 --- a/credentialsd-ui/src/gui/view_model/gtk/mod.rs +++ b/credentialsd-ui/src/gui/view_model/gtk/mod.rs @@ -4,7 +4,7 @@ pub mod device; mod window; use async_std::channel::{Receiver, Sender}; -use gettextrs::{LocaleCategory, gettext}; +use gettextrs::{LocaleCategory, gettext, ngettext}; use glib::clone; use gtk::gdk::Texture; use gtk::gdk_pixbuf::Pixbuf; @@ -36,6 +36,9 @@ mod imp { #[property(get, set)] pub title: RefCell, + #[property(get, set)] + pub subtitle: RefCell, + #[property(get, set)] pub devices: RefCell, @@ -122,7 +125,10 @@ impl ViewModel { // TODO: hack so I don't have to unset this in every event manually. view_model.set_usb_pin_entry_visible(false); match update { - ViewUpdate::SetTitle(title) => view_model.set_title(title), + ViewUpdate::SetTitle((title, subtitle)) => { + view_model.set_title(title); + view_model.set_subtitle(subtitle); + } ViewUpdate::SetDevices(devices) => { view_model.update_devices(&devices) } @@ -134,34 +140,38 @@ impl ViewModel { view_model.waiting_for_device(&device) } ViewUpdate::UsbNeedsPin { attempts_left } => { - let prompt = match attempts_left { - Some(1) => { - "Enter your PIN. 1 attempt remaining.".to_string() - } - Some(attempts_left) => format!( - "Enter your PIN. {attempts_left} attempts remaining." - ), - None => "Enter your PIN.".to_string(), + let prompt = if let Some(left) = attempts_left { + let localized = ngettext( + "Enter your PIN. One attempt remaining.", + "Enter your PIN. %d attempts remaining.", + left, + ); + localized.replace("%d", &format!("{}", left)) + } else { + gettext("Enter your PIN.") }; view_model.set_prompt(prompt); view_model.set_usb_pin_entry_visible(true); } ViewUpdate::UsbNeedsUserVerification { attempts_left } => { let prompt = match attempts_left { - Some(1) => "Touch your device again. 1 attempt remaining." - .to_string(), - Some(attempts_left) => format!( - "Touch your device again. {attempts_left} attempts remaining." - ), - None => "Touch your device.".to_string(), + Some(left) => { + let localized = ngettext( + "Touch your device again. One attempt remaining.", + "Touch your device again. %d attempts remaining.", + left, + ); + localized.replace("%d", &format!("{}", left)) + } + None => gettext("Touch your device."), }; view_model.set_prompt(prompt); } ViewUpdate::UsbNeedsUserPresence => { - view_model.set_prompt("Touch your device"); + view_model.set_prompt(gettext("Touch your device")); } ViewUpdate::HybridNeedsQrCode(qr_code) => { - view_model.set_prompt("Scan the QR code with your device to begin authentication."); + view_model.set_prompt(gettext("Scan the QR code with your device to begin authentication.")); let texture = view_model.draw_qr_code(&qr_code); view_model.set_qr_code_paintable(&texture); view_model.set_qr_code_visible(true); @@ -170,17 +180,17 @@ impl ViewModel { ViewUpdate::HybridConnecting => { view_model.set_qr_code_visible(false); _ = view_model.qr_code_paintable().take(); - view_model.set_prompt( + view_model.set_prompt(gettext( "Connecting to your device. Make sure both devices are near each other and have Bluetooth enabled.", - ); + )); view_model.set_qr_spinner_visible(true); } ViewUpdate::HybridConnected => { view_model.set_qr_code_visible(false); _ = view_model.qr_code_paintable().take(); - view_model.set_prompt( + view_model.set_prompt(gettext( "Device connected. Follow the instructions on your device", - ); + )); view_model.set_qr_spinner_visible(false); } ViewUpdate::Completed => { @@ -190,6 +200,7 @@ impl ViewModel { ViewUpdate::Failed(error_msg) => { view_model.set_qr_spinner_visible(false); view_model.set_failed(true); + // These are already gettext messages view_model.set_prompt(error_msg); } ViewUpdate::Cancelled => { @@ -304,7 +315,7 @@ impl ViewModel { fn waiting_for_device(&self, device: &Device) { match device.transport { Transport::Usb => { - self.set_prompt("Insert your security key."); + self.set_prompt(gettext("Insert your security key.")); } Transport::HybridQr => { self.set_prompt(""); @@ -319,7 +330,9 @@ impl ViewModel { } fn selecting_device(&self) { - self.set_prompt("Multiple devices found. Please select with which to proceed."); + self.set_prompt(gettext( + "Multiple devices found. Please select with which to proceed.", + )); } pub async fn send_usb_device_pin(&self, pin: String) { @@ -358,6 +371,8 @@ pub fn start_gtk_app( gettextrs::setlocale(LocaleCategory::LcAll, ""); gettextrs::bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR).expect("Unable to bind the text domain"); gettextrs::textdomain(GETTEXT_PACKAGE).expect("Unable to switch to the text domain"); + gettextrs::bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8") + .expect("Unable to set codeset to UTF-8"); if glib::application_name().is_none() { glib::set_application_name(&gettext("Credential Manager")); diff --git a/credentialsd-ui/src/gui/view_model/mod.rs b/credentialsd-ui/src/gui/view_model/mod.rs index 6f8ceae..9d545fd 100644 --- a/credentialsd-ui/src/gui/view_model/mod.rs +++ b/credentialsd-ui/src/gui/view_model/mod.rs @@ -7,6 +7,9 @@ use async_std::{ channel::{Receiver, Sender}, sync::Mutex as AsyncMutex, }; +use credentialsd_common::model::RequestingApplication; +use credentialsd_common::server::ViewRequest; +use gettextrs::gettext; use serde::{Deserialize, Serialize}; use tracing::{error, info}; @@ -27,7 +30,10 @@ where tx_update: Sender, rx_event: Receiver, title: String, + subtitle: String, operation: Operation, + rp_id: String, + requesting_app: RequestingApplication, // This includes devices like platform authenticator, USB, hybrid devices: Vec, @@ -41,7 +47,7 @@ where impl ViewModel { pub(crate) fn new( - operation: Operation, + request: ViewRequest, flow_controller: Arc>, rx_event: Receiver, tx_update: Sender, @@ -50,8 +56,11 @@ impl ViewModel { flow_controller, rx_event, tx_update, - operation, + operation: request.operation, + rp_id: request.rp_id, + requesting_app: request.requesting_app, title: String::default(), + subtitle: String::default(), devices: Vec::new(), selected_device: None, hybrid_qr_state: HybridState::default(), @@ -60,13 +69,52 @@ impl ViewModel { } async fn update_title(&mut self) { - self.title = match self.operation { - Operation::Create => "Create new credential", - Operation::Get => "Use a credential", + let mut requesting_app = self.requesting_app.clone(); + + if requesting_app.name.is_empty() { + requesting_app.name = gettext("unknown application"); + }; + let mut title = match self.operation { + Operation::Create => { + // TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from + gettext("Create a passkey for %s1") + } + Operation::Get => { + // TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from + gettext("Use a passkey for %s1") + } + } + .to_string(); + title = title.replace("%s1", &self.rp_id); + + let mut subtitle = match self.operation { + Operation::Create => { + // 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 + gettext("\"%s2\" (process ID: %i1, binary: %s3) is asking to create a credential to register at \"%s1\". Only proceed if you trust this process.") + } + Operation::Get => { + // 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 + gettext("\"%s2\" (process ID: %i1, binary: %s3) is asking to use a credential to sign in to \"%s1\". Only proceed if you trust this process.") + } } .to_string(); + subtitle = subtitle.replace("%s1", &self.rp_id); + subtitle = subtitle.replace("%i1", &format!("{}", requesting_app.pid)); + subtitle = subtitle.replace("%s2", &requesting_app.name); + subtitle = subtitle.replace("%s3", &requesting_app.path.to_string_lossy()); + self.title = title; + self.subtitle = subtitle; self.tx_update - .send(ViewUpdate::SetTitle(self.title.to_string())) + .send(ViewUpdate::SetTitle(( + self.title.to_string(), + self.subtitle.to_string(), + ))) .await .unwrap(); } @@ -166,10 +214,11 @@ impl ViewModel { .await .is_err() { - let error_msg = "Failed to select credential from device."; - tracing::error!(error_msg); + tracing::error!("Failed to select credential from device."); self.tx_update - .send(ViewUpdate::Failed(error_msg.to_string())) + .send(ViewUpdate::Failed(gettext( + "Failed to select credential from device.", + ))) .await .unwrap(); } @@ -220,20 +269,20 @@ impl ViewModel { } // TODO: Provide more specific error messages using the wrapped Error. UsbState::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::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 @@ -279,7 +328,7 @@ impl ViewModel { } HybridState::Failed => { self.hybrid_qr_code_data = None; - self.tx_update.send(ViewUpdate::Failed(String::from("Something went wrong. Try again later or use a different authenticator."))).await.unwrap(); + self.tx_update.send(ViewUpdate::Failed(gettext("Something went wrong. Try again later or use a different authenticator."))).await.unwrap(); } }; } /* diff --git a/credentialsd-ui/src/meson.build b/credentialsd-ui/src/meson.build index 02e7d8c..198de49 100644 --- a/credentialsd-ui/src/meson.build +++ b/credentialsd-ui/src/meson.build @@ -8,10 +8,17 @@ if (get_option('profile') == 'development') else global_conf.set_quoted('PKGDATADIR', pkgdatadir) endif +if (get_option('profile') == 'development') + global_conf.set_quoted( + 'LOCALEDIR', + meson.project_build_root() / gui_build_dir / 'po', + ) +else + global_conf.set_quoted('LOCALEDIR', localedir) +endif global_conf.set_quoted('PROFILE', profile) global_conf.set_quoted('VERSION', version + version_suffix) global_conf.set_quoted('GETTEXT_PACKAGE', gettext_package) -global_conf.set_quoted('LOCALEDIR', localedir) configure_file(input: 'config.rs.in', output: 'config.rs', configuration: global_conf) # Copy the config.rs output to the source directory. @@ -67,4 +74,4 @@ test( '--nocapture', ], protocol: 'exitcode', -) \ No newline at end of file +) diff --git a/credentialsd/src/credential_service/mod.rs b/credentialsd/src/credential_service/mod.rs index 6bc8a03..9b098df 100644 --- a/credentialsd/src/credential_service/mod.rs +++ b/credentialsd/src/credential_service/mod.rs @@ -20,7 +20,7 @@ use tokio::sync::oneshot::Sender; use credentialsd_common::{ model::{ CredentialRequest, CredentialResponse, Device, Error as CredentialServiceError, Operation, - Transport, + RequestingApplication, Transport, }, server::{RequestId, ViewRequest}, }; @@ -101,6 +101,7 @@ impl pub async fn init_request( &self, request: &CredentialRequest, + requesting_app: Option, tx: Sender>, ) { let request_id = { @@ -126,9 +127,15 @@ impl CredentialRequest::CreatePublicKeyCredentialRequest(_) => Operation::Create, CredentialRequest::GetPublicKeyCredentialRequest(_) => Operation::Get, }; + let rp_id = match &request { + CredentialRequest::CreatePublicKeyCredentialRequest(r) => r.relying_party.id.clone(), + CredentialRequest::GetPublicKeyCredentialRequest(r) => r.relying_party_id.clone(), + }; let view_request = ViewRequest { operation, id: request_id, + rp_id, + requesting_app: requesting_app.unwrap_or_default(), // We can't send Options, so we send an empty string instead, if we don't know the peer }; let launch_ui_response = self @@ -364,7 +371,7 @@ mod test { cred_service .lock() .await - .init_request(&request, request_tx) + .init_request(&request, None, request_tx) .await; user.request_hybrid_credential().await; tokio::time::timeout(Duration::from_secs(5), request_rx) diff --git a/credentialsd/src/dbus/flow_control.rs b/credentialsd/src/dbus/flow_control.rs index 9bd1e7e..b7ad274 100644 --- a/credentialsd/src/dbus/flow_control.rs +++ b/credentialsd/src/dbus/flow_control.rs @@ -6,7 +6,7 @@ use std::{collections::VecDeque, fmt::Debug, sync::Arc}; use credentialsd_common::model::{ BackgroundEvent, CredentialRequest, CredentialResponse, Error as CredentialServiceError, - WebAuthnError, + RequestingApplication, WebAuthnError, }; use credentialsd_common::server::{Device, RequestId}; use futures_lite::StreamExt; @@ -43,6 +43,7 @@ pub async fn start_flow_control_service< Connection, Sender<( CredentialRequest, + Option, // Application name sending the request oneshot::Sender>, )>, )> { @@ -66,8 +67,11 @@ pub async fn start_flow_control_service< let (initiator_tx, mut initiator_rx) = mpsc::channel(2); tokio::spawn(async move { let svc = svc2; - while let Some((msg, tx)) = initiator_rx.recv().await { - svc.lock().await.init_request(&msg, tx).await; + while let Some((msg, requesting_app, tx)) = initiator_rx.recv().await { + svc.lock() + .await + .init_request(&msg, requesting_app, tx) + .await; } }); Ok((conn, initiator_tx)) @@ -300,6 +304,7 @@ enum SignalState { pub trait CredentialRequestController { fn request_credential( &self, + requesting_app: Option, request: CredentialRequest, ) -> impl Future> + Send; } @@ -307,6 +312,7 @@ pub trait CredentialRequestController { pub struct CredentialRequestControllerClient { pub initiator: Sender<( CredentialRequest, + Option, // Application name sending the request oneshot::Sender>, )>, } @@ -314,10 +320,14 @@ pub struct CredentialRequestControllerClient { impl CredentialRequestController for CredentialRequestControllerClient { async fn request_credential( &self, + requesting_app: Option, request: CredentialRequest, ) -> Result { let (tx, rx) = oneshot::channel(); - self.initiator.send((request, tx)).await.unwrap(); + self.initiator + .send((request, requesting_app, tx)) + .await + .unwrap(); let response = rx.await.map_err(|_| { tracing::error!("Credential response channel closed prematurely"); WebAuthnError::NotAllowedError diff --git a/credentialsd/src/dbus/gateway.rs b/credentialsd/src/dbus/gateway.rs index 10d0bd5..c2288e3 100644 --- a/credentialsd/src/dbus/gateway.rs +++ b/credentialsd/src/dbus/gateway.rs @@ -4,14 +4,17 @@ use std::sync::Arc; use credentialsd_common::{ - model::{CredentialRequest, CredentialResponse, GetClientCapabilitiesResponse, WebAuthnError}, + model::{ + CredentialRequest, CredentialResponse, GetClientCapabilitiesResponse, + RequestingApplication, WebAuthnError, + }, server::{ CreateCredentialRequest, CreateCredentialResponse, GetCredentialRequest, GetCredentialResponse, }, }; use tokio::sync::Mutex as AsyncMutex; -use zbus::{fdo, interface, Connection, DBusError}; +use zbus::{fdo, interface, message::Header, Connection, DBusError}; use crate::dbus::{ create_credential_request_try_into_ctap2, create_credential_response_try_from_ctap2, @@ -44,11 +47,91 @@ struct CredentialGateway { controller: Arc>, } +async fn query_connection_peer_binary( + header: Header<'_>, + connection: &Connection, +) -> Option { + // 1. Get the sender's unique bus name + let sender_unique_name = header.sender()?; + + tracing::debug!("Received request from sender: {}", sender_unique_name); + + // 2. Use the connection to query the D-Bus daemon for more info + let proxy = match zbus::Proxy::new( + connection, + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + ) + .await + { + Ok(p) => p, + Err(e) => { + tracing::error!("Failed to establish DBus proxy to query peer info: {e:?}"); + return None; + } + }; + + // 3. Get the Process ID (PID) of the peer + let pid_result = match proxy + .call_method("GetConnectionUnixProcessID", &(sender_unique_name)) + .await + { + Ok(pid) => pid, + Err(e) => { + tracing::error!("Failed to get peer PID via DBus: {e:?}"); + return None; + } + }; + let pid: u32 = match pid_result.body().deserialize() { + Ok(pid) => pid, + Err(e) => { + tracing::error!("Retrieved peer PID is not an integer: {e:?}"); + return None; + } + }; + + // 4. Get binary path via PID from /proc file-system + // TODO: To be REALLY sure, we may want to look at /proc/PID/exe instead. It is a symlink to + // the actual binary, giving a full path instead of only the command name. + // This should in theory be "more secure", but also may disconcert novice users with no + // technical background. + let command_name = match std::fs::read_to_string(format!("/proc/{pid}/comm")) { + Ok(c) => c.trim().to_string(), + Err(e) => { + tracing::error!( + "Failed to read /proc/{pid}/comm, so we don't know the command name of peer: {e:?}" + ); + return None; + } + }; + tracing::debug!("Request is from: {command_name}"); + + let exe_path = match std::fs::read_link(format!("/proc/{pid}/exe")) { + Ok(p) => p, + Err(e) => { + tracing::error!( + "Failed to follow link of /proc/{pid}/exe, so we don't know the executable path of peer: {e:?}" + ); + return None; + } + }; + tracing::debug!("Request is from: {exe_path:?}"); + + Some(RequestingApplication { + name: command_name, + path: exe_path, + pid, + }) +} + /// These are public methods that can be called by arbitrary clients to begin a credential flow. #[interface(name = "xyz.iinuwa.credentialsd.Credentials1")] impl CredentialGateway { async fn create_credential( &self, + #[zbus(header)] header: Header<'_>, + #[zbus(connection)] connection: &Connection, request: CreateCredentialRequest, ) -> Result { let (_origin, is_same_origin, _top_origin) = @@ -75,6 +158,8 @@ impl CredentialGateway CredentialGateway CredentialGateway, + #[zbus(connection)] connection: &Connection, request: GetCredentialRequest, ) -> Result { let (_origin, is_same_origin, _top_origin) = @@ -137,12 +224,14 @@ impl CredentialGateway