use crate::error::ErrorCode;
use crate::resp_buf::ResponseBuffer;
use crate::ConversationHandler;
use crate::PAM_SUCCESS;
use libc::{c_char, c_int, c_void};
use pam_sys::PAM_BUF_ERR;
use pam_sys::{
pam_conv as PamConversation, pam_message as PamMessage, pam_response as PamResponse,
};
use std::ffi::{CStr, CString};
use std::mem::size_of;
use std::slice;
#[allow(clippy::borrowed_box)]
pub(crate) fn to_pam_conv<T: ConversationHandler>(callback: &mut Box<T>) -> PamConversation {
PamConversation {
conv: Some(pam_converse::<T>),
appdata_ptr: ((&mut **callback) as *mut T).cast(),
}
}
#[allow(clippy::unnecessary_wraps)]
const fn map_conv_string(input: CString) -> Option<CString> {
Some(input)
}
#[cfg(not(target_os = "solaris"))]
#[inline]
#[allow(clippy::cast_sign_loss)]
fn msg_to_slice(msg: &*mut *const PamMessage, num_msg: c_int) -> &[&PamMessage] {
assert!(num_msg >= 0 || msg.is_null());
unsafe { slice::from_raw_parts((*msg) as *const &PamMessage, num_msg as usize) }
}
#[cfg(target_os = "solaris")]
#[inline]
#[allow(clippy::cast_sign_loss)]
fn msg_to_slice(msg: &*mut *const PamMessage, num_msg: c_int) -> &'static [PamMessage] {
assert!(num_msg >= 0 || msg.is_null());
unsafe { slice::from_raw_parts((**msg), num_msg as usize) }
}
#[cfg(not(target_os = "solaris"))]
#[allow(clippy::cast_possible_wrap)]
const fn max_msg_num() -> isize {
isize::MAX / size_of::<*const PamMessage>() as isize
}
#[cfg(target_os = "solaris")]
#[allow(clippy::cast_possible_wrap)]
const fn max_msg_num() -> isize {
isize::MAX / size_of::<PamMessage>() as isize
}
unsafe fn msg_content_as_cstr(msg: &*const c_char) -> &CStr {
if msg.is_null() {
CStr::from_bytes_with_nul_unchecked(b"\0")
} else {
CStr::from_ptr(*msg)
}
}
unsafe fn msg_content_to_cstr(msg: &*const c_char) -> &CStr {
if msg.is_null() {
CStr::from_bytes_with_nul_unchecked(b"\0")
} else {
CStr::from_ptr(*msg)
}
}
#[allow(clippy::cast_sign_loss)]
unsafe fn msg_content_to_bin(msg: &*const c_char) -> (u8, &[u8]) {
if msg.is_null() {
(0, &[])
} else {
let len = u32::from_be_bytes(*(*msg).cast());
let len = len.saturating_sub(5); let type_ = *msg.add(4) as u8;
let data = slice::from_raw_parts(msg.add(5).cast(), len as usize);
(type_, data)
}
}
pub(crate) unsafe extern "C" fn pam_converse<T: ConversationHandler>(
num_msg: c_int,
msg: *mut *const PamMessage,
out_resp: *mut *mut PamResponse,
appdata_ptr: *mut c_void,
) -> c_int {
const MAX_MSG_NUM: isize = max_msg_num();
if msg.is_null() || out_resp.is_null() || appdata_ptr.is_null() {
return PAM_BUF_ERR as c_int;
}
let handler = &mut *(appdata_ptr.cast::<T>());
let mut responses = match ResponseBuffer::new(num_msg as isize) {
Ok(buf) => buf,
Err(e) => return e.code().repr(),
};
if !(0..=MAX_MSG_NUM).contains(&(num_msg as isize)) {
return PAM_BUF_ERR as c_int;
}
let messages = msg_to_slice(&msg, num_msg);
for (i, message) in messages.iter().enumerate() {
match message.msg_style as c_int {
#[cfg(target_os = "linux")]
pam_sys::PAM_BINARY_PROMPT => {
let (type_, data) = msg_content_to_bin(&message.msg);
let result = handler.binary_prompt(type_, data);
match result {
Ok(response) => responses.put_binary(i, response.0, &response.1),
Err(code) => return code.repr(),
}
}
_ => {
let result = match message.msg_style as c_int {
pam_sys::PAM_PROMPT_ECHO_ON => {
let text = msg_content_as_cstr(&message.msg);
handler.prompt_echo_on(text).map(map_conv_string)
}
pam_sys::PAM_PROMPT_ECHO_OFF => {
let text = msg_content_as_cstr(&message.msg);
handler.prompt_echo_off(text).map(map_conv_string)
}
pam_sys::PAM_TEXT_INFO => {
let text = msg_content_as_cstr(&message.msg);
handler.text_info(text);
Ok(None)
}
pam_sys::PAM_ERROR_MSG => {
let text = msg_content_as_cstr(&message.msg);
handler.error_msg(text);
Ok(None)
}
#[cfg(target_os = "linux")]
pam_sys::PAM_RADIO_TYPE => {
let text = msg_content_to_cstr(&message.msg);
handler.radio_prompt(text).map(|b| {
if b {
CString::new("yes").ok()
} else {
CString::new("no").ok()
}
})
}
_ => Err(ErrorCode::CONV_ERR),
};
match result {
Ok(response) => responses.put(i, response),
Err(code) => return code.repr(),
}
}
}
}
*out_resp = responses.into();
PAM_SUCCESS as i32
}
#[cfg(test)]
mod tests {
use super::*;
use crate::conv_mock::{Conversation, LogEntry};
use crate::conv_null::Conversation as NullConversation;
use libc::free;
use std::ptr;
fn make_handler() -> Box<Conversation> {
Box::new(Conversation::with_credentials("test usër", "paßword"))
}
#[test]
fn test_edge_cases() {
let mut handler = make_handler();
let pam_conv = to_pam_conv(&mut handler);
let c_callback = pam_conv.conv.unwrap();
let appdata = pam_conv.appdata_ptr;
let text = CString::new("").unwrap();
let mut msg = PamMessage {
msg_style: pam_sys::PAM_PROMPT_ECHO_ON as c_int,
msg: text.as_ptr(),
};
let mut msg_ptr = &mut msg as *const _;
let mut responses: *mut PamResponse = ptr::null_mut();
assert_eq!(
unsafe { c_callback(1, ptr::null_mut(), &mut responses as *mut *mut _, appdata,) },
ErrorCode::BUF_ERR.repr(),
"pam_conv with null `msg` arg returned `left` instead of BUF_ERR"
);
assert_eq!(
unsafe {
c_callback(
1,
&mut msg_ptr as *mut *const PamMessage,
ptr::null_mut(),
appdata,
)
},
ErrorCode::BUF_ERR.repr(),
"pam_conv with null `out_resp` arg returned `left` instead of BUF_ERR"
);
assert_eq!(
unsafe {
c_callback(
1,
&mut msg_ptr as *mut *const _,
&mut responses as *mut *mut _,
ptr::null_mut(),
)
},
ErrorCode::BUF_ERR.repr(),
"pam_conv with null `appdata_ptr` arg returned `left` instead of BUF_ERR"
);
assert_eq!(
unsafe {
c_callback(
-1,
&mut msg_ptr as *mut *const _,
&mut responses as *mut *mut _,
appdata,
)
},
ErrorCode::BUF_ERR.repr(),
"pam_conv with negative `msg_num` arg returned `left` instead of BUF_ERR"
);
assert_eq!(
handler.log.len(),
0,
"log contained `left` messages instead of `right`"
);
}
#[test]
fn test_zero_num() {
let mut handler = make_handler();
let pam_conv = to_pam_conv(&mut handler);
let c_callback = pam_conv.conv.unwrap();
let appdata = pam_conv.appdata_ptr;
let mut msg = PamMessage {
msg_style: pam_sys::PAM_PROMPT_ECHO_ON as c_int,
msg: ptr::null_mut(),
};
let mut msg_ptr = &mut msg as *const _;
let mut responses: *mut PamResponse = ptr::null_mut();
assert_eq!(
unsafe {
c_callback(
0,
&mut msg_ptr as *mut *const _,
&mut responses as *mut *mut _,
appdata,
)
},
ErrorCode::BUF_ERR.repr(),
"pam_conv with zero `msg_num` arg returned `left` instead of BUF_ERR"
);
assert_eq!(
handler.log.len(),
0,
"log contained `left` messages instead of `right`"
);
}
fn test_prompt(style: c_int, prompt: &str, expected: &str) {
let mut handler = make_handler();
let pam_conv = to_pam_conv(&mut handler);
let c_callback = pam_conv.conv.unwrap();
let appdata = pam_conv.appdata_ptr;
let text = CString::new(prompt).unwrap();
let mut msg = PamMessage {
msg_style: style as c_int,
msg: text.as_ptr(),
};
let mut msg_ptr = &mut msg as *const _;
let mut responses: *mut PamResponse = ptr::null_mut();
let code = unsafe {
c_callback(
1,
&mut msg_ptr as *mut *const _,
&mut responses as *mut *mut _,
appdata,
)
};
assert_eq!(
code,
pam_sys::PAM_SUCCESS as c_int,
"pam_conv failed with error code `left`"
);
assert!(
!responses.is_null(),
"response is still null after conversation"
);
assert_eq!(
unsafe { (*responses).resp_retcode },
0,
"retcode in response is `left` instead of 0"
);
let response = unsafe { CStr::from_ptr((*responses).resp) };
assert_eq!(
response,
CString::new(expected).unwrap().as_c_str(),
"response contained `left` instead of expected `right`"
);
unsafe {
free((*responses).resp as *mut _);
free(responses as *mut _);
}
assert_eq!(
handler.log.len(),
0,
"log contained `left` messages instead of `right`"
);
}
#[test]
fn test_prompt_err() {
let mut handler = Box::new(NullConversation::new());
let pam_conv = to_pam_conv(&mut handler);
let c_callback = pam_conv.conv.unwrap();
let appdata = pam_conv.appdata_ptr;
let text = CString::new("").unwrap();
let mut msg = PamMessage {
msg_style: pam_sys::PAM_PROMPT_ECHO_OFF as c_int,
msg: text.as_ptr(),
};
let mut msg_ptr = &mut msg as *const _;
let mut responses: *mut PamResponse = ptr::null_mut();
let code = unsafe {
c_callback(
1,
&mut msg_ptr as *mut *const _,
&mut responses as *mut *mut _,
appdata,
)
};
assert_eq!(
code,
ErrorCode::CONV_ERR.repr(),
"pam_conv failed with error code `left`"
);
assert!(
responses.is_null(),
"response is not null after conversation with error"
);
}
#[test]
fn test_prompt_echo_on() {
test_prompt(pam_sys::PAM_PROMPT_ECHO_ON, "username? ", "test usër")
}
#[test]
fn test_prompt_echo_off() {
test_prompt(pam_sys::PAM_PROMPT_ECHO_OFF, "password? ", "paßword")
}
#[test]
#[cfg(target_os = "linux")]
fn test_prompt_radio() {
test_prompt(pam_sys::PAM_RADIO_TYPE, "no? ", "no")
}
fn test_output_msg(style: c_int, text: Option<&str>) -> LogEntry {
let mut handler = make_handler();
let pam_conv = to_pam_conv(&mut handler);
let c_callback = pam_conv.conv.unwrap();
let appdata = pam_conv.appdata_ptr;
let c_text = CString::new(text.unwrap_or("")).unwrap();
let mut msg = PamMessage {
msg_style: style as c_int,
msg: match text {
Some(_) => c_text.as_ptr(),
None => ptr::null(),
},
};
let mut msg_ptr = &mut msg as *const _;
let mut responses: *mut PamResponse = ptr::null_mut();
let code = unsafe {
c_callback(
1,
&mut msg_ptr as *mut *const _,
&mut responses as *mut *mut _,
appdata,
)
};
assert_eq!(
code,
pam_sys::PAM_SUCCESS as c_int,
"pam_conv failed with error code `left`"
);
assert!(
!responses.is_null(),
"response is still null after conversation"
);
assert_eq!(
unsafe { (*responses).resp_retcode },
0,
"retcode in response is `left` instead of 0"
);
assert_eq!(
unsafe { (*responses).resp },
ptr::null_mut(),
"response text is non-null after output-only conversation"
);
unsafe {
free(responses as *mut _);
}
assert_eq!(
handler.log.len(),
1,
"log contained `left` messages instead of `right`"
);
let result = handler.log[0].clone();
handler.clear_log();
assert_eq!(handler.log.len(), 0, "log could not be emptied");
result
}
#[test]
fn test_error_msg() {
const MSG: &str = "test error öäüß";
let logentry = test_output_msg(pam_sys::PAM_ERROR_MSG, Some(MSG));
if let LogEntry::Error(msg) = logentry {
assert_eq!(
msg.to_string_lossy(),
MSG,
"log contained unexpected message `left`"
);
} else {
assert!(false, "log contained unexpected message type");
}
}
#[test]
fn test_info_msg() {
const MSG: &str = "test info äöüß";
let logentry = test_output_msg(pam_sys::PAM_TEXT_INFO, Some(MSG));
if let LogEntry::Info(msg) = logentry {
assert_eq!(
msg.to_string_lossy(),
MSG,
"log contained unexpected message `left`"
);
} else {
assert!(false, "log contained unexpected message type");
}
}
#[test]
fn test_null_msg() {
let logentry = test_output_msg(pam_sys::PAM_TEXT_INFO, None);
if let LogEntry::Info(msg) = logentry {
assert_eq!(
msg.to_string_lossy(),
"",
"log contained unexpected message `left`"
);
} else {
assert!(false, "log contained unexpected message type");
}
}
#[test]
#[should_panic = "assertion failed"]
fn test_inval_msg() {
test_output_msg(65535, None);
}
#[test]
#[cfg(target_os = "linux")]
fn test_binary() {
let mut handler = make_handler();
let pam_conv = to_pam_conv(&mut handler);
let c_callback = pam_conv.conv.unwrap();
let appdata = pam_conv.appdata_ptr;
let buffer: Vec<u8> = vec![0, 0, 0, 6, 0xFF, 0x42];
let msg = PamMessage {
msg_style: pam_sys::PAM_BINARY_PROMPT as c_int,
msg: buffer.as_ptr() as *const _,
};
let mut msg_ptr = &msg as *const _;
let mut responses: *mut PamResponse = ptr::null_mut();
let code = unsafe {
c_callback(
1,
&mut msg_ptr as *mut *const _,
&mut responses as *mut *mut _,
appdata,
)
};
assert_eq!(
code,
pam_sys::PAM_CONV_ERR as c_int,
"pam_conv returned unexpected error code `left`"
);
}
}