use std::cell::Cell;
use std::ffi::{c_int, c_void};
use std::time::Duration;
use crate::cutils::string_from_ptr;
use crate::pam::rpassword::Hidden;
use crate::system::signal::{self, SignalSet};
use super::sys::*;
use super::{PamError, PamErrorType, error::PamResult, rpassword, securemem::PamBuffer};
#[derive(Clone, Copy, Eq, PartialEq)]
pub enum PamMessageStyle {
PromptEchoOff = PAM_PROMPT_ECHO_OFF as isize,
PromptEchoOn = PAM_PROMPT_ECHO_ON as isize,
ErrorMessage = PAM_ERROR_MSG as isize,
TextInfo = PAM_TEXT_INFO as isize,
}
impl PamMessageStyle {
pub fn from_int(val: c_int) -> Option<PamMessageStyle> {
use PamMessageStyle::*;
match val as _ {
PAM_PROMPT_ECHO_OFF => Some(PromptEchoOff),
PAM_PROMPT_ECHO_ON => Some(PromptEchoOn),
PAM_ERROR_MSG => Some(ErrorMessage),
PAM_TEXT_INFO => Some(TextInfo),
_ => None,
}
}
}
pub trait Converser {
fn handle_normal_prompt(&self, msg: &str) -> PamResult<PamBuffer>;
fn handle_hidden_prompt(&self, msg: &str) -> PamResult<PamBuffer>;
fn handle_error(&self, msg: &str) -> PamResult<()>;
fn handle_info(&self, msg: &str) -> PamResult<()>;
}
fn handle_message<C: Converser>(
app_data: &ConverserData<C>,
style: PamMessageStyle,
msg: &str,
) -> PamResult<Option<PamBuffer>> {
use PamMessageStyle::*;
match style {
PromptEchoOn | PromptEchoOff if app_data.no_interact => Err(PamError::InteractionRequired),
PromptEchoOn => app_data.converser.handle_normal_prompt(msg).map(Some),
PromptEchoOff => {
let final_prompt = match app_data.auth_prompt.as_deref() {
None => {
String::new()
}
Some(prompt) => {
format!("[{}: {prompt}] {msg}", app_data.converser_name)
}
};
app_data
.converser
.handle_hidden_prompt(&final_prompt)
.map(Some)
}
ErrorMessage => app_data.converser.handle_error(msg).map(|()| None),
TextInfo => app_data.converser.handle_info(msg).map(|()| None),
}
}
pub struct CLIConverser {
pub(super) name: String,
pub(super) use_askpass: bool,
pub(super) use_stdin: bool,
pub(super) bell: Cell<bool>,
pub(super) password_feedback: bool,
pub(super) password_timeout: Option<Duration>,
}
use rpassword::Terminal;
struct SignalGuard(Option<SignalSet>);
impl SignalGuard {
fn unblock_interrupts() -> Self {
let cur_signals = SignalSet::empty().and_then(|mut set| {
set.add(signal::consts::SIGINT)?;
set.add(signal::consts::SIGQUIT)?;
set.unblock()
});
Self(cur_signals.ok())
}
}
impl Drop for SignalGuard {
fn drop(&mut self) {
if let Some(signal) = &self.0 {
let _ = signal.set_mask();
}
}
}
impl CLIConverser {
fn open(&self) -> PamResult<(Terminal<'_>, SignalGuard)> {
let term = if self.use_askpass {
Terminal::open_askpass()?
} else if self.use_stdin {
Terminal::open_stdie()?
} else {
let mut tty = Terminal::open_tty()?;
if self.bell.replace(false) {
tty.bell()?;
}
tty
};
Ok((term, SignalGuard::unblock_interrupts()))
}
}
impl Converser for CLIConverser {
fn handle_normal_prompt(&self, msg: &str) -> PamResult<PamBuffer> {
let (mut tty, _guard) = self.open()?;
let input_needed = xlat!("input needed");
tty.read_input(
&format!("[{}: {input_needed} {msg} ", self.name),
None,
Hidden::No,
)
}
fn handle_hidden_prompt(&self, msg: &str) -> PamResult<PamBuffer> {
let (mut tty, _guard) = self.open()?;
tty.read_input(
msg,
self.password_timeout,
if self.password_feedback {
Hidden::WithFeedback(())
} else {
Hidden::Yes(())
},
)
}
fn handle_error(&self, msg: &str) -> PamResult<()> {
let (mut tty, _) = self.open()?;
Ok(tty.prompt(&format!("[{} error] {msg}\n", self.name))?)
}
fn handle_info(&self, msg: &str) -> PamResult<()> {
let (mut tty, _) = self.open()?;
Ok(tty.prompt(&format!("[{}] {msg}\n", self.name))?)
}
}
pub(super) struct ConverserData<C> {
pub(super) converser: C,
pub(super) converser_name: String,
pub(super) no_interact: bool,
pub(super) auth_prompt: Option<String>,
pub(super) error: Option<PamError>,
pub(super) panicked: bool,
}
pub(super) unsafe extern "C" fn converse<C: Converser>(
num_msg: c_int,
msg: *mut *const pam_message,
response: *mut *mut pam_response,
appdata_ptr: *mut c_void,
) -> c_int {
let result = std::panic::catch_unwind(|| {
let mut resp_bufs = Vec::with_capacity(num_msg as usize);
for i in 0..num_msg as usize {
let message: &pam_message = unsafe { &**msg.add(i) };
let msg = unsafe { string_from_ptr(message.msg) };
let style = if let Some(style) = PamMessageStyle::from_int(message.msg_style) {
style
} else {
return PamErrorType::ConversationError;
};
let app_data = unsafe { &mut *(appdata_ptr as *mut ConverserData<C>) };
if app_data.error.is_some()
&& (style == PamMessageStyle::PromptEchoOff
|| style == PamMessageStyle::PromptEchoOn)
{
return PamErrorType::ConversationError;
}
match handle_message(app_data, style, &msg) {
Ok(resp_buf) => {
resp_bufs.push(resp_buf);
}
Err(err) => {
app_data.error = Some(err);
return PamErrorType::ConversationError;
}
}
}
let temp_resp = unsafe {
libc::calloc(
num_msg as libc::size_t,
std::mem::size_of::<pam_response>() as libc::size_t,
)
} as *mut pam_response;
if temp_resp.is_null() {
return PamErrorType::BufferError;
}
for (i, resp_buf) in resp_bufs.into_iter().enumerate() {
let response: &mut pam_response = unsafe { &mut *(temp_resp.add(i)) };
if let Some(secbuf) = resp_buf {
response.resp = secbuf.leak().as_ptr().cast();
}
}
unsafe { *response = temp_resp };
PamErrorType::Success
});
let res = match result {
Ok(r) => r,
Err(_) => {
let app_data = unsafe { &mut *(appdata_ptr as *mut ConverserData<C>) };
app_data.panicked = true;
PamErrorType::ConversationError
}
};
res.as_int()
}
#[allow(clippy::undocumented_unsafe_blocks)]
#[cfg(test)]
mod test {
use super::*;
use PamMessageStyle::*;
use std::pin::Pin;
struct PamMessage {
msg: String,
style: PamMessageStyle,
}
impl Converser for String {
fn handle_normal_prompt(&self, msg: &str) -> PamResult<PamBuffer> {
Ok(PamBuffer::new(format!("{self} says {msg}").into_bytes()))
}
fn handle_hidden_prompt(&self, msg: &str) -> PamResult<PamBuffer> {
Ok(PamBuffer::new(msg.as_bytes().to_vec()))
}
fn handle_error(&self, msg: &str) -> PamResult<()> {
panic!("{msg}")
}
fn handle_info(&self, _msg: &str) -> PamResult<()> {
Ok(())
}
}
fn dummy_pam(msgs: &[PamMessage], talkie: &pam_conv) -> Vec<Option<String>> {
let pam_msgs = msgs
.iter()
.map(|PamMessage { msg, style, .. }| pam_message {
msg: std::ffi::CString::new(&msg[..]).unwrap().into_raw(),
msg_style: *style as i32,
})
.rev()
.collect::<Vec<pam_message>>();
let mut ptrs = pam_msgs
.iter()
.map(|x| x as *const pam_message)
.rev()
.collect::<Vec<*const pam_message>>();
let mut raw_response = std::ptr::null_mut::<pam_response>();
let conv_err = unsafe {
talkie.conv.expect("non-null fn ptr")(
ptrs.len() as i32,
ptrs.as_mut_ptr(),
&mut raw_response,
talkie.appdata_ptr,
)
};
for rec in ptrs {
unsafe {
drop(std::ffi::CString::from_raw((*rec).msg as *mut _));
}
}
if conv_err != 0 {
return vec![];
}
let result = msgs
.iter()
.enumerate()
.map(|(i, _)| unsafe {
let ptr = raw_response.add(i);
if (*ptr).resp.is_null() {
None
} else {
assert_eq!((*ptr).resp_retcode, 0);
let response = string_from_ptr((*ptr).resp);
libc::free((*ptr).resp as *mut _);
Some(response)
}
})
.collect();
unsafe { libc::free(raw_response as *mut _) };
result
}
fn msg(style: PamMessageStyle, msg: &str) -> PamMessage {
let msg = msg.to_string();
PamMessage { style, msg }
}
use std::marker::PhantomData;
struct PamConvBorrow<'a> {
pam_conv: pam_conv,
_marker: std::marker::PhantomData<&'a ()>,
}
impl<'a> PamConvBorrow<'a> {
fn new<C: Converser>(data: Pin<&'a mut ConverserData<C>>) -> PamConvBorrow<'a> {
let appdata_ptr =
unsafe { data.get_unchecked_mut() as *mut ConverserData<C> as *mut c_void };
PamConvBorrow {
pam_conv: pam_conv {
conv: Some(converse::<C>),
appdata_ptr,
},
_marker: PhantomData,
}
}
fn borrow(&self) -> &pam_conv {
&self.pam_conv
}
}
#[test]
fn miri_pam_gpt() {
let mut hello = Box::pin(ConverserData {
converser: "tux".to_string(),
converser_name: "tux".to_string(),
no_interact: false,
auth_prompt: Some("authenticate".to_owned()),
error: None,
panicked: false,
});
let cookie = PamConvBorrow::new(hello.as_mut());
let pam_conv = cookie.borrow();
assert_eq!(dummy_pam(&[], pam_conv), vec![]);
assert_eq!(
dummy_pam(&[msg(PromptEchoOn, "hello")], pam_conv),
vec![Some("tux says hello".to_string())]
);
assert_eq!(
dummy_pam(&[msg(PromptEchoOff, "fish")], pam_conv),
vec![Some("[tux: authenticate] fish".to_string())]
);
assert_eq!(dummy_pam(&[msg(TextInfo, "mars")], pam_conv), vec![None]);
assert_eq!(
dummy_pam(
&[
msg(PromptEchoOff, "banging the rocks together"),
msg(TextInfo, ""),
msg(PromptEchoOn, ""),
],
pam_conv
),
vec![
Some("[tux: authenticate] banging the rocks together".to_string()),
None,
Some("tux says ".to_string()),
]
);
let real_hello = unsafe { &mut *(pam_conv.appdata_ptr as *mut ConverserData<String>) };
assert!(!real_hello.panicked);
assert_eq!(dummy_pam(&[msg(ErrorMessage, "oops")], pam_conv), vec![]);
assert!(hello.panicked); }
}