diff --git a/APKBUILD b/APKBUILD index 1c511d9f..52826ab2 100644 --- a/APKBUILD +++ b/APKBUILD @@ -12,12 +12,14 @@ depends=" phosh greetd greetd-phrog-schemas - libphosh" + libphosh + linux-pam" makedepends=" cargo cargo-auditable foot - libphosh-dev" + libphosh-dev + linux-pam-dev" checkdepends="xvfb-run" _gitrev=main @@ -52,6 +54,7 @@ package() { install -d "$pkgdir"/usr/share/phrog/autostart install -d "$pkgdir"/etc/phrog/autostart install -Dm755 target/release/phrog -t "$pkgdir"/usr/bin/ + install -Dm755 target/release/phrog-gdm-shim -t "$pkgdir"/usr/bin/ install -Dm755 data/phrog-greetd-session -t "$pkgdir"/usr/libexec/ } diff --git a/Cargo.lock b/Cargo.lock index 9ef9119b..a215dd9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -247,6 +247,26 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.117", +] + [[package]] name = "bitflags" version = "2.11.0" @@ -313,6 +333,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-expr" version = "0.15.8" @@ -345,6 +374,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", +] + [[package]] name = "clap" version = "4.6.0" @@ -412,6 +451,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "endi" version = "1.1.1" @@ -993,6 +1038,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -1015,6 +1069,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -1142,6 +1202,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "nix" version = "0.30.1" @@ -1154,6 +1220,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "objc" version = "0.2.7" @@ -1205,6 +1281,40 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "pam" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ab553c52103edb295d8f7d6a3b593dc22a30b1fb99643c777a8f36915e285ba" +dependencies = [ + "libc", + "memchr", + "pam-macros", + "pam-sys", + "users", +] + +[[package]] +name = "pam-macros" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "pam-sys" +version = "1.0.0-alpha5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce9484729b3e52c0bacdc5191cb6a6a5f31ef4c09c5e4ab1209d3340ad9e997b" +dependencies = [ + "bindgen", + "libc", +] + [[package]] name = "pango" version = "0.18.3" @@ -1257,6 +1367,7 @@ dependencies = [ "libphosh", "log", "nix", + "pam", "serde", "tempfile", "wayland-client", @@ -1428,6 +1539,12 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1575,6 +1692,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", + "quote", "unicode-ident", ] @@ -1826,6 +1944,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "users" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486" +dependencies = [ + "libc", + "log", +] + [[package]] name = "utf8parse" version = "0.2.2" @@ -1838,6 +1966,7 @@ version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ + "getrandom", "js-sys", "serde_core", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 221f723c..70a54a05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,13 +20,14 @@ anyhow = "1.0.82" libphosh = "0.0.7" clap = { version = "4.5.4", features = ["derive"] } wayland-client = "0.31" -zbus = { version = "5", default-features = false, features = ["blocking", "async-io"] } +zbus = { version = "5", default-features = false, features = ["blocking", "async-io", "p2p"] } nix = { version = "0.30", features = ["signal"] } async-global-executor = "3.0.0" futures-util = "0.3.30" log = "0.4.22" lazy_static = "^1.4" gettext-rs = "0.7" +pam = "0.8" [dependencies.glib] version = "0.18" diff --git a/data/org.gnome.DisplayManager.phrog.conf b/data/org.gnome.DisplayManager.phrog.conf new file mode 100644 index 00000000..f68a6100 --- /dev/null +++ b/data/org.gnome.DisplayManager.phrog.conf @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/data/phrog-gdm-shim.service b/data/phrog-gdm-shim.service new file mode 100644 index 00000000..619b336e --- /dev/null +++ b/data/phrog-gdm-shim.service @@ -0,0 +1,14 @@ +[Unit] +Description=Phrog GNOME DisplayManager compatibility shim +Documentation=https://github.com/samcday/phrog +Conflicts=gdm.service gdm3.service +After=dbus.service systemd-logind.service + +[Service] +Type=dbus +BusName=org.gnome.DisplayManager +ExecStart=/usr/bin/phrog-gdm-shim +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/debian/control b/debian/control index 2eeed24b..a4a71f37 100644 --- a/debian/control +++ b/debian/control @@ -20,9 +20,12 @@ Build-Depends: librust-libhandy-0.11+v1-6-dev, librust-libphosh-0.0.7-dev, librust-nix-0.30+signal-dev, + librust-pam-0.8-dev, librust-wayland-client-0.31-dev, librust-zbus-5+async-io-dev (>= 4.3.1~~), librust-zbus-5+blocking-dev (>= 4.3.1~~), + librust-zbus-5+p2p-dev (>= 4.3.1~~), + libpam0g-dev, # Dependencies needed only for tests at-spi2-core , dbus-daemon , @@ -57,6 +60,7 @@ Depends: gnome-settings-daemon, gnome-shell-common, greetd, + libpam0g, librsvg2-common, phoc, phosh-common, diff --git a/debian/phrog.install b/debian/phrog.install index b1e47b61..54251b2b 100644 --- a/debian/phrog.install +++ b/debian/phrog.install @@ -5,6 +5,8 @@ data/mobi.phosh.Phrog.desktop usr/share/applications data/mobi.phosh.Phrog.service /usr/lib/systemd/user/ data/mobi.phosh.Phrog.target /usr/lib/systemd/user/ data/systemd-session.conf usr/lib/systemd/user/gnome-session@phrog.target.d +data/phrog-gdm-shim.service usr/lib/systemd/system +data/org.gnome.DisplayManager.phrog.conf usr/share/dbus-1/system.d # Debian-specific config target/dist-data/phrog.toml etc/greetd diff --git a/debian/rules b/debian/rules index a7555d6e..e8d0b933 100755 --- a/debian/rules +++ b/debian/rules @@ -18,6 +18,8 @@ override_dh_auto_build: override_dh_auto_install: install -D -m0755 target/$(DEB_HOST_RUST_TYPE)/debug/phrog \ $(INSTALL_DIR)/usr/bin/phrog + install -D -m0755 target/$(DEB_HOST_RUST_TYPE)/debug/phrog-gdm-shim \ + $(INSTALL_DIR)/usr/bin/phrog-gdm-shim install -D -m0755 data/phrog-greetd-session \ $(INSTALL_DIR)/usr/libexec/phrog-greetd-session diff --git a/phrog.spec b/phrog.spec index cc092ced..11de5007 100644 --- a/phrog.spec +++ b/phrog.spec @@ -30,6 +30,7 @@ BuildRequires: dbus-daemon BuildRequires: xorg-x11-server-Xvfb # first-run test uses foot BuildRequires: foot +BuildRequires: pam-devel %if %{with vendor} BuildRequires: pkgconfig(atk) @@ -46,6 +47,7 @@ BuildRequires: gettext-devel Requires: accountsservice Requires: gnome-session Requires: greetd +Requires: pam Requires: phoc Requires: phosh-osk = 1.0 @@ -84,6 +86,8 @@ tar -xf %{SOURCE1} %{__install} -Dpm 0644 data/mobi.phosh.Phrog.desktop -t %{buildroot}%{_datadir}/applications/ %{__install} -Dpm 0644 target/dist-data/greetd-config.toml -t %{buildroot}%{_sysconfdir}/phrog/ %{__install} -Dpm 0644 dist/fedora/phrog.service -t %{buildroot}%{_unitdir}/ +%{__install} -Dpm 0644 data/phrog-gdm-shim.service -t %{buildroot}%{_unitdir}/ +%{__install} -Dpm 0644 data/org.gnome.DisplayManager.phrog.conf -t %{buildroot}%{_datadir}/dbus-1/system.d/ %{__install} -Dpm 0644 data/systemd-session.conf -T %{buildroot}%{_userunitdir}/gnome-session@phrog.target.d/session.conf %{__install} -Dpm 0755 data/phrog-greetd-session -t %{buildroot}%{_libexecdir}/ %{__install} -d %{buildroot}%{_datadir}/phrog/autostart @@ -108,6 +112,7 @@ dbus-run-session xvfb-run -a -s -noreset phoc -S -E ./test.sh %license LICENSE %doc README.md %{_bindir}/phrog +%{_bindir}/phrog-gdm-shim %{_datadir}/applications/mobi.phosh.Phrog.desktop %{_datadir}/glib-2.0/schemas/mobi.phosh.phrog.gschema.xml %{_datadir}/gnome-session/sessions/phrog.session @@ -118,6 +123,8 @@ dbus-run-session xvfb-run -a -s -noreset phoc -S -E ./test.sh %{_sysconfdir}/phrog/autostart %config(noreplace) %{_sysconfdir}/phrog/greetd-config.toml %{_unitdir}/phrog.service +%{_unitdir}/phrog-gdm-shim.service +%{_datadir}/dbus-1/system.d/org.gnome.DisplayManager.phrog.conf %{_userunitdir}/gnome-session@phrog.target.d/session.conf %{_userunitdir}/mobi.phosh.Phrog.service %{_userunitdir}/mobi.phosh.Phrog.target diff --git a/src/bin/phrog-gdm-shim.rs b/src/bin/phrog-gdm-shim.rs new file mode 100644 index 00000000..706afc89 --- /dev/null +++ b/src/bin/phrog-gdm-shim.rs @@ -0,0 +1,503 @@ +use anyhow::{Context, Result}; +use async_channel::Sender; +use pam::Client; +use std::ffi::CString; +use std::fs; +use std::os::unix::net::{UnixListener, UnixStream}; +use std::path::PathBuf; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Mutex; +use std::time::{SystemTime, UNIX_EPOCH}; +use std::{error::Error, fmt::Display}; +use zbus::connection::Builder; +use zbus::fdo; +use zbus::message::Header; +use zbus::names::BusName; +use zbus::object_server::SignalEmitter; +use zbus::zvariant::OwnedObjectPath; +use zbus::Connection; + +const GDM_BUS_NAME: &str = "org.gnome.DisplayManager"; +const MANAGER_PATH: &str = "/org/gnome/DisplayManager/Manager"; +const SESSION_PATH: &str = "/org/gnome/DisplayManager/Session"; +const PASSWORD_SERVICE: &str = "gdm-password"; +const DEFAULT_PAM_SERVICE: &str = "login"; + +static CHANNEL_COUNTER: AtomicU64 = AtomicU64::new(0); + +#[zbus::proxy( + default_service = "org.freedesktop.login1", + default_path = "/org/freedesktop/login1", + interface = "org.freedesktop.login1.Manager" +)] +trait LoginManager { + #[zbus(name = "GetSessionByPID")] + fn get_session_by_pid(&self, pid: u32) -> zbus::Result; + + #[zbus(name = "UnlockSession")] + fn unlock_session(&self, id: &str) -> zbus::Result<()>; +} + +#[zbus::proxy( + default_service = "org.freedesktop.login1", + interface = "org.freedesktop.login1.Session" +)] +trait LoginSession { + #[zbus(property)] + fn active(&self) -> zbus::Result; + + #[zbus(property)] + fn class(&self) -> zbus::Result; + + #[zbus(property)] + fn id(&self) -> zbus::Result; + + #[zbus(property)] + fn name(&self) -> zbus::Result; + + #[zbus(property)] + fn state(&self) -> zbus::Result; + + #[zbus(property)] + fn type_(&self) -> zbus::Result; +} + +struct DisplayManager; + +#[zbus::interface(name = "org.gnome.DisplayManager.Manager")] +impl DisplayManager { + async fn register_session(&self) -> fdo::Result<()> { + Ok(()) + } + + async fn register_display(&self) -> fdo::Result<()> { + Ok(()) + } + + async fn open_session(&self) -> fdo::Result { + Err(fdo::Error::AccessDenied( + "phrog only supports reauthentication channels".into(), + )) + } + + async fn open_reauthentication_channel( + &self, + username: &str, + #[zbus(connection)] connection: &Connection, + #[zbus(header)] header: Header<'_>, + ) -> fdo::Result { + let context = authorize_reauthentication(connection, &header, username).await?; + let (listener, path, address) = create_channel_listener(context.uid)?; + + spawn_reauthentication_server(listener, path, context); + + Ok(address) + } + + #[zbus(property)] + fn version(&self) -> &str { + "3.5.91" + } +} + +#[derive(Clone)] +struct ReauthContext { + username: String, + session_id: String, + uid: u32, +} + +struct UserVerifier { + context: ReauthContext, + done: Sender<()>, + state: Mutex, +} + +#[derive(Default)] +struct VerifierState { + service_name: Option, +} + +#[zbus::interface(name = "org.gnome.DisplayManager.UserVerifier")] +impl UserVerifier { + fn enable_extensions(&self, _extensions: Vec) -> fdo::Result<()> { + Ok(()) + } + + async fn begin_verification( + &self, + service_name: &str, + #[zbus(signal_emitter)] emitter: SignalEmitter<'_>, + ) -> fdo::Result<()> { + self.begin(service_name, emitter).await + } + + async fn begin_verification_for_user( + &self, + service_name: &str, + username: &str, + #[zbus(signal_emitter)] emitter: SignalEmitter<'_>, + ) -> fdo::Result<()> { + if username != self.context.username { + return Err(fdo::Error::AccessDenied( + "reauthentication username does not match caller session".into(), + )); + } + + self.begin(service_name, emitter).await + } + + async fn answer_query( + &self, + service_name: &str, + answer: &str, + #[zbus(connection)] connection: &Connection, + #[zbus(signal_emitter)] emitter: SignalEmitter<'_>, + ) -> fdo::Result<()> { + let expected_service = self + .state + .lock() + .map_err(|_| fdo::Error::Failed("verifier state lock poisoned".into()))? + .service_name + .clone(); + + if expected_service.as_deref() != Some(service_name) { + return Err(fdo::Error::InvalidArgs( + "answer did not match the active authentication service".into(), + )); + } + + let auth_result = authenticate_password(&self.context.username, answer); + match auth_result { + Ok(()) => { + emitter.verification_complete(service_name).await?; + unlock_session(connection, &self.context.session_id).await?; + let _ = self.done.try_send(()); + } + Err(err) => { + emitter.problem(service_name, &err.to_string()).await?; + emitter.conversation_stopped(service_name).await?; + self.state + .lock() + .map_err(|_| fdo::Error::Failed("verifier state lock poisoned".into()))? + .service_name = None; + } + } + + Ok(()) + } + + async fn cancel(&self, #[zbus(signal_emitter)] emitter: SignalEmitter<'_>) -> fdo::Result<()> { + let service_name = self + .state + .lock() + .map_err(|_| fdo::Error::Failed("verifier state lock poisoned".into()))? + .service_name + .take(); + + if let Some(service_name) = service_name { + emitter.conversation_stopped(&service_name).await?; + } + + let _ = self.done.try_send(()); + Ok(()) + } + + #[zbus(signal)] + async fn conversation_started( + signal_emitter: &SignalEmitter<'_>, + service_name: &str, + ) -> zbus::Result<()>; + + #[zbus(signal)] + async fn conversation_stopped( + signal_emitter: &SignalEmitter<'_>, + service_name: &str, + ) -> zbus::Result<()>; + + #[zbus(signal)] + async fn info( + signal_emitter: &SignalEmitter<'_>, + service_name: &str, + info: &str, + ) -> zbus::Result<()>; + + #[zbus(signal)] + async fn problem( + signal_emitter: &SignalEmitter<'_>, + service_name: &str, + problem: &str, + ) -> zbus::Result<()>; + + #[zbus(signal)] + async fn info_query( + signal_emitter: &SignalEmitter<'_>, + service_name: &str, + query: &str, + ) -> zbus::Result<()>; + + #[zbus(signal)] + async fn secret_info_query( + signal_emitter: &SignalEmitter<'_>, + service_name: &str, + query: &str, + ) -> zbus::Result<()>; + + #[zbus(signal)] + async fn reset(signal_emitter: &SignalEmitter<'_>) -> zbus::Result<()>; + + #[zbus(signal)] + async fn service_unavailable( + signal_emitter: &SignalEmitter<'_>, + service_name: &str, + message: &str, + ) -> zbus::Result<()>; + + #[zbus(signal)] + async fn verification_failed( + signal_emitter: &SignalEmitter<'_>, + service_name: &str, + ) -> zbus::Result<()>; + + #[zbus(signal)] + async fn verification_complete( + signal_emitter: &SignalEmitter<'_>, + service_name: &str, + ) -> zbus::Result<()>; +} + +impl UserVerifier { + fn new(context: ReauthContext, done: Sender<()>) -> Self { + Self { + context, + done, + state: Mutex::default(), + } + } + + async fn begin(&self, service_name: &str, emitter: SignalEmitter<'_>) -> fdo::Result<()> { + if service_name != PASSWORD_SERVICE { + emitter + .service_unavailable(service_name, "only password authentication is supported") + .await?; + return Ok(()); + } + + self.state + .lock() + .map_err(|_| fdo::Error::Failed("verifier state lock poisoned".into()))? + .service_name = Some(service_name.to_string()); + + emitter.conversation_started(service_name).await?; + emitter.secret_info_query(service_name, "Password:").await?; + Ok(()) + } +} + +async fn authorize_reauthentication( + connection: &Connection, + header: &Header<'_>, + username: &str, +) -> fdo::Result { + let sender = header + .sender() + .ok_or_else(|| fdo::Error::AccessDenied("method call has no sender".into()))?; + let sender = BusName::from(sender.to_owned()); + + let dbus = fdo::DBusProxy::new(connection) + .await + .map_err(to_fdo_error)?; + let pid = dbus + .get_connection_unix_process_id(sender.clone()) + .await + .map_err(to_fdo_error)?; + let uid = dbus + .get_connection_unix_user(sender) + .await + .map_err(to_fdo_error)?; + + let login_manager = LoginManagerProxy::new(connection) + .await + .map_err(to_fdo_error)?; + let session_path = login_manager + .get_session_by_pid(pid) + .await + .map_err(|err| fdo::Error::AccessDenied(format!("caller has no logind session: {err}")))?; + let session = LoginSessionProxy::builder(connection) + .path(session_path) + .map_err(to_fdo_error)? + .build() + .await + .map_err(to_fdo_error)?; + + let session_username = session.name().await.map_err(to_fdo_error)?; + if session_username != username { + return Err(fdo::Error::AccessDenied( + "requested user does not match caller session".into(), + )); + } + + let class = session.class().await.map_err(to_fdo_error)?; + if class != "user" { + return Err(fdo::Error::AccessDenied( + "caller is not a user session".into(), + )); + } + + let session_type = session.type_().await.map_err(to_fdo_error)?; + if !matches!(session_type.as_str(), "wayland" | "x11") { + return Err(fdo::Error::AccessDenied( + "caller session is not graphical".into(), + )); + } + + let state = session.state().await.map_err(to_fdo_error)?; + let active = session.active().await.map_err(to_fdo_error)?; + if !active || !matches!(state.as_str(), "active" | "online") { + return Err(fdo::Error::AccessDenied( + "caller session is not active".into(), + )); + } + + Ok(ReauthContext { + username: username.to_string(), + session_id: session.id().await.map_err(to_fdo_error)?, + uid, + }) +} + +fn create_channel_listener(uid: u32) -> fdo::Result<(UnixListener, PathBuf, String)> { + let runtime_dir = PathBuf::from(format!("/run/user/{uid}")); + if !runtime_dir.is_dir() { + return Err(fdo::Error::Failed(format!( + "runtime directory {} does not exist", + runtime_dir.display() + ))); + } + + let counter = CHANNEL_COUNTER.fetch_add(1, Ordering::Relaxed); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|err| fdo::Error::Failed(err.to_string()))? + .as_nanos(); + let path = runtime_dir.join(format!( + "phrog-gdm-shim-{}-{now}-{counter}.sock", + std::process::id() + )); + + let listener = UnixListener::bind(&path).map_err(|err| { + fdo::Error::Failed(format!( + "failed to bind private reauthentication channel {}: {err}", + path.display() + )) + })?; + + chown_and_chmod(&path, uid, 0o600)?; + + let address = format!("unix:path={}", path.display()); + Ok((listener, path, address)) +} + +fn chown_and_chmod(path: &PathBuf, uid: u32, mode: u32) -> fdo::Result<()> { + let path = CString::new(path.as_os_str().as_encoded_bytes()).map_err(|_| { + fdo::Error::Failed("private reauthentication channel path contains NUL".into()) + })?; + + let no_group = !0 as nix::libc::gid_t; + let chown_result = + unsafe { nix::libc::chown(path.as_ptr(), uid as nix::libc::uid_t, no_group) }; + if chown_result != 0 { + return Err(fdo::Error::Failed(format!( + "failed to chown private reauthentication channel: {}", + std::io::Error::last_os_error() + ))); + } + + let chmod_result = unsafe { nix::libc::chmod(path.as_ptr(), mode as nix::libc::mode_t) }; + if chmod_result != 0 { + return Err(fdo::Error::Failed(format!( + "failed to chmod private reauthentication channel: {}", + std::io::Error::last_os_error() + ))); + } + + Ok(()) +} + +fn spawn_reauthentication_server(listener: UnixListener, path: PathBuf, context: ReauthContext) { + std::thread::spawn(move || { + let result = match listener.accept() { + Ok((stream, _)) => { + async_global_executor::block_on(serve_reauthentication(stream, context)) + } + Err(err) => Err(err).context("failed to accept private reauthentication connection"), + }; + + if let Err(err) = result { + eprintln!("phrog-gdm-shim: reauthentication channel failed: {err:#}"); + } + + if let Err(err) = fs::remove_file(&path) { + if err.kind() != std::io::ErrorKind::NotFound { + eprintln!("phrog-gdm-shim: failed to remove {}: {err}", path.display()); + } + } + }); +} + +async fn serve_reauthentication(stream: UnixStream, context: ReauthContext) -> Result<()> { + let (done_sender, done_receiver) = async_channel::bounded(1); + let verifier = UserVerifier::new(context, done_sender); + let connection = Builder::unix_stream(stream) + .p2p() + .server(zbus::Guid::generate())? + .serve_at(SESSION_PATH, verifier)? + .build() + .await?; + + let _ = done_receiver.recv().await; + connection.close().await?; + Ok(()) +} + +fn authenticate_password(username: &str, password: &str) -> Result<()> { + let service = std::env::var("PHROG_GDM_SHIM_PAM_SERVICE") + .unwrap_or_else(|_| DEFAULT_PAM_SERVICE.to_string()); + let mut client = Client::with_password(&service) + .with_context(|| format!("failed to start PAM service {service}"))?; + + client + .conversation_mut() + .set_credentials(username, password); + client.authenticate().context("PAM authentication failed") +} + +async fn unlock_session(connection: &Connection, session_id: &str) -> fdo::Result<()> { + let login_manager = LoginManagerProxy::new(connection) + .await + .map_err(to_fdo_error)?; + login_manager + .unlock_session(session_id) + .await + .map_err(to_fdo_error) +} + +fn to_fdo_error(err: impl Display + Error + Send + Sync + 'static) -> fdo::Error { + fdo::Error::Failed(err.to_string()) +} + +async fn run() -> Result<()> { + let _connection = Builder::system()? + .serve_at(MANAGER_PATH, DisplayManager)? + .name(GDM_BUS_NAME)? + .build() + .await + .context("failed to export org.gnome.DisplayManager")?; + + std::future::pending::<()>().await; + Ok(()) +} + +fn main() -> Result<()> { + async_global_executor::block_on(run()) +}