├── demo_client ├── .gitignore ├── style.css ├── util.py ├── resources.gresource.xml ├── window.blp ├── meson.build ├── cbor.py └── cbor_tests.py ├── credentialsd-ui ├── .gitignore ├── po │ ├── LINGUAS │ ├── POTFILES.in │ ├── meson.build │ ├── credentialsd-ui.pot │ ├── en_US.po │ └── de_DE.po ├── data │ ├── resources │ │ ├── style.css │ │ ├── meson.build │ │ ├── resources.gresource.xml │ │ └── ui │ │ │ └── shortcuts.ui │ ├── icons │ │ ├── meson.build │ │ ├── symbolic-link-symbolic.svg │ │ ├── check-round-outline-symbolic.svg │ │ ├── dialpad-symbolic.svg │ │ ├── fingerprint-symbolic.svg │ │ ├── xyz.iinuwa.credentialsd.CredentialsUi-symbolic.svg │ │ └── xyz.iinuwa.credentialsd.CredentialsUi.svg │ ├── xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in │ ├── xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in │ ├── xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in │ └── meson.build ├── src │ ├── config.rs.in │ ├── main.rs │ ├── dbus.rs │ ├── gui │ │ ├── mod.rs │ │ └── view_model │ │ │ └── gtk │ │ │ ├── credential.rs │ │ │ ├── device.rs │ │ │ ├── application.rs │ │ │ └── window.rs │ ├── meson.build │ └── client.rs ├── Cargo.toml └── meson.build ├── credentialsd ├── .gitignore ├── tests │ ├── services │ │ └── xyz.iinuwa.credentialsd.Credentials.service.in │ ├── config │ │ └── mod.rs.in │ ├── meson.build │ └── dbus.rs ├── src │ ├── serde │ │ └── mod.rs │ ├── meson.build │ ├── dbus │ │ ├── mod.rs │ │ └── ui_control.rs │ ├── main.rs │ ├── cose.rs │ └── cbor.rs ├── Cargo.toml └── meson.build ├── webext ├── meson.build ├── app │ ├── credential_manager_shim.json.in │ └── meson.build ├── add-on │ ├── meson.build │ ├── manifest.json │ ├── icons │ │ └── logo.svg │ └── background.js └── README.md ├── credentialsd-common ├── src │ ├── lib.rs │ ├── client.rs │ └── meson.build ├── Cargo.toml └── meson.build ├── images ├── end.png ├── qr-flow-2.png ├── qr-flow-3.png ├── internal-pin-2.png ├── internal-pin-3.png ├── internal-pin-4.png ├── register-start.png ├── security-key-2.png └── security-key-3.png ├── doc ├── meson.build ├── historical │ ├── credential-landscape.odg │ └── ecosystem.md └── xyz.iinuwa.credentialsd.Credentials.xml ├── dbus ├── xyz.iinuwa.credentialsd.UiControl.service.in ├── xyz.iinuwa.credentialsd.Credentials.service.in ├── xyz.iinuwa.credentialsd.FlowControl.service.in └── meson.build ├── systemd ├── xyz.iinuwa.credentialsd.UiControl.service.in ├── xyz.iinuwa.credentialsd.FlowControl.service.in ├── xyz.iinuwa.credentialsd.Credentials.service.in └── meson.build ├── .gitignore ├── CHANGELOG.md ├── .editorconfig ├── meson.options ├── meson.build ├── .vscode └── launch.json ├── hooks └── pre-commit.hook ├── .github └── workflows │ └── main.yml ├── SECURITY.md ├── README.md ├── GOALS.md ├── BUILDING.md ├── LICENSE.md └── CONTRIBUTING.md /demo_client/.gitignore: -------------------------------------------------------------------------------- 1 | user.json 2 | -------------------------------------------------------------------------------- /credentialsd-ui/.gitignore: -------------------------------------------------------------------------------- 1 | src/config.rs 2 | -------------------------------------------------------------------------------- /credentialsd-ui/po/LINGUAS: -------------------------------------------------------------------------------- 1 | en_US 2 | de_DE 3 | -------------------------------------------------------------------------------- /credentialsd/.gitignore: -------------------------------------------------------------------------------- 1 | tests/config/mod.rs 2 | -------------------------------------------------------------------------------- /webext/meson.build: -------------------------------------------------------------------------------- 1 | subdir('add-on') 2 | subdir('app') -------------------------------------------------------------------------------- /credentialsd-common/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod model; 3 | pub mod server; 4 | -------------------------------------------------------------------------------- /images/end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linux-credentials/credentialsd/HEAD/images/end.png -------------------------------------------------------------------------------- /images/qr-flow-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linux-credentials/credentialsd/HEAD/images/qr-flow-2.png -------------------------------------------------------------------------------- /images/qr-flow-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linux-credentials/credentialsd/HEAD/images/qr-flow-3.png -------------------------------------------------------------------------------- /credentialsd-ui/data/resources/style.css: -------------------------------------------------------------------------------- 1 | .title-header{ 2 | font-size: 24px; 3 | font-weight: bold; 4 | } 5 | -------------------------------------------------------------------------------- /images/internal-pin-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linux-credentials/credentialsd/HEAD/images/internal-pin-2.png -------------------------------------------------------------------------------- /images/internal-pin-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linux-credentials/credentialsd/HEAD/images/internal-pin-3.png -------------------------------------------------------------------------------- /images/internal-pin-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linux-credentials/credentialsd/HEAD/images/internal-pin-4.png -------------------------------------------------------------------------------- /images/register-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linux-credentials/credentialsd/HEAD/images/register-start.png -------------------------------------------------------------------------------- /images/security-key-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linux-credentials/credentialsd/HEAD/images/security-key-2.png -------------------------------------------------------------------------------- /images/security-key-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linux-credentials/credentialsd/HEAD/images/security-key-3.png -------------------------------------------------------------------------------- /doc/meson.build: -------------------------------------------------------------------------------- 1 | install_data( 2 | 'xyz.iinuwa.credentialsd.Credentials.xml', 3 | install_dir: datadir / 'credentialsd', 4 | ) -------------------------------------------------------------------------------- /demo_client/style.css: -------------------------------------------------------------------------------- 1 | box { 2 | margin: 20px; 3 | } 4 | 5 | button { 6 | margin-left: 10px; 7 | margin-right: 10px; 8 | } 9 | -------------------------------------------------------------------------------- /doc/historical/credential-landscape.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linux-credentials/credentialsd/HEAD/doc/historical/credential-landscape.odg -------------------------------------------------------------------------------- /credentialsd/tests/services/xyz.iinuwa.credentialsd.Credentials.service.in: -------------------------------------------------------------------------------- 1 | [D-BUS Service] 2 | Name=xyz.iinuwa.credentialsd.Credentials 3 | Exec=@DBUS_EXECUTABLE@ 4 | -------------------------------------------------------------------------------- /dbus/xyz.iinuwa.credentialsd.UiControl.service.in: -------------------------------------------------------------------------------- 1 | [D-BUS Service] 2 | Name=xyz.iinuwa.credentialsd.UiControl 3 | Exec=@UI_EXECUTABLE@ 4 | SystemdService=xyz.iinuwa.credentialsd.UiControl.service 5 | -------------------------------------------------------------------------------- /dbus/xyz.iinuwa.credentialsd.Credentials.service.in: -------------------------------------------------------------------------------- 1 | [D-BUS Service] 2 | Name=xyz.iinuwa.credentialsd.Credentials 3 | Exec=@DAEMON_EXECUTABLE@ 4 | SystemdService=xyz.iinuwa.credentialsd.Credentials.service 5 | -------------------------------------------------------------------------------- /dbus/xyz.iinuwa.credentialsd.FlowControl.service.in: -------------------------------------------------------------------------------- 1 | [D-BUS Service] 2 | Name=xyz.iinuwa.credentialsd.FlowControl 3 | Exec=@DAEMON_EXECUTABLE@ 4 | SystemdService=xyz.iinuwa.credentialsd.FlowControl.service 5 | -------------------------------------------------------------------------------- /systemd/xyz.iinuwa.credentialsd.UiControl.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Internal helper service for credentialsd 3 | 4 | [Service] 5 | Type=dbus 6 | BusName=xyz.iinuwa.credentialsd.UiControl 7 | ExecStart=@UI_EXECUTABLE@ 8 | -------------------------------------------------------------------------------- /systemd/xyz.iinuwa.credentialsd.FlowControl.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Internal helper service for credentialsd 3 | 4 | [Service] 5 | Type=dbus 6 | BusName=xyz.iinuwa.credentialsd.FlowControl 7 | ExecStart=@DAEMON_EXECUTABLE@ 8 | -------------------------------------------------------------------------------- /systemd/xyz.iinuwa.credentialsd.Credentials.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Service for creating and storing user credentials 3 | 4 | [Service] 5 | Type=dbus 6 | BusName=xyz.iinuwa.credentialsd.Credentials 7 | ExecStart=@DAEMON_EXECUTABLE@ 8 | -------------------------------------------------------------------------------- /credentialsd-ui/data/resources/meson.build: -------------------------------------------------------------------------------- 1 | # Resources 2 | resources = gnome.compile_resources( 3 | 'resources', 4 | 'resources.gresource.xml', 5 | gresource_bundle: true, 6 | source_dir: meson.current_build_dir(), 7 | install: true, 8 | install_dir: pkgdatadir, 9 | ) 10 | -------------------------------------------------------------------------------- /demo_client/util.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | def b64_encode(data: bytes) -> str: 4 | return base64.urlsafe_b64encode(data).rstrip(b'=').decode('ascii') 5 | 6 | def b64_decode(s) -> bytes: 7 | padding = '=' * (len(s) % 4) 8 | return base64.urlsafe_b64decode(s + padding) 9 | 10 | -------------------------------------------------------------------------------- /webext/app/credential_manager_shim.json.in: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xyz.iinuwa.credentialsd_helper", 3 | "description": "Helper for integrating browser with credentialsd project", 4 | "path": "@SHIM_SCRIPT@", 5 | "type": "stdio", 6 | "allowed_extensions": [ "credentialsd-helper@iinuwa.xyz" ] 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust 2 | target/ 3 | /Cargo.lock 4 | 5 | # Python 6 | /env 7 | __pycache__ 8 | 9 | # Build 10 | /build/ 11 | /_build/ 12 | /builddir/ 13 | /build-aux/app 14 | /build-aux/.flatpak-builder/ 15 | *.ui.in~ 16 | *.ui~ 17 | /.flatpak/ 18 | /vendor 19 | 20 | # IDE 21 | /.vscode 22 | .idea 23 | -------------------------------------------------------------------------------- /credentialsd/tests/config/mod.rs.in: -------------------------------------------------------------------------------- 1 | pub const SERVICE_DIR: &'static str = @SERVICE_DIR@; 2 | pub const SERVICE_NAME: &'static str = "xyz.iinuwa.credentialsd.Credentials"; 3 | pub const PATH: &'static str = "/xyz/iinuwa/credentialsd/Credentials"; 4 | pub const INTERFACE: &'static str = "xyz.iinuwa.credentialsd.Credentials1"; 5 | -------------------------------------------------------------------------------- /demo_client/resources.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | window.ui 5 | style.css 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /credentialsd-ui/data/icons/meson.build: -------------------------------------------------------------------------------- 1 | install_data( 2 | '@0@.svg'.format(application_id), 3 | install_dir: iconsdir / 'hicolor' / 'scalable' / 'apps' 4 | ) 5 | 6 | install_data( 7 | '@0@-symbolic.svg'.format(base_id), 8 | install_dir: iconsdir / 'hicolor' / 'symbolic' / 'apps', 9 | rename: '@0@-symbolic.svg'.format(application_id) 10 | ) 11 | -------------------------------------------------------------------------------- /credentialsd-ui/po/POTFILES.in: -------------------------------------------------------------------------------- 1 | data/xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in 2 | data/xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in 3 | data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in 4 | data/resources/ui/shortcuts.ui 5 | data/resources/ui/window.ui 6 | src/gui/view_model/gtk/mod.rs 7 | src/gui/view_model/gtk/device.rs 8 | src/gui/view_model/mod.rs 9 | -------------------------------------------------------------------------------- /credentialsd-ui/src/config.rs.in: -------------------------------------------------------------------------------- 1 | pub const APP_ID: &str = @APP_ID@; 2 | pub const GETTEXT_PACKAGE: &str = @GETTEXT_PACKAGE@; 3 | pub const LOCALEDIR: &str = @LOCALEDIR@; 4 | pub const PKGDATADIR: &str = @PKGDATADIR@; 5 | pub const PROFILE: &str = @PROFILE@; 6 | pub const RESOURCES_FILE: &str = concat!(@PKGDATADIR@, "/resources.gresource"); 7 | pub const VERSION: &str = @VERSION@; 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [unreleased] 2 | 3 | ## Breaking Changes 4 | 5 | ### UI Controller API 6 | 7 | - Renamed `InitiateEventStream()` to `Subscribe()` 8 | - Serialize `BackgroundEvent`, `HybridState`, `UsbState` as tag-value structs 9 | 10 | # [0.1.0] - 2025-08-14 11 | 12 | ## Breaking Changes 13 | 14 | None. 15 | 16 | ## Improvements 17 | 18 | - Initial release! 🎉 Includes support for USB and hybrid QR code credentials. 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | indent_style = space 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | charset = utf-8 8 | 9 | [*.{build,css,doap,scss,ui,xml,xml.in,xml.in.in,yaml,yml}] 10 | indent_size = 2 11 | 12 | [*.{json,py,rs}] 13 | indent_size = 4 14 | 15 | [*.{c,h,h.in}] 16 | indent_size = 2 17 | max_line_length = 80 18 | 19 | [NEWS] 20 | indent_size = 2 21 | max_line_length = 72 22 | -------------------------------------------------------------------------------- /credentialsd-ui/po/meson.build: -------------------------------------------------------------------------------- 1 | i18n = import('i18n') 2 | 3 | # This creates build targets: 'credentialsd-ui-pot', 'credentialsd-ui-update-po', etc. 4 | i18n.gettext(gettext_package, 5 | args: ['--directory=' + meson.project_source_root() / 'credentialsd-ui', 6 | '--from-code=UTF-8', 7 | '--copyright-holder="The Credentials for Linux Project"', 8 | '--msgid-bugs-address="https://github.com/linux-credentials/credentialsd/issues"', 9 | '--add-comments=TRANSLATORS:' 10 | ], 11 | ) 12 | -------------------------------------------------------------------------------- /credentialsd-ui/data/xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Credential Manager 3 | Comment=Write a GTK + Rust application 4 | Type=Application 5 | Exec=credentialsd 6 | Terminal=false 7 | Categories=GNOME;GTK; 8 | # Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! 9 | Keywords=Gnome;GTK; 10 | # Translators: Do NOT translate or transliterate this text (this is an icon file name)! 11 | Icon=@icon@ 12 | StartupNotify=true 13 | -------------------------------------------------------------------------------- /credentialsd-ui/data/resources/resources.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ui/shortcuts.ui 6 | ui/window.ui 7 | style.css 8 | 9 | 10 | -------------------------------------------------------------------------------- /meson.options: -------------------------------------------------------------------------------- 1 | option( 2 | 'profile', 3 | type: 'combo', 4 | choices: ['default', 'development'], 5 | value: 'default', 6 | description: 'The build profile for Credential Manager. One of "default" or "development".', 7 | ) 8 | option( 9 | 'cargo_home', 10 | type: 'string', 11 | description: 'The directory to store files downloaded by Cargo', 12 | ) 13 | option( 14 | 'cargo_offline', 15 | type: 'boolean', 16 | value: false, 17 | description: 'Whether to perform an offline build with Cargo. Defaults to false to download crates from registries.', 18 | ) -------------------------------------------------------------------------------- /credentialsd-common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "credentialsd-common" 3 | version = "0.1.0" 4 | edition = "2024" 5 | authors = ["Isaiah Inuwa ", "Martin Sirringhaus ", "Alfie Fresta "] 6 | license = "LGPL-3.0-only" 7 | 8 | [dependencies] 9 | futures-lite = "2.6.0" 10 | # libwebauthn = "0.2" 11 | libwebauthn = { git = "https://github.com/linux-credentials/libwebauthn.git", revision="604b53b", features = ["libnfc","pcsc"] } 12 | serde = { version = "1", features = ["derive"] } 13 | zvariant = "5.6.0" 14 | -------------------------------------------------------------------------------- /webext/add-on/meson.build: -------------------------------------------------------------------------------- 1 | zip = find_program('zip') 2 | 3 | addon_dir = datadir / 'credentialsd' 4 | xpi_files = ['manifest.json', 'background.js', 'content.js', 'icons' / 'logo.svg'] 5 | custom_target( 6 | 'xpi', 7 | output: 'credentialsd-firefox-helper.xpi', 8 | input: xpi_files, 9 | command: [ 10 | 'pwd', 11 | '&&', 12 | 'cd', 13 | '@CURRENT_SOURCE_DIR@', 14 | '&&', 15 | zip, 16 | '-r', 17 | '-FS', meson.project_build_root() / '@OUTPUT@', 18 | xpi_files, 19 | '--exclude', 'icons/LICENSE', 20 | ], 21 | install: true, 22 | install_dir: addon_dir, 23 | ) -------------------------------------------------------------------------------- /credentialsd-ui/data/xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 600 6 | Window width 7 | 8 | 9 | 400 10 | Window height 11 | 12 | 13 | false 14 | Window maximized state 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /credentialsd-common/meson.build: -------------------------------------------------------------------------------- 1 | # Currently, we're not building this with meson and just letting cargo path dependencies for the other projects build this lib. 2 | # Not efficient, since the UI and daemon projects will build it separately, but leaving it this way for now. 3 | 4 | common_lib_name = 'credentialsd-common' 5 | cargo = find_program('cargo', required: true) 6 | 7 | cargo_options = [ 8 | '--manifest-path', meson.project_source_root() / meson.current_source_dir() / 'Cargo.toml', 9 | ] 10 | cargo_options += [ 11 | '--target-dir', meson.project_build_root() / meson.current_build_dir() / 'target', 12 | ] 13 | if get_option('cargo_offline') == true 14 | cargo_options += ['--offline'] 15 | endif 16 | 17 | subdir('src') -------------------------------------------------------------------------------- /credentialsd-ui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "credentialsd-ui" 3 | version = "0.1.0" 4 | edition = "2024" 5 | authors = ["Isaiah Inuwa ", "Martin Sirringhaus ", "Alfie Fresta "] 6 | license = "LGPL-3.0-only" 7 | 8 | [dependencies] 9 | async-std = { version = "1.13.1", features = ["unstable"] } 10 | credentialsd-common = { path = "../credentialsd-common" } 11 | futures-lite = "2.6.0" 12 | gettext-rs = { version = "0.7", features = ["gettext-system"] } 13 | gtk = { version = "0.9.6", package = "gtk4", features = ["v4_6"] } 14 | qrcode = "0.14.1" 15 | serde = { version = "1.0.219", features = ["derive"] } 16 | tracing = "0.1.41" 17 | tracing-subscriber = "0.3.19" 18 | zbus = "5.9.0" 19 | -------------------------------------------------------------------------------- /webext/app/meson.build: -------------------------------------------------------------------------------- 1 | addon_app_executable_name = 'credentialsd-firefox-helper' 2 | addon_app_config = configuration_data() 3 | addon_app_config.set('SHIM_SCRIPT', bindir / addon_app_executable_name) 4 | addon_app_config.set( 5 | 'DBUS_DOC_FILE', 6 | datadir / 'credentialsd' / 'xyz.iinuwa.credentialsd.Credentials.xml', 7 | ) 8 | 9 | native_messaging_manifest_dir = libdir / 'mozilla' / 'native-messaging-hosts' 10 | 11 | configure_file( 12 | input: 'credential_manager_shim.json.in', 13 | install_dir: native_messaging_manifest_dir, 14 | output: 'xyz.iinuwa.credentialsd_helper.json', 15 | configuration: addon_app_config, 16 | ) 17 | 18 | configure_file( 19 | input: 'credential_manager_shim.py', 20 | install_dir: bindir, 21 | output: addon_app_executable_name, 22 | configuration: addon_app_config, 23 | ) -------------------------------------------------------------------------------- /webext/add-on/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Helper to integrate credentialsd with the browser", 3 | "manifest_version": 3, 4 | "name": "credentialsd-helper", 5 | "version": "0.1.0", 6 | "icons": { 7 | "48": "icons/logo.svg" 8 | }, 9 | 10 | "browser_specific_settings": { 11 | "gecko": { 12 | "id": "credentialsd-helper@iinuwa.xyz", 13 | "strict_min_version": "140.0" 14 | } 15 | }, 16 | 17 | "background": { 18 | "scripts": ["background.js"] 19 | }, 20 | "content_scripts": [ 21 | { 22 | "matches": ["https://webauthn.io/*", "https://demo.yubico.com/*"], 23 | "js": ["content.js"], 24 | "run_at": "document_start" 25 | } 26 | ], 27 | 28 | "action": { 29 | "default_icon": "icons/logo.svg" 30 | }, 31 | 32 | "permissions": ["nativeMessaging"] 33 | } 34 | -------------------------------------------------------------------------------- /credentialsd/src/serde/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod b64 { 2 | use base64::{self, engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; 3 | 4 | use serde::{de, Deserialize, Deserializer}; 5 | 6 | pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 7 | where 8 | D: Deserializer<'de>, 9 | { 10 | let s = String::deserialize(deserializer)?; 11 | URL_SAFE_NO_PAD.decode(s).map_err(de::Error::custom) 12 | } 13 | } 14 | 15 | pub(crate) mod duration { 16 | use std::time::Duration; 17 | 18 | use serde::{Deserialize, Deserializer}; 19 | 20 | pub(crate) fn from_opt_ms<'de, D>(deserializer: D) -> Result, D::Error> 21 | where 22 | D: Deserializer<'de>, 23 | { 24 | Option::::deserialize(deserializer) 25 | .map(|ms_opt| ms_opt.map(|ms| Duration::from_millis(ms as u64))) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /dbus/meson.build: -------------------------------------------------------------------------------- 1 | dbus_config = configuration_data() 2 | dbus_service_dir = datadir / 'dbus-1' / 'services' 3 | dbus_config.set('DAEMON_EXECUTABLE', bindir / backend_executable_name) 4 | dbus_config.set( 5 | 'UI_EXECUTABLE', 6 | bindir / gui_executable_name, 7 | ) 8 | configure_file( 9 | input: 'xyz.iinuwa.credentialsd.Credentials.service.in', 10 | install_dir: dbus_service_dir, 11 | output: 'xyz.iinuwa.credentialsd.Credentials.service', 12 | configuration: dbus_config, 13 | ) 14 | configure_file( 15 | input: 'xyz.iinuwa.credentialsd.FlowControl.service.in', 16 | install_dir: dbus_service_dir, 17 | output: 'xyz.iinuwa.credentialsd.FlowControl.service', 18 | configuration: dbus_config, 19 | ) 20 | configure_file( 21 | input: 'xyz.iinuwa.credentialsd.UiControl.service.in', 22 | install_dir: dbus_service_dir, 23 | output: 'xyz.iinuwa.credentialsd.UiControl.service', 24 | configuration: dbus_config, 25 | ) -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'credentialsd', 3 | 'rust', 4 | version: '0.1.0', 5 | meson_version: '>= 1.5.0', 6 | license: 'LGPL-3.0-only', 7 | ) 8 | 9 | version = meson.project_version() 10 | 11 | prefix = get_option('prefix') 12 | bindir = prefix / get_option('bindir') 13 | libdir = prefix / get_option('libdir') 14 | localedir = prefix / get_option('localedir') 15 | 16 | cargo_home = get_option('cargo_home') 17 | if cargo_home == '' 18 | cargo_home = meson.project_build_root() / 'cargo-home' 19 | endif 20 | 21 | meson.add_dist_script( 22 | 'build-aux/dist-vendor.sh', 23 | meson.project_build_root() / 'meson-dist' / meson.project_name() 24 | + '-' 25 | + version, 26 | meson.project_source_root(), 27 | ) 28 | 29 | subdir('credentialsd-common') 30 | subdir('credentialsd') 31 | subdir('credentialsd-ui') 32 | subdir('dbus') 33 | subdir('systemd') 34 | subdir('webext') 35 | subdir('doc') 36 | subdir('demo_client') 37 | -------------------------------------------------------------------------------- /credentialsd/src/meson.build: -------------------------------------------------------------------------------- 1 | if get_option('profile') == 'default' 2 | cargo_options += ['--release'] 3 | rust_target = 'release' 4 | message('Building in release mode') 5 | else 6 | rust_target = 'debug' 7 | message('Building in debug mode') 8 | endif 9 | 10 | cargo_env = ['CARGO_HOME=' + cargo_home] 11 | message('@0@'.format(cargo_options)) 12 | 13 | custom_target( 14 | 'cargo-build', 15 | build_by_default: true, 16 | build_always_stale: true, 17 | output: backend_executable_name, 18 | console: true, 19 | install: true, 20 | install_dir: bindir, 21 | command: [ 22 | 'env', 23 | cargo_env, 24 | cargo, 25 | 'build', 26 | cargo_options, 27 | '&&', 28 | 'cp', 29 | backend_build_dir / 'target' / rust_target / backend_executable_name, 30 | '@OUTPUT@', 31 | ], 32 | ) 33 | 34 | test( 35 | 'credentialsd cargo-unit-tests', 36 | cargo, 37 | env: [cargo_env], 38 | args: [ 39 | 'test', 40 | '--bins', 41 | '--no-fail-fast', cargo_options, 42 | '--', 43 | '--nocapture', 44 | ], 45 | protocol: 'exitcode', 46 | ) -------------------------------------------------------------------------------- /systemd/meson.build: -------------------------------------------------------------------------------- 1 | systemd_config = configuration_data() 2 | # HACK: Not using libdir option, since on some distros (Fedora), libdir is `lib64`, but systemd is always in `lib` 3 | # If you know of a better way to do this, let me know 4 | systemd_user_service_dir = prefix / 'lib' / 'systemd' / 'user' 5 | systemd_config.set('DAEMON_EXECUTABLE', bindir / backend_executable_name) 6 | systemd_config.set( 7 | 'UI_EXECUTABLE', 8 | bindir / gui_executable_name, 9 | ) 10 | configure_file( 11 | input: 'xyz.iinuwa.credentialsd.Credentials.service.in', 12 | install_dir: systemd_user_service_dir, 13 | output: 'xyz.iinuwa.credentialsd.Credentials.service', 14 | configuration: systemd_config, 15 | ) 16 | configure_file( 17 | input: 'xyz.iinuwa.credentialsd.FlowControl.service.in', 18 | install_dir: systemd_user_service_dir, 19 | output: 'xyz.iinuwa.credentialsd.FlowControl.service', 20 | configuration: systemd_config, 21 | ) 22 | configure_file( 23 | input: 'xyz.iinuwa.credentialsd.UiControl.service.in', 24 | install_dir: systemd_user_service_dir, 25 | output: 'xyz.iinuwa.credentialsd.UiControl.service', 26 | configuration: systemd_config, 27 | ) 28 | -------------------------------------------------------------------------------- /credentialsd/src/dbus/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module hosts the D-Bus endpoints needed for this service. 2 | //! 3 | //! There are two services that run in this process: the gateway and the flow 4 | //! controller. 5 | //! 6 | //! The gateway is accessed by public clients and initiates new requests. 7 | //! 8 | //! The flow controller launches a UI and receives user interaction events. 9 | //! 10 | //! There is also a client to reach out to the UI controller hosted by the trusted UI. 11 | 12 | mod flow_control; 13 | mod gateway; 14 | mod model; 15 | mod ui_control; 16 | 17 | use self::model::{ 18 | create_credential_request_try_into_ctap2, create_credential_response_try_from_ctap2, 19 | get_credential_request_try_into_ctap2, get_credential_response_try_from_ctap2, 20 | }; 21 | 22 | pub use self::{ 23 | flow_control::{ 24 | start_flow_control_service, CredentialRequestController, CredentialRequestControllerClient, 25 | }, 26 | gateway::start_gateway, 27 | ui_control::UiControlServiceClient, 28 | }; 29 | 30 | #[cfg(test)] 31 | pub mod test { 32 | pub use super::flow_control::test::DummyFlowServer; 33 | pub use super::ui_control::test::DummyUiServer; 34 | } 35 | -------------------------------------------------------------------------------- /credentialsd/tests/meson.build: -------------------------------------------------------------------------------- 1 | test_config = configuration_data() 2 | test_config.set_quoted( 3 | 'SERVICE_DIR', 4 | meson.project_build_root() / meson.current_build_dir(), 5 | ) 6 | test_config.set( 7 | 'DBUS_EXECUTABLE', 8 | meson.project_build_root() / backend_build_dir / 'target' / rust_target / backend_executable_name, 9 | ) 10 | configure_file( 11 | input: 'config' / 'mod.rs.in', 12 | output: 'config.rs', 13 | configuration: test_config, 14 | ) 15 | 16 | # Copy the config output to the source directory. 17 | run_command( 18 | 'cp', 19 | meson.project_build_root() / meson.current_build_dir() / 'config.rs', 20 | meson.project_source_root() / meson.current_source_dir() / 'config' / 'mod.rs', 21 | check: true, 22 | ) 23 | 24 | configure_file( 25 | input: 'services' / 'xyz.iinuwa.credentialsd.Credentials.service.in', 26 | output: 'xyz.iinuwa.credentialsd.Credentials.service', 27 | configuration: test_config, 28 | ) 29 | 30 | test( 31 | 'dbus', 32 | cargo, 33 | env: [cargo_env], 34 | args: [ 35 | 'test', 36 | '--test', 'dbus', 37 | '--no-fail-fast', cargo_options, 38 | '--', 39 | '--nocapture', 40 | ], 41 | protocol: 'exitcode', 42 | verbose: true, 43 | ) -------------------------------------------------------------------------------- /credentialsd-common/src/client.rs: -------------------------------------------------------------------------------- 1 | use std::pin::Pin; 2 | 3 | use futures_lite::Stream; 4 | 5 | use crate::{ 6 | model::{BackgroundEvent, Device}, 7 | server::RequestId, 8 | }; 9 | 10 | /// Used for communication from trusted UI to credential service 11 | pub trait FlowController { 12 | fn get_available_public_key_devices( 13 | &self, 14 | ) -> impl Future, ()>> + Send; 15 | 16 | fn get_hybrid_credential(&mut self) -> impl Future> + Send; 17 | fn get_usb_credential(&mut self) -> impl Future> + Send; 18 | fn get_nfc_credential(&mut self) -> impl Future> + Send; 19 | fn subscribe( 20 | &mut self, 21 | ) -> impl Future< 22 | Output = Result + Send + 'static>>, ()>, 23 | > + Send; 24 | fn enter_client_pin(&mut self, pin: String) -> impl Future> + Send; 25 | fn select_credential( 26 | &self, 27 | credential_id: String, 28 | ) -> impl Future> + Send; 29 | fn cancel_request(&self, request_id: RequestId) -> impl Future> + Send; 30 | } 31 | -------------------------------------------------------------------------------- /credentialsd-ui/data/resources/ui/shortcuts.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | True 5 | 6 | 7 | shortcuts 8 | 10 9 | 10 | 11 | General 12 | 13 | 14 | Show Shortcuts 15 | win.show-help-overlay 16 | 17 | 18 | 19 | 20 | Quit 21 | app.quit 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /credentialsd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "credentialsd" 3 | version = "0.1.0" 4 | authors = ["Isaiah Inuwa ", "Martin Sirringhaus ", "Alfie Fresta "] 5 | edition = "2021" 6 | license = "LGPL-3.0-only" 7 | 8 | [profile.release] 9 | lto = true 10 | 11 | [dependencies] 12 | async-stream = "0.3.6" 13 | async-trait = "0.1.88" 14 | base64 = "0.22.1" 15 | credentialsd-common = { path = "../credentialsd-common" } 16 | futures-lite = "2.6.0" 17 | libwebauthn = { git = "https://github.com/linux-credentials/libwebauthn.git", revision="604b53b", features = ["libnfc","pcsc"] } 18 | # libwebauthn = "~0.2.2" 19 | openssl = "0.10.72" 20 | rand = "0.9.2" 21 | ring = "0.17.14" 22 | rustls = { version = "0.23.27", default-features = false, features = ["std", "tls12", "ring", "log", "logging", "prefer-post-quantum"] } 23 | serde = { version = "1.0.219", features = ["derive"] } 24 | serde_json = "1.0.140" 25 | tokio = { version = "1.45.0", features = ["rt-multi-thread"] } 26 | tracing = "0.1.41" 27 | tracing-subscriber = "0.3" 28 | zbus = { version = "5.9.0", default-features = false, features = ["tokio"] } 29 | 30 | [dev-dependencies] 31 | gio = "0.21.0" 32 | zbus = { version = "5.9.0", default-features = false, features = ["blocking-api", "tokio"] } 33 | -------------------------------------------------------------------------------- /doc/historical/ecosystem.md: -------------------------------------------------------------------------------- 1 | ```mermaid 2 | graph 3 | Apps; 4 | CS[Credential Service]; 5 | CMA[Credential Management App]; 6 | 7 | AuthUI[Authentication UI]; 8 | 9 | AFS[Autofill Service]; 10 | AFP[Autofill Providers]; 11 | AFUI[Autofill UI]; 12 | 13 | FPD[FingerPrint Sensor Drivers] 14 | SDCP[Secure Device Connection Protocol]; 15 | FPS[Match-on-sensor FingerPrint Sensors]; 16 | BIO[Biometric Service]; 17 | PAM; 18 | GNOME-AFW[GNOME Autofill Widgets]; 19 | 20 | PWAuth[Password Auth] 21 | PinAuth[PIN Auth] 22 | Flatpak; 23 | Flathub; 24 | SIGN[App Signatures]; 25 | Sigstore; 26 | WAF[Web Application Manifest]; 27 | 28 | XDGP[XDG Portal]; 29 | DBUS[D-Bus]; 30 | 31 | 32 | Apps-->CS; 33 | Apps-->GNOME-AFW; 34 | CMA-->CS; 35 | CMA-->GNOME; 36 | 37 | AuthUI-->GNOME-AFW; 38 | GNOME-AFW-->GNOME; 39 | GNOME-->GTK; 40 | AFUI-->AFS; 41 | AFUI-->AFP; 42 | 43 | AFP-->CS; 44 | 45 | CS-->PAM; 46 | PAM-->BIO; 47 | BIO-->libfprint; 48 | libfprint-->FPD; 49 | FPD-->SDCP; 50 | SDCP-->FPS; 51 | 52 | PAM-->PWAuth; 53 | PAM-->PinAuth; 54 | 55 | Flathub -->Flatpak; 56 | Flatpak-->SIGN; 57 | SIGN-->Sigstore; 58 | SIGN-->WAF; 59 | Flatpak-->XDGP; 60 | XDGP-->DBUS; 61 | DBUS-->CS-REG; 62 | DBUS-->CS-AUTH; 63 | CS-->DBUS; 64 | ``` 65 | -------------------------------------------------------------------------------- /credentialsd-common/src/meson.build: -------------------------------------------------------------------------------- 1 | # Currently, we're not building this with meson and just letting cargo path dependencies for the other projects build this lib. 2 | # Not efficient, since the UI and daemon projects will build it separately, but leaving it this way for now. 3 | 4 | if get_option('profile') == 'default' 5 | cargo_options += ['--release'] 6 | rust_target = 'release' 7 | message('Building in release mode') 8 | else 9 | rust_target = 'debug' 10 | message('Building in debug mode') 11 | endif 12 | 13 | cargo_env = ['CARGO_HOME=' + cargo_home] 14 | # 15 | # custom_target( 16 | # 'cargo-build', 17 | # build_by_default: true, 18 | # build_always_stale: true, 19 | # output: common_lib_name, 20 | # console: true, 21 | # install: true, 22 | # install_dir: bindir, 23 | # command: [ 24 | # 'env', 25 | # cargo_env, 26 | # cargo, 27 | # 'build', 28 | # cargo_options, 29 | # '&&', 30 | # 'cp', 31 | # common_lib_name / 'src' / rust_target / common_lib_name, 32 | # '@OUTPUT@', 33 | # ], 34 | # ) 35 | test( 36 | 'credentialsd-common cargo-unit-tests', 37 | cargo, 38 | env: [cargo_env], 39 | args: [ 40 | 'test', 41 | '--lib', 42 | '--no-fail-fast', cargo_options, 43 | '--', 44 | '--nocapture', 45 | ], 46 | protocol: 'exitcode', 47 | ) -------------------------------------------------------------------------------- /demo_client/window.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $MyAppWindow: ApplicationWindow { 5 | default-width: 600; 6 | default-height: 300; 7 | title: _("Hello, Blueprint!"); 8 | 9 | Box { 10 | orientation: vertical; 11 | margin-start: 100; 12 | margin-end: 100; 13 | 14 | Entry username { 15 | placeholder-text: _("Enter your username"); 16 | } 17 | 18 | Box { 19 | orientation: horizontal; 20 | 21 | Button make_credential_btn { 22 | label: "Register"; 23 | clicked => $on_register(); 24 | } 25 | 26 | Button get_assertion_btn { 27 | label: "Authenticate"; 28 | clicked => $on_authenticate(); 29 | } 30 | } 31 | 32 | Adw.ExpanderRow { 33 | title: "Settings"; 34 | margin-top: 50; 35 | 36 | Adw.PreferencesGroup { 37 | Adw.ActionRow { 38 | title: "User Verification"; 39 | 40 | DropDown uv_pref_dropdown { 41 | model: bind template.uv_pref; 42 | } 43 | } 44 | 45 | Adw.ActionRow { 46 | title: "Discoverable Credential"; 47 | 48 | DropDown discoverable_cred_pref_dropdown { 49 | model: bind template.discoverable_cred_pref; 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /demo_client/meson.build: -------------------------------------------------------------------------------- 1 | dependency('dbus-1', version: '>= 1.6') 2 | dependency('glib-2.0', version: '>= 2.66') 3 | dependency('gio-2.0', version: '>= 2.66') 4 | dependency('gtk4', version: '>= 4.6.2') 5 | 6 | glib_compile_resources = find_program('glib-compile-resources', required: true) 7 | 8 | demo_blueprints = custom_target( 9 | 'demo-blueprints', 10 | input: files('window.blp'), 11 | output: 'ui', 12 | command: [ 13 | find_program('blueprint-compiler', required: false), 14 | 'batch-compile', 15 | '@OUTPUT@', 16 | '@CURRENT_SOURCE_DIR@', 17 | '@INPUT@', 18 | ], 19 | ) 20 | 21 | demo_resources = custom_target( 22 | 'demo-resource', 23 | input: files('resources.gresource.xml'), 24 | depend_files: files('style.css'), 25 | depfile: 'gresource.deps', 26 | output: 'resources.gresource', 27 | command: [ 28 | glib_compile_resources, 29 | '--target=@OUTPUT@', 30 | '--dependency-file=@DEPFILE@', 31 | '--sourcedir=@CURRENT_SOURCE_DIR@', 32 | '--sourcedir', demo_blueprints[0], 33 | '@INPUT@', 34 | ], 35 | ) 36 | 37 | gui_sources = files( 38 | '../doc/xyz.iinuwa.credentialsd.Credentials.xml', 39 | 'cbor.py', 40 | 'gui.py', 41 | 'main.py', 42 | 'util.py', 43 | 'webauthn.py', 44 | ) 45 | 46 | custom_target( 47 | 'demo-gui', 48 | input: [gui_sources], 49 | build_by_default: false, 50 | output: '.', 51 | depends: [demo_resources], 52 | command: ['cp', '@INPUT@', '@OUTPUT@'], 53 | ) 54 | -------------------------------------------------------------------------------- /credentialsd-ui/src/main.rs: -------------------------------------------------------------------------------- 1 | mod client; 2 | #[rustfmt::skip] 3 | mod config; 4 | mod dbus; 5 | mod gui; 6 | 7 | use std::error::Error; 8 | 9 | use crate::{client::DbusCredentialClient, dbus::UiControlService}; 10 | 11 | fn main() -> Result<(), Box> { 12 | tracing_subscriber::fmt::init(); 13 | tracing::debug!("Starting credentials UI service"); 14 | async_std::task::block_on(run()) 15 | } 16 | 17 | async fn run() -> Result<(), Box> { 18 | print!("Starting GUI thread...\t"); 19 | let (request_tx, request_rx) = async_std::channel::bounded(2); 20 | // this allows the D-Bus service to signal to the GUI to draw a window for 21 | // executing the credential flow. 22 | let client_conn = zbus::connection::Builder::session()?.build().await?; 23 | let cred_client = DbusCredentialClient::new(client_conn); 24 | let _handle = gui::start_gui_thread(request_rx, cred_client)?; 25 | println!(" ✅"); 26 | 27 | print!("Starting UI Control listener...\t"); 28 | let interface = UiControlService { request_tx }; 29 | let path = "/xyz/iinuwa/credentialsd/UiControl"; 30 | let service = "xyz.iinuwa.credentialsd.UiControl"; 31 | let _server_conn = zbus::connection::Builder::session()? 32 | .name(service)? 33 | .serve_at(path, interface)? 34 | .build() 35 | .await?; 36 | println!(" ✅"); 37 | loop { 38 | std::future::pending::<()>().await; 39 | } 40 | #[allow(unreachable_code)] 41 | { 42 | _ = _handle.join(); 43 | Ok(()) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug Daemon (credentialsd)", 11 | "program": "${workspaceFolder}/build/credentialsd/src/credentialsd", 12 | "args": [], 13 | "env": { 14 | "RUST_LOG": "credentialsd=debug,libwebauthn=debug,libwebauthn::webauthn=debug,libwebauthn=warn,libwebauthn::proto::ctap2::preflight=debug,libwebauthn::transport::channel=debug,zbus::object_server::debug,zbus=debug" 15 | }, 16 | "sourceLanguages": ["rust"], 17 | "cwd": "${workspaceFolder}", 18 | "preLaunchTask": "Meson: Build Daemon" 19 | }, 20 | { 21 | "type": "lldb", 22 | "request": "launch", 23 | "name": "Debug UI (credentialsd-ui)", 24 | "program": "${workspaceFolder}/build/credentialsd-ui/src/credentialsd-ui", 25 | "args": [], 26 | "env": { 27 | "GSETTINGS_SCHEMA_DIR": "${workspaceFolder}/build/credentialsd-ui/data", 28 | "RUST_LOG": "credentialsd_ui=debug,zbus::trace,zbus::object_server::debug" 29 | }, 30 | "sourceLanguages": ["rust"], 31 | "cwd": "${workspaceFolder}", 32 | "preLaunchTask": "Meson: Build UI" 33 | }, 34 | ], 35 | "compounds": [ 36 | { 37 | "name": "Server/Client", 38 | "configurations": ["Debug UI (credentialsd-ui)", "Debug Daemon (credentialsd)"] 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /credentialsd-ui/src/dbus.rs: -------------------------------------------------------------------------------- 1 | use async_std::channel::Sender; 2 | use credentialsd_common::{ 3 | model::BackgroundEvent, 4 | server::{Device, RequestId, ViewRequest}, 5 | }; 6 | use zbus::{fdo, interface, proxy}; 7 | 8 | #[proxy( 9 | gen_blocking = false, 10 | interface = "xyz.iinuwa.credentialsd.FlowControl1", 11 | default_path = "/xyz/iinuwa/credentialsd/FlowControl", 12 | default_service = "xyz.iinuwa.credentialsd.FlowControl" 13 | )] 14 | pub trait FlowControlService { 15 | async fn subscribe(&self) -> fdo::Result<()>; 16 | 17 | async fn get_available_public_key_devices(&self) -> fdo::Result>; 18 | 19 | async fn get_hybrid_credential(&self) -> fdo::Result<()>; 20 | 21 | async fn get_usb_credential(&self) -> fdo::Result<()>; 22 | async fn get_nfc_credential(&self) -> fdo::Result<()>; 23 | 24 | async fn select_device(&self, device_id: String) -> fdo::Result<()>; 25 | async fn enter_client_pin(&self, pin: String) -> fdo::Result<()>; 26 | async fn select_credential(&self, credential_id: String) -> fdo::Result<()>; 27 | async fn cancel_request(&self, request_id: RequestId) -> fdo::Result<()>; 28 | 29 | #[zbus(signal)] 30 | async fn state_changed(update: BackgroundEvent) -> zbus::Result<()>; 31 | } 32 | 33 | pub struct UiControlService { 34 | pub request_tx: Sender, 35 | } 36 | 37 | /// These methods are called by the credential service to control the UI. 38 | #[interface(name = "xyz.iinuwa.credentialsd.UiControl1")] 39 | impl UiControlService { 40 | async fn launch_ui(&self, request: ViewRequest) -> fdo::Result<()> { 41 | tracing::debug!("Received UI launch request"); 42 | self.request_tx 43 | .send(request) 44 | .await 45 | .map_err(|_| fdo::Error::Failed("UI failed to launch".to_string())) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /credentialsd-ui/src/gui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod view_model; 2 | 3 | use std::thread; 4 | use std::{sync::Arc, thread::JoinHandle}; 5 | 6 | use async_std::{channel::Receiver, sync::Mutex as AsyncMutex}; 7 | 8 | use credentialsd_common::server::ViewRequest; 9 | use credentialsd_common::{client::FlowController, model::ViewUpdate}; 10 | 11 | use view_model::ViewEvent; 12 | 13 | pub(super) fn start_gui_thread( 14 | rx: Receiver, 15 | flow_controller: F, 16 | ) -> Result, std::io::Error> { 17 | thread::Builder::new().name("gui".into()).spawn(move || { 18 | let flow_controller = Arc::new(AsyncMutex::new(flow_controller)); 19 | // D-Bus received a request and needs a window open 20 | while let Ok(view_request) = rx.recv_blocking() { 21 | run_gui(flow_controller.clone(), view_request); 22 | } 23 | }) 24 | } 25 | 26 | fn run_gui( 27 | flow_controller: Arc>, 28 | request: ViewRequest, 29 | ) { 30 | let (tx_update, rx_update) = async_std::channel::unbounded::(); 31 | let (tx_event, rx_event) = async_std::channel::unbounded::(); 32 | let event_loop = async_std::task::spawn(async move { 33 | let request_id = request.id; 34 | let mut vm = 35 | view_model::ViewModel::new(request, flow_controller.clone(), rx_event, tx_update); 36 | vm.start_event_loop().await; 37 | tracing::debug!("Finishing user request."); 38 | // If cancellation fails, that's fine. 39 | let _ = flow_controller 40 | .lock() 41 | .await 42 | .cancel_request(request_id) 43 | .await; 44 | }); 45 | 46 | view_model::gtk::start_gtk_app(tx_event, rx_update); 47 | 48 | async_std::task::block_on(event_loop.cancel()); 49 | } 50 | -------------------------------------------------------------------------------- /credentialsd/meson.build: -------------------------------------------------------------------------------- 1 | backend_executable_name = 'credentialsd' 2 | backend_build_dir = meson.current_build_dir() 3 | base_id = 'xyz.iinuwa.credentialsd.Credentialsd' 4 | 5 | dependency('dbus-1', version: '>= 1.6') 6 | dependency('glib-2.0', version: '>= 2.66') 7 | dependency('gio-2.0', version: '>= 2.66') 8 | dependency('gtk4', version: '>= 4.6.2') 9 | dependency('ssl', 'openssl', version: '>= 3.0') 10 | dependency('udev', version: '>= 249') 11 | 12 | cargo = find_program('cargo', required: true) 13 | 14 | version = meson.project_version() 15 | 16 | if get_option('profile') == 'development' 17 | profile = 'Devel' 18 | vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD', check: false).stdout().strip() 19 | if vcs_tag == '' 20 | version_suffix = '-devel' 21 | else 22 | version_suffix = '-@0@'.format(vcs_tag) 23 | endif 24 | application_id = '@0@.@1@'.format(base_id, profile) 25 | else 26 | profile = '' 27 | version_suffix = '' 28 | application_id = base_id 29 | endif 30 | 31 | meson.add_dist_script( 32 | meson.project_source_root() / 'build-aux/dist-vendor.sh', 33 | meson.project_build_root() / 'meson-dist' / backend_executable_name 34 | + '-' 35 | + version, 36 | meson.project_source_root(), 37 | ) 38 | 39 | if get_option('profile') == 'development' 40 | # Setup pre-commit hook for ensuring coding style is always consistent 41 | message('Setting up git pre-commit hook..') 42 | run_command('cp', '-f', 'hooks/pre-commit.hook', '.git/hooks/pre-commit', check: false) 43 | endif 44 | 45 | cargo_options = [ 46 | '--manifest-path', meson.project_source_root() / meson.current_source_dir() / 'Cargo.toml', 47 | ] 48 | cargo_options += [ 49 | '--target-dir', meson.project_build_root() / meson.current_build_dir() / 'target', 50 | ] 51 | if get_option('cargo_offline') == true 52 | cargo_options += ['--offline'] 53 | endif 54 | 55 | subdir('src') 56 | subdir('tests') -------------------------------------------------------------------------------- /hooks/pre-commit.hook: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Source: https://gitlab.gnome.org/GNOME/fractal/blob/master/hooks/pre-commit.hook 3 | 4 | install_rustfmt() { 5 | if ! which rustup >/dev/null 2>&1; then 6 | curl https://sh.rustup.rs -sSf | sh -s -- -y 7 | export PATH=$PATH:$HOME/.cargo/bin 8 | if ! which rustup >/dev/null 2>&1; then 9 | echo "Failed to install rustup. Performing the commit without style checking." 10 | exit 0 11 | fi 12 | fi 13 | 14 | if ! rustup component list|grep rustfmt >/dev/null 2>&1; then 15 | echo "Installing rustfmt…" 16 | rustup component add rustfmt 17 | fi 18 | } 19 | 20 | if ! which cargo >/dev/null 2>&1 || ! cargo fmt --help >/dev/null 2>&1; then 21 | echo "Unable to check the project’s code style, because rustfmt could not be run." 22 | 23 | if [ ! -t 1 ]; then 24 | # No input is possible 25 | echo "Performing commit." 26 | exit 0 27 | fi 28 | 29 | echo "" 30 | echo "y: Install rustfmt via rustup" 31 | echo "n: Don't install rustfmt and perform the commit" 32 | echo "Q: Don't install rustfmt and abort the commit" 33 | 34 | echo "" 35 | while true 36 | do 37 | printf "%s" "Install rustfmt via rustup? [y/n/Q]: "; read yn < /dev/tty 38 | case $yn in 39 | [Yy]* ) install_rustfmt; break;; 40 | [Nn]* ) echo "Performing commit."; exit 0;; 41 | [Qq]* | "" ) echo "Aborting commit."; exit 1 >/dev/null 2>&1;; 42 | * ) echo "Invalid input";; 43 | esac 44 | done 45 | 46 | fi 47 | 48 | echo "--Checking style--" 49 | cargo fmt --all -- --check 50 | if test $? != 0; then 51 | echo "--Checking style fail--" 52 | echo "Please fix the above issues, either manually or by running: cargo fmt --all" 53 | 54 | exit 1 55 | else 56 | echo "--Checking style pass--" 57 | fi 58 | -------------------------------------------------------------------------------- /credentialsd/src/main.rs: -------------------------------------------------------------------------------- 1 | mod cbor; 2 | mod cose; 3 | mod credential_service; 4 | mod dbus; 5 | mod serde; 6 | mod webauthn; 7 | 8 | use std::{error::Error, sync::Arc}; 9 | 10 | use credential_service::nfc::InProcessNfcHandler; 11 | 12 | use crate::{ 13 | credential_service::{ 14 | hybrid::InternalHybridHandler, usb::InProcessUsbHandler, CredentialService, 15 | }, 16 | dbus::{CredentialRequestControllerClient, UiControlServiceClient}, 17 | }; 18 | 19 | #[tokio::main] 20 | async fn main() { 21 | // Initialize logger 22 | tracing_subscriber::fmt::init(); 23 | rustls::crypto::ring::default_provider() 24 | .install_default() 25 | .expect("Failed to install rustls crypto provider"); 26 | 27 | println!("Starting..."); 28 | run().await.unwrap(); 29 | } 30 | 31 | async fn run() -> Result<(), Box> { 32 | print!("Connecting to D-Bus as client...\t"); 33 | let dbus_client_conn = zbus::connection::Builder::session()?.build().await?; 34 | println!(" ✅"); 35 | 36 | print!("Starting D-Bus UI -> Credential control service..."); 37 | let ui_controller = UiControlServiceClient::new(dbus_client_conn); 38 | let credential_service = CredentialService::new( 39 | InternalHybridHandler::new(), 40 | InProcessUsbHandler {}, 41 | InProcessNfcHandler {}, 42 | Arc::new(ui_controller), 43 | ); 44 | let (_flow_control_conn, initiator) = 45 | dbus::start_flow_control_service(credential_service).await?; 46 | println!(" ✅"); 47 | 48 | print!("Starting D-Bus public client service..."); 49 | let initiator = CredentialRequestControllerClient { initiator }; 50 | let _gateway_conn = dbus::start_gateway(initiator).await?; 51 | println!(" ✅"); 52 | 53 | println!("Waiting for messages..."); 54 | loop { 55 | // wait forever, handle D-Bus in the background 56 | std::future::pending::<()>().await; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /credentialsd-ui/data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @app-id@ 5 | CC0 6 | 7 | 8 | Credential Manager 9 | Write a GTK + Rust application 10 | 11 | A boilerplate template for GTK + Rust. It uses Meson as a build system and has flatpak support by default. 12 | 13 | 14 | 15 | https://github.com/iinuwa/linux-webauthn-platform-api/raw/main/images/register-start.png 16 | Registering a credential 17 | 18 | 19 | https://github.com/iinuwa/linux-webauthn-platform-api 20 | https://github.com/iinuwa/linux-webauthn-platform-api/issues 21 | 22 | 23 | 24 | 25 | 26 | 30 | ModernToolkit 31 | HiDpiIcon 32 | 33 | 34 | Isaiah Inuwa 35 | 36 | 37 | Isaiah Inuwa 38 | 39 | isaiah.inuwa@gmail.com 40 | @gettext-package@ 41 | @app-id@.desktop 42 | 43 | -------------------------------------------------------------------------------- /doc/xyz.iinuwa.credentialsd.Credentials.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /credentialsd-ui/src/meson.build: -------------------------------------------------------------------------------- 1 | global_conf = configuration_data() 2 | global_conf.set_quoted('APP_ID', application_id) 3 | if (get_option('profile') == 'development') 4 | global_conf.set_quoted( 5 | 'PKGDATADIR', 6 | meson.project_build_root() / gui_build_dir / 'data' / 'resources', 7 | ) 8 | else 9 | global_conf.set_quoted('PKGDATADIR', pkgdatadir) 10 | endif 11 | if (get_option('profile') == 'development') 12 | global_conf.set_quoted( 13 | 'LOCALEDIR', 14 | meson.project_build_root() / gui_build_dir / 'po', 15 | ) 16 | else 17 | global_conf.set_quoted('LOCALEDIR', localedir) 18 | endif 19 | global_conf.set_quoted('PROFILE', profile) 20 | global_conf.set_quoted('VERSION', version + version_suffix) 21 | global_conf.set_quoted('GETTEXT_PACKAGE', gettext_package) 22 | configure_file(input: 'config.rs.in', output: 'config.rs', configuration: global_conf) 23 | 24 | # Copy the config.rs output to the source directory. 25 | run_command( 26 | 'cp', 27 | meson.project_build_root() / meson.current_build_dir() / 'config.rs', 28 | meson.project_source_root() / meson.current_source_dir() / 'config.rs', 29 | check: true, 30 | ) 31 | 32 | if get_option('profile') == 'default' 33 | cargo_options += ['--release'] 34 | rust_target = 'release' 35 | message('Building in release mode') 36 | else 37 | rust_target = 'debug' 38 | message('Building in debug mode') 39 | endif 40 | 41 | cargo_env = ['CARGO_HOME=' + cargo_home] 42 | 43 | custom_target( 44 | 'cargo-build', 45 | build_by_default: true, 46 | build_always_stale: true, 47 | output: gui_executable_name, 48 | console: true, 49 | install: true, 50 | install_dir: bindir, 51 | depends: resources, 52 | command: [ 53 | 'env', 54 | cargo_env, 55 | cargo, 56 | 'build', 57 | cargo_options, 58 | '&&', 59 | 'cp', 60 | gui_build_dir / 'target' / rust_target / gui_executable_name, 61 | '@OUTPUT@', 62 | ], 63 | ) 64 | 65 | test( 66 | 'credentials-ui cargo-unit-tests', 67 | cargo, 68 | env: [cargo_env], 69 | args: [ 70 | 'test', 71 | '--bins', 72 | '--no-fail-fast', cargo_options, 73 | '--', 74 | '--nocapture', 75 | ], 76 | protocol: 'exitcode', 77 | ) 78 | -------------------------------------------------------------------------------- /credentialsd-ui/src/gui/view_model/gtk/credential.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | 3 | use glib::Object; 4 | use gtk::glib; 5 | use gtk::prelude::*; 6 | use gtk::subclass::prelude::*; 7 | 8 | mod imp { 9 | use super::*; 10 | 11 | #[derive(glib::Properties, Default)] 12 | #[properties(wrapper_type = super::CredentialObject)] 13 | pub struct CredentialObject { 14 | #[property(get, set)] 15 | pub id: RefCell, 16 | 17 | #[property(get, set)] 18 | pub name: RefCell, 19 | 20 | #[property(get, set)] 21 | pub username: RefCell>, 22 | } 23 | 24 | // The central trait for subclassing a GObject 25 | #[glib::object_subclass] 26 | impl ObjectSubclass for CredentialObject { 27 | const NAME: &'static str = "CredentialManagerCredential"; 28 | type Type = super::CredentialObject; 29 | } 30 | 31 | // Trait shared by all GObjects 32 | #[glib::derived_properties] 33 | impl ObjectImpl for CredentialObject {} 34 | } 35 | 36 | glib::wrapper! { 37 | pub struct CredentialObject(ObjectSubclass); 38 | } 39 | 40 | impl CredentialObject { 41 | pub fn new(id: &str, name: &str, username: &Option) -> Self { 42 | let mut builder = Object::builder().property("id", id).property("name", name); 43 | if let Some(username) = username { 44 | builder = builder.property("username", username); 45 | } 46 | builder.build() 47 | } 48 | } 49 | 50 | impl From for CredentialObject { 51 | fn from(value: crate::gui::view_model::Credential) -> Self { 52 | Self::new(&value.id, &value.name, &value.username) 53 | } 54 | } 55 | 56 | impl From<&crate::gui::view_model::Credential> for CredentialObject { 57 | fn from(value: &crate::gui::view_model::Credential) -> Self { 58 | Self::new(&value.id, &value.name, &value.username) 59 | } 60 | } 61 | 62 | impl From for crate::gui::view_model::Credential { 63 | fn from(value: CredentialObject) -> Self { 64 | Self { 65 | id: value.id(), 66 | name: value.name(), 67 | username: value.username(), 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build project 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | env: 11 | RUST_LOG: debug 12 | name: Build 13 | runs-on: ubuntu-22.04 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Update apt cache 17 | run: sudo apt update 18 | - name: Install system dependencies 19 | run: | 20 | sudo apt install -y --no-install-recommends \ 21 | curl git build-essential \ 22 | libgtk-4-dev gettext libdbus-1-dev libssl-dev libudev-dev \ 23 | libxml2-utils blueprint-compiler desktop-file-utils \ 24 | python3-pip ninja-build libnfc-dev libpcsclite-dev 25 | - name: Install Meson 26 | run: | 27 | # Newer version needed for --interactive flag needed below 28 | python3 -m pip install --user -v 'meson==1.5.0' 29 | - name: Setup meson project 30 | run: meson setup build 31 | - name: Build 32 | run: ninja -C build 33 | - name: Test 34 | # We have to use the --interactive flag because of some 35 | # weird issue with meson hanging after cargo exits due to the TestDBus. 36 | # Probably has to do with forking the test processes. 37 | run: meson test --interactive 38 | working-directory: build/ 39 | - name: Check clippy recommendations (Common) 40 | run: env CARGO_HOME=build/cargo-home cargo clippy --manifest-path credentialsd-common/Cargo.toml --target-dir build/credentialsd-common/target/release 41 | - name: Check clippy recommendations (UI) 42 | run: env CARGO_HOME=build/cargo-home cargo clippy --manifest-path credentialsd-ui/Cargo.toml --target-dir build/credentialsd-ui/target/release 43 | - name: Check clippy recommendations (Server) 44 | run: env CARGO_HOME=build/cargo-home cargo clippy --manifest-path credentialsd/Cargo.toml --target-dir build/credentialsd/target/release 45 | - name: Check formatting (Common) 46 | run: cargo fmt --check 47 | working-directory: credentialsd-common 48 | - name: Check formatting (UI) 49 | run: cargo fmt --check 50 | working-directory: credentialsd-ui 51 | - name: Check formatting (Server) 52 | run: cargo fmt --check 53 | working-directory: credentialsd 54 | -------------------------------------------------------------------------------- /credentialsd-ui/data/meson.build: -------------------------------------------------------------------------------- 1 | subdir('icons') 2 | subdir('resources') 3 | # Desktop file 4 | desktop_conf = configuration_data() 5 | desktop_conf.set('icon', application_id) 6 | desktop_file = i18n.merge_file( 7 | type: 'desktop', 8 | input: configure_file( 9 | input: '@0@.desktop.in.in'.format(base_id), 10 | output: '@BASENAME@', 11 | configuration: desktop_conf, 12 | ), 13 | output: '@0@.desktop'.format(application_id), 14 | po_dir: podir, 15 | install: true, 16 | install_dir: datadir / 'applications', 17 | ) 18 | # Validate Desktop file 19 | if desktop_file_validate.found() 20 | test( 21 | 'validate-desktop', 22 | desktop_file_validate, 23 | args: [desktop_file.full_path()], 24 | depends: desktop_file, 25 | ) 26 | endif 27 | 28 | # Appdata 29 | appdata_conf = configuration_data() 30 | appdata_conf.set('app-id', application_id) 31 | appdata_conf.set('gettext-package', gettext_package) 32 | appdata_file = i18n.merge_file( 33 | input: configure_file( 34 | input: '@0@.metainfo.xml.in.in'.format(base_id), 35 | output: '@BASENAME@', 36 | configuration: appdata_conf, 37 | ), 38 | output: '@0@.metainfo.xml'.format(application_id), 39 | po_dir: podir, 40 | install: true, 41 | install_dir: datadir / 'metainfo', 42 | ) 43 | # Validate Appdata 44 | if appstreamcli.found() 45 | test( 46 | 'validate-appdata', 47 | appstreamcli, 48 | args: ['validate', '--no-net', '--explain', appdata_file.full_path()], 49 | depends: appdata_file, 50 | ) 51 | endif 52 | 53 | # GSchema 54 | gschema_conf = configuration_data() 55 | gschema_conf.set('app-id', application_id) 56 | gschema_conf.set('gettext-package', gettext_package) 57 | gschema_xml = configure_file( 58 | input: '@0@.gschema.xml.in'.format(base_id), 59 | output: '@0@.gschema.xml'.format(application_id), 60 | configuration: gschema_conf, 61 | install: true, 62 | install_dir: datadir / 'glib-2.0' / 'schemas', 63 | ) 64 | 65 | # Validata GSchema 66 | if glib_compile_schemas.found() 67 | test( 68 | 'validate-gschema', 69 | glib_compile_schemas, 70 | args: ['--strict', '--dry-run', meson.current_build_dir()], 71 | ) 72 | endif 73 | 74 | if get_option('profile') == 'development' 75 | custom_target( 76 | 'gschema', 77 | input: gschema_xml, 78 | output: 'gschema.compiled', 79 | command: [glib_compile_schemas, '--strict', meson.current_build_dir()], 80 | install: false, 81 | build_by_default: true, 82 | build_always_stale: true, 83 | ) 84 | endif -------------------------------------------------------------------------------- /credentialsd/tests/dbus.rs: -------------------------------------------------------------------------------- 1 | #[rustfmt::skip] 2 | mod config; 3 | 4 | use std::collections::HashMap; 5 | 6 | use client::DbusClient; 7 | use zbus::zvariant::Value; 8 | 9 | #[test] 10 | fn test_client_capabilities() { 11 | let client = DbusClient::new(); 12 | let msg = client.call_method("GetClientCapabilities", &()).unwrap(); 13 | let body = msg.body(); 14 | let rsp: HashMap = body 15 | .deserialize::>() 16 | .unwrap() 17 | .into_iter() 18 | .map(|(k, v)| (k, v.try_into().unwrap())) 19 | .collect(); 20 | 21 | let capabilities = HashMap::from([ 22 | ("conditionalCreate", false), 23 | ("conditionalGet", false), 24 | ("hybridTransport", true), 25 | ("passkeyPlatformAuthenticator", false), 26 | ("userVerifyingPlatformAuthenticator", false), 27 | ("relatedOrigins", false), 28 | ("signalAllAcceptedCredentials", false), 29 | ("signalCurrentUserDetails", false), 30 | ("signalUnknownCredential", false), 31 | ]); 32 | for (key, expected) in capabilities.iter() { 33 | let actual = rsp.get(*key).unwrap(); 34 | assert_eq!(*expected, *actual); 35 | } 36 | } 37 | 38 | mod client { 39 | use crate::config::{INTERFACE, PATH, SERVICE_DIR, SERVICE_NAME}; 40 | use gio::{TestDBus, TestDBusFlags}; 41 | use serde::Serialize; 42 | use zbus::{blocking::Connection, zvariant::DynamicType, Message}; 43 | 44 | pub(super) struct DbusClient { 45 | bus: TestDBus, 46 | } 47 | 48 | impl DbusClient { 49 | pub fn new() -> Self { 50 | let bus = TestDBus::new(TestDBusFlags::NONE); 51 | bus.add_service_dir(SERVICE_DIR); 52 | bus.up(); 53 | Self { bus } 54 | } 55 | 56 | pub fn call_method(&self, method_name: &str, body: &B) -> zbus::Result 57 | where 58 | B: Serialize + DynamicType, 59 | { 60 | let connection = Connection::session().unwrap(); 61 | let message = connection.call_method( 62 | Some(SERVICE_NAME), 63 | PATH, 64 | Some(INTERFACE), 65 | method_name, 66 | body, 67 | ); 68 | connection.close().unwrap(); 69 | message 70 | } 71 | } 72 | impl Drop for DbusClient { 73 | fn drop(&mut self) { 74 | self.bus.stop(); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /webext/add-on/icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 38 | 40 | 45 | 53 | 58 | 62 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /credentialsd/src/cose.rs: -------------------------------------------------------------------------------- 1 | use libwebauthn::proto::ctap2::Ctap2COSEAlgorithmIdentifier; 2 | use tracing::debug; 3 | 4 | #[derive(Clone, Copy, Debug, PartialEq)] 5 | #[repr(i64)] 6 | pub(super) enum CoseKeyType { 7 | Es256P256, 8 | EddsaEd25519, 9 | RS256, 10 | } 11 | 12 | #[derive(Clone, Copy, Debug, PartialEq)] 13 | pub enum CoseKeyAlgorithmIdentifier { 14 | ES256, 15 | EdDSA, 16 | RS256, 17 | } 18 | 19 | impl From for i64 { 20 | fn from(value: CoseKeyAlgorithmIdentifier) -> Self { 21 | match value { 22 | CoseKeyAlgorithmIdentifier::ES256 => -7, 23 | CoseKeyAlgorithmIdentifier::EdDSA => -8, 24 | CoseKeyAlgorithmIdentifier::RS256 => -257, 25 | } 26 | } 27 | } 28 | 29 | impl From for i128 { 30 | fn from(value: CoseKeyAlgorithmIdentifier) -> Self { 31 | match value { 32 | CoseKeyAlgorithmIdentifier::ES256 => -7, 33 | CoseKeyAlgorithmIdentifier::EdDSA => -8, 34 | CoseKeyAlgorithmIdentifier::RS256 => -257, 35 | } 36 | } 37 | } 38 | 39 | impl TryFrom for CoseKeyAlgorithmIdentifier { 40 | type Error = Error; 41 | 42 | fn try_from(value: Ctap2COSEAlgorithmIdentifier) -> Result { 43 | match value { 44 | Ctap2COSEAlgorithmIdentifier::EDDSA => Ok(CoseKeyAlgorithmIdentifier::EdDSA), 45 | Ctap2COSEAlgorithmIdentifier::ES256 => Ok(CoseKeyAlgorithmIdentifier::ES256), 46 | Ctap2COSEAlgorithmIdentifier::TOPT => { 47 | debug!("Unknown public key algorithm type: {:?}", value); 48 | Err(Error::Unsupported) 49 | } 50 | Ctap2COSEAlgorithmIdentifier::Unknown => Err(Error::Unsupported), 51 | } 52 | } 53 | } 54 | 55 | #[derive(Clone, Copy, PartialEq)] 56 | pub enum CoseEllipticCurveIdentifier { 57 | /// P-256 Elliptic Curve using uncompressed points. 58 | P256, 59 | /// P-384 Elliptic Curve using uncompressed points. 60 | P384, 61 | /// P-521 Elliptic Curve using uncompressed points. 62 | P521, 63 | /// Ed25519 Elliptic Curve using compressed points. 64 | Ed25519, 65 | } 66 | 67 | impl From for i64 { 68 | fn from(value: CoseEllipticCurveIdentifier) -> Self { 69 | match value { 70 | CoseEllipticCurveIdentifier::P256 => 1, 71 | CoseEllipticCurveIdentifier::P384 => 2, 72 | CoseEllipticCurveIdentifier::P521 => 3, 73 | CoseEllipticCurveIdentifier::Ed25519 => 6, 74 | } 75 | } 76 | } 77 | 78 | #[derive(Debug)] 79 | pub enum Error { 80 | InvalidKey, 81 | Unsupported, 82 | } 83 | -------------------------------------------------------------------------------- /credentialsd-ui/data/icons/symbolic-link-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /webext/README.md: -------------------------------------------------------------------------------- 1 | This is a web extension that allows browsers to connect to the D-Bus service 2 | provided by this project. It can be used for testing. 3 | 4 | Currently, this is written only for Firefox; there will be some slight API 5 | tweaks required to make this work in Chrome. 6 | 7 | This requires some setup to make it work: 8 | 9 | # Prerequisites 10 | 11 | Currently, this web extension relies on the `dbus-next` Python package to 12 | interact with D-Bus services. If you have that package installed in your system 13 | Python, this should work. You can test using the following: 14 | 15 | ```shell 16 | python3 -c 'import dbus_next; print("dbus-next is installed")' 17 | ``` 18 | 19 | If that completes without error, then you're good to go. Otherwise, you have a 20 | couple of options: 21 | 22 | - Install the system package for your operating system, for example: 23 | ```shell 24 | # Fedora 25 | dnf install python3-dbus-next 26 | # Debian/Ubuntu 27 | apt install python3-dbus-next 28 | # Arch 29 | pacman -S python-dbus-next 30 | ``` 31 | - Modify the shebang to point to a Python instance that does have the package installed. 32 | ```shell 33 | cd webext/ 34 | python3 -m venv env 35 | source ./env/bin/activate 36 | pip3 install dbus-next 37 | echo "Change the first line in webext/app/credential_manager_shim.py to:" 38 | echo "#!$(readlink -f ./env/bin/python3)" 39 | ``` 40 | 41 | # Setup Instructions 42 | 43 | ## For Testing 44 | 45 | 1. Follow the instructions in the ["For Installing/Testing" section of `BUILDING.md`](/BUILDING.md#for-installing-testing). 46 | 2. Open Firefox and go to `about:debugging`. 47 | 3. Click "This Firefox" > Load Temporary Extension. Select `/usr/local/share/credentialsd/credentialsd-firefox-helper.xpi`. 48 | 4. Navigate to [https://webauthn.io](). 49 | 5. Run through the registration and creation process. 50 | 51 | ## For Development 52 | 53 | (Note: Paths are relative to root of this repository) 54 | 55 | 1. Copy `webext/app/credential_manager_shim.json` to `~/.mozilla/native-messaging-hosts/credential_manager_shim.json`. 56 | 2. In `webext/app/credential_manager_shim.py`, point the `DBUS_DOC_FILE` 57 | variable to the absolute path to 58 | `doc/xyz.iinuwa.credentialsd.Credentials.xml`. 59 | 3. In the copied file, replace the `path` key with the absolute path to `webext/app/credential_manager_shim.py` 60 | 4. Open Firefox and go to `about:debugging` 61 | 5. Click "This Firefox" > Load Temporary Extension. Select `webext/add-on/manifest.json` 62 | 6. Build with `ninja -C ./build` and run the following binaries binary to start the D-Bus services. 63 | - `GSCHEMA_SCHEMA_DIR=build/credentialsd-ui/data ./build/credentialsd-ui/target/debug/credentialsd-ui` 64 | - `./build/credentialsd/target/debug/credentialsd` 65 | 7. Navigate to [https://webauthn.io](). 66 | 8. Run through the registration and creation process. 67 | -------------------------------------------------------------------------------- /credentialsd-ui/meson.build: -------------------------------------------------------------------------------- 1 | i18n = import('i18n') 2 | gnome = import('gnome') 3 | 4 | gui_executable_name = 'credentialsd-ui' 5 | gui_build_dir = meson.current_build_dir() 6 | gui_source_dir = meson.current_source_dir() 7 | base_id = 'xyz.iinuwa.credentialsd.CredentialsUi' 8 | 9 | dependency('dbus-1', version: '>= 1.6') 10 | dependency('glib-2.0', version: '>= 2.66') 11 | dependency('gio-2.0', version: '>= 2.66') 12 | dependency('gtk4', version: '>= 4.6.2') 13 | 14 | glib_compile_resources = find_program('glib-compile-resources', required: true) 15 | glib_compile_schemas = find_program('glib-compile-schemas', required: true) 16 | # Usually provided by gettext package 17 | msgfmt = find_program('msgfmt', required: false) 18 | xmllint = find_program('xmllint', required: false) 19 | 20 | desktop_file_validate = find_program('desktop-file-validate', required: false) 21 | appstreamcli = find_program('appstreamcli', required: false) 22 | 23 | cargo = find_program('cargo', required: true) 24 | 25 | version = meson.project_version() 26 | 27 | prefix = get_option('prefix') 28 | bindir = prefix / get_option('bindir') 29 | localedir = prefix / get_option('localedir') 30 | 31 | datadir = prefix / get_option('datadir') 32 | pkgdatadir = datadir / gui_executable_name 33 | iconsdir = datadir / 'icons' 34 | 35 | if get_option('profile') == 'development' 36 | profile = 'Devel' 37 | vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD', check: false).stdout().strip() 38 | if vcs_tag == '' 39 | version_suffix = '-devel' 40 | else 41 | version_suffix = '-@0@'.format(vcs_tag) 42 | endif 43 | application_id = '@0@.@1@'.format(base_id, profile) 44 | else 45 | profile = '' 46 | version_suffix = '' 47 | application_id = base_id 48 | endif 49 | 50 | meson.add_dist_script( 51 | meson.project_source_root() / 'build-aux/dist-vendor.sh', 52 | meson.project_build_root() / 'meson-dist' / gui_executable_name 53 | + '-' 54 | + version, 55 | meson.project_source_root(), 56 | ) 57 | 58 | if get_option('profile') == 'development' 59 | # Setup pre-commit hook for ensuring coding style is always consistent 60 | message('Setting up git pre-commit hook..') 61 | run_command('cp', '-f', 'hooks/pre-commit.hook', '.git/hooks/pre-commit', check: false) 62 | endif 63 | 64 | cargo_options = [ 65 | '--manifest-path', meson.project_source_root() / gui_source_dir / 'Cargo.toml', 66 | ] 67 | cargo_options += ['--target-dir', meson.project_build_root() / gui_build_dir / 'target'] 68 | if get_option('cargo_offline') == true 69 | cargo_options += ['--offline'] 70 | endif 71 | 72 | # Localization setup 73 | podir = meson.project_source_root() / meson.current_source_dir() / 'po' 74 | gettext_package = gui_executable_name 75 | 76 | subdir('data') 77 | subdir('po') 78 | subdir('src') 79 | 80 | gnome.post_install( 81 | gtk_update_icon_cache: true, 82 | glib_compile_schemas: true, 83 | update_desktop_database: true, 84 | ) 85 | -------------------------------------------------------------------------------- /credentialsd-ui/src/gui/view_model/gtk/device.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | 3 | use gettextrs::gettext; 4 | use glib::Object; 5 | use gtk::glib; 6 | use gtk::prelude::*; 7 | use gtk::subclass::prelude::*; 8 | 9 | use crate::gui::view_model::Transport; 10 | 11 | mod imp { 12 | use super::*; 13 | 14 | #[derive(glib::Properties, Default)] 15 | #[properties(wrapper_type = super::DeviceObject)] 16 | pub struct DeviceObject { 17 | #[property(get, set)] 18 | pub id: RefCell, 19 | 20 | #[property(get, set)] 21 | pub transport: RefCell, 22 | 23 | #[property(get, set)] 24 | pub name: RefCell, 25 | } 26 | 27 | // The central trait for subclassing a GObject 28 | #[glib::object_subclass] 29 | impl ObjectSubclass for DeviceObject { 30 | const NAME: &'static str = "CredentialManagerDevice"; 31 | type Type = super::DeviceObject; 32 | } 33 | 34 | // Trait shared by all GObjects 35 | #[glib::derived_properties] 36 | impl ObjectImpl for DeviceObject {} 37 | } 38 | 39 | glib::wrapper! { 40 | pub struct DeviceObject(ObjectSubclass); 41 | } 42 | 43 | impl DeviceObject { 44 | pub fn new(id: &str, transport: &Transport, name: &str) -> Self { 45 | //, label: &str, icon_name: &str) -> Self { 46 | let transport = transport.as_str(); 47 | Object::builder() 48 | .property("id", id) 49 | .property("transport", transport) 50 | .property("name", name) 51 | .build() 52 | } 53 | } 54 | 55 | fn transport_name(transport: &Transport) -> String { 56 | match transport { 57 | Transport::Ble => gettext("A Bluetooth device"), 58 | Transport::Internal => gettext("This device"), 59 | Transport::HybridQr => gettext("A mobile device"), 60 | Transport::HybridLinked => gettext("Linked Device"), 61 | Transport::Nfc => gettext("An security key or card (NFC)"), 62 | Transport::Usb => gettext("A security key (USB)"), 63 | // Transport::PasskeyProvider => ("symbolic-link-symbolic", "ACME Password Manager"), 64 | } 65 | } 66 | impl From for DeviceObject { 67 | fn from(value: crate::gui::view_model::Device) -> Self { 68 | let name = transport_name(&value.transport); 69 | Self::new(&value.id, &value.transport, &name) 70 | } 71 | } 72 | 73 | impl From<&crate::gui::view_model::Device> for DeviceObject { 74 | fn from(value: &crate::gui::view_model::Device) -> Self { 75 | let name = transport_name(&value.transport); 76 | Self::new(&value.id, &value.transport, &name) 77 | } 78 | } 79 | 80 | impl TryFrom for crate::gui::view_model::Device { 81 | type Error = String; 82 | 83 | fn try_from(value: DeviceObject) -> Result { 84 | let transport: Transport = value.transport().try_into()?; 85 | Ok(Self { 86 | id: value.id(), 87 | transport, 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /credentialsd-ui/data/icons/check-round-outline-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # credentialsd Security Policy 2 | 3 | Since this project handles very sensitive data, we, the maintainers of 4 | credentialsd, take security seriously. This policy outlines our intentions for 5 | addressing security issues and practices for security researchers investigating 6 | this project. 7 | 8 | ## Submitting Vulnerability Reports 9 | 10 | If you have discovered a security vulnerability in this project, please report it 11 | to us privately via the process below. 12 | 13 | We use GitHub for private vulnerability disclosure. To report a vulnerability: 14 | 15 | 1. Go to [Security > Advisories > New draft security advisory][new-advisory]. 16 | 2. Fill out the report and submit the draft. 17 | 3. The maintainers will be privately notified about the advisory and get back to 18 | you. 19 | 20 | [new-advisory]: https://github.com/linux-credentials/credentialsd/security/advisories/new 21 | 22 | ## Expected Response 23 | 24 | We aim to acknowledge the receipt of the report as soon as possible and will 25 | work with you. We seek to investigate issues within 30 days. 26 | 27 | If the issue is confirmed upon investigation, we will collaborate with you to 28 | remediate the vulnerability. Depending on the severity or developer 29 | availability, we may request more time to remediate the issue before 30 | public disclosure. 31 | 32 | # Supported Releases 33 | 34 | We only support the latest published release. We may backport patches when 35 | possible to help users running on distributions that package older versions of 36 | our software. 37 | 38 | # Threat Model 39 | 40 | We do not currently have a formally defined threat model; we will continue to 41 | document it over time. However, the basic security guarantees we would like to 42 | achieve are defined below. 43 | 44 | Please note, that if you believe you have discovered a security problem outside 45 | of this scope, we still want to know about it! We would still like to discuss 46 | the issue privately, but we may decide to address it beyond the response 47 | time described above. 48 | 49 | ## Definitons 50 | 51 | - _privileged client_: A client that is allowed to make requests for credentials 52 | for any origin. 53 | - _unprivileged client_: A client that is allowed to make requests for 54 | credentials for only a preconfigured set of origins. 55 | 56 | ## Scope 57 | 58 | Here is the current list of items that are in scope: 59 | 60 | - Privileged clients may request credentials via this service[^1] for any origin. 61 | - The list of privileged clients cannot change without: 62 | - `root` privileges, or 63 | - user consent[^2] 64 | - The list of unprivileged clients cannot change without: 65 | - `root` privileges, or 66 | - user consent[^2] 67 | 68 | We implicitly trust the kernel and D-Bus, so any attacks that exploit those are 69 | out of scope for this project. 70 | 71 | Some other attacks that are explicitly out of scope are those that require: 72 | 73 | - physical access 74 | - direct access to authenticators 75 | - root privilege escalation 76 | 77 | [^1]: 78 | Various systems may allow users to interact with authenticators directly 79 | (e.g. allowing unrestricted permission to USB devices or Bluetooth service 80 | data), so those are out of scope. 81 | 82 | [^2]: 83 | In the future we may offer a stricter guarantee that privileged clients 84 | must include permission in application metadata signed by a trusted party. 85 | -------------------------------------------------------------------------------- /credentialsd-ui/data/icons/dialpad-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # credentialsd 2 | 3 | A Linux Credential Manager API. 4 | 5 | (Previously called `linux-webauthn-platform-api`.) 6 | 7 | ## Goals 8 | 9 | The primary goal of this project is to provide a spec and reference 10 | implementation of an API to mediate access to web credentials, initially local 11 | and remote FIDO2 authenticators. See [GOALS.md](/GOALS.md) for more information. 12 | 13 | ## How to install 14 | 15 | ### From packages 16 | 17 | We have [precompiled RPM packages for Fedora and openSUSE][obs-packages] hosted 18 | by the Open Build Service (OBS). We also copy these for released versions to the 19 | [release page][release-page]. 20 | 21 | There are several sub-packages: 22 | 23 | - `credentialsd`: The core credential service 24 | - `credentialsd-ui`: The reference implementation of the UI component for 25 | credentialsd. 26 | - `credentialsd-webextension`: Binaries and manifest files required for the 27 | Firefox add-on to function 28 | 29 | [obs-packages]: https://build.opensuse.org/package/show/home:MSirringhaus:webauthn_devel/credentialsd 30 | [release-page]: https://github.com/linux-credentials/credentialsd/releases 31 | 32 | ### From source 33 | 34 | Alternatively, you can build the project yourself using the instructions in 35 | [BUILDING.md](/BUILDING.md). 36 | 37 | ## How to use 38 | 39 | Right now, there are two ways to use this service. 40 | 41 | ### Experimental Firefox Add-On 42 | 43 | There is an add-on that you can install in Firefox 140+ that allows you to test 44 | `credentialsd` without a custom Firefox build. You can get the XPI from the 45 | [releases page][release-page] for the corresponding version of 46 | `credentialsd-webextension` package that you installed. 47 | 48 | Currently, this add-on only works for https://webauthn.io and 49 | https://demo.yubico.com, but can be used to test various WebAuthn options and 50 | hardware. 51 | 52 | ### Experimental Firefox Build 53 | 54 | There is also an experimental Firefox build that contains a patch to interact 55 | with `credentialsd` directly without an add-on. You can access a 56 | [Flatpak package for it on OBS][firefox-patch-flatpak] as well. 57 | 58 | [firefox-patch-flatpak]: https://download.opensuse.org/repositories/home:/MSirringhaus:/webauthn_devel/openSUSE_Factory_flatpak/ 59 | 60 | ## Mockups 61 | 62 | Here are some mockups of what this would look like for a user: 63 | 64 | ### Internal platform authenticator flow (device PIN) 65 | 66 |  67 |  68 |  69 | 70 | Alternatively, lock out the credential based on incorrect attempts. 71 | 72 |  73 |  74 | 75 | ### Hybrid credential flow 76 | 77 |  78 |  79 |  80 |  81 | 82 | ### Security key flow 83 | 84 |  85 |  86 |  87 |  88 | 89 | ## Related projects: 90 | 91 | - https://github.com/linux-credentials/libwebauthn (previously https://github.com/AlfioEmanueleFresta/xdg-credentials-portal) 92 | - authenticator-rs 93 | - webauthn-rs 94 | 95 | # Security Policy 96 | 97 | See [SECURITY.md](/SECURITY.md) for our security policy. 98 | 99 | # License 100 | 101 | See the [LICENSE.md](/LICENSE.md) file for license rights and limitations (LGPL-3.0-only). 102 | -------------------------------------------------------------------------------- /GOALS.md: -------------------------------------------------------------------------------- 1 | # Goals 2 | 3 | The goal of this repository is to define a spec and implementation for clients 4 | (apps, browsers, etc.) to retrieve user credentials in a uniform way across 5 | Linux desktop environments. 6 | 7 | ## Motivation 8 | 9 | Our primary motivation is to get passkey into the hands of users. Passkeys are 10 | growing as a powerful authentication mechanism for users. As the ecosystem 11 | becomes more mature, browsers are deferring access to passkeys to OS APIs on 12 | other platforms, like Windows Hello, Keychain on macOS and iOS, and 13 | Credential Manager on Android. 14 | 15 | On Linux, there is no OS API so handling passkeys is entirely up to the browser, 16 | which is at a disadvantage to the OS in terms of hardware access and desktop 17 | integration. This situation also requires each individual browser or application 18 | to reimplement the same features. We want to change that! 19 | 20 | ## Direction 21 | 22 | Some high-level goals: 23 | 24 | - define an API to securely create and retrieve local credentials 25 | (passwords, passkeys, security keys) 26 | - create and retrieve credentials on remote devices (e.g. via CTAP 2 BLE/hybrid 27 | transports) 28 | - Provide a uniform interface for third-party credential providers 29 | (password/passkey managers like GNOME Secrets, Bitwarden, Keepass, LastPass, 30 | etc.) to hook into 31 | 32 | Some nice-to-haves: 33 | 34 | - Design a specification for a platform authenticator. I'm not sure whether this 35 | needs to be specified, or whether it could be considered and implemented as a 36 | first-party credential provider. 37 | - A security key manager (e.g., for setting security key client PIN) 38 | 39 | Some non-goals: 40 | 41 | - Fully integrate with any specific desktop environment. Each desktop 42 | environment (GNOME, KDE, etc.) has its own UI and UX conventions, as well as 43 | system configuration methods (e.g., GNOME Settings), which this API will need 44 | to integrate with. 45 | Because of the variation, we intend to leave integration with these other 46 | components to developers more familiar with each of the desktop environments. 47 | For now, we are using bare GTK to build a UI for testing, but any UI 48 | implementation in this repository is for reference purposes. If anyone is 49 | willing to do some of this integration work, feel free to contact us! 50 | 51 | - Create a full-featured password manager. Features like Password syncing, 52 | password generation, rotation, etc. is not part of this specficiation. Other 53 | password manager projects should be able to use this to make their credentials 54 | available to the user uniformly, though. 55 | 56 | - BSD support. While we'd love to help out all open desktop environments, we don't 57 | know enough about any BSD to make it useful for them. Hopefully, the design 58 | process is transparent enough that someone else could design something that 59 | works for BSDs. 60 | 61 | ## Progress 62 | 63 | - April 2025: Added web extension for testing in Firefox. 64 | - March 2025: Integrated libwebauthn to support USB authenticators. 65 | - May 2024: Met with developers in GNOME and systemd to design internals for 66 | securely storing device credentials. 67 | - Jan 2024: Defined the [scenarios](/doc/historical/scenarios.md) that we expect this 68 | API to cover. We are working on extracting [API methods](/doc/api.md) required to 69 | implement the interactions between the client, portal frontend, portal backend, 70 | machine and mobile devices. Once that is done, I intend to convert the API into 71 | a [portal spec](/doc/historical/design-doc.md), making it fit normal D-Bus/portal patterns. 72 | -------------------------------------------------------------------------------- /credentialsd-ui/data/icons/fingerprint-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /credentialsd-ui/src/client.rs: -------------------------------------------------------------------------------- 1 | use async_std::stream::Stream; 2 | use credentialsd_common::{client::FlowController, server::RequestId}; 3 | use futures_lite::StreamExt; 4 | use zbus::Connection; 5 | 6 | use crate::dbus::FlowControlServiceProxy; 7 | 8 | pub struct DbusCredentialClient { 9 | conn: Connection, 10 | } 11 | 12 | impl DbusCredentialClient { 13 | pub fn new(conn: Connection) -> Self { 14 | Self { conn } 15 | } 16 | async fn proxy(&self) -> std::result::Result { 17 | FlowControlServiceProxy::new(&self.conn) 18 | .await 19 | .map_err(|err| tracing::error!("Failed to communicate with D-Bus service: {err}")) 20 | } 21 | } 22 | 23 | impl FlowController for DbusCredentialClient { 24 | async fn get_available_public_key_devices( 25 | &self, 26 | ) -> std::result::Result, ()> { 27 | let dbus_devices = self 28 | .proxy() 29 | .await? 30 | .get_available_public_key_devices() 31 | .await 32 | .map_err(|err| { 33 | tracing::error!("Failed to retrieve available devices/transports: {err}") 34 | })?; 35 | dbus_devices.into_iter().map(|d| d.try_into()).collect() 36 | } 37 | 38 | async fn get_hybrid_credential(&mut self) -> std::result::Result<(), ()> { 39 | self.proxy() 40 | .await? 41 | .get_hybrid_credential() 42 | .await 43 | .inspect_err(|err| tracing::error!("Failed to start hybrid credential flow: {err}")) 44 | .map_err(|_| ()) 45 | } 46 | 47 | async fn get_usb_credential(&mut self) -> std::result::Result<(), ()> { 48 | self.proxy() 49 | .await? 50 | .get_usb_credential() 51 | .await 52 | .inspect_err(|err| tracing::error!("Failed to start USB credential flow: {err}")) 53 | .map_err(|_| ()) 54 | } 55 | 56 | async fn get_nfc_credential(&mut self) -> std::result::Result<(), ()> { 57 | self.proxy() 58 | .await? 59 | .get_nfc_credential() 60 | .await 61 | .inspect_err(|err| tracing::error!("Failed to start NFC credential flow: {err}")) 62 | .map_err(|_| ()) 63 | } 64 | 65 | async fn subscribe( 66 | &mut self, 67 | ) -> std::result::Result< 68 | std::pin::Pin< 69 | Box + Send + 'static>, 70 | >, 71 | (), 72 | > { 73 | let stream = self 74 | .proxy() 75 | .await? 76 | .receive_state_changed() 77 | .await 78 | .map_err(|err| tracing::error!("Failed to initalize event stream: {err}"))? 79 | .filter_map(|msg| { 80 | msg.args() 81 | .map(|args| args.update) 82 | .inspect_err(|err| tracing::warn!("Failed to parse StateChanged signal: {err}")) 83 | .ok() 84 | }) 85 | .boxed(); 86 | self.proxy() 87 | .await? 88 | .subscribe() 89 | .await 90 | .map_err(|err| tracing::error!("Failed to initialize event stream: {err}")) 91 | .map(|_| stream) 92 | } 93 | 94 | async fn enter_client_pin(&mut self, pin: String) -> std::result::Result<(), ()> { 95 | self.proxy() 96 | .await? 97 | .enter_client_pin(pin) 98 | .await 99 | .map_err(|err| tracing::error!("Failed to send PIN to authenticator: {err}")) 100 | } 101 | 102 | async fn select_credential(&self, credential_id: String) -> std::result::Result<(), ()> { 103 | self.proxy() 104 | .await? 105 | .select_credential(credential_id) 106 | .await 107 | .map_err(|err| tracing::error!("Failed to select credential: {err}")) 108 | } 109 | 110 | async fn cancel_request(&self, request_id: RequestId) -> Result<(), ()> { 111 | if self 112 | .proxy() 113 | .await? 114 | .cancel_request(request_id) 115 | .await 116 | .is_err() 117 | { 118 | tracing::warn!("Failed to cancel request {request_id}"); 119 | } 120 | Ok(()) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /credentialsd-ui/data/icons/xyz.iinuwa.credentialsd.CredentialsUi-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /webext/add-on/background.js: -------------------------------------------------------------------------------- 1 | /* 2 | On startup, connect to the "credential_shim" app. 3 | */ 4 | let contentPort; 5 | let nativePort; 6 | 7 | function connected(port) { 8 | console.log("received connection from content script"); 9 | 10 | // initialize content port 11 | contentPort = port; 12 | console.log(contentPort); 13 | 14 | // Initialize native port 15 | nativePort = browser.runtime.connectNative("xyz.iinuwa.credentialsd_helper"); 16 | console.debug(nativePort); 17 | if (nativePort.error !== null) { 18 | console.error(nativePort.error) 19 | throw nativePort.error 20 | } 21 | console.log(`connected to native app`) 22 | console.log(nativePort) 23 | 24 | // Set up content port listener 25 | contentPort.onMessage.addListener(rcvFromContent) 26 | 27 | // Set up native port listener 28 | console.log("setting up native port response listener") 29 | nativePort.onMessage.addListener(rcvFromNative); 30 | 31 | } 32 | 33 | function rcvFromContent(msg) { 34 | const { requestId, cmd, options } = msg; 35 | const origin = contentPort.sender.origin 36 | const topOrigin = new URL(contentPort.sender.tab.url).origin 37 | // const isCrossOrigin = origin === topOrigin 38 | // const isTopLevel = contentPort.sender.frameId === 0; 39 | 40 | if (options) { 41 | const serializedOptions = serializeRequest(options) 42 | 43 | console.debug(options.publicKey.challenge) 44 | console.debug("background script received options, passing onto native app") 45 | nativePort.postMessage({ requestId, cmd, options: serializedOptions, origin, topOrigin }) 46 | } else { 47 | console.debug("background script received message without arguments, passing onto native app") 48 | nativePort.postMessage({ requestId, cmd, origin, topOrigin }) 49 | } 50 | } 51 | 52 | function rcvFromNative(msg) { 53 | console.log("Received (native -> background): " + msg); 54 | console.log("forwarding to content script"); 55 | const { requestId, data, error } = msg; 56 | contentPort.postMessage(msg); 57 | } 58 | 59 | function serializeBytes(buffer) { 60 | const options = {alphabet: "base64url", omitPadding: true}; 61 | return new Uint8Array(buffer).toBase64(options) 62 | } 63 | 64 | function deserializeBytes(base64str) { 65 | const options = {alphabet: "base64url"} 66 | return Uint8Array.fromBase64(base64str, options) 67 | } 68 | 69 | function serializeRequest(options) { 70 | // Serialize ArrayBuffers 71 | const clone = structuredClone(options) 72 | clone.publicKey.challenge = serializeBytes(clone.publicKey.challenge) 73 | if (clone.publicKey.user) { 74 | clone.publicKey.user.id = serializeBytes(clone.publicKey.user.id) 75 | } 76 | if (clone.publicKey.excludeCredentials) { 77 | for (const cred of clone.publicKey.excludeCredentials) { 78 | cred.id = serializeBytes(cred.id) 79 | } 80 | } 81 | if (clone.publicKey.allowCredentials) { 82 | for (const cred of clone.publicKey.allowCredentials) { 83 | cred.id = serializeBytes(cred.id); 84 | } 85 | } 86 | if (clone.publicKey.extensions && clone.publicKey.extensions.prf) { 87 | if (clone.publicKey.extensions.prf.eval) { 88 | clone.publicKey.extensions.prf.eval.first = serializeBytes(clone.publicKey.extensions.prf.eval.first); 89 | if (clone.publicKey.extensions.prf.eval.second) { 90 | clone.publicKey.extensions.prf.eval.second = serializeBytes(clone.publicKey.extensions.prf.eval.second); 91 | } 92 | } 93 | if (clone.publicKey.extensions.prf.evalByCredential) { 94 | const evalByCredential = clone.publicKey.extensions.prf.evalByCredential; 95 | 96 | // Iterate over all credentialIDs, serialize the first/second bytebuffer and replace the original evalByCredential map 97 | const result = {}; 98 | for (const credId in evalByCredentialData) { 99 | const prfValue = evalByCredentialData[credId]; 100 | 101 | if (prfValue && prfValue.first) { 102 | const newPrfValue = { 103 | first: serializeBytes(prfValue.first) 104 | }; 105 | 106 | if (prfValue.second) { 107 | newPrfValue.second = serializeBytes(prfValue.second); 108 | } 109 | result[credId] = newPrfValue; 110 | }; 111 | } 112 | clone.publicKey.extensions.prf.evalByCredential = result; 113 | } 114 | 115 | if (clone.publicKey.extensions && clone.publicKey.extensions.credBlob) { 116 | clone.publicKey.extensions.credBlob = serializeBytes(clone.publicKey.extensions.credBlob); 117 | } 118 | } 119 | return clone 120 | } 121 | 122 | 123 | // Listen for connections from content script 124 | console.log("Starting up credential_manager_shim background script") 125 | browser.runtime.onConnect.addListener(connected); 126 | -------------------------------------------------------------------------------- /credentialsd-ui/data/icons/xyz.iinuwa.credentialsd.CredentialsUi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /demo_client/cbor.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import codecs 3 | from enum import Enum 4 | import struct 5 | import unittest 6 | 7 | 8 | class MajorType(Enum): 9 | PositiveInteger = 0, 10 | NegativeInteger = 1, 11 | ByteString = 2, 12 | TextString = 3, 13 | Array = 4, 14 | Map = 5, 15 | Tag = 6, 16 | SimpleOrFloat = 7, 17 | 18 | 19 | class Parser: 20 | def __init__(self, cbor): 21 | self.data = memoryview(cbor).toreadonly() 22 | self.pos = 0 23 | 24 | def parse(self): 25 | value = self._read_value(self.data) 26 | return value 27 | 28 | def _read_value(self, buf): 29 | if len(buf) == 0: 30 | return None 31 | additional_info = buf[0] & 0b000_11111 32 | if additional_info < 24: 33 | argument = additional_info 34 | argument_len = 0 35 | elif additional_info == 24: 36 | argument_len = 1 37 | argument = struct.unpack('>B', buf[1:1+argument_len])[0] 38 | elif additional_info == 25: 39 | argument_len = 2 40 | argument = struct.unpack('>H', buf[1:1+argument_len])[0] 41 | elif additional_info == 26: 42 | argument_len = 4 43 | argument = struct.unpack('>I', buf[1:1+argument_len])[0] 44 | elif additional_info == 27: 45 | argument_len = 8 46 | argument = struct.unpack('>Q', buf[1:1+argument_len])[0] 47 | elif additional_info == 31: 48 | # Indefinite length for types 2-5 49 | argument = None 50 | argument_len = 0 51 | match buf[0] >> 5: 52 | case 0: 53 | major_type = MajorType.PositiveInteger 54 | case 1: 55 | major_type = MajorType.NegativeInteger 56 | case 2: 57 | major_type = MajorType.ByteString 58 | case 3: 59 | major_type = MajorType.TextString 60 | case 4: 61 | major_type = MajorType.Array 62 | case 5: 63 | major_type = MajorType.Map 64 | case 6: 65 | major_type = MajorType.Tag 66 | case 7: 67 | major_type = MajorType.SimpleOrFloat 68 | # advance beyond type info 69 | self.pos += 1 70 | self.pos += argument_len 71 | 72 | bytes_consumed = 0 73 | match major_type: 74 | case MajorType.PositiveInteger: 75 | value = argument 76 | 77 | case MajorType.NegativeInteger: 78 | value = -1 - argument 79 | 80 | case MajorType.ByteString: 81 | string_len = argument 82 | if string_len is None: 83 | string_len = 0 84 | # indefinite length 85 | value = "" 86 | while self.data[self.pos] != 0xff: 87 | val = self._read_value(self.data[self.pos:])[0] 88 | value += val 89 | string_len = 1 90 | else: 91 | value = self.data[self.pos:self.pos+string_len] 92 | bytes_consumed = string_len 93 | 94 | case MajorType.TextString: 95 | string_len = argument 96 | if string_len is None: 97 | # indefinite length 98 | value = "" 99 | while self.data[self.pos] != 0xff: 100 | val = self._read_value(self.data[self.pos:]) 101 | value += val 102 | bytes_consumed = 1 103 | else: 104 | value = codecs.utf_8_decode(self.data[self.pos:self.pos+string_len])[0] 105 | bytes_consumed = string_len 106 | 107 | case MajorType.Map: 108 | value = {} 109 | if argument is None: 110 | argument = 0 111 | value = {} 112 | while self.data[self.pos] != 0xff: 113 | inner_key = self._read_value(self.data[self.pos:]) 114 | inner_value = self._read_value(self.data[self.pos:]) 115 | value[inner_key] = inner_value 116 | bytes_consumed = 1 117 | else: 118 | for _ in range(argument): 119 | inner_key = self._read_value(self.data[self.pos:]) 120 | inner_value = self._read_value(self.data[self.pos:]) 121 | value[inner_key] = inner_value 122 | 123 | case MajorType.Array: 124 | value = [] 125 | if argument is None: 126 | argument = 0 127 | value = [] 128 | while self.data[self.pos] != 0xff: 129 | inner_value = self._read_value(self.data[self.pos:]) 130 | value.append(inner_value) 131 | bytes_consumed = 1 132 | else: 133 | for _ in range(argument): 134 | inner_value = self._read_value(self.data[self.pos:]) 135 | value.append(inner_value) 136 | 137 | case MajorType.Tag: 138 | raise Exception("Tag support not implemented") 139 | 140 | case MajorType.SimpleOrFloat: 141 | if argument == 20: 142 | value = False 143 | elif argument == 21: 144 | value = True 145 | elif argument == 22: 146 | value = None 147 | elif argument == 23: 148 | value = None 149 | else: 150 | raise Exception("Float parsing not implemented") 151 | 152 | self.pos += bytes_consumed 153 | return value 154 | 155 | 156 | def loads(data): 157 | parser = Parser(data) 158 | return parser.parse() 159 | -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | 3 | ## Build system 4 | 5 | This project uses Meson, Ninja, and Cargo. 6 | 7 | We use Meson 1.5.0+. If your package manager has an older version, you can 8 | install a new version [using the pip module][meson-pip-install]. 9 | 10 | There is currently no documented minimum support Rust version (MSRV), but 1.85+ 11 | should work. 12 | 13 | [meson-pip-install]: https://mesonbuild.com/Quick-guide.html#installation-using-python 14 | 15 | ## Package requirements 16 | 17 | To build, you need the following utility packages and development library packages. 18 | 19 | - GTK4 20 | - gettext 21 | - libdbus-1 22 | - libnfc 23 | - libpcsclite 24 | - libssl/openssl 25 | - libudev 26 | - desktop-file-utils 27 | 28 | Using the web extension also requires `python3-dbus-next`. 29 | 30 | ## Examples 31 | 32 | ### Debian/Ubuntu 33 | 34 | ```shell 35 | sudo apt update && sudo apt install \ 36 | # Build dependencies 37 | curl git build-essential \ 38 | # Meson/Ninja dependencies 39 | meson ninja-build \ 40 | # project dependencies 41 | libgtk-4-dev gettext libdbus-1-dev libnfc-dev libpcsclite-dev libssl-dev libudev-dev \ 42 | # packaging dependencies 43 | desktop-file-utils \ 44 | # web extension dependencies 45 | python3-dbus-next 46 | ``` 47 | 48 | ### Fedora 49 | 50 | ```shell 51 | # Build dependencies 52 | sudo dnf groupinstall "Development Tools" 53 | sudo dnf install \ 54 | curl git \ 55 | # Meson/Ninja dependencies 56 | meson ninja-build \ 57 | # project dependencies 58 | gtk4-devel gettext dbus-devel libnfc-devel pcsc-lite-devel openssl-devel systemd-udev \ 59 | # packaging dependencies 60 | desktop-file-utils \ 61 | # web extension dependencies 62 | python3-dbus-next 63 | ``` 64 | 65 | # For Installing/Testing 66 | 67 | If you are interested in installing the program, you can use `meson install` to 68 | install the details. (If you would like to test without installing, you can 69 | follow the [build instructions for development](#for-development) below.) 70 | 71 | ```shell 72 | git clone https://github.com/linux-credentials/credentialsd 73 | cd credentialsd 74 | meson setup -Dprefix=/usr/local build-release 75 | cd build-release 76 | meson install 77 | ``` 78 | 79 | Note that since Meson is installing to `/usr/local`, it will ask you to use 80 | `sudo` to elevate privileges to install. 81 | 82 | ## Running the installed server 83 | 84 | When using the installed server, systemd or D-Bus should take care of starting 85 | the services on demand, so you don't need to start it manually. 86 | 87 | The first time you install this, though, you must log out and log back in again 88 | for the service activation files to take effect. 89 | 90 | ## Testing installed builds with Firefox Web Add-On 91 | 92 | Note: If you are testing the Firefox web extension, you will need to link the 93 | native messaging manifest to your home directory, since Firefox does not read 94 | from `/usr/local`: 95 | 96 | ```shell 97 | mkdir -p ~/.mozilla/native-messaging-hosts/ 98 | ln -s /usr/local/lib64/mozilla/native-messaging-hosts/xyz.iinuwa.credentialsd_helper.json ~/.mozilla/native-messaging-hosts/ 99 | ``` 100 | 101 | # For Development 102 | 103 | ``` 104 | git clone https://github.com/linux-credentials/credentialsd 105 | cd credentialsd 106 | meson setup -Dprofile=development build 107 | ninja -C build 108 | ``` 109 | 110 | ## Running the server for development 111 | 112 | To run the required services during development, you need to add some 113 | environment variables. 114 | 115 | ```shell 116 | # Run the server, with debug logging enabled 117 | export GSETTINGS_SCHEMA_DIR=build/credentialsd-ui/data 118 | export RUST_LOG=credentialsd=debug,credentials_ui=debug 119 | ./build/credentialsd/target/debug/credentialsd & 120 | ./build/credentialsd-ui/target/debug/credentialsd-ui 121 | ``` 122 | 123 | ## Testing development builds with Firefox Web Add-On 124 | 125 | If you are using the Firefox add-on to build, follow the instructions for 126 | development in [`webext/README.md`](/webext/README.md#for-development). 127 | 128 | # For Packaging 129 | 130 | There are a few Meson options to control the build that may be useful for packagers. 131 | 132 | ``` 133 | # list available options 134 | 135 | > meson configure 136 | # ... 137 | Project options Default Value Possible Values Description 138 | ----------------- ------------- --------------- ----------- 139 | cargo_home The directory to 140 | store files 141 | downloaded by 142 | Cargo 143 | cargo_offline false [true, false] Whether to 144 | perform an 145 | offline build 146 | with Cargo. 147 | Defaults to false 148 | to download 149 | crates from 150 | registries. 151 | profile default [default, The build profile 152 | development] for Credential 153 | Manager. One of 154 | "default" or 155 | "development". 156 | ``` 157 | 158 | > TODO: rename `default` profile to `release` to reduce confusion. 159 | 160 | # Running Tests 161 | 162 | Due to some unknown reason, tests hang unless you pass the `--interactive` flag to Meson, available since 1.5.0. 163 | 164 | ``` 165 | cd build 166 | meson test --interactive 167 | ``` 168 | -------------------------------------------------------------------------------- /credentialsd-ui/src/gui/view_model/gtk/application.rs: -------------------------------------------------------------------------------- 1 | use async_std::channel::{Receiver, Sender}; 2 | use tracing::{debug, info}; 3 | 4 | use gtk::prelude::*; 5 | use gtk::subclass::prelude::*; 6 | use gtk::{gdk, gio, glib}; 7 | 8 | use super::{ViewModel, window::CredentialsUiWindow}; 9 | use crate::config::{APP_ID, LOCALEDIR, PKGDATADIR, PROFILE, VERSION}; 10 | use crate::gui::view_model::{ViewEvent, ViewUpdate}; 11 | 12 | mod imp { 13 | use crate::gui::view_model::gtk::ModelState; 14 | 15 | use super::*; 16 | use glib::{WeakRef, clone}; 17 | use std::{ 18 | cell::{OnceCell, RefCell}, 19 | time::Duration, 20 | }; 21 | 22 | #[derive(Debug, Default)] 23 | pub struct CredentialsUi { 24 | pub window: OnceCell>, 25 | 26 | pub(super) tx: RefCell>>, 27 | pub(super) rx: RefCell>>, 28 | } 29 | 30 | #[glib::object_subclass] 31 | impl ObjectSubclass for CredentialsUi { 32 | const NAME: &'static str = "CredentialsUi"; 33 | type Type = super::CredentialsUi; 34 | type ParentType = gtk::Application; 35 | } 36 | 37 | impl ObjectImpl for CredentialsUi {} 38 | 39 | impl ApplicationImpl for CredentialsUi { 40 | fn activate(&self) { 41 | debug!("GtkApplication::activate"); 42 | self.parent_activate(); 43 | let app = self.obj(); 44 | 45 | if let Some(window) = self.window.get() { 46 | let window = window.upgrade().unwrap(); 47 | window.present(); 48 | return; 49 | } 50 | 51 | let tx = self.tx.take().expect("sender to be initiated"); 52 | let rx = self.rx.take().expect("receiver to be initiated"); 53 | let view_model = ViewModel::new(tx, rx); 54 | let vm2 = view_model.clone(); 55 | let window = CredentialsUiWindow::new(&app, view_model); 56 | let window2 = window.clone(); 57 | vm2.clone().connect_completed_notify(move |vm| { 58 | if vm.completed() { 59 | glib::spawn_future_local(clone!( 60 | #[weak] 61 | window2, 62 | async move { 63 | // Wait to show confirmation before closing. 64 | async_std::task::sleep(Duration::from_millis(500)).await; 65 | gtk::prelude::WidgetExt::activate_action(&window2, "window.close", None) 66 | .unwrap() 67 | } 68 | )); 69 | } 70 | }); 71 | let window3 = window.clone(); 72 | // TODO: merge these state callbacks into a single function 73 | vm2.clone().connect_state_notify(move |vm| { 74 | if let ModelState::Cancelled = vm.state() { 75 | glib::spawn_future_local(clone!( 76 | #[weak] 77 | window3, 78 | async move { 79 | gtk::prelude::WidgetExt::activate_action(&window3, "window.close", None) 80 | .unwrap() 81 | } 82 | )); 83 | } 84 | }); 85 | self.window 86 | .set(window.downgrade()) 87 | .expect("Window already set."); 88 | 89 | app.main_window().present(); 90 | } 91 | 92 | fn startup(&self) { 93 | debug!("GtkApplication::startup"); 94 | self.parent_startup(); 95 | let app = self.obj(); 96 | 97 | // Set icons for shell 98 | gtk::Window::set_default_icon_name(APP_ID); 99 | 100 | app.setup_css(); 101 | app.setup_gactions(); 102 | app.setup_accels(); 103 | } 104 | } 105 | 106 | impl GtkApplicationImpl for CredentialsUi {} 107 | } 108 | 109 | glib::wrapper! { 110 | pub struct CredentialsUi(ObjectSubclass) 111 | @extends gio::Application, gtk::Application, 112 | @implements gio::ActionMap, gio::ActionGroup; 113 | } 114 | 115 | impl CredentialsUi { 116 | fn main_window(&self) -> CredentialsUiWindow { 117 | self.imp().window.get().unwrap().upgrade().unwrap() 118 | } 119 | 120 | fn setup_gactions(&self) { 121 | // Quit 122 | let action_quit = gio::ActionEntry::builder("quit") 123 | .activate(move |app: &Self, _, _| { 124 | // This is needed to trigger the delete event and saving the window state 125 | app.main_window().close(); 126 | app.quit(); 127 | }) 128 | .build(); 129 | 130 | self.add_action_entries([action_quit]); 131 | } 132 | 133 | // Sets up keyboard shortcuts 134 | fn setup_accels(&self) { 135 | self.set_accels_for_action("app.quit", &["q"]); 136 | self.set_accels_for_action("window.close", &["w"]); 137 | } 138 | 139 | fn setup_css(&self) { 140 | let provider = gtk::CssProvider::new(); 141 | provider.load_from_resource("/xyz/iinuwa/credentialsd/CredentialsUi/style.css"); 142 | if let Some(display) = gdk::Display::default() { 143 | gtk::style_context_add_provider_for_display( 144 | &display, 145 | &provider, 146 | gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, 147 | ); 148 | } 149 | } 150 | 151 | pub fn run(&self) -> glib::ExitCode { 152 | info!("Credentials UI ({})", APP_ID); 153 | info!("Version: {} ({})", VERSION, PROFILE); 154 | info!("Datadir: {}", PKGDATADIR); 155 | info!("Localedir: {}", LOCALEDIR); 156 | 157 | ApplicationExtManual::run(self) 158 | } 159 | 160 | pub(crate) fn new(tx: Sender, rx: Receiver) -> Self { 161 | let app: Self = glib::Object::builder() 162 | .property("application-id", APP_ID) 163 | .property( 164 | "resource-base-path", 165 | "/xyz/iinuwa/credentialsd/CredentialUI/", 166 | ) 167 | .build(); 168 | app.imp().tx.replace(Some(tx)); 169 | app.imp().rx.replace(Some(rx)); 170 | app 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # GNU LESSER GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 29 June 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | This version of the GNU Lesser General Public License incorporates the 12 | terms and conditions of version 3 of the GNU General Public License, 13 | supplemented by the additional permissions listed below. 14 | 15 | ## 0. Additional Definitions. 16 | 17 | As used herein, "this License" refers to version 3 of the GNU Lesser 18 | General Public License, and the "GNU GPL" refers to version 3 of the 19 | GNU General Public License. 20 | 21 | "The Library" refers to a covered work governed by this License, other 22 | than an Application or a Combined Work as defined below. 23 | 24 | An "Application" is any work that makes use of an interface provided 25 | by the Library, but which is not otherwise based on the Library. 26 | Defining a subclass of a class defined by the Library is deemed a mode 27 | of using an interface provided by the Library. 28 | 29 | A "Combined Work" is a work produced by combining or linking an 30 | Application with the Library. The particular version of the Library 31 | with which the Combined Work was made is also called the "Linked 32 | Version". 33 | 34 | The "Minimal Corresponding Source" for a Combined Work means the 35 | Corresponding Source for the Combined Work, excluding any source code 36 | for portions of the Combined Work that, considered in isolation, are 37 | based on the Application, and not on the Linked Version. 38 | 39 | The "Corresponding Application Code" for a Combined Work means the 40 | object code and/or source code for the Application, including any data 41 | and utility programs needed for reproducing the Combined Work from the 42 | Application, but excluding the System Libraries of the Combined Work. 43 | 44 | ## 1. Exception to Section 3 of the GNU GPL. 45 | 46 | You may convey a covered work under sections 3 and 4 of this License 47 | without being bound by section 3 of the GNU GPL. 48 | 49 | ## 2. Conveying Modified Versions. 50 | 51 | If you modify a copy of the Library, and, in your modifications, a 52 | facility refers to a function or data to be supplied by an Application 53 | that uses the facility (other than as an argument passed when the 54 | facility is invoked), then you may convey a copy of the modified 55 | version: 56 | 57 | - a) under this License, provided that you make a good faith effort 58 | to ensure that, in the event an Application does not supply the 59 | function or data, the facility still operates, and performs 60 | whatever part of its purpose remains meaningful, or 61 | - b) under the GNU GPL, with none of the additional permissions of 62 | this License applicable to that copy. 63 | 64 | ## 3. Object Code Incorporating Material from Library Header Files. 65 | 66 | The object code form of an Application may incorporate material from a 67 | header file that is part of the Library. You may convey such object 68 | code under terms of your choice, provided that, if the incorporated 69 | material is not limited to numerical parameters, data structure 70 | layouts and accessors, or small macros, inline functions and templates 71 | (ten or fewer lines in length), you do both of the following: 72 | 73 | - a) Give prominent notice with each copy of the object code that 74 | the Library is used in it and that the Library and its use are 75 | covered by this License. 76 | - b) Accompany the object code with a copy of the GNU GPL and this 77 | license document. 78 | 79 | ## 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, taken 82 | together, effectively do not restrict modification of the portions of 83 | the Library contained in the Combined Work and reverse engineering for 84 | debugging such modifications, if you also do each of the following: 85 | 86 | - a) Give prominent notice with each copy of the Combined Work that 87 | the Library is used in it and that the Library and its use are 88 | covered by this License. 89 | - b) Accompany the Combined Work with a copy of the GNU GPL and this 90 | license document. 91 | - c) For a Combined Work that displays copyright notices during 92 | execution, include the copyright notice for the Library among 93 | these notices, as well as a reference directing the user to the 94 | copies of the GNU GPL and this license document. 95 | - d) Do one of the following: 96 | - 0. Convey the Minimal Corresponding Source under the terms of 97 | this License, and the Corresponding Application Code in a form 98 | suitable for, and under terms that permit, the user to 99 | recombine or relink the Application with a modified version of 100 | the Linked Version to produce a modified Combined Work, in the 101 | manner specified by section 6 of the GNU GPL for conveying 102 | Corresponding Source. 103 | - 1. Use a suitable shared library mechanism for linking with 104 | the Library. A suitable mechanism is one that (a) uses at run 105 | time a copy of the Library already present on the user's 106 | computer system, and (b) will operate properly with a modified 107 | version of the Library that is interface-compatible with the 108 | Linked Version. 109 | - e) Provide Installation Information, but only if you would 110 | otherwise be required to provide such information under section 6 111 | of the GNU GPL, and only to the extent that such information is 112 | necessary to install and execute a modified version of the 113 | Combined Work produced by recombining or relinking the Application 114 | with a modified version of the Linked Version. (If you use option 115 | 4d0, the Installation Information must accompany the Minimal 116 | Corresponding Source and Corresponding Application Code. If you 117 | use option 4d1, you must provide the Installation Information in 118 | the manner specified by section 6 of the GNU GPL for conveying 119 | Corresponding Source.) 120 | 121 | ## 5. Combined Libraries. 122 | 123 | You may place library facilities that are a work based on the Library 124 | side by side in a single library together with other library 125 | facilities that are not Applications and are not covered by this 126 | License, and convey such a combined library under terms of your 127 | choice, if you do both of the following: 128 | 129 | - a) Accompany the combined library with a copy of the same work 130 | based on the Library, uncombined with any other library 131 | facilities, conveyed under the terms of this License. 132 | - b) Give prominent notice with the combined library that part of it 133 | is a work based on the Library, and explaining where to find the 134 | accompanying uncombined form of the same work. 135 | 136 | ## 6. Revised Versions of the GNU Lesser General Public License. 137 | 138 | The Free Software Foundation may publish revised and/or new versions 139 | of the GNU Lesser General Public License from time to time. Such new 140 | versions will be similar in spirit to the present version, but may 141 | differ in detail to address new problems or concerns. 142 | 143 | Each version is given a distinguishing version number. If the Library 144 | as you received it specifies that a certain numbered version of the 145 | GNU Lesser General Public License "or any later version" applies to 146 | it, you have the option of following the terms and conditions either 147 | of that published version or of any later version published by the 148 | Free Software Foundation. If the Library as you received it does not 149 | specify a version number of the GNU Lesser General Public License, you 150 | may choose any version of the GNU Lesser General Public License ever 151 | published by the Free Software Foundation. 152 | 153 | If the Library as you received it specifies that a proxy can decide 154 | whether future versions of the GNU Lesser General Public License shall 155 | apply, that proxy's public statement of acceptance of any version is 156 | permanent authorization for you to choose that version for the 157 | Library. 158 | -------------------------------------------------------------------------------- /credentialsd-ui/po/credentialsd-ui.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR "The Credentials for Linux Project" 3 | # This file is distributed under the same license as the credentialsd-ui package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: credentialsd-ui\n" 10 | "Report-Msgid-Bugs-To: \"https://github.com/linux-credentials/credentialsd/" 11 | "issues\"\n" 12 | "POT-Creation-Date: 2025-11-28 12:53-0600\n" 13 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 14 | "Last-Translator: FULL NAME \n" 15 | "Language-Team: LANGUAGE \n" 16 | "Language: \n" 17 | "MIME-Version: 1.0\n" 18 | "Content-Type: text/plain; charset=CHARSET\n" 19 | "Content-Transfer-Encoding: 8bit\n" 20 | "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" 21 | 22 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in:2 23 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:8 24 | #: src/gui/view_model/gtk/mod.rs:380 25 | msgid "Credential Manager" 26 | msgstr "" 27 | 28 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in:3 29 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:9 30 | msgid "Write a GTK + Rust application" 31 | msgstr "" 32 | 33 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in:9 34 | msgid "Gnome;GTK;" 35 | msgstr "" 36 | 37 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in:6 38 | msgid "Window width" 39 | msgstr "" 40 | 41 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in:10 42 | msgid "Window height" 43 | msgstr "" 44 | 45 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in:14 46 | msgid "Window maximized state" 47 | msgstr "" 48 | 49 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:11 50 | msgid "" 51 | "A boilerplate template for GTK + Rust. It uses Meson as a build system and " 52 | "has flatpak support by default." 53 | msgstr "" 54 | 55 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:16 56 | msgid "Registering a credential" 57 | msgstr "" 58 | 59 | #. developer_name tag deprecated with Appstream 1.0 60 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:34 61 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:37 62 | msgid "Isaiah Inuwa" 63 | msgstr "" 64 | 65 | #: data/resources/ui/shortcuts.ui:11 66 | msgctxt "shortcut window" 67 | msgid "General" 68 | msgstr "" 69 | 70 | #: data/resources/ui/shortcuts.ui:14 71 | msgctxt "shortcut window" 72 | msgid "Show Shortcuts" 73 | msgstr "" 74 | 75 | #: data/resources/ui/shortcuts.ui:20 76 | msgctxt "shortcut window" 77 | msgid "Quit" 78 | msgstr "" 79 | 80 | #: data/resources/ui/window.ui:6 81 | msgid "_Preferences" 82 | msgstr "" 83 | 84 | #: data/resources/ui/window.ui:10 85 | msgid "_Keyboard Shortcuts" 86 | msgstr "" 87 | 88 | #: data/resources/ui/window.ui:68 89 | msgid "Choose device" 90 | msgstr "" 91 | 92 | #: data/resources/ui/window.ui:74 93 | msgid "Devices" 94 | msgstr "" 95 | 96 | #: data/resources/ui/window.ui:98 97 | msgid "Connect a security key" 98 | msgstr "" 99 | 100 | #: data/resources/ui/window.ui:138 101 | msgid "Scan the QR code to connect your device" 102 | msgstr "" 103 | 104 | #: data/resources/ui/window.ui:183 data/resources/ui/window.ui:189 105 | msgid "Choose credential" 106 | msgstr "" 107 | 108 | #: data/resources/ui/window.ui:212 109 | msgid "Complete" 110 | msgstr "" 111 | 112 | #: data/resources/ui/window.ui:218 113 | msgid "Done!" 114 | msgstr "" 115 | 116 | #: data/resources/ui/window.ui:229 117 | msgid "Something went wrong." 118 | msgstr "" 119 | 120 | #: data/resources/ui/window.ui:242 src/gui/view_model/mod.rs:244 121 | msgid "" 122 | "Something went wrong while retrieving a credential. Please try again later " 123 | "or use a different authenticator." 124 | msgstr "" 125 | 126 | #: src/gui/view_model/gtk/mod.rs:146 127 | msgid "Enter your PIN. One attempt remaining." 128 | msgid_plural "Enter your PIN. %d attempts remaining." 129 | msgstr[0] "" 130 | msgstr[1] "" 131 | 132 | #: src/gui/view_model/gtk/mod.rs:152 133 | msgid "Enter your PIN." 134 | msgstr "" 135 | 136 | #: src/gui/view_model/gtk/mod.rs:162 137 | msgid "Touch your device again. One attempt remaining." 138 | msgid_plural "Touch your device again. %d attempts remaining." 139 | msgstr[0] "" 140 | msgstr[1] "" 141 | 142 | #: src/gui/view_model/gtk/mod.rs:168 143 | #: src/gui/view_model/gtk/mod.rs:173 144 | msgid "Touch your device" 145 | msgstr "" 146 | 147 | #: src/gui/view_model/gtk/mod.rs:176 148 | msgid "Scan the QR code with your device to begin authentication." 149 | msgstr "" 150 | 151 | #: src/gui/view_model/gtk/mod.rs:186 152 | msgid "" 153 | "Connecting to your device. Make sure both devices are near each other and " 154 | "have Bluetooth enabled." 155 | msgstr "" 156 | 157 | #: src/gui/view_model/gtk/mod.rs:194 158 | msgid "Device connected. Follow the instructions on your device" 159 | msgstr "" 160 | 161 | #: src/gui/view_model/gtk/mod.rs:320 162 | msgid "Insert your security key." 163 | msgstr "" 164 | 165 | #: src/gui/view_model/gtk/mod.rs:339 166 | msgid "Multiple devices found. Please select with which to proceed." 167 | msgstr "" 168 | 169 | #: src/gui/view_model/gtk/device.rs:57 170 | msgid "A Bluetooth device" 171 | msgstr "" 172 | 173 | #: src/gui/view_model/gtk/device.rs:58 174 | msgid "This device" 175 | msgstr "" 176 | 177 | #: src/gui/view_model/gtk/device.rs:59 178 | msgid "A mobile device" 179 | msgstr "" 180 | 181 | #: src/gui/view_model/gtk/device.rs:60 182 | msgid "Linked Device" 183 | msgstr "" 184 | 185 | #: src/gui/view_model/gtk/device.rs:61 186 | msgid "An security key or card (NFC)" 187 | msgstr "" 188 | 189 | #: src/gui/view_model/gtk/device.rs:62 190 | msgid "A security key (USB)" 191 | msgstr "" 192 | 193 | #: src/gui/view_model/mod.rs:75 194 | msgid "unknown application" 195 | msgstr "" 196 | 197 | #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from 198 | #: src/gui/view_model/mod.rs:80 199 | msgid "Create a passkey for %s1" 200 | msgstr "" 201 | 202 | #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from 203 | #: src/gui/view_model/mod.rs:84 204 | msgid "Use a passkey for %s1" 205 | msgstr "" 206 | 207 | #. TRANSLATORS: %s1 is the "relying party" (e.g.: domain name) where the request is coming from 208 | #. TRANSLATORS: %s2 is the application name (e.g.: firefox) where the request is coming from, must be left untouched to make the name bold 209 | #. TRANSLATORS: %i1 is the process ID of the requesting application 210 | #. TRANSLATORS: %s3 is the absolute path (think: /usr/bin/firefox) of the requesting application 211 | #: src/gui/view_model/mod.rs:96 212 | msgid "" 213 | "\"%s2\" (process ID: %i1, binary: %s3) is asking to create a " 214 | "credential to sign in to \"%s1\". Only proceed if you trust this process." 215 | msgstr "" 216 | 217 | #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from 218 | #. TRANSLATORS: %s2 is the application name (e.g.: firefox) where the request is coming from, must be left untouched to make the name bold 219 | #. TRANSLATORS: %i1 is the process ID of the requesting application 220 | #. TRANSLATORS: %s3 is the absolute path (think: /usr/bin/firefox) of the requesting application 221 | #: src/gui/view_model/mod.rs:103 222 | msgid "" 223 | "\"%s2\" (process ID: %i1, binary: %s3) is asking to use a credential " 224 | "to sign in to \"%s1\". Only proceed if you trust this process." 225 | msgstr "" 226 | 227 | #: src/gui/view_model/mod.rs:224 228 | msgid "Failed to select credential from device." 229 | msgstr "" 230 | 231 | #: src/gui/view_model/mod.rs:281 232 | #: src/gui/view_model/mod.rs:332 233 | msgid "No matching credentials found on this authenticator." 234 | msgstr "" 235 | 236 | #: src/gui/view_model/mod.rs:284 237 | #: src/gui/view_model/mod.rs:335 238 | msgid "" 239 | "No more PIN attempts allowed. Try removing your device and plugging it back " 240 | "in." 241 | msgstr "" 242 | 243 | #: src/gui/view_model/mod.rs:290 244 | msgid "This credential is already registered on this authenticator." 245 | msgstr "" 246 | 247 | #: src/gui/view_model/mod.rs:389 248 | msgid "Something went wrong. Try again later or use a different authenticator." 249 | msgstr "" 250 | -------------------------------------------------------------------------------- /credentialsd-ui/src/gui/view_model/gtk/window.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | 3 | use glib::Properties; 4 | use gtk::gdk::Texture; 5 | use gtk::subclass::prelude::*; 6 | use gtk::{Picture, prelude::*}; 7 | use gtk::{ 8 | gio, 9 | glib::{self, clone}, 10 | }; 11 | 12 | use super::application::CredentialsUi; 13 | use super::{ViewModel, device::DeviceObject}; 14 | use crate::config::{APP_ID, PROFILE}; 15 | use crate::gui::view_model::Transport; 16 | 17 | mod imp { 18 | use gtk::Picture; 19 | 20 | use crate::gui::view_model::ViewEvent; 21 | 22 | use super::*; 23 | 24 | #[derive(Debug, Properties, gtk::CompositeTemplate)] 25 | #[properties(wrapper_type = super::CredentialsUiWindow)] 26 | #[template(resource = "/xyz/iinuwa/credentialsd/CredentialsUi/ui/window.ui")] 27 | pub struct CredentialsUiWindow { 28 | #[template_child] 29 | pub headerbar: TemplateChild, 30 | pub settings: gio::Settings, 31 | #[property(get, set)] 32 | pub view_model: RefCell>, 33 | 34 | #[template_child] 35 | pub stack: TemplateChild, 36 | 37 | #[template_child] 38 | pub usb_nfc_pin_entry: TemplateChild, 39 | 40 | #[template_child] 41 | pub qr_code_pic: TemplateChild, 42 | } 43 | 44 | #[gtk::template_callbacks] 45 | impl CredentialsUiWindow { 46 | #[template_callback] 47 | fn handle_usb_nfc_pin_entered(&self, entry: >k::PasswordEntry) { 48 | let view_model = &self.view_model.borrow(); 49 | let view_model = view_model.as_ref().unwrap(); 50 | let pin = entry.text().to_string(); 51 | glib::spawn_future_local(clone!( 52 | #[weak] 53 | view_model, 54 | async move { 55 | view_model.send_usb_nfc_device_pin(pin).await; 56 | } 57 | )); 58 | } 59 | } 60 | 61 | impl Default for CredentialsUiWindow { 62 | fn default() -> Self { 63 | Self { 64 | headerbar: TemplateChild::default(), 65 | settings: gio::Settings::new(APP_ID), 66 | view_model: RefCell::default(), 67 | stack: TemplateChild::default(), 68 | usb_nfc_pin_entry: TemplateChild::default(), 69 | qr_code_pic: TemplateChild::default(), 70 | } 71 | } 72 | } 73 | 74 | #[glib::object_subclass] 75 | impl ObjectSubclass for CredentialsUiWindow { 76 | const NAME: &'static str = "CredentialsUiWindow"; 77 | type Type = super::CredentialsUiWindow; 78 | type ParentType = gtk::ApplicationWindow; 79 | 80 | fn class_init(klass: &mut Self::Class) { 81 | klass.bind_template(); 82 | klass.bind_template_callbacks(); 83 | } 84 | 85 | // You must call `Widget`'s `init_template()` within `instance_init()`. 86 | fn instance_init(obj: &glib::subclass::InitializingObject) { 87 | obj.init_template(); 88 | } 89 | } 90 | 91 | #[glib::derived_properties] 92 | impl ObjectImpl for CredentialsUiWindow { 93 | fn constructed(&self) { 94 | self.parent_constructed(); 95 | let obj = self.obj(); 96 | 97 | // Devel Profile 98 | if PROFILE == "Devel" { 99 | obj.add_css_class("devel"); 100 | } 101 | 102 | // Load latest window state 103 | obj.load_window_size(); 104 | } 105 | } 106 | 107 | impl WidgetImpl for CredentialsUiWindow {} 108 | impl WindowImpl for CredentialsUiWindow { 109 | // Save window state on delete event 110 | fn close_request(&self) -> glib::Propagation { 111 | if let Some(vm) = self.view_model.borrow().as_ref() { 112 | if vm 113 | .get_sender() 114 | .send_blocking(ViewEvent::UserCancelled) 115 | .is_err() 116 | { 117 | tracing::warn!( 118 | "Failed to notify the backend service that the user cancelled the request." 119 | ); 120 | }; 121 | } 122 | if let Err(err) = self.obj().save_window_size() { 123 | tracing::warn!("Failed to save window state, {}", &err); 124 | } 125 | 126 | // Pass close request on to the parent 127 | self.parent_close_request() 128 | } 129 | } 130 | 131 | impl ApplicationWindowImpl for CredentialsUiWindow {} 132 | } 133 | 134 | glib::wrapper! { 135 | pub struct CredentialsUiWindow(ObjectSubclass) 136 | @extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, 137 | @implements gio::ActionMap, gio::ActionGroup, gtk::Root; 138 | 139 | } 140 | 141 | impl CredentialsUiWindow { 142 | pub fn new(app: &CredentialsUi, view_model: ViewModel) -> Self { 143 | let window: CredentialsUiWindow = glib::Object::builder() 144 | .property("application", app) 145 | .property("view-model", view_model) 146 | .build(); 147 | window.setup_callbacks(); 148 | window 149 | } 150 | 151 | fn setup_callbacks(&self) { 152 | let view_model = &self.view_model(); 153 | let view_model = view_model.as_ref().expect("view model to exist"); 154 | let stack: >k::Stack = &self.imp().stack.get(); 155 | let qr_code_pic: &Picture = &self.imp().qr_code_pic.get(); 156 | view_model.connect_selected_device_notify(clone!( 157 | #[weak] 158 | stack, 159 | move |vm| { 160 | let d = vm.selected_device(); 161 | let d = d 162 | .and_downcast_ref::() 163 | .expect("selected device to exist at notify"); 164 | match d.transport().try_into() { 165 | Ok(Transport::Usb) => stack.set_visible_child_name("usb_or_nfc"), 166 | Ok(Transport::HybridQr) => stack.set_visible_child_name("hybrid_qr"), 167 | Ok(Transport::Nfc) => stack.set_visible_child_name("usb_or_nfc"), 168 | _ => {} 169 | }; 170 | } 171 | )); 172 | 173 | view_model.connect_qr_code_paintable_notify(clone!( 174 | #[weak] 175 | qr_code_pic, 176 | move |vm| { 177 | let paintable = vm.qr_code_paintable(); 178 | let paintable = paintable.and_downcast_ref::(); 179 | qr_code_pic.set_paintable(paintable); 180 | } 181 | )); 182 | 183 | view_model.connect_completed_notify(clone!( 184 | #[weak] 185 | stack, 186 | move |vm| { 187 | if vm.completed() { 188 | stack.set_visible_child_name("completed"); 189 | } 190 | } 191 | )); 192 | 193 | view_model.connect_failed_notify(clone!( 194 | #[weak] 195 | stack, 196 | move |vm| { 197 | if vm.failed() { 198 | stack.set_visible_child_name("failed"); 199 | } 200 | } 201 | )); 202 | 203 | view_model.connect_credentials_notify(clone!( 204 | #[weak] 205 | stack, 206 | move |_vm| { 207 | stack.set_visible_child_name("choose_credential"); 208 | } 209 | )); 210 | } 211 | 212 | fn save_window_size(&self) -> Result<(), glib::BoolError> { 213 | let imp = self.imp(); 214 | 215 | let (width, height) = self.default_size(); 216 | 217 | imp.settings.set_int("window-width", width)?; 218 | imp.settings.set_int("window-height", height)?; 219 | 220 | Ok(()) 221 | } 222 | 223 | fn load_window_size(&self) { 224 | let imp = self.imp(); 225 | 226 | let width = imp.settings.int("window-width"); 227 | let height = imp.settings.int("window-height"); 228 | 229 | self.set_default_size(width, height); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /credentialsd/src/cbor.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | use std::io::{Error, Write}; 3 | 4 | pub(crate) struct CborWriter<'a, W> { 5 | writer: &'a mut W, 6 | } 7 | 8 | impl CborWriter<'_, W> 9 | where 10 | W: Write, 11 | { 12 | pub fn new(writer: &'_ mut W) -> CborWriter<'_, W> { 13 | CborWriter { writer } 14 | } 15 | 16 | pub fn write_bytes(&mut self, data: T) -> Result<(), Error> 17 | where 18 | T: AsRef<[u8]>, 19 | { 20 | self.write_cbor_value( 21 | MajorType::ByteString, 22 | data.as_ref().len().try_into().unwrap(), 23 | Some(data.as_ref()), 24 | )?; 25 | Ok(()) 26 | } 27 | 28 | pub fn write_number(&mut self, num: i128) -> Result<(), Error> { 29 | const POSITIVE_INTEGER_MASK: u8 = 0b000_00000; 30 | const NEGATIVE_INTEGER_MASK: u8 = 0b001_00000; 31 | let (mask, num) = if num >= 0 { 32 | (POSITIVE_INTEGER_MASK, num as u64) 33 | } else { 34 | (NEGATIVE_INTEGER_MASK, (-num - 1) as u64) 35 | }; 36 | if num < 24 { 37 | let d: u8 = num.try_into().unwrap(); 38 | self.writer.write_all(&[mask | d])?; 39 | Ok(()) 40 | } else if num < 2u64.pow(8) { 41 | let d: u8 = num.try_into().unwrap(); 42 | self.writer.write_all(&[mask | 24])?; 43 | self.writer.write_all(&d.to_be_bytes())?; 44 | Ok(()) 45 | } else if num < 2u64.pow(16) { 46 | let d: u16 = num.try_into().unwrap(); 47 | self.writer.write_all(&[mask | 25])?; 48 | self.writer.write_all(&d.to_be_bytes())?; 49 | Ok(()) 50 | } else if num < 2u64.pow(32) { 51 | let d: u32 = num.try_into().unwrap(); 52 | self.writer.write_all(&[mask | 26])?; 53 | self.writer.write_all(&d.to_be_bytes())?; 54 | Ok(()) 55 | } else if num < 2u64.pow(64) { 56 | let d: u64 = num; 57 | self.writer.write_all(&[mask | 27])?; 58 | self.writer.write_all(&d.to_be_bytes())?; 59 | Ok(()) 60 | } else { 61 | Err(Error::new( 62 | std::io::ErrorKind::InvalidInput, 63 | "value too large".to_string(), 64 | )) 65 | } 66 | } 67 | 68 | pub fn write_map_start(&mut self, len: usize) -> Result<(), Error> { 69 | self.write_cbor_value(MajorType::Map, len as u64, None)?; 70 | Ok(()) 71 | } 72 | 73 | pub fn write_array_start(&mut self, len: usize) -> Result<(), Error> { 74 | self.write_cbor_value(MajorType::Array, len as u64, None)?; 75 | Ok(()) 76 | } 77 | 78 | pub fn write_text(&mut self, text: &str) -> Result<(), Error> { 79 | let data = text.as_bytes(); 80 | self.write_cbor_value( 81 | MajorType::TextString, 82 | data.len().try_into().unwrap(), 83 | Some(data), 84 | )?; 85 | Ok(()) 86 | } 87 | 88 | fn write_cbor_value( 89 | &mut self, 90 | major_type: MajorType, 91 | len: u64, 92 | data: Option<&[u8]>, 93 | ) -> Result<(), Error> { 94 | let major_type_mask = match major_type { 95 | MajorType::PositiveInteger => 0b000_00000, 96 | MajorType::NegativeInteger => 0b001_00000, 97 | MajorType::ByteString => 0b010_00000, 98 | MajorType::TextString => 0b011_00000, 99 | MajorType::Array => 0b100_00000, 100 | MajorType::Map => 0b101_00000, 101 | MajorType::Tag => 0b110_00000, 102 | MajorType::Float => 0b111_00000, 103 | }; 104 | 105 | let mut major_type_buf = [0; 9]; 106 | if len < 24 { 107 | let l: u8 = len.try_into().unwrap(); 108 | self.writer.write_all(&[l | major_type_mask])?; 109 | } else if len < 2u64.pow(8) { 110 | let l: u8 = len.try_into().unwrap(); 111 | major_type_buf[0] = 24u8 | major_type_mask; 112 | major_type_buf[1..2].copy_from_slice(&l.to_be_bytes()); 113 | self.writer.write_all(&major_type_buf[0..2])?; 114 | } else if len < 2u64.pow(16) { 115 | let l: u16 = len.try_into().unwrap(); 116 | major_type_buf[0] = 25u8 | major_type_mask; 117 | major_type_buf[1..3].copy_from_slice(&l.to_be_bytes()); 118 | self.writer.write_all(&major_type_buf[0..3])?; 119 | } else if len < 2u64.pow(32) { 120 | let l: u32 = len.try_into().unwrap(); 121 | major_type_buf[0] = 26u8 | major_type_mask; 122 | major_type_buf[1..5].copy_from_slice(&l.to_be_bytes()); 123 | self.writer.write_all(&major_type_buf[0..5])?; 124 | } else if len < 2u64.pow(64) { 125 | let l: u64 = len; 126 | major_type_buf[0] = 27u8 | major_type_mask; 127 | major_type_buf[1..9].copy_from_slice(&l.to_be_bytes()); 128 | self.writer.write_all(&major_type_buf[0..9])?; 129 | } else { 130 | return Err(Error::new( 131 | std::io::ErrorKind::Unsupported, 132 | "Value too large".to_string(), 133 | )); 134 | } 135 | if let Some(data) = data { 136 | self.writer.write_all(data)?; 137 | } 138 | Ok(()) 139 | } 140 | } 141 | 142 | #[allow(dead_code)] 143 | enum MajorType { 144 | PositiveInteger, 145 | NegativeInteger, 146 | ByteString, 147 | TextString, 148 | Array, 149 | Map, 150 | Tag, 151 | Float, 152 | } 153 | 154 | #[cfg(test)] 155 | mod tests { 156 | use super::CborWriter; 157 | 158 | #[test] 159 | fn write_bytes() { 160 | let mut buf: Vec = Vec::with_capacity(16); 161 | let mut cbor_writer = CborWriter::new(&mut buf); 162 | let data: &[u8] = &[0x01, 0x23, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xff]; 163 | cbor_writer.write_bytes(data).unwrap(); 164 | assert_eq!( 165 | buf, 166 | &[ 167 | 0b010_01001, 168 | 0x01, 169 | 0x23, 170 | 0x34, 171 | 0x56, 172 | 0x78, 173 | 0x9a, 174 | 0xbc, 175 | 0xde, 176 | 0xff 177 | ] 178 | ); 179 | } 180 | 181 | #[test] 182 | fn write_bytes_over24() { 183 | let mut buf: Vec = Vec::new(); 184 | let mut cbor_writer = CborWriter::new(&mut buf); 185 | let data = vec![0; 32]; 186 | cbor_writer.write_bytes(data.clone()).unwrap(); 187 | assert_eq!(&buf[0..2], &[0b010_11000, 32u8]); 188 | assert_eq!(&buf[2..34], &data); 189 | } 190 | 191 | #[test] 192 | fn write_uint() { 193 | let mut buf: Vec = Vec::with_capacity(16); 194 | let mut cbor_writer = CborWriter::new(&mut buf); 195 | cbor_writer.write_number(22_i128).unwrap(); 196 | assert_eq!(buf, &[0b000_10110]); 197 | } 198 | 199 | #[test] 200 | fn write_number_u8() { 201 | let mut buf: Vec = Vec::with_capacity(16); 202 | let mut cbor_writer = CborWriter::new(&mut buf); 203 | cbor_writer.write_number(500_i128).unwrap(); 204 | assert_eq!(buf, &[0b000_11001, 0x01, 0xf4]); 205 | } 206 | 207 | #[test] 208 | fn write_negative_number() { 209 | let mut buf: Vec = Vec::with_capacity(16); 210 | let mut cbor_writer = CborWriter::new(&mut buf); 211 | cbor_writer.write_number(-22_i128).unwrap(); 212 | assert_eq!(buf, &[0b001_10101]); 213 | } 214 | 215 | #[test] 216 | fn write_negative_number_u8() { 217 | let mut buf: Vec = Vec::with_capacity(16); 218 | let mut cbor_writer = CborWriter::new(&mut buf); 219 | cbor_writer.write_number(-500_i128).unwrap(); 220 | assert_eq!(buf, &[0b001_11001, 0x01, 0xf3]); 221 | } 222 | 223 | #[test] 224 | fn write_map_start() { 225 | let mut buf: Vec = Vec::with_capacity(3); 226 | let mut cbor_writer = CborWriter::new(&mut buf); 227 | cbor_writer.write_map_start(800).unwrap(); 228 | assert_eq!(buf, &[0b101_11001, 0b0000_0011, 0b0010_0000,]); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Welcome! Thanks for looking into contributing to our project! 2 | 3 | # Table of Contents 4 | 5 | - [Ways to Contribute](#ways-to-contribute) 6 | - [Looking for Help?](#looking-for-help) 7 | - [Documentation](#documentation) 8 | - [Reporting Issues](#reporting-issues) 9 | - [Submitting Code](#submitting-code) 10 | - [Building](#building) 11 | - [Coding Style](#coding-style) 12 | - [Submitting PRs](#submitting-prs) 13 | - [Where do I start?](#where-do-i-start) 14 | - [Testing](#testing) 15 | 16 | # Ways to Contribute 17 | 18 | This document below primarily focuses on writing code for this 19 | project, but there are many different ways you can help out: 20 | 21 | - **Testing**: We only have access to a limited set of hardware and accounts. 22 | Installing the project and using it yourself is a great way to get us more 23 | feedback to improve the project. 24 | 25 | - **Writing UI implementations**: The UI included in this document is for reference 26 | purposes. We need developers who are familiar with (or willing to learn about) 27 | the specifics of integrating with particular desktop environments, like GNOME, 28 | KDE, etc. 29 | 30 | - **Writing documentation**: Documentation is always hard. If you notice 31 | something missing or incorrect, then feel free to ask a question about it or 32 | send a pull request to address it. 33 | 34 | - **Sponsorship**: We haven't set up any sort of platform to receive monetary 35 | donations, but if you are interested, you can reach out by filing an issue, 36 | and we can see what we can do. Individual sponsorships are not the only way to 37 | support the project: getting us in touch with companies or foundations 38 | offering grants for open source or hardware for testing is also helpful. 39 | 40 | - **Spread the word**: We believe that this project is important to get in the hands 41 | of users. Whether you can help or not, telling others about the project and 42 | asking them to get involved is another way you can help! 43 | 44 | And then there is, of course, writing code! 45 | 46 | # Looking for Help? 47 | 48 | Here is a list of helpful resources you can consult: 49 | 50 | ## Documentation 51 | 52 | To help you get started, we have provided documentation for various parts of the 53 | project. Take a look at these: 54 | 55 | - [credentialsd API Specification](/doc/api.md) 56 | - [ARCHITECTURE.md](/ARCHITECTURE.md), our architecture guide. 57 | - [BUILDING.md](/BUILDING.md) 58 | 59 | You may also need to consult various specifications while developing. 60 | 61 | - [WebAuthn (Level 3) Specification](https://www.w3.org/TR/webauthn-3/) 62 | - [CTAP 2.2 Specification](https://fidoalliance.org/specs/fido-v2.2-ps-20250714/fido-client-to-authenticator-protocol-v2.2-ps-20250714.html) 63 | 64 | # Reporting Issues 65 | 66 | If you find any bugs, inconsistencies or other problems, feel free to submit 67 | a GitHub [issue](https://github.com/linux-credentials/credentialsd/issues/new). 68 | 69 | Also, if you have trouble getting on board, let us know so we can help future 70 | contributors to the project overcome that hurdle too. 71 | 72 | ## Security Issues 73 | 74 | If you are reporting a security issue, please review 75 | [`SECURITY.md`](/SECURITY.md) for the prodedure to follow. 76 | 77 | # Submitting Code 78 | 79 | Ready to write some code? Great! Here are some guidelines to follow to 80 | help you on your way: 81 | 82 | ## Building 83 | 84 | When you first start making changes, make sure you can build the code and run 85 | the tests. 86 | 87 | To build the project, follow the build instructions in [`BUILDING.md`](/BUILDING.md). 88 | 89 | To run tests, follow the [test instructions](/BUILDING.md#running-tests) in the 90 | same file. 91 | 92 | ## Coding Style 93 | 94 | In general, try to replicate the coding style that is already present. Specifically: 95 | 96 | ### Naming 97 | 98 | For internal consistency, credentialsd uses `snake_case` for D-Bus field names 99 | and `SCREAMING_SNAKE_CASE` for enum values. This is consistent with D-Bus 100 | conventions, but it is distinct from Web Credential Management/WebAuthn 101 | conventions, which this API is based on. Values specified within JSON string 102 | payloads should stick to the naming conventions as documented in the WebAuthn 103 | spec. 104 | 105 | ### Code Formatting and Linting 106 | 107 | For Rust code, we use [rustfmt][] to ensure consistent formatting code and 108 | [clippy][] to catch common mistakes not caught by the compiler. 109 | 110 | ```sh 111 | # if you don't have them installed, install or update the stable toolchain 112 | rustup install stable 113 | # … and install prebuilt rustfmt and clippy executables (available for most platforms) 114 | rustup component add rustfmt clippy 115 | ``` 116 | 117 | Before committing your changes, run `cargo fmt` to format the code (if your 118 | editor / IDE isn't set up to run it automatically) and `cargo clippy` to run 119 | lints. You'll need to run this from each Cargo project (`credentialsd/`, 120 | `credentialsd-ui/`, `credentialsd-common/`). 121 | 122 | For Python code, we use [ruff][]. 123 | 124 | [rustfmt]: https://github.com/rust-lang/rustfmt#readme 125 | [clippy]: https://github.com/rust-lang/rust-clippy#readme 126 | [ruff]: https://docs.astral.sh/ruff/installation/ 127 | 128 | ### Import Formatting 129 | 130 | Organize your imports into three groups separated by blank lines: 131 | 132 | 1. `std` imports 133 | 1. External imports (from other crates) 134 | 1. Local imports (`crate::`, `super::`, `self::` and things like `LocalEnum::*`) 135 | 136 | For example, 137 | 138 | ```rust 139 | use std::collections::HashMap; 140 | 141 | use credentialsd_common::model::Operation; 142 | 143 | use super::MyType; 144 | ``` 145 | 146 | ### Commit Messages 147 | 148 | The commit message should start with the _area_ that is affected by the change, which is usually the name of the folder withouth the `credentialsd-` prefix. The exception is `credentialsd/` itself, which should use `daemon`. 149 | 150 | Write commit messages using the imperative mood, as if completing the sentence: 151 | "If applied, this commit will \_\_\_." For example, use "Fix some bug" instead 152 | of "Fixed some bug" or "Add a feature" instead of "Added a feature". 153 | 154 | Some examples: 155 | 156 | - "daemon: Allow clients to cancel their own requests" 157 | - "ui: Allow users to go back to device selection" 158 | 159 | (Take a look at this [blog post][commit-messages-guide] for more information on 160 | writing good commit messages.) 161 | 162 | [commit-messages-guide]: https://www.freecodecamp.org/news/writing-good-commit-messages-a-practical-guide/ 163 | 164 | ### Tracking Changes 165 | 166 | For bug fixes, breaking changes and improvements add an entry about them to the 167 | [changelog](/CHANGELOG.md). 168 | 169 | If your changes affect the the public D-Bus API (Gateway, Flow Control or UI 170 | Control APIs), also make sure to document the change in [doc/api.md](/doc/api.md) and 171 | add a note to the [revision history](/doc/api.md#revision-history) in that file. 172 | 173 | ## Submitting PRs 174 | 175 | Once you're ready to submit your code, create a pull request, and one of our 176 | maintainers will review it. Once your PR has passed review, a maintainer will 177 | merge the request and you're done! 🎉 178 | 179 | ## Where do I start? 180 | 181 | If this is your first contribution to the project, we recommend taking a look 182 | at one of the [open issues][] we've marked for new contributors. 183 | 184 | [open issues]: https://github.com/linux-credentials/credentialsd/issues?q=is%3Aissue+is%3Aopen+label%3A"help+wanted" 185 | 186 | # Testing 187 | 188 | Before committing, run `meson test --interactive` from the `build/` directory to 189 | make sure that your changes can build and pass all tests, as well as running the 190 | formatting and linting tools [mentioned above](#code-formatting-and-linting). 191 | 192 | You should also follow the install instructions in [`BUILDING.md`](/BUILDING.md) 193 | and execute authentication flows in a browser to ensure that everything 194 | still works as it should. 195 | 196 | # Translations 197 | 198 | credentialsd-ui is using [gettext-rs](https://github.com/gettext-rs/gettext-rs) to translate user-facing strings. 199 | 200 | Please wrap all user-facing messages in `gettext("my string")`-calls and add the files you add them to, to `credentialsd-ui/po/POTFILES`. 201 | 202 | If you introduce a new language, also add them to `credentialsd-ui/po/LINGUAS`. 203 | 204 | Then `cd` into your build-directory (e.g. `build/`) and run 205 | 206 | ``` 207 | # To update the POT template file, in case new strings have been added in the sources 208 | meson compile credentialsd-ui-pot 209 | # and to update the individual language files 210 | meson compile credentialsd-ui-update-po 211 | ``` 212 | to update the template, so it contains all messages to be translated. 213 | 214 | Meson should take care of building the translations. 215 | 216 | When using the development-profile to build, meson will use the locally built translations. 217 | -------------------------------------------------------------------------------- /credentialsd/src/dbus/ui_control.rs: -------------------------------------------------------------------------------- 1 | //! These methods are called by the flow controller to launch the trusted UI. 2 | 3 | use std::error::Error; 4 | 5 | use zbus::{fdo, proxy, Connection}; 6 | 7 | use credentialsd_common::server::{RequestId, ViewRequest}; 8 | 9 | use crate::credential_service::UiController; 10 | 11 | #[proxy( 12 | gen_blocking = false, 13 | interface = "xyz.iinuwa.credentialsd.UiControl1", 14 | default_service = "xyz.iinuwa.credentialsd.UiControl", 15 | default_path = "/xyz/iinuwa/credentialsd/UiControl" 16 | )] 17 | trait UiControlService { 18 | fn launch_ui(&self, request: ViewRequest) -> fdo::Result<()>; 19 | fn cancel_request(&self, request_id: RequestId) -> fdo::Result<()>; 20 | } 21 | 22 | #[derive(Debug)] 23 | pub struct UiControlServiceClient { 24 | conn: Connection, 25 | } 26 | 27 | impl UiControlServiceClient { 28 | pub fn new(conn: Connection) -> Self { 29 | Self { conn } 30 | } 31 | 32 | async fn proxy(&self) -> Result { 33 | UiControlServiceProxy::new(&self.conn).await 34 | } 35 | } 36 | impl UiController for UiControlServiceClient { 37 | async fn launch_ui(&self, request: ViewRequest) -> Result<(), Box> { 38 | self.proxy() 39 | .await? 40 | .launch_ui(request) 41 | .await 42 | .map_err(|err| err.into()) 43 | } 44 | } 45 | 46 | #[cfg(test)] 47 | pub mod test { 48 | use std::{ 49 | fmt::Debug, 50 | sync::{ 51 | atomic::{AtomicBool, Ordering}, 52 | Arc, 53 | }, 54 | }; 55 | 56 | use credentialsd_common::{ 57 | client::FlowController, model::BackgroundEvent, server::ViewRequest, 58 | }; 59 | use futures_lite::StreamExt; 60 | use tokio::sync::{ 61 | mpsc::{self, Receiver, Sender}, 62 | Mutex as AsyncMutex, Notify, 63 | }; 64 | 65 | use super::UiController; 66 | 67 | #[derive(Debug)] 68 | pub struct DummyUiClient { 69 | tx: Sender, 70 | } 71 | 72 | impl UiController for DummyUiClient { 73 | async fn launch_ui(&self, request: ViewRequest) -> Result<(), Box> { 74 | tracing::debug!( 75 | target: "DummyUiClient", 76 | "Sending launch_ui() request" 77 | ); 78 | self.tx.send(request).await.unwrap(); 79 | tracing::debug!( 80 | target: "DummyUiClient", 81 | "Finish launch_ui() request" 82 | ); 83 | Ok(()) 84 | } 85 | } 86 | 87 | pub struct DummyUiServer 88 | where 89 | F: FlowController + Debug, 90 | { 91 | rx: AsyncMutex>, 92 | svc: Arc>>, 93 | events: Arc>>, 94 | stream_initialized: AtomicBool, 95 | stream_initialized_notifier: Notify, 96 | } 97 | impl DummyUiServer { 98 | pub fn new(events: Vec) -> (Self, DummyUiClient) { 99 | let (tx, rx) = mpsc::channel(32); 100 | let server = Self { 101 | rx: AsyncMutex::new(rx), 102 | svc: Arc::new(AsyncMutex::new(None)), 103 | events: Arc::new(AsyncMutex::new(events)), 104 | stream_initialized: AtomicBool::new(false), 105 | stream_initialized_notifier: Notify::new(), 106 | }; 107 | let client = DummyUiClient { tx }; 108 | (server, client) 109 | } 110 | 111 | pub async fn init(&self, flow_controller: F) { 112 | _ = self.svc.lock().await.insert(flow_controller); 113 | } 114 | 115 | pub async fn run(&self) { 116 | tracing::debug!( 117 | target: "DummyUiServer", 118 | "Starting launch_ui() request listener" 119 | ); 120 | let mut rx = self.rx.lock().await; 121 | while let Some(request) = rx.recv().await { 122 | self.launch_ui(request).await.unwrap(); 123 | } 124 | } 125 | 126 | pub async fn request_hybrid_credential(&self) { 127 | tracing::debug!( 128 | target: "DummyUiServer", 129 | "Received request_hybrid_credential() request" 130 | ); 131 | loop { 132 | if !self.stream_initialized.load(Ordering::Relaxed) { 133 | self.stream_initialized_notifier.notified().await; 134 | } else { 135 | break; 136 | } 137 | } 138 | self.svc 139 | .lock() 140 | .await 141 | .as_mut() 142 | .unwrap() 143 | .get_hybrid_credential() 144 | .await 145 | .unwrap() 146 | } 147 | 148 | pub async fn request_usb_credential(&self) { 149 | tracing::debug!( 150 | target: "DummyUiServer", 151 | "Received request_usb_credential() request" 152 | ); 153 | loop { 154 | if !self.stream_initialized.load(Ordering::Relaxed) { 155 | self.stream_initialized_notifier.notified().await; 156 | } else { 157 | break; 158 | } 159 | } 160 | self.svc 161 | .lock() 162 | .await 163 | .as_mut() 164 | .unwrap() 165 | .get_usb_credential() 166 | .await 167 | .unwrap() 168 | } 169 | 170 | pub async fn request_nfc_credential(&self) { 171 | tracing::debug!( 172 | target: "DummyUiServer", 173 | "Received request_nfc_credential() request" 174 | ); 175 | loop { 176 | if !self.stream_initialized.load(Ordering::Relaxed) { 177 | self.stream_initialized_notifier.notified().await; 178 | } else { 179 | break; 180 | } 181 | } 182 | self.svc 183 | .lock() 184 | .await 185 | .as_mut() 186 | .unwrap() 187 | .get_nfc_credential() 188 | .await 189 | .unwrap() 190 | } 191 | 192 | pub async fn enter_client_pin(&self, pin: String) { 193 | tracing::debug!( 194 | target: "DummyUiServer", 195 | "Received enter_client_pin() request" 196 | ); 197 | self.svc 198 | .lock() 199 | .await 200 | .as_mut() 201 | .unwrap() 202 | .enter_client_pin(pin) 203 | .await 204 | .unwrap(); 205 | } 206 | 207 | pub async fn select_credential(&self, _cred_id: String) { 208 | tracing::debug!( 209 | target: "DummyUiServer", 210 | "Received select_credential() request" 211 | ); 212 | } 213 | 214 | async fn launch_ui(&self, request: ViewRequest) -> Result<(), Box> { 215 | tracing::debug!( 216 | target: "DummyUiServer", 217 | "Received launch_ui() request" 218 | ); 219 | println!("Starting {:?} request UI", request.operation); 220 | let events = self.events.clone(); 221 | let mut stream = self 222 | .svc 223 | .lock() 224 | .await 225 | .as_mut() 226 | .unwrap() 227 | .subscribe() 228 | .await 229 | .unwrap(); 230 | self.stream_initialized.store(true, Ordering::Release); 231 | self.stream_initialized_notifier.notify_waiters(); 232 | tokio::spawn(async move { 233 | tracing::debug!(target: "DummyUiServer", "Starting background event stream"); 234 | while let Some(event) = stream.next().await { 235 | tracing::debug!( 236 | target: "DummyUiServer", 237 | "Received background event: {event:?}" 238 | ); 239 | events.lock().await.push(event); 240 | } 241 | }); 242 | self.svc 243 | .lock() 244 | .await 245 | .as_ref() 246 | .unwrap() 247 | .get_available_public_key_devices() 248 | .await 249 | .unwrap(); 250 | tracing::debug!( 251 | target: "DummyUiServer", 252 | "Finished launch_ui() request" 253 | ); 254 | Ok(()) 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /demo_client/cbor_tests.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import cbor 3 | import json 4 | import unittest 5 | test_vectors = json.loads(r"""[ 6 | { 7 | "cbor": "AA==", 8 | "hex": "00", 9 | "roundtrip": true, 10 | "decoded": 0 11 | }, 12 | { 13 | "cbor": "AQ==", 14 | "hex": "01", 15 | "roundtrip": true, 16 | "decoded": 1 17 | }, 18 | { 19 | "cbor": "Cg==", 20 | "hex": "0a", 21 | "roundtrip": true, 22 | "decoded": 10 23 | }, 24 | { 25 | "cbor": "Fw==", 26 | "hex": "17", 27 | "roundtrip": true, 28 | "decoded": 23 29 | }, 30 | { 31 | "cbor": "GBg=", 32 | "hex": "1818", 33 | "roundtrip": true, 34 | "decoded": 24 35 | }, 36 | { 37 | "cbor": "GBk=", 38 | "hex": "1819", 39 | "roundtrip": true, 40 | "decoded": 25 41 | }, 42 | { 43 | "cbor": "GGQ=", 44 | "hex": "1864", 45 | "roundtrip": true, 46 | "decoded": 100 47 | }, 48 | { 49 | "cbor": "GQPo", 50 | "hex": "1903e8", 51 | "roundtrip": true, 52 | "decoded": 1000 53 | }, 54 | { 55 | "cbor": "GgAPQkA=", 56 | "hex": "1a000f4240", 57 | "roundtrip": true, 58 | "decoded": 1000000 59 | }, 60 | { 61 | "cbor": "GwAAAOjUpRAA", 62 | "hex": "1b000000e8d4a51000", 63 | "roundtrip": true, 64 | "decoded": 1000000000000 65 | }, 66 | { 67 | "cbor": "G///////////", 68 | "hex": "1bffffffffffffffff", 69 | "roundtrip": true, 70 | "decoded": 18446744073709551615 71 | }, 72 | { 73 | "cbor": "wkkBAAAAAAAAAAA=", 74 | "hex": "c249010000000000000000", 75 | "roundtrip": true, 76 | "decoded": 18446744073709551616 77 | }, 78 | { 79 | "cbor": "O///////////", 80 | "hex": "3bffffffffffffffff", 81 | "roundtrip": true, 82 | "decoded": -18446744073709551616 83 | }, 84 | { 85 | "cbor": "w0kBAAAAAAAAAAA=", 86 | "hex": "c349010000000000000000", 87 | "roundtrip": true, 88 | "decoded": -18446744073709551617 89 | }, 90 | { 91 | "cbor": "IA==", 92 | "hex": "20", 93 | "roundtrip": true, 94 | "decoded": -1 95 | }, 96 | { 97 | "cbor": "KQ==", 98 | "hex": "29", 99 | "roundtrip": true, 100 | "decoded": -10 101 | }, 102 | { 103 | "cbor": "OGM=", 104 | "hex": "3863", 105 | "roundtrip": true, 106 | "decoded": -100 107 | }, 108 | { 109 | "cbor": "OQPn", 110 | "hex": "3903e7", 111 | "roundtrip": true, 112 | "decoded": -1000 113 | }, 114 | { 115 | "cbor": "9A==", 116 | "hex": "f4", 117 | "roundtrip": true, 118 | "decoded": false 119 | }, 120 | { 121 | "cbor": "9Q==", 122 | "hex": "f5", 123 | "roundtrip": true, 124 | "decoded": true 125 | }, 126 | { 127 | "cbor": "9g==", 128 | "hex": "f6", 129 | "roundtrip": true, 130 | "decoded": null 131 | }, 132 | { 133 | "cbor": "YA==", 134 | "hex": "60", 135 | "roundtrip": true, 136 | "decoded": "" 137 | }, 138 | { 139 | "cbor": "YWE=", 140 | "hex": "6161", 141 | "roundtrip": true, 142 | "decoded": "a" 143 | }, 144 | { 145 | "cbor": "ZElFVEY=", 146 | "hex": "6449455446", 147 | "roundtrip": true, 148 | "decoded": "IETF" 149 | }, 150 | { 151 | "cbor": "YiJc", 152 | "hex": "62225c", 153 | "roundtrip": true, 154 | "decoded": "\"\\" 155 | }, 156 | { 157 | "cbor": "YsO8", 158 | "hex": "62c3bc", 159 | "roundtrip": true, 160 | "decoded": "ü" 161 | }, 162 | { 163 | "cbor": "Y+awtA==", 164 | "hex": "63e6b0b4", 165 | "roundtrip": true, 166 | "decoded": "水" 167 | }, 168 | { 169 | "cbor": "ZPCQhZE=", 170 | "hex": "64f0908591", 171 | "roundtrip": true, 172 | "decoded": "𐅑" 173 | }, 174 | { 175 | "cbor": "gA==", 176 | "hex": "80", 177 | "roundtrip": true, 178 | "decoded": [ 179 | 180 | ] 181 | }, 182 | { 183 | "cbor": "gwECAw==", 184 | "hex": "83010203", 185 | "roundtrip": true, 186 | "decoded": [ 187 | 1, 188 | 2, 189 | 3 190 | ] 191 | }, 192 | { 193 | "cbor": "gwGCAgOCBAU=", 194 | "hex": "8301820203820405", 195 | "roundtrip": true, 196 | "decoded": [ 197 | 1, 198 | [ 199 | 2, 200 | 3 201 | ], 202 | [ 203 | 4, 204 | 5 205 | ] 206 | ] 207 | }, 208 | { 209 | "cbor": "mBkBAgMEBQYHCAkKCwwNDg8QERITFBUWFxgYGBk=", 210 | "hex": "98190102030405060708090a0b0c0d0e0f101112131415161718181819", 211 | "roundtrip": true, 212 | "decoded": [ 213 | 1, 214 | 2, 215 | 3, 216 | 4, 217 | 5, 218 | 6, 219 | 7, 220 | 8, 221 | 9, 222 | 10, 223 | 11, 224 | 12, 225 | 13, 226 | 14, 227 | 15, 228 | 16, 229 | 17, 230 | 18, 231 | 19, 232 | 20, 233 | 21, 234 | 22, 235 | 23, 236 | 24, 237 | 25 238 | ] 239 | }, 240 | { 241 | "cbor": "oA==", 242 | "hex": "a0", 243 | "roundtrip": true, 244 | "decoded": { 245 | } 246 | }, 247 | { 248 | "cbor": "omFhAWFiggID", 249 | "hex": "a26161016162820203", 250 | "roundtrip": true, 251 | "decoded": { 252 | "a": 1, 253 | "b": [ 254 | 2, 255 | 3 256 | ] 257 | } 258 | }, 259 | { 260 | "cbor": "gmFhoWFiYWM=", 261 | "hex": "826161a161626163", 262 | "roundtrip": true, 263 | "decoded": [ 264 | "a", 265 | { 266 | "b": "c" 267 | } 268 | ] 269 | }, 270 | { 271 | "cbor": "pWFhYUFhYmFCYWNhQ2FkYURhZWFF", 272 | "hex": "a56161614161626142616361436164614461656145", 273 | "roundtrip": true, 274 | "decoded": { 275 | "a": "A", 276 | "b": "B", 277 | "c": "C", 278 | "d": "D", 279 | "e": "E" 280 | } 281 | }, 282 | { 283 | "cbor": "f2VzdHJlYWRtaW5n/w==", 284 | "hex": "7f657374726561646d696e67ff", 285 | "roundtrip": false, 286 | "decoded": "streaming" 287 | }, 288 | { 289 | "cbor": "n/8=", 290 | "hex": "9fff", 291 | "roundtrip": false, 292 | "decoded": [ 293 | 294 | ] 295 | }, 296 | { 297 | "cbor": "nwGCAgOfBAX//w==", 298 | "hex": "9f018202039f0405ffff", 299 | "roundtrip": false, 300 | "decoded": [ 301 | 1, 302 | [ 303 | 2, 304 | 3 305 | ], 306 | [ 307 | 4, 308 | 5 309 | ] 310 | ] 311 | }, 312 | { 313 | "cbor": "nwGCAgOCBAX/", 314 | "hex": "9f01820203820405ff", 315 | "roundtrip": false, 316 | "decoded": [ 317 | 1, 318 | [ 319 | 2, 320 | 3 321 | ], 322 | [ 323 | 4, 324 | 5 325 | ] 326 | ] 327 | }, 328 | { 329 | "cbor": "gwGCAgOfBAX/", 330 | "hex": "83018202039f0405ff", 331 | "roundtrip": false, 332 | "decoded": [ 333 | 1, 334 | [ 335 | 2, 336 | 3 337 | ], 338 | [ 339 | 4, 340 | 5 341 | ] 342 | ] 343 | }, 344 | { 345 | "cbor": "gwGfAgP/ggQF", 346 | "hex": "83019f0203ff820405", 347 | "roundtrip": false, 348 | "decoded": [ 349 | 1, 350 | [ 351 | 2, 352 | 3 353 | ], 354 | [ 355 | 4, 356 | 5 357 | ] 358 | ] 359 | }, 360 | { 361 | "cbor": "nwECAwQFBgcICQoLDA0ODxAREhMUFRYXGBgYGf8=", 362 | "hex": "9f0102030405060708090a0b0c0d0e0f101112131415161718181819ff", 363 | "roundtrip": false, 364 | "decoded": [ 365 | 1, 366 | 2, 367 | 3, 368 | 4, 369 | 5, 370 | 6, 371 | 7, 372 | 8, 373 | 9, 374 | 10, 375 | 11, 376 | 12, 377 | 13, 378 | 14, 379 | 15, 380 | 16, 381 | 17, 382 | 18, 383 | 19, 384 | 20, 385 | 21, 386 | 22, 387 | 23, 388 | 24, 389 | 25 390 | ] 391 | }, 392 | { 393 | "cbor": "v2FhAWFinwID//8=", 394 | "hex": "bf61610161629f0203ffff", 395 | "roundtrip": false, 396 | "decoded": { 397 | "a": 1, 398 | "b": [ 399 | 2, 400 | 3 401 | ] 402 | } 403 | }, 404 | { 405 | "cbor": "gmFhv2FiYWP/", 406 | "hex": "826161bf61626163ff", 407 | "roundtrip": false, 408 | "decoded": [ 409 | "a", 410 | { 411 | "b": "c" 412 | } 413 | ] 414 | }, 415 | { 416 | "cbor": "v2NGdW71Y0FtdCH/", 417 | "hex": "bf6346756ef563416d7421ff", 418 | "roundtrip": false, 419 | "decoded": { 420 | "Fun": true, 421 | "Amt": -2 422 | } 423 | } 424 | ]""") 425 | 426 | class CborTests(unittest.TestCase): 427 | def test_execute_vectors(self): 428 | for i, tv in enumerate(test_vectors): 429 | with self.subTest(i=i, hex=tv['hex'], expected=json.dumps(tv['decoded']), major_type=format(int(tv['hex'][:2], 16), '08b')): 430 | data = base64.b64decode(tv['cbor']) 431 | if (data[0] >> 5) >= 6: 432 | continue 433 | expected = tv['decoded'] 434 | actual = cbor.loads(data) 435 | self.assertEqual(expected, actual) 436 | -------------------------------------------------------------------------------- /credentialsd-ui/po/en_US.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Report-Msgid-Bugs-To: \"https://github.com/linux-credentials/credentialsd/" 4 | "issues\"\n" 5 | "POT-Creation-Date: 2025-10-30 14:43+0100\n" 6 | "PO-Revision-Date: 2025-10-10 14:45+0200\n" 7 | "Last-Translator: Martin Sirringhaus \n" 8 | "Language: en_US\n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" 13 | 14 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in:2 15 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:8 16 | #: src/gui/view_model/gtk/mod.rs:378 17 | msgid "Credential Manager" 18 | msgstr "Credential Manager" 19 | 20 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in:3 21 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:9 22 | msgid "Write a GTK + Rust application" 23 | msgstr "Write a GTK + Rust application" 24 | 25 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in:9 26 | msgid "Gnome;GTK;" 27 | msgstr "Gnome;GTK;" 28 | 29 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in:6 30 | msgid "Window width" 31 | msgstr "Window width" 32 | 33 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in:10 34 | msgid "Window height" 35 | msgstr "Window height" 36 | 37 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in:14 38 | msgid "Window maximized state" 39 | msgstr "Window maximized state" 40 | 41 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:11 42 | msgid "" 43 | "A boilerplate template for GTK + Rust. It uses Meson as a build system and " 44 | "has flatpak support by default." 45 | msgstr "" 46 | "A boilerplate template for GTK + Rust. It uses Meson as a build system and " 47 | "has flatpak support by default." 48 | 49 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:16 50 | msgid "Registering a credential" 51 | msgstr "Registering a credential" 52 | 53 | #: data/resources/ui/shortcuts.ui:11 54 | msgctxt "shortcut window" 55 | msgid "General" 56 | msgstr "General" 57 | 58 | #: data/resources/ui/shortcuts.ui:14 59 | msgctxt "shortcut window" 60 | msgid "Show Shortcuts" 61 | msgstr "Show Shortcuts" 62 | 63 | #: data/resources/ui/shortcuts.ui:20 64 | msgctxt "shortcut window" 65 | msgid "Quit" 66 | msgstr "Quit" 67 | 68 | #: data/resources/ui/window.ui:6 69 | msgid "_Preferences" 70 | msgstr "_Preferences" 71 | 72 | #: data/resources/ui/window.ui:10 73 | msgid "_Keyboard Shortcuts" 74 | msgstr "Keyboard Shortcuts" 75 | 76 | #: data/resources/ui/window.ui:68 77 | msgid "Choose device" 78 | msgstr "Choose device" 79 | 80 | #: data/resources/ui/window.ui:74 81 | msgid "Devices" 82 | msgstr "Devices" 83 | 84 | #: data/resources/ui/window.ui:98 85 | msgid "Connect a security key" 86 | msgstr "Connect a security key" 87 | 88 | #: data/resources/ui/window.ui:138 89 | msgid "Scan the QR code to connect your device" 90 | msgstr "Scan the QR code to connect your device" 91 | 92 | #: data/resources/ui/window.ui:183 data/resources/ui/window.ui:189 93 | msgid "Choose credential" 94 | msgstr "Choose credential" 95 | 96 | #: data/resources/ui/window.ui:212 97 | msgid "Complete" 98 | msgstr "Complete" 99 | 100 | #: data/resources/ui/window.ui:218 101 | msgid "Done!" 102 | msgstr "Done!" 103 | 104 | #: data/resources/ui/window.ui:229 105 | msgid "Something went wrong." 106 | msgstr "Something went wrong." 107 | 108 | #: data/resources/ui/window.ui:242 src/gui/view_model/mod.rs:280 109 | msgid "" 110 | "Something went wrong while retrieving a credential. Please try again later " 111 | "or use a different authenticator." 112 | msgstr "" 113 | "Something went wrong while retrieving a credential. Please try again later " 114 | "or use a different authenticator." 115 | 116 | #: src/gui/view_model/gtk/mod.rs:145 117 | #, fuzzy 118 | msgid "Enter your PIN. One attempt remaining." 119 | msgid_plural "Enter your PIN. %d attempts remaining." 120 | msgstr[0] "Enter your PIN. One attempt remaining." 121 | msgstr[1] "Enter your PIN. %d attempts remaining." 122 | 123 | #: src/gui/view_model/gtk/mod.rs:151 124 | msgid "Enter your PIN." 125 | msgstr "Enter your PIN." 126 | 127 | #: src/gui/view_model/gtk/mod.rs:160 128 | msgid "Touch your device again. One attempt remaining." 129 | msgid_plural "Touch your device again. %d attempts remaining." 130 | msgstr[0] "Touch your device again. One attempt remaining." 131 | msgstr[1] "Touch your device again. %d attempts remaining." 132 | 133 | #: src/gui/view_model/gtk/mod.rs:166 134 | msgid "Touch your device." 135 | msgstr "Touch your device." 136 | 137 | #: src/gui/view_model/gtk/mod.rs:171 138 | msgid "Touch your device" 139 | msgstr "Touch your device" 140 | 141 | #: src/gui/view_model/gtk/mod.rs:174 142 | msgid "Scan the QR code with your device to begin authentication." 143 | msgstr "Scan the QR code with your device to begin authentication." 144 | 145 | #: src/gui/view_model/gtk/mod.rs:184 146 | msgid "" 147 | "Connecting to your device. Make sure both devices are near each other and " 148 | "have Bluetooth enabled." 149 | msgstr "" 150 | "Connecting to your device. Make sure both devices are near each other and " 151 | "have Bluetooth enabled." 152 | 153 | #: src/gui/view_model/gtk/mod.rs:192 154 | msgid "Device connected. Follow the instructions on your device" 155 | msgstr "Device connected. Follow the instructions on your device" 156 | 157 | #: src/gui/view_model/gtk/mod.rs:318 158 | msgid "Insert your security key." 159 | msgstr "Insert your security key." 160 | 161 | #: src/gui/view_model/gtk/mod.rs:334 162 | msgid "Multiple devices found. Please select with which to proceed." 163 | msgstr "Multiple devices found. Please select with which to proceed." 164 | 165 | #: src/gui/view_model/gtk/device.rs:57 166 | msgid "A Bluetooth device" 167 | msgstr "A Bluetooth device" 168 | 169 | #: src/gui/view_model/gtk/device.rs:58 170 | msgid "This device" 171 | msgstr "This device" 172 | 173 | #: src/gui/view_model/gtk/device.rs:59 174 | msgid "A mobile device" 175 | msgstr "A mobile device" 176 | 177 | #: src/gui/view_model/gtk/device.rs:60 178 | msgid "Linked Device" 179 | msgstr "Linked Device" 180 | 181 | #: src/gui/view_model/gtk/device.rs:61 182 | msgid "A security key or card (NFC)" 183 | msgstr "A security key or card (NFC)" 184 | 185 | #: src/gui/view_model/gtk/device.rs:62 186 | msgid "A security key (USB)" 187 | msgstr "A security key (USB)" 188 | 189 | #: src/gui/view_model/mod.rs:75 190 | msgid "unknown application" 191 | msgstr "unknown application" 192 | 193 | #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from 194 | #: src/gui/view_model/mod.rs:80 195 | msgid "Create a passkey for %s1" 196 | msgstr "Create a passkey for %s1" 197 | 198 | #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from 199 | #: src/gui/view_model/mod.rs:84 200 | msgid "Use a passkey for %s1" 201 | msgstr "Use a passkey for %s1" 202 | 203 | #. TRANSLATORS: %s1 is the "relying party" (e.g.: domain name) where the request is coming from 204 | #. TRANSLATORS: %s2 is the application name (e.g.: firefox) where the request is coming from, must be left untouched to make the name bold 205 | #. TRANSLATORS: %i1 is the process ID of the requesting application 206 | #. TRANSLATORS: %s3 is the absolute path (think: /usr/bin/firefox) of the requesting application 207 | #: src/gui/view_model/mod.rs:96 208 | msgid "" 209 | "\"%s2\" (process ID: %i1, binary: %s3) is asking to create a " 210 | "credential to register at \"%s1\". Only proceed if you trust this process." 211 | msgstr "" 212 | "\"%s2\" (process ID: %i1, binary: %s3) is asking to create a " 213 | "credential to register at \"%s1\". Only proceed if you trust this process." 214 | 215 | #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from 216 | #. TRANSLATORS: %s2 is the application name (e.g.: firefox) where the request is coming from, must be left untouched to make the name bold 217 | #. TRANSLATORS: %i1 is the process ID of the requesting application 218 | #. TRANSLATORS: %s3 is the absolute path (think: /usr/bin/firefox) of the requesting application 219 | #: src/gui/view_model/mod.rs:103 220 | msgid "" 221 | "\"%s2\" (process ID: %i1, binary: %s3) is asking to use a credential " 222 | "to sign in to \"%s1\". Only proceed if you trust this process." 223 | msgstr "" 224 | "\"%s2\" (process ID: %i1, binary: %s3) is asking to use a credential " 225 | "to sign in to \"%s1\". Only proceed if you trust this process." 226 | 227 | #: src/gui/view_model/mod.rs:220 228 | msgid "Failed to select credential from device." 229 | msgstr "Failed to select credential from device." 230 | 231 | #: src/gui/view_model/mod.rs:274 232 | msgid "No matching credentials found on this authenticator." 233 | msgstr "No matching credentials found on this authenticator." 234 | 235 | #: src/gui/view_model/mod.rs:277 236 | msgid "" 237 | "No more PIN attempts allowed. Try removing your device and plugging it back " 238 | "in." 239 | msgstr "" 240 | "No more PIN attempts allowed. Try removing your device and plugging it back " 241 | "in." 242 | 243 | #: src/gui/view_model/mod.rs:283 244 | msgid "This credential is already registered on this authenticator." 245 | msgstr "This credential is already registered on this authenticator." 246 | 247 | #: src/gui/view_model/mod.rs:331 248 | msgid "Something went wrong. Try again later or use a different authenticator." 249 | msgstr "" 250 | "Something went wrong. Try again later or use a different authenticator." 251 | -------------------------------------------------------------------------------- /credentialsd-ui/po/de_DE.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: PACKAGE VERSION\n" 4 | "Report-Msgid-Bugs-To: \"https://github.com/linux-credentials/credentialsd/" 5 | "issues\"\n" 6 | "POT-Creation-Date: 2025-10-30 14:43+0100\n" 7 | "PO-Revision-Date: 2025-10-10 14:45+0200\n" 8 | "Last-Translator: Martin Sirringhaus \n" 9 | "Language: de_DE\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" 14 | 15 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in:2 16 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:8 17 | #: src/gui/view_model/gtk/mod.rs:378 18 | msgid "Credential Manager" 19 | msgstr "Zugangsdatenmanager" 20 | 21 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in:3 22 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:9 23 | msgid "Write a GTK + Rust application" 24 | msgstr "" 25 | 26 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.desktop.in.in:9 27 | msgid "Gnome;GTK;" 28 | msgstr "Gnome;GTK;" 29 | 30 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in:6 31 | msgid "Window width" 32 | msgstr "Fensterbreite" 33 | 34 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in:10 35 | msgid "Window height" 36 | msgstr "Fensterhöhe" 37 | 38 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.gschema.xml.in:14 39 | msgid "Window maximized state" 40 | msgstr "Fenster maximiert" 41 | 42 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:11 43 | msgid "" 44 | "A boilerplate template for GTK + Rust. It uses Meson as a build system and " 45 | "has flatpak support by default." 46 | msgstr "Eine Vorlage für eine GTK + Rust Anwendung." 47 | 48 | #: data/xyz.iinuwa.credentialsd.CredentialsUi.metainfo.xml.in.in:16 49 | msgid "Registering a credential" 50 | msgstr "Zugangsdaten registrieren" 51 | 52 | #: data/resources/ui/shortcuts.ui:11 53 | msgctxt "shortcut window" 54 | msgid "General" 55 | msgstr "Allgemein" 56 | 57 | #: data/resources/ui/shortcuts.ui:14 58 | msgctxt "shortcut window" 59 | msgid "Show Shortcuts" 60 | msgstr "Zeige Kürzel" 61 | 62 | #: data/resources/ui/shortcuts.ui:20 63 | msgctxt "shortcut window" 64 | msgid "Quit" 65 | msgstr "Beenden" 66 | 67 | #: data/resources/ui/window.ui:6 68 | msgid "_Preferences" 69 | msgstr "Einstellungen" 70 | 71 | #: data/resources/ui/window.ui:10 72 | msgid "_Keyboard Shortcuts" 73 | msgstr "Tastaturkürzel" 74 | 75 | #: data/resources/ui/window.ui:68 76 | msgid "Choose device" 77 | msgstr "Gerät auswählen" 78 | 79 | #: data/resources/ui/window.ui:74 80 | msgid "Devices" 81 | msgstr "Geräte" 82 | 83 | #: data/resources/ui/window.ui:98 84 | msgid "Plug in security key" 85 | msgstr "Stecken Sie Ihren Security-Token ein" 86 | 87 | #: data/resources/ui/window.ui:138 88 | msgid "Scan the QR code to connect your device" 89 | msgstr "Scannen Sie den QR-Code, um Ihr Gerät zu verbinden" 90 | 91 | #: data/resources/ui/window.ui:183 data/resources/ui/window.ui:189 92 | msgid "Choose credential" 93 | msgstr "Wählen Sie Zugangsdaten aus" 94 | 95 | #: data/resources/ui/window.ui:212 96 | msgid "Complete" 97 | msgstr "Abgeschlossen" 98 | 99 | #: data/resources/ui/window.ui:218 100 | msgid "Done!" 101 | msgstr "Fertig!" 102 | 103 | #: data/resources/ui/window.ui:229 104 | msgid "Something went wrong." 105 | msgstr "Etwas ist schief gegangen." 106 | 107 | #: data/resources/ui/window.ui:242 src/gui/view_model/mod.rs:280 108 | msgid "" 109 | "Something went wrong while retrieving a credential. Please try again later " 110 | "or use a different authenticator." 111 | msgstr "" 112 | "Beim Abrufen Ihrer Zugangsdaten ist ein Fehler aufgetreten. Versuchen Sie es " 113 | "später wieder, oder verwenden Sie einen anderen Security-Token." 114 | 115 | #: src/gui/view_model/gtk/mod.rs:145 116 | #, fuzzy 117 | msgid "Enter your PIN. One attempt remaining." 118 | msgid_plural "Enter your PIN. %d attempts remaining." 119 | msgstr[0] "Geben Sie Ihren PIN ein. Sie haben nur noch einen Versuch." 120 | msgstr[1] "Geben Sie Ihren PIN ein. Sie haben noch %d Versuche." 121 | 122 | #: src/gui/view_model/gtk/mod.rs:151 123 | msgid "Enter your PIN." 124 | msgstr "Geben Sie Ihren PIN ein." 125 | 126 | #: src/gui/view_model/gtk/mod.rs:160 127 | msgid "Touch your device again. One attempt remaining." 128 | msgid_plural "Touch your device again. %d attempts remaining." 129 | msgstr[0] "Berühren Sie Ihr Gerät. Sie haben nur noch einen Versuch." 130 | msgstr[1] "Berühren Sie nochmal Ihr Gerät. Sie haben nur noch %d Versuche." 131 | 132 | #: src/gui/view_model/gtk/mod.rs:166 133 | msgid "Touch your device." 134 | msgstr "Berühren Sie Ihr Gerät." 135 | 136 | #: src/gui/view_model/gtk/mod.rs:171 137 | msgid "Touch your device" 138 | msgstr "Berühren Sie Ihr Gerät." 139 | 140 | #: src/gui/view_model/gtk/mod.rs:174 141 | msgid "Scan the QR code with your device to begin authentication." 142 | msgstr "" 143 | "Scannen Sie den QR code mit ihrem Gerät um die Authentifizierung zu beginnen." 144 | 145 | #: src/gui/view_model/gtk/mod.rs:184 146 | msgid "" 147 | "Connecting to your device. Make sure both devices are near each other and " 148 | "have Bluetooth enabled." 149 | msgstr "" 150 | "Verbindung zu Ihrem Gerät wird aufgebaut. Stellen Sie sicher, dass beide " 151 | "Geräte nah beieinander sind und Bluetooth aktiviert haben." 152 | 153 | #: src/gui/view_model/gtk/mod.rs:192 154 | msgid "Device connected. Follow the instructions on your device" 155 | msgstr "Verbindung hergestellt. Folgen Sie den Anweisungen auf Ihrem Gerät." 156 | 157 | #: src/gui/view_model/gtk/mod.rs:318 158 | msgid "Insert your security key." 159 | msgstr "Stecken Sie Ihren Security-Token ein." 160 | 161 | #: src/gui/view_model/gtk/mod.rs:334 162 | msgid "Multiple devices found. Please select with which to proceed." 163 | msgstr "Mehrere Geräte gefunden. Bitte wählen Sie einen aus, um fortzufahren." 164 | 165 | #: src/gui/view_model/gtk/device.rs:57 166 | msgid "A Bluetooth device" 167 | msgstr "Ein Bluetooth-Gerät" 168 | 169 | #: src/gui/view_model/gtk/device.rs:58 170 | msgid "This device" 171 | msgstr "Dieses Gerät" 172 | 173 | #: src/gui/view_model/gtk/device.rs:59 174 | msgid "A mobile device" 175 | msgstr "Ein mobiles Gerät" 176 | 177 | #: src/gui/view_model/gtk/device.rs:60 178 | msgid "Linked Device" 179 | msgstr "Verbundenes Gerät" 180 | 181 | #: src/gui/view_model/gtk/device.rs:61 182 | msgid "A security key or card (NFC)" 183 | msgstr "Ein NFC-Gerät" 184 | 185 | #: src/gui/view_model/gtk/device.rs:62 186 | msgid "A security key (USB)" 187 | msgstr "Ein Security-Token" 188 | 189 | #: src/gui/view_model/mod.rs:75 190 | msgid "unknown application" 191 | msgstr "unbekannter Applikation" 192 | 193 | #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from 194 | #: src/gui/view_model/mod.rs:80 195 | msgid "Create a passkey for %s1" 196 | msgstr "Neuen Passkey für %s1 erstellen" 197 | 198 | #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from 199 | #: src/gui/view_model/mod.rs:84 200 | msgid "Use a passkey for %s1" 201 | msgstr "Passkey für %s1 abrufen" 202 | 203 | #. TRANSLATORS: %s1 is the "relying party" (e.g.: domain name) where the request is coming from 204 | #. TRANSLATORS: %s2 is the application name (e.g.: firefox) where the request is coming from, must be left untouched to make the name bold 205 | #. TRANSLATORS: %i1 is the process ID of the requesting application 206 | #. TRANSLATORS: %s3 is the absolute path (think: /usr/bin/firefox) of the requesting application 207 | #: src/gui/view_model/mod.rs:96 208 | msgid "" 209 | "\"%s2\" (process ID: %i1, binary: %s3) is asking to create a " 210 | "credential to register at \"%s1\". Only proceed if you trust this process." 211 | msgstr "" 212 | "\"%s2\" (Prozess-ID: %i1, ausführbare Datei: %s3) möchte neue Zugangsdaten erstellen, " 213 | "um Sie bei \"%s1\" zu registrieren. Fahren Sie nur fort, wenn Sie diesem Prozess vertrauen." 214 | 215 | #. TRANSLATORS: %s1 is the "relying party" (think: domain name) where the request is coming from 216 | #. TRANSLATORS: %s2 is the application name (e.g.: firefox) where the request is coming from, must be left untouched to make the name bold 217 | #. TRANSLATORS: %i1 is the process ID of the requesting application 218 | #. TRANSLATORS: %s3 is the absolute path (think: /usr/bin/firefox) of the requesting application 219 | #: src/gui/view_model/mod.rs:103 220 | msgid "" 221 | "\"%s2\" (process ID: %i1, binary: %s3) is asking to use a credential " 222 | "to sign in to \"%s1\". Only proceed if you trust this process." 223 | msgstr "" 224 | "\"%s2\" (Prozess-ID: %i1, ausführbare Datei: %s3) möchte Zugangsdaten abrufen, um " 225 | "Sie bei \"%s1\" anzumelden. Fahren Sie nur fort, wenn Sie diesem Prozess vertrauen." 226 | 227 | #: src/gui/view_model/mod.rs:220 228 | msgid "Failed to select credential from device." 229 | msgstr "Zugangsdaten vom Gerät konnten nicht ausgewählt werden." 230 | 231 | #: src/gui/view_model/mod.rs:274 232 | msgid "No matching credentials found on this authenticator." 233 | msgstr "Keine passenden Zugangsdaten auf diesem Gerät gefunden." 234 | 235 | #: src/gui/view_model/mod.rs:277 236 | msgid "" 237 | "No more PIN attempts allowed. Try removing your device and plugging it back " 238 | "in." 239 | msgstr "" 240 | "Keine weiteren PIN-Eingaben erlaubt. Versuchen Sie ihr Gerät aus- und wieder " 241 | "einzustecken." 242 | 243 | #: src/gui/view_model/mod.rs:283 244 | msgid "This credential is already registered on this authenticator." 245 | msgstr "Diese Zugangsdaten sind bereits auf diesem Gerät registriert." 246 | 247 | #: src/gui/view_model/mod.rs:331 248 | msgid "Something went wrong. Try again later or use a different authenticator." 249 | msgstr "" 250 | "Es ist ein Fehler aufgetreten. Bitte versuchen Sie es später noch einmal, " 251 | "oder benutzen Sie ein anderes Gerät." 252 | --------------------------------------------------------------------------------
A boilerplate template for GTK + Rust. It uses Meson as a build system and has flatpak support by default.