use std::ffi::{CStr, CString, c_char, c_int, c_void};
use std::sync::OnceLock;
use dlsym::Bindings;
use purecrypto::zeroize::{Zeroize, Zeroizing};
const PAM_SUCCESS: c_int = 0;
const PAM_BUF_ERR: c_int = 5;
const PAM_CONV_ERR: c_int = 19;
const PAM_SILENT: c_int = 0x8000;
const PAM_ESTABLISH_CRED: c_int = 0x2;
const PAM_DELETE_CRED: c_int = 0x4;
const PAM_REINITIALIZE_CRED: c_int = 0x8;
const PAM_TTY: c_int = 3;
const PAM_RHOST: c_int = 4;
const PAM_PROMPT_ECHO_OFF: c_int = 1;
const PAM_PROMPT_ECHO_ON: c_int = 2;
const PAM_ERROR_MSG: c_int = 3;
const PAM_TEXT_INFO: c_int = 4;
const PAM_MAX_NUM_MSG: c_int = 32;
#[repr(C)]
struct PamMessage {
msg_style: c_int,
msg: *const c_char,
}
#[repr(C)]
struct PamResponse {
resp: *mut c_char,
resp_retcode: c_int,
}
type ConvFn = unsafe extern "C" fn(
num_msg: c_int,
msg: *mut *const PamMessage,
resp: *mut *mut PamResponse,
appdata_ptr: *mut c_void,
) -> c_int;
#[repr(C)]
struct PamConv {
conv: ConvFn,
appdata_ptr: *mut c_void,
}
dlsym::library! {
struct LibPam {
unsafe extern "C" {
fn pam_start(
service: *const c_char,
user: *const c_char,
conv: *const PamConv,
pamh: *mut *mut c_void,
) -> c_int;
fn pam_end(pamh: *mut c_void, status: c_int) -> c_int;
fn pam_set_item(pamh: *mut c_void, item_type: c_int, item: *const c_void) -> c_int;
fn pam_authenticate(pamh: *mut c_void, flags: c_int) -> c_int;
fn pam_acct_mgmt(pamh: *mut c_void, flags: c_int) -> c_int;
fn pam_setcred(pamh: *mut c_void, flags: c_int) -> c_int;
fn pam_open_session(pamh: *mut c_void, flags: c_int) -> c_int;
fn pam_close_session(pamh: *mut c_void, flags: c_int) -> c_int;
fn pam_getenvlist(pamh: *mut c_void) -> *mut *mut c_char;
fn pam_strerror(pamh: *mut c_void, errnum: c_int) -> *const c_char;
}
}
}
fn libpam() -> Result<&'static LibPam, Error> {
static LIB: OnceLock<Result<LibPam, String>> = OnceLock::new();
LIB.get_or_init(|| {
unsafe { LibPam::open_first(["libpam.so.0", "libpam.so"]) }.map_err(|e| e.to_string())
})
.as_ref()
.map_err(|e| Error(format!("cannot load libpam: {e}")))
}
#[derive(Debug)]
pub struct Error(String);
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
pub trait ConversationHandler {
fn prompt(&mut self, prompt: &CStr, echo: bool) -> Option<Zeroizing<Vec<u8>>>;
fn info(&mut self, msg: &CStr, error: bool);
}
pub struct NullConversation;
impl ConversationHandler for NullConversation {
fn prompt(&mut self, _prompt: &CStr, _echo: bool) -> Option<Zeroizing<Vec<u8>>> {
None
}
fn info(&mut self, _msg: &CStr, _error: bool) {}
}
unsafe fn free_replies(replies: *mut PamResponse, n: usize) {
for i in 0..n {
let resp = unsafe { (*replies.add(i)).resp };
if !resp.is_null() {
let len = unsafe { libc::strlen(resp) };
unsafe { std::slice::from_raw_parts_mut(resp.cast::<u8>(), len) }.zeroize();
unsafe { libc::free(resp.cast()) };
}
}
unsafe { libc::free(replies.cast()) };
}
unsafe extern "C" fn converse<C: ConversationHandler>(
num_msg: c_int,
msg: *mut *const PamMessage,
resp: *mut *mut PamResponse,
appdata_ptr: *mut c_void,
) -> c_int {
if !(1..=PAM_MAX_NUM_MSG).contains(&num_msg)
|| msg.is_null()
|| resp.is_null()
|| appdata_ptr.is_null()
{
return PAM_CONV_ERR;
}
let n = num_msg as usize;
let handler = unsafe { &mut *appdata_ptr.cast::<C>() };
let replies = unsafe { libc::calloc(n, size_of::<PamResponse>()) }.cast::<PamResponse>();
if replies.is_null() {
return PAM_BUF_ERR;
}
for i in 0..n {
let m = unsafe { *msg.add(i) };
if m.is_null() {
unsafe { free_replies(replies, n) };
return PAM_CONV_ERR;
}
let (style, text) = unsafe { ((*m).msg_style, (*m).msg) };
let text = if text.is_null() {
c""
} else {
unsafe { CStr::from_ptr(text) }
};
let ok = match style {
PAM_PROMPT_ECHO_OFF | PAM_PROMPT_ECHO_ON => {
match handler.prompt(text, style == PAM_PROMPT_ECHO_ON) {
Some(answer) if !answer.contains(&0) => {
let p = unsafe { libc::malloc(answer.len() + 1) }.cast::<u8>();
if !p.is_null() {
unsafe {
std::ptr::copy_nonoverlapping(answer.as_ptr(), p, answer.len());
}
unsafe { *p.add(answer.len()) = 0 };
unsafe { (*replies.add(i)).resp = p.cast() };
}
!p.is_null()
}
_ => false,
}
}
PAM_ERROR_MSG | PAM_TEXT_INFO => {
handler.info(text, style == PAM_ERROR_MSG);
true
}
_ => false,
};
if !ok {
unsafe { free_replies(replies, n) };
return PAM_CONV_ERR;
}
}
unsafe { *resp = replies };
PAM_SUCCESS
}
pub struct Context<C: ConversationHandler> {
lib: &'static LibPam,
handle: *mut c_void,
last: c_int,
session: bool,
_conv: Box<PamConv>,
handler: *mut C,
}
unsafe impl<C: ConversationHandler + Send> Send for Context<C> {}
impl<C: ConversationHandler> Context<C> {
pub fn new(service: &str, user: &str, handler: C) -> Result<Self, Error> {
let lib = libpam()?;
let service = CString::new(service).map_err(|_| Error("NUL in service".into()))?;
let user = CString::new(user).map_err(|_| Error("NUL in user".into()))?;
let handler = Box::into_raw(Box::new(handler));
let conv = Box::new(PamConv {
conv: converse::<C>,
appdata_ptr: handler.cast(),
});
let mut handle = std::ptr::null_mut();
let rc = unsafe { lib.pam_start(service.as_ptr(), user.as_ptr(), &*conv, &mut handle) };
if rc != PAM_SUCCESS || handle.is_null() {
drop(unsafe { Box::from_raw(handler) });
return Err(unsafe { describe(lib, std::ptr::null_mut(), rc) });
}
Ok(Self {
lib,
handle,
last: PAM_SUCCESS,
session: false,
_conv: conv,
handler,
})
}
fn check(&mut self, rc: c_int) -> Result<(), Error> {
self.last = rc;
if rc == PAM_SUCCESS {
Ok(())
} else {
Err(unsafe { describe(self.lib, self.handle, rc) })
}
}
fn set_str_item(&mut self, item: c_int, value: &str) -> Result<(), Error> {
let value = CString::new(value).map_err(|_| Error("NUL in PAM item".into()))?;
let rc = unsafe {
self.lib
.pam_set_item(self.handle, item, value.as_ptr().cast())
};
self.check(rc)
}
pub fn set_rhost(&mut self, rhost: &str) -> Result<(), Error> {
self.set_str_item(PAM_RHOST, rhost)
}
pub fn set_tty(&mut self, tty: &str) -> Result<(), Error> {
self.set_str_item(PAM_TTY, tty)
}
pub fn authenticate(&mut self) -> Result<(), Error> {
let rc = unsafe { self.lib.pam_authenticate(self.handle, 0) };
self.check(rc)
}
pub fn acct_mgmt(&mut self) -> Result<(), Error> {
let rc = unsafe { self.lib.pam_acct_mgmt(self.handle, 0) };
self.check(rc)
}
pub fn open_session(&mut self) -> Result<(), Error> {
let (lib, h) = (self.lib, self.handle);
let rc = unsafe { lib.pam_setcred(h, PAM_ESTABLISH_CRED) };
self.check(rc)?;
let rc = unsafe { lib.pam_open_session(h, 0) };
if let Err(e) = self.check(rc) {
unsafe { lib.pam_setcred(h, PAM_DELETE_CRED) };
return Err(e);
}
let rc = unsafe { lib.pam_setcred(h, PAM_REINITIALIZE_CRED) };
if let Err(e) = self.check(rc) {
unsafe { lib.pam_close_session(h, 0) };
unsafe { lib.pam_setcred(h, PAM_DELETE_CRED) };
return Err(e);
}
self.session = true;
Ok(())
}
pub fn envlist(&mut self) -> Vec<(CString, CString)> {
let mut out = Vec::new();
let list = unsafe { self.lib.pam_getenvlist(self.handle) };
if list.is_null() {
return out;
}
let mut i = 0;
loop {
let entry = unsafe { *list.add(i) };
if entry.is_null() {
break;
}
let bytes = unsafe { CStr::from_ptr(entry) }.to_bytes();
if let Some(eq) = bytes.iter().position(|&b| b == b'=')
&& let (Ok(k), Ok(v)) = (CString::new(&bytes[..eq]), CString::new(&bytes[eq + 1..]))
{
out.push((k, v));
}
unsafe { libc::free(entry.cast()) };
i += 1;
}
unsafe { libc::free(list.cast()) };
out
}
}
impl<C: ConversationHandler> Drop for Context<C> {
fn drop(&mut self) {
let (lib, h) = (self.lib, self.handle);
if self.session {
self.last = unsafe { lib.pam_close_session(h, 0) };
self.last = unsafe { lib.pam_setcred(h, PAM_DELETE_CRED | PAM_SILENT) };
}
unsafe { lib.pam_end(h, self.last) };
drop(unsafe { Box::from_raw(self.handler) });
}
}
unsafe fn describe(lib: &LibPam, handle: *mut c_void, rc: c_int) -> Error {
let s = unsafe { lib.pam_strerror(handle, rc) };
if s.is_null() {
Error(format!("error {rc}"))
} else {
Error(unsafe { CStr::from_ptr(s) }.to_string_lossy().into_owned())
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Wrong(Vec<(String, bool)>);
impl ConversationHandler for Wrong {
fn prompt(&mut self, prompt: &CStr, echo: bool) -> Option<Zeroizing<Vec<u8>>> {
self.0.push((prompt.to_string_lossy().into_owned(), echo));
Some(Zeroizing::new(b"definitely-not-the-password".to_vec()))
}
fn info(&mut self, _msg: &CStr, _error: bool) {}
}
#[test]
#[ignore = "uses the host's PAM configuration"]
fn wrong_password_is_refused() {
let user = std::env::var("USER").expect("USER");
let mut ctx = Context::new("sshd", &user, Wrong(Vec::new())).expect("pam_start");
ctx.set_rhost("127.0.0.1").expect("set_rhost");
let err = ctx.authenticate().expect_err("wrong password accepted");
let prompts = &unsafe { &*ctx.handler }.0;
eprintln!("prompts={prompts:?} err={err}");
assert!(prompts.iter().any(|(_, echo)| !echo), "no echo-off prompt");
assert!(!err.to_string().is_empty());
}
}