use std::{
collections::HashMap,
ffi::{CStr, CString, OsStr, OsString},
os::unix::prelude::OsStrExt,
};
use converse::ConverserData;
use error::pam_err;
pub use error::{PamError, PamErrorType, PamResult};
use sys::*;
mod converse;
mod error;
mod rpassword;
mod securemem;
#[allow(nonstandard_style)]
#[allow(unused)]
pub mod sys;
pub use converse::{CLIConverser, Converser};
pub struct PamContext<C: Converser> {
data_ptr: *mut ConverserData<C>,
pamh: *mut pam_handle_t,
silent: bool,
allow_null_auth_token: bool,
last_pam_status: Option<libc::c_int>,
session_started: bool,
}
pub struct PamContextBuilder<C> {
converser: Option<C>,
service_name: Option<String>,
target_user: Option<String>,
}
impl<C: Converser> PamContextBuilder<C> {
pub fn build(self) -> PamResult<PamContext<C>> {
if let (Some(converser), Some(service_name)) = (self.converser, self.service_name) {
let c_service_name = CString::new(service_name)?;
let c_user = self.target_user.map(CString::new).transpose()?;
let c_user_ptr = match c_user {
Some(ref c) => c.as_ptr(),
None => std::ptr::null(),
};
let data_ptr = Box::into_raw(Box::new(ConverserData {
converser,
panicked: false,
}));
let mut pamh = std::ptr::null_mut();
let res = unsafe {
pam_start(
c_service_name.as_ptr(),
c_user_ptr,
&pam_conv {
conv: Some(converse::converse::<C>),
appdata_ptr: data_ptr as *mut libc::c_void,
},
&mut pamh,
)
};
pam_err(res)?;
if pamh.is_null() {
Err(PamError::InvalidState)
} else {
Ok(PamContext {
data_ptr,
pamh,
silent: false,
allow_null_auth_token: true,
last_pam_status: None,
session_started: false,
})
}
} else {
Err(PamError::InvalidState)
}
}
pub fn converser(mut self, converser: C) -> PamContextBuilder<C> {
self.converser = Some(converser);
self
}
pub fn service_name<T: Into<String>>(mut self, name: T) -> PamContextBuilder<C> {
self.service_name = Some(name.into());
self
}
pub fn target_user<T: Into<String>>(mut self, user: T) -> PamContextBuilder<C> {
self.target_user = Some(user.into());
self
}
}
impl<C> Default for PamContextBuilder<C> {
fn default() -> Self {
Self {
converser: None,
service_name: None,
target_user: None,
}
}
}
impl<C: Converser> PamContext<C> {
pub fn mark_silent(&mut self, silent: bool) {
self.silent = silent;
}
pub fn mark_allow_null_auth_token(&mut self, allow: bool) {
self.allow_null_auth_token = allow;
}
fn silent_flag(&self) -> i32 {
if self.silent {
PAM_SILENT as i32
} else {
0
}
}
fn disallow_null_auth_token_flag(&self) -> i32 {
if self.allow_null_auth_token {
0
} else {
PAM_DISALLOW_NULL_AUTHTOK as i32
}
}
pub fn authenticate(&mut self) -> PamResult<()> {
let mut flags = 0;
flags |= self.silent_flag();
flags |= self.disallow_null_auth_token_flag();
pam_err(unsafe { pam_authenticate(self.pamh, flags) })?;
if self.has_panicked() {
panic!("Panic during pam authentication");
}
Ok(())
}
pub fn validate_account(&mut self) -> PamResult<()> {
let mut flags = 0;
flags |= self.silent_flag();
flags |= self.disallow_null_auth_token_flag();
pam_err(unsafe { pam_acct_mgmt(self.pamh, flags) })
}
pub fn validate_account_or_change_auth_token(&mut self) -> PamResult<()> {
let check_val = self.validate_account();
match check_val {
Ok(()) => Ok(()),
Err(PamError::Pam(PamErrorType::NewAuthTokenRequired, _)) => {
self.change_auth_token(true)?;
Ok(())
}
Err(e) => Err(e),
}
}
pub fn set_user(&mut self, user: &str) -> PamResult<()> {
let c_user = CString::new(user)?;
pam_err(unsafe {
pam_set_item(
self.pamh,
PAM_USER as i32,
c_user.as_ptr() as *const libc::c_void,
)
})
}
pub fn credentials_reinitialize(&mut self) -> PamResult<()> {
self.credentials(PAM_REINITIALIZE_CRED as libc::c_int)
}
fn credentials(&mut self, action: libc::c_int) -> PamResult<()> {
let mut flags = action;
flags |= self.silent_flag();
pam_err(unsafe { pam_setcred(self.pamh, flags) })
}
pub fn change_auth_token(&mut self, expired_only: bool) -> PamResult<()> {
let mut flags = 0;
flags |= self.silent_flag();
if expired_only {
flags |= PAM_CHANGE_EXPIRED_AUTHTOK as i32;
}
pam_err(unsafe { pam_chauthtok(self.pamh, flags) })
}
pub fn open_session(&mut self) -> PamResult<()> {
if !self.session_started {
pam_err(unsafe { pam_open_session(self.pamh, self.silent_flag()) })?;
self.session_started = true;
Ok(())
} else {
Err(PamError::SessionAlreadyOpen)
}
}
pub fn close_session(&mut self) -> PamResult<()> {
if self.session_started {
pam_err(unsafe { pam_close_session(self.pamh, self.silent_flag()) })?;
self.session_started = false;
Ok(())
} else {
Err(PamError::SessionNotOpen)
}
}
pub fn env(&mut self) -> PamResult<HashMap<OsString, OsString>> {
let mut res = HashMap::new();
let envs = unsafe { pam_getenvlist(self.pamh) };
if envs.is_null() {
return Err(PamError::EnvListFailure);
}
let mut curr_env = envs;
while unsafe { !(*curr_env).is_null() } {
let curr_str = unsafe { *curr_env };
let data = {
let cstr = unsafe { CStr::from_ptr(curr_str) };
let bytes = cstr.to_bytes();
if let Some(pos) = bytes.iter().position(|b| *b == b'=') {
let key = OsStr::from_bytes(&bytes[..pos]).to_owned();
let value = OsStr::from_bytes(&bytes[pos + 1..]).to_owned();
Some((key, value))
} else {
None
}
};
if let Some((k, v)) = data {
res.insert(k, v);
}
unsafe { libc::free(curr_str as *mut libc::c_void) };
curr_env = unsafe { curr_env.offset(1) };
}
unsafe { libc::free(envs as *mut libc::c_void) };
Ok(res)
}
pub fn has_panicked(&self) -> bool {
unsafe { (*self.data_ptr).panicked }
}
}
impl PamContext<CLIConverser> {
pub fn builder_cli(
name: &str,
use_stdin: bool,
no_interact: bool,
) -> PamContextBuilder<CLIConverser> {
PamContextBuilder::default().converser(CLIConverser {
name: name.to_owned(),
use_stdin,
no_interact,
})
}
}
impl<C: Converser> Drop for PamContext<C> {
fn drop(&mut self) {
let _data = unsafe { Box::from_raw(self.data_ptr) };
let _ = self.close_session();
unsafe {
pam_end(
self.pamh,
self.last_pam_status.unwrap_or(PAM_SUCCESS as libc::c_int) | PAM_DATA_SILENT as i32,
)
};
}
}