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())
+}