use crate::cutils::string_from_ptr;
use super::sys::*;
use super::{error::PamResult, rpassword, securemem::PamBuffer, PamError, PamErrorType};
#[derive(Clone, Copy)]
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: libc::c_int) -> Option<PamMessageStyle> {
use PamMessageStyle::*;
match val as libc::c_uint {
PAM_PROMPT_ECHO_OFF => Some(PromptEchoOff),
PAM_PROMPT_ECHO_ON => Some(PromptEchoOn),
PAM_ERROR_MSG => Some(ErrorMessage),
PAM_TEXT_INFO => Some(TextInfo),
_ => None,
}
}
}
pub struct PamMessage {
pub msg: String,
pub style: PamMessageStyle,
response: Option<PamBuffer>,
}
impl PamMessage {
pub fn set_response(&mut self, resp: PamBuffer) {
self.response = Some(resp);
}
}
pub struct Conversation {
messages: Vec<PamMessage>,
}
impl Conversation {
pub fn messages_mut(&mut self) -> impl Iterator<Item = &mut PamMessage> {
self.messages.iter_mut()
}
}
pub trait Converser {
fn handle_conversation(&self, conversation: &mut Conversation) -> PamResult<()>;
}
pub trait SequentialConverser: 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<()>;
}
impl<T> Converser for T
where
T: SequentialConverser,
{
fn handle_conversation(&self, conversation: &mut Conversation) -> PamResult<()> {
use PamMessageStyle::*;
for msg in conversation.messages_mut() {
match msg.style {
PromptEchoOn => {
msg.set_response(self.handle_normal_prompt(&msg.msg)?);
}
PromptEchoOff => {
msg.set_response(self.handle_hidden_prompt(&msg.msg)?);
}
ErrorMessage => {
self.handle_error(&msg.msg)?;
}
TextInfo => {
self.handle_info(&msg.msg)?;
}
}
}
Ok(())
}
}
pub struct CLIConverser {
pub(super) name: String,
pub(super) use_stdin: bool,
pub(super) no_interact: bool,
}
use rpassword::Terminal;
impl CLIConverser {
fn open(&self) -> std::io::Result<Terminal> {
if self.use_stdin {
Terminal::open_stdie()
} else {
Terminal::open_tty()
}
}
}
impl SequentialConverser for CLIConverser {
fn handle_normal_prompt(&self, msg: &str) -> PamResult<PamBuffer> {
if self.no_interact {
return Err(PamError::InteractionRequired);
}
let mut tty = self.open()?;
tty.prompt(&format!("[{}: input needed] {msg} ", self.name))?;
Ok(tty.read_cleartext()?)
}
fn handle_hidden_prompt(&self, msg: &str) -> PamResult<PamBuffer> {
if self.no_interact {
return Err(PamError::InteractionRequired);
}
let mut tty = self.open()?;
tty.prompt(&format!("[{}: authenticate] {msg}", self.name))?;
Ok(tty.read_password()?)
}
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) panicked: bool,
}
pub(super) unsafe extern "C" fn converse<C: Converser>(
num_msg: libc::c_int,
msg: *mut *const pam_message,
response: *mut *mut pam_response,
appdata_ptr: *mut libc::c_void,
) -> libc::c_int {
let result = std::panic::catch_unwind(|| {
let mut conversation = Conversation {
messages: Vec::with_capacity(num_msg as usize),
};
for i in 0..num_msg as isize {
let message: &pam_message = unsafe { &**msg.offset(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;
};
conversation.messages.push(PamMessage {
msg,
style,
response: None,
});
}
let app_data = unsafe { &mut *(appdata_ptr as *mut ConverserData<C>) };
if app_data
.converser
.handle_conversation(&mut conversation)
.is_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, msg) in conversation.messages.into_iter().enumerate() {
let response: &mut pam_response = unsafe { &mut *(temp_resp.add(i)) };
if let Some(secbuf) = msg.response {
response.resp = secbuf.leak() as *mut _;
}
}
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()
}
#[cfg(test)]
mod test {
use super::*;
use std::pin::Pin;
use PamMessageStyle::*;
impl SequentialConverser 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(
format!("{self}s secret is {msg}").into_bytes(),
))
}
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,
response: None,
}
}
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 libc::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(),
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("tuxs secret is 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("tuxs secret is 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); }
}