1use std::collections::{HashMap, HashSet};2use std::ffi::{CStr, CString};3use std::future::pending;4use std::sync::LazyLock;56use anyhow::Context as _;7use clap::Parser;8use nix::unistd::{setuid, Uid, User};9use pam_client::{Context, ConversationHandler, ErrorCode};10use tokio::task::spawn_blocking;11use zbus::blocking;12use zbus::message::Header;13use zbus::zvariant::OwnedValue;14use zbus::{15 blocking::Connection as BlockingConnection,16 fdo::{Error as FdoError, Result as FdoResult},17 interface, proxy, Connection,18};1920struct Helper {21 connection: Connection,22 blocking_connection: BlockingConnection,23}2425static ALLOWED_ENVIRONMENT: LazyLock<HashSet<&str>> = LazyLock::new(|| {26 [27 28 "SSH_AUTH_SOCK",29 30 "SSH_AUTH_INFO_0",31 32 "SUDO_USER",33 ]34 .into_iter()35 .collect()36});3738struct Conversation {39 responder: PolkitResponderProxyBlocking<'static>,40}41impl Conversation {42 fn prompt_inner(&self, echo: bool, prompt: &CStr) -> Result<CString, ErrorCode> {43 let out = self44 .responder45 .prompt(echo, &prompt.to_string_lossy())46 .map_err(|_| ErrorCode::CONV_ERR)?;47 Ok(CString::new(out).map_err(|_| ErrorCode::CONV_AGAIN)?)48 }49 fn text_inner(&self, error: bool, msg: &CStr) {50 let msg = msg.to_string_lossy();51 let _ = self.responder.text(error, &msg);52 }53}54impl ConversationHandler for Conversation {55 fn prompt_echo_on(&mut self, prompt: &CStr) -> Result<CString, ErrorCode> {56 self.prompt_inner(true, prompt)57 }5859 fn prompt_echo_off(&mut self, prompt: &CStr) -> Result<CString, ErrorCode> {60 self.prompt_inner(false, prompt)61 }6263 fn text_info(&mut self, msg: &CStr) {64 self.text_inner(false, msg)65 }6667 fn error_msg(&mut self, msg: &CStr) {68 self.text_inner(true, msg)69 }7071 fn radio_prompt(&mut self, prompt: &CStr) -> Result<bool, ErrorCode> {72 let prompt = prompt.to_string_lossy();73 let result = self74 .responder75 .radio(&prompt)76 .map_err(|_| ErrorCode::CONV_ERR)?;77 Ok(result)78 }79}8081#[proxy(82 default_service = "org.freedesktop.DBus",83 default_path = "/org/freedesktop/DBus"84)]85trait DBus {86 fn get_connection_credentials(&self, body: &str) -> zbus::Result<HashMap<String, OwnedValue>>;87}8889#[proxy(default_path = "/lach/PolkitResponder")]90trait PolkitResponder {91 fn prompt(&self, echo: bool, prompt: &str) -> zbus::Result<String>;92 fn text(&self, error: bool, msg: &str) -> zbus::Result<()>;93 fn radio(&self, msg: &str) -> zbus::Result<bool>;94}9596#[interface(name = "lach.PolkitHelper")]97impl Helper {98 async fn init_conversation(99 &self,100 environment: HashMap<String, String>,101 #[zbus(header)] hdr: Header<'_>,102 ) -> zbus::fdo::Result<()> {103 let Some(sender) = hdr.sender().map(|v| v.to_owned()) else {104 return Err(zbus::fdo::Error::AuthFailed("missing sender".to_owned()));105 };106107 let dbus = DBusProxy::new(&self.connection).await?;108109 let reply = dbus110 .get_connection_credentials("org.freedesktop.DBus")111 .await?;112 let uid: u32 = (&reply["UnixUserID"]).try_into().unwrap();113114 let blocking_connection = self.blocking_connection.clone();115 let thread_result: FdoResult<()> = spawn_blocking(move || {116 let user = User::from_uid(Uid::from_raw(uid))117 .map_err(|_| zbus::fdo::Error::AuthFailed("error querying user".to_owned()))?118 .ok_or_else(|| zbus::fdo::Error::AuthFailed("uid not found".to_owned()))?;119120 let responder = PolkitResponderProxyBlocking::new(&blocking_connection, sender)?;121 let conversation = Conversation { responder };122 let mut ctx = Context::new(123 124 "login",125 Some(&user.name),126 conversation,127 )128 .map_err(|_| FdoError::Failed("pam context init failed".to_owned()))?;129130 for (k, v) in environment {131 if k.contains('=') || !ALLOWED_ENVIRONMENT.contains(k.as_str()) {132 continue;133 }134 let _ = ctx.putenv(format!("{k}={v}"));135 }136137 Ok(())138 })139 .await140 .map_err(|_| FdoError::Failed("thread spawn failed".to_owned()))?;141142 thread_result?;143 144 145 Ok(())146 }147}148149const OBJ_PATH: &str = "/lach/polkitHelper";150151#[derive(Parser)]152struct Opts {153 154 155 #[arg(long)]156 session: bool,157}158159#[tokio::main(flavor = "current_thread")]160async fn main() -> anyhow::Result<()> {161 let opts = Opts::parse();162 let connection = if opts.session {163 Connection::session().await164 } else {165 Connection::system().await166 }167 .context("failed to open connection")?;168169 let session = opts.session;170 let blocking_connection: anyhow::Result<BlockingConnection> = spawn_blocking(move || {171 Ok(if session {172 BlockingConnection::session()?173 } else {174 BlockingConnection::system()?175 })176 })177 .await?;178 let blocking_connection = blocking_connection.context("failed to open blocking connection")?;179180 if opts.session {181 setuid(Uid::from_raw(0))182 .context("polkit-helper needs to be suid if run in session mode")?;183 }184185 connection186 .object_server()187 .at(188 OBJ_PATH,189 Helper {190 connection: connection.clone(),191 blocking_connection,192 },193 )194 .await195 .context("failed listen path")?;196197 connection198 .request_name("lach.PolkitHelper")199 .await200 .context("failed to request name")?;201202 pending().await203}