Use PAM properly, which allows us to handle other auth types

Previously I was just presenting a static username/password box, and
then running PAM with pre-set credentials.  This works just fine when
PAM is expecting a username and password, but if it's expecting
something like a fingerprint scan or a hardware security token, this
wouldn't entirely work right.  Well, it would "work", but the
username/password dialog would be displayed, and then hitting "Unlock"
would start a different auth process with no visible feedback as to
what's supposed to happen.

This also means I need to switch PAM wrapper crates; the one I was using
before did not allow passing a fixed username to the underlying
pam_start() call, which meant that PAM would try to prompt the user for
it, which is not what we want.
This commit is contained in:
2022-08-14 22:17:58 -07:00
parent 7d05bcb0d5
commit 49394b3e53
4 changed files with 351 additions and 175 deletions

View File

@ -14,5 +14,5 @@ gtk-sys = "0.15"
gdk-sys = "0.15"
gdkx11 = "0.15"
log = "0.4"
pam = "0.7"
pam-client = "0.5"
shell-words = "1"

View File

@ -1,16 +1,78 @@
use chrono::prelude::*;
use gdkx11::X11Window;
use gethostname::gethostname;
use glib::{GString, clone, SourceId};
use glib::{GString, clone};
use gtk::{prelude::*, Button, Entry, Label, Plug, Window};
use log::{debug, error, warn};
use std::{io::{self, Write}, thread, time::Duration};
use std::{io::{self, Write}, sync::mpsc, thread, time::Duration, ffi::CString};
use bscreensaver_util::{init_logging, dialog::DialogExitStatus, settings::Configuration, desktop::NewLoginCommand};
const PAM_SERVICE_NAME: &str = "xscreensaver";
const DIALOG_UPDATE_INTERVAL: Duration = Duration::from_millis(100);
const DIALOG_TIMEOUT: Duration = Duration::from_secs(60);
#[derive(Debug)]
enum GtkConverseOp {
ShowInfo(String),
ShowError(String),
ShowPrompt(String, mpsc::Sender<String>),
ShowBlindPrompt(String, mpsc::Sender<String>),
}
struct Authenticator {
username: String,
sender: glib::Sender<GtkConverseOp>,
}
impl Authenticator {
pub fn new<S: AsRef<str>>(username: S, op_sender: glib::Sender<GtkConverseOp>) -> Self {
Self {
username: username.as_ref().to_string(),
sender: op_sender,
}
}
pub fn authenticate(mut self) -> anyhow::Result<()> {
let username = std::mem::take(&mut self.username);
let mut context = pam_client::Context::new(PAM_SERVICE_NAME, Some(username.as_str()), self)?;
context.authenticate(pam_client::Flag::NONE)?;
context.acct_mgmt(pam_client::Flag::NONE)?;
let _ = context.open_session(pam_client::Flag::NONE)?;
Ok(())
}
}
impl pam_client::ConversationHandler for Authenticator {
fn text_info(&mut self, msg: &std::ffi::CStr) {
let msg = msg.to_str().unwrap_or("(OS sent us a non-UTF-8 message)").trim();
let _ = self.sender.send(GtkConverseOp::ShowInfo(msg.to_string()));
thread::sleep(Duration::from_secs(2));
}
fn error_msg(&mut self, msg: &std::ffi::CStr) {
let msg = msg.to_str().unwrap_or("(OS sent us a non-UTF-8 message)").trim();
let _ = self.sender.send(GtkConverseOp::ShowError(msg.to_string()));
thread::sleep(Duration::from_secs(2));
}
fn prompt_echo_on(&mut self, msg: &std::ffi::CStr) -> Result<CString, pam_client::ErrorCode> {
let msg = msg.to_str().unwrap_or("Password:").trim();
let (tx, rx) = mpsc::channel();
self.sender.send(GtkConverseOp::ShowPrompt(msg.to_string(), tx)).map_err(|_| pam_client::ErrorCode::ABORT)?;
let result = rx.recv().map_err(|_| pam_client::ErrorCode::ABORT)?;
CString::new(result).map_err(|_| pam_client::ErrorCode::CRED_ERR)
}
fn prompt_echo_off(&mut self, msg: &std::ffi::CStr) -> Result<CString, pam_client::ErrorCode> {
let msg = msg.to_str().unwrap_or("Password:").trim();
let (tx, rx) = mpsc::channel();
self.sender.send(GtkConverseOp::ShowBlindPrompt(msg.to_string(), tx)).map_err(|_| pam_client::ErrorCode::ABORT)?;
let result = rx.recv().map_err(|_| pam_client::ErrorCode::ABORT)?;
CString::new(result).map_err(|_| pam_client::ErrorCode::CRED_ERR)
}
}
fn main() -> anyhow::Result<()> {
init_logging("BSCREENSAVER_DIALOG_GTK3_LOG");
glib::log_set_default_handler(glib::rust_log_handler);
@ -27,8 +89,6 @@ fn main() -> anyhow::Result<()> {
gtk::init()?;
let (tx, rx) = glib::MainContext::sync_channel(glib::PRIORITY_DEFAULT, 1);
let top_sg = gtk::SizeGroup::builder()
.mode(gtk::SizeGroupMode::Horizontal)
.build();
@ -150,32 +210,6 @@ fn main() -> anyhow::Result<()> {
glib::Continue(true)
});
let attrs = gtk::pango::AttrList::new();
attrs.insert(gtk::pango::AttrFloat::new_scale(gtk::pango::SCALE_XX_LARGE));
let mut bold_desc = gtk::pango::FontDescription::new();
bold_desc.set_weight(gtk::pango::Weight::Bold);
attrs.insert(gtk::pango::AttrFontDesc::new(&bold_desc));
let auth_status_label = gtk::Label::builder()
.label("")
.xalign(0.5)
.yalign(0.5)
.attributes(&attrs)
.opacity(0.0)
.build();
vbox.pack_start(&auth_status_label, false, false, 0);
rx.attach(None, clone!(@strong auth_status_label => move |(exit_status, auth_dots_id): (DialogExitStatus, SourceId)| {
auth_dots_id.remove();
if exit_status != DialogExitStatus::AuthSucceeded {
auth_status_label.set_label("Authentication Failed!");
auth_status_label.set_opacity(1.0);
glib::timeout_add_seconds_local_once(1, move || exit_status.exit());
} else {
exit_status.exit();
}
glib::Continue(true)
}));
let sep = gtk::Separator::builder()
.orientation(gtk::Orientation::Vertical)
.build();
@ -183,118 +217,46 @@ fn main() -> anyhow::Result<()> {
let vbox = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(4)
.spacing(0)
.margin(0)
.build();
top_sg.add_widget(&vbox);
top_hbox.pack_start(&vbox, true, true, 0);
let hbox = gtk::Box::builder()
let auth_vbox = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(4)
.build();
vbox.pack_start(&auth_vbox, true, true, 2);
let button_hbox = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(8)
.build();
vbox.pack_start(&hbox, true, true, 2);
let label = Label::builder()
.label("Username:")
.xalign(0.0)
.build();
label_sg.add_widget(&label);
hbox.pack_start(&label, false, true, 8);
let username = bscreensaver_util::get_username()?;
let username_box = Entry::builder()
.text(&username)
.sensitive(false)
.build();
entry_sg.add_widget(&username_box);
hbox.pack_start(&username_box, true, true, 8);
let hbox = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(8)
.build();
vbox.pack_start(&hbox, true, true, 0);
let label = Label::builder()
.label("Password:")
.xalign(0.0)
.build();
label_sg.add_widget(&label);
hbox.pack_start(&label, false, true, 8);
let password_box = Entry::builder()
.visibility(false)
.input_purpose(gtk::InputPurpose::Password)
.activates_default(true)
.width_chars(25)
.build();
entry_sg.add_widget(&password_box);
hbox.pack_start(&password_box, true, true, 8);
password_box.connect_key_press_event(|_, ev| {
if ev.keyval().name() == Some(GString::from("Escape")) {
DialogExitStatus::AuthCanceled.exit();
}
gtk::Inhibit(false)
});
password_box.grab_focus();
let hbox = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(8)
.build();
vbox.pack_start(&hbox, true, true, 2);
vbox.pack_end(&button_hbox, false, false, 4);
if let Some(new_login_command) = new_login_command {
let new_login_button = Button::builder()
let button = Button::builder()
.label("New Login")
.build();
new_login_button.connect_clicked(move |_| {
let new_login_command = new_login_command.clone();
thread::spawn(move || {
button.connect_clicked(clone!(@strong new_login_command => move |_| {
thread::spawn(clone!(@strong new_login_command => move || {
if let Err(err) = new_login_command.run() {
warn!("Failed to run new login command: {}", err);
} else {
DialogExitStatus::OtherError.exit();
DialogExitStatus::AuthCanceled.exit();
}
});
});
hbox.pack_start(&new_login_button, false, true, 8);
}));
}));
button_hbox.pack_start(&button, false, true, 8);
}
let unlock_button = Button::builder()
.label("Unlock")
let accept_button_box = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(0)
.margin(0)
.build();
unlock_button.connect_clicked(clone!(@strong password_box, @strong auth_status_label => move |unlock_button| {
unlock_button.set_sensitive(false);
password_box.set_sensitive(false);
let username = username.clone();
let password = password_box.text().to_string();
auth_status_label.set_label("Authenticating");
auth_status_label.set_opacity(1.0);
let auth_dots_id = glib::timeout_add_local(Duration::from_millis(500), clone!(@strong auth_status_label => move || {
let s = auth_status_label.label();
auth_status_label.set_label(&(s.as_str().to_string() + "."));
Continue(true)
}));
let tx = tx.clone();
thread::spawn(move || {
let status = if authenticate(&username, &password) {
DialogExitStatus::AuthSucceeded
} else {
DialogExitStatus::AuthFailed
};
if let Err(err) = tx.send((status, auth_dots_id)) {
error!("Failed to send exit status to main thread: {}", err);
DialogExitStatus::OtherError.exit();
}
});
}));
hbox.pack_end(&unlock_button, false, true, 8);
unlock_button.set_can_default(true);
unlock_button.set_has_default(true);
button_hbox.pack_end(&accept_button_box, false, true, 8);
let timer = gtk::ProgressBar::builder()
.orientation(gtk::Orientation::Horizontal)
@ -304,9 +266,8 @@ fn main() -> anyhow::Result<()> {
.margin(2)
.build();
top_vbox.pack_end(&timer, false, false, 0);
let delta = (DIALOG_UPDATE_INTERVAL.as_millis() as f64) / (DIALOG_TIMEOUT.as_millis() as f64);
glib::timeout_add_local(DIALOG_UPDATE_INTERVAL, clone!(@strong timer => move || {
glib::timeout_add_local(DIALOG_UPDATE_INTERVAL, clone!(@weak timer => @default-return Continue(false), move || {
let new_fraction = timer.fraction() - delta;
if new_fraction <= 0.0 {
DialogExitStatus::AuthTimedOut.exit();
@ -315,40 +276,136 @@ fn main() -> anyhow::Result<()> {
Continue(true)
}));
password_box.connect_key_press_event(clone!(@strong timer => move |_, _| {
let new_fraction = timer.fraction() + 0.05;
timer.set_fraction(if new_fraction >= 1.0 { 1.0 } else { new_fraction });
Inhibit(false)
let (tx, rx) = glib::MainContext::channel(glib::Priority::default());
let username = bscreensaver_util::get_username()?;
rx.attach(None, clone!(@weak dialog, @weak auth_vbox, @weak accept_button_box, @weak timer, @strong username, => @default-return glib::Continue(false), move |op| {
for child in auth_vbox.children() {
auth_vbox.remove(&child);
}
for child in accept_button_box.children() {
accept_button_box.remove(&child);
}
debug!("PAM saysL {:?}", op);
let input_purpose = match op {
GtkConverseOp::ShowBlindPrompt(_, _) => gtk::InputPurpose::Password,
_ => gtk::InputPurpose::FreeForm,
};
match op {
GtkConverseOp::ShowInfo(msg) | GtkConverseOp::ShowError(msg) => {
let attrs = gtk::pango::AttrList::new();
attrs.insert(gtk::pango::AttrFloat::new_scale(gtk::pango::SCALE_LARGE));
let label = gtk::Label::builder()
.label(&msg)
.xalign(0.0)
.width_chars(40)
.wrap(true)
.wrap_mode(gtk::pango::WrapMode::Word)
.attributes(&attrs)
.build();
auth_vbox.pack_start(&label, false, false, 0);
},
GtkConverseOp::ShowPrompt(msg, result_sender) | GtkConverseOp::ShowBlindPrompt(msg, result_sender) => {
let hbox = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(8)
.build();
auth_vbox.pack_start(&hbox, true, true, 2);
let label = Label::builder()
.label("Username:")
.xalign(0.0)
.build();
label_sg.add_widget(&label);
hbox.pack_start(&label, false, true, 8);
let username_box = Entry::builder()
.text(&username)
.sensitive(false)
.build();
entry_sg.add_widget(&username_box);
hbox.pack_start(&username_box, true, true, 8);
let hbox = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(8)
.build();
auth_vbox.pack_start(&hbox, true, true, 0);
let label = Label::builder()
.label(&msg)
.xalign(0.0)
.build();
label_sg.add_widget(&label);
hbox.pack_start(&label, false, true, 8);
let input_box = Entry::builder()
.visibility(false)
.input_purpose(input_purpose)
.activates_default(true)
.width_chars(25)
.build();
entry_sg.add_widget(&input_box);
hbox.pack_start(&input_box, true, true, 8);
input_box.connect_key_press_event(clone!(@strong timer => move |_, ev| {
let new_fraction = timer.fraction() + 0.05;
timer.set_fraction(if new_fraction >= 1.0 { 1.0 } else { new_fraction });
if ev.keyval().name() == Some(GString::from("Escape")) {
DialogExitStatus::AuthCanceled.exit();
}
gtk::Inhibit(false)
}));
input_box.grab_focus();
let accept_button = Button::builder()
.label("Unlock")
.build();
accept_button.connect_clicked(clone!(@strong input_box => move |accept_button| {
accept_button.set_sensitive(false);
input_box.set_sensitive(false);
if let Err(err) = result_sender.send(input_box.text().to_string()) {
warn!("Failed to send result to main thread: {}", err);
}
}));
accept_button_box.pack_start(&accept_button, true, true, 0);
accept_button.set_can_default(true);
accept_button.set_has_default(true);
}
};
dialog.show_all();
glib::Continue(true)
}));
dialog.show_all();
thread::spawn(move || {
let authenticator = Authenticator::new(&username, tx.clone());
if let Err(err) = authenticator.authenticate() {
error!("PAM: {}", err);
if tx.send(GtkConverseOp::ShowError("Authentication Failed".to_string())).is_ok() {
thread::sleep(Duration::from_secs(2));
}
DialogExitStatus::AuthFailed.exit();
} else {
debug!("PAM success");
DialogExitStatus::AuthSucceeded.exit();
}
});
gtk::main();
DialogExitStatus::OtherError.exit(); // We should never get here!
// We never call gtk::main_quit(), so we should never get here. If
// we do, it'd be a bad bug, because we'd exit with status 0, which
// would be interpreted as "auth success".
DialogExitStatus::OtherError.exit();
}
fn set_time_label(label: &gtk::Label) {
let now = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
label.set_label(&now);
}
fn authenticate(username: &String, password: &String) -> bool {
let mut authenticator = match pam::Authenticator::with_password("xscreensaver") {
Err(err) => {
error!("[PAM] {}", err);
return false;
},
Ok(authenticator) => authenticator,
};
authenticator.get_handler().set_credentials(username, password);
if let Err(err) = authenticator.authenticate() {
error!("[PAM] {}", err);
return false;
}
if let Err(err) = authenticator.open_session() {
error!("[PAM] {}", err);
false
} else {
true
}
}