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:
@ -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"
|
||||
|
@ -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: >k::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
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user