use {
anyhow::{
Context,
Result,
},
std::{
ffi::{
CStr,
CString,
},
fs::OpenOptions,
io::{
BufRead,
BufReader,
Write,
},
os::raw::{
c_char,
c_int,
c_void,
},
panic::AssertUnwindSafe,
},
zeroize::Zeroize,
};
const SERVICE: &str = "sudo";
const PAM_LIBRARY: &str = "libpam.so.0";
const PAM_SUCCESS: c_int = 0;
const PAM_BUF_ERR: c_int = 5;
const PAM_CONV_ERR: c_int = 19;
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_DISALLOW_NULL_AUTHTOK: c_int = 0x0001;
pub(crate) fn authenticate(reason: &str) -> Result<()> {
if reason.is_empty() {
anyhow::bail!("System authentication reason must not be empty");
}
OpenOptions::new()
.read(true)
.write(true)
.open("/dev/tty")
.context("System authentication requires a terminal")?;
let username = whoami::fallible::username().context("Failed to resolve the current user")?;
eprintln!("secenv is trying to {reason}");
SystemPam::open()?.authenticate(&username)
}
struct SystemPam {
library: libloading::Library,
}
impl SystemPam {
fn open() -> Result<Self> {
let library = unsafe { libloading::Library::new(PAM_LIBRARY) }
.with_context(|| format!("System authentication requires {PAM_LIBRARY}"))?;
Ok(Self { library })
}
fn authenticate(&self, username: &str) -> Result<()> {
let start = self.symbol::<PamStart>(b"pam_start\0")?;
let authenticate = self.symbol::<PamAuthenticate>(b"pam_authenticate\0")?;
let acct_mgmt = self.symbol::<PamAcctMgmt>(b"pam_acct_mgmt\0")?;
let end = self.symbol::<PamEnd>(b"pam_end\0")?;
let strerror = self.symbol::<PamStrerror>(b"pam_strerror\0")?;
let service = CString::new(SERVICE).context("PAM service name is not a C string")?;
let user = CString::new(username).context("Current user name is not a C string")?;
let conversation = Conversation {
username: username.to_owned(),
};
let conv = PamConv {
conv: Some(converse),
appdata_ptr: (&conversation as *const Conversation).cast_mut().cast(),
};
let mut handle = std::ptr::null_mut();
let started = unsafe { start(service.as_ptr(), user.as_ptr(), &conv, &mut handle) };
if started != PAM_SUCCESS {
anyhow::bail!("Failed to start system authentication: {}", explain(&strerror, handle, started));
}
let authenticated = unsafe { authenticate(handle, PAM_DISALLOW_NULL_AUTHTOK) };
if authenticated != PAM_SUCCESS {
let message = explain(&strerror, handle, authenticated);
unsafe { end(handle, authenticated) };
anyhow::bail!("System authentication failed: {message}");
}
let accounted = unsafe { acct_mgmt(handle, PAM_DISALLOW_NULL_AUTHTOK) };
let message = explain(&strerror, handle, accounted);
unsafe { end(handle, accounted) };
if accounted != PAM_SUCCESS {
anyhow::bail!("System authentication rejected the current account: {message}");
}
Ok(())
}
fn symbol<T>(&self, name: &[u8]) -> Result<libloading::Symbol<'_, T>> {
unsafe { self.library.get(name) }.with_context(|| {
format!(
"System authentication library {PAM_LIBRARY} is missing {}",
String::from_utf8_lossy(name).trim_end_matches('\0')
)
})
}
}
type PamStart = unsafe extern "C" fn(*const c_char, *const c_char, *const PamConv, *mut *mut PamHandle) -> c_int;
type PamAuthenticate = unsafe extern "C" fn(*mut PamHandle, c_int) -> c_int;
type PamAcctMgmt = unsafe extern "C" fn(*mut PamHandle, c_int) -> c_int;
type PamEnd = unsafe extern "C" fn(*mut PamHandle, c_int) -> c_int;
type PamStrerror = unsafe extern "C" fn(*mut PamHandle, c_int) -> *const c_char;
enum PamHandle {}
#[repr(C)]
struct PamMessage {
msg_style: c_int,
msg: *const c_char,
}
#[repr(C)]
struct PamResponse {
resp: *mut c_char,
resp_retcode: c_int,
}
#[repr(C)]
struct PamConv {
conv: Option<unsafe extern "C" fn(c_int, *mut *const PamMessage, *mut *mut PamResponse, *mut c_void) -> c_int>,
appdata_ptr: *mut c_void,
}
struct Conversation {
username: String,
}
unsafe extern "C" fn converse(
num_msg: c_int,
messages: *mut *const PamMessage,
responses: *mut *mut PamResponse,
appdata: *mut c_void,
) -> c_int {
let result = std::panic::catch_unwind(AssertUnwindSafe(|| unsafe {
converse_inner(num_msg, messages, responses, appdata)
}));
result.unwrap_or(PAM_CONV_ERR)
}
unsafe fn converse_inner(
num_msg: c_int,
messages: *mut *const PamMessage,
responses: *mut *mut PamResponse,
appdata: *mut c_void,
) -> c_int {
if num_msg <= 0 || messages.is_null() || responses.is_null() || appdata.is_null() {
return PAM_CONV_ERR;
}
let conversation = unsafe { &*(appdata as *const Conversation) };
let count = num_msg as usize;
let allocated = unsafe { libc::calloc(count, std::mem::size_of::<PamResponse>()) }.cast::<PamResponse>();
if allocated.is_null() {
return PAM_BUF_ERR;
}
for index in 0..count {
let message = unsafe { *messages.add(index) };
if message.is_null() {
free_responses(allocated, index);
return PAM_CONV_ERR;
}
let message = unsafe { &*message };
let reply = match message.msg_style {
| PAM_PROMPT_ECHO_ON => conversation.prompt(message.msg, false),
| PAM_PROMPT_ECHO_OFF => conversation.prompt(message.msg, true),
| PAM_ERROR_MSG | PAM_TEXT_INFO => {
if !message.msg.is_null() {
eprintln!("{}", unsafe { CStr::from_ptr(message.msg) }.to_string_lossy());
}
Ok(std::ptr::null_mut())
},
| _ => Err(PAM_CONV_ERR),
};
match reply {
| Ok(resp) => unsafe {
(*allocated.add(index)).resp = resp;
(*allocated.add(index)).resp_retcode = 0;
},
| Err(code) => {
free_responses(allocated, index);
return code;
},
}
}
unsafe { *responses = allocated };
PAM_SUCCESS
}
impl Conversation {
fn prompt(&self, prompt: *const c_char, hidden: bool) -> Result<*mut c_char, c_int> {
if prompt.is_null() {
return Err(PAM_CONV_ERR);
}
let prompt = unsafe { CStr::from_ptr(prompt) }.to_string_lossy();
if !hidden && is_username_prompt(&prompt) {
return cstring(&self.username);
}
let mut value = if hidden {
rpassword::prompt_password(prompt.as_ref()).map_err(|_| PAM_CONV_ERR)?
} else {
eprint!("{prompt}");
let _ = std::io::stderr().flush();
let tty = OpenOptions::new()
.read(true)
.open("/dev/tty")
.map_err(|_| PAM_CONV_ERR)?;
let mut line = String::new();
BufReader::new(tty)
.read_line(&mut line)
.map_err(|_| PAM_CONV_ERR)?;
line.trim_end_matches(['\n', '\r']).to_string()
};
let encoded = cstring(&value);
value.zeroize();
encoded
}
}
fn cstring(value: &str) -> Result<*mut c_char, c_int> {
CString::new(value).map(|value| value.into_raw()).map_err(|_| PAM_CONV_ERR)
}
fn free_responses(responses: *mut PamResponse, count: usize) {
if responses.is_null() {
return;
}
for index in 0..count {
let resp = unsafe { (*responses.add(index)).resp };
if !resp.is_null() {
unsafe { drop(CString::from_raw(resp)) };
}
}
unsafe { libc::free(responses.cast()) };
}
fn explain(strerror: &libloading::Symbol<'_, PamStrerror>, handle: *mut PamHandle, status: c_int) -> String {
let message = unsafe { strerror(handle, status) };
if message.is_null() {
return format!("PAM error {status}");
}
unsafe { CStr::from_ptr(message) }.to_string_lossy().into_owned()
}
fn is_username_prompt(prompt: &str) -> bool {
let prompt = prompt.to_ascii_lowercase();
prompt.contains("login") || prompt.contains("username") || prompt.contains("user name")
}
#[cfg(test)]
mod tests {
use super::is_username_prompt;
#[test]
fn username_prompts_are_not_asked_again() {
assert!(is_username_prompt("login: "));
assert!(is_username_prompt("Username: "));
assert!(!is_username_prompt("Password: "));
}
}