Mercurial > crates > nonstick
diff src/pam_ffi/message.rs @ 71:58f9d2a4df38
Reorganize everything again???
- Splits ffi/memory stuff into a bunch of stuff in the pam_ffi module.
- Builds infrastructure for passing Messages and Responses.
- Adds tests for some things at least.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Tue, 03 Jun 2025 21:54:58 -0400 |
parents | src/pam_ffi.rs@9f8381a1c09c |
children | 47eb242a4f88 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pam_ffi/message.rs Tue Jun 03 21:54:58 2025 -0400 @@ -0,0 +1,292 @@ +//! Data and types dealing with PAM messages. + +use crate::constants::InvalidEnum; +use crate::pam_ffi::memory; +use crate::pam_ffi::memory::{CBinaryData, NulError, TooBigError}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; +use std::ffi::{c_char, c_int, c_void, CStr}; +use std::result::Result as StdResult; +use std::slice; +use std::str::Utf8Error; + +/// The types of message and request that can be sent to a user. +/// +/// The data within each enum value is the prompt (or other information) +/// that will be presented to the user. +#[derive(Debug)] +pub enum Message<'a> { + /// Requests information from the user; will be masked when typing. + /// + /// Response: [`Response::MaskedText`] + MaskedPrompt(&'a str), + /// Requests information from the user; will not be masked. + /// + /// Response: [`Response::Text`] + Prompt(&'a str), + /// "Yes/No/Maybe conditionals" (a Linux-PAM extension). + /// + /// Response: [`Response::Text`] + /// (Linux-PAM documentation doesn't define its contents.) + RadioPrompt(&'a str), + /// Raises an error message to the user. + /// + /// Response: [`Response::NoResponse`] + Error(&'a str), + /// Sends an informational message to the user. + /// + /// Response: [`Response::NoResponse`] + Info(&'a str), + /// Requests binary data from the client (a Linux-PAM extension). + /// + /// This is used for non-human or non-keyboard prompts (security key?). + /// NOT part of the X/Open PAM specification. + /// + /// Response: [`Response::Binary`] + BinaryPrompt { + /// Some binary data. + data: &'a [u8], + /// A "type" that you can use for signalling. Has no strict definition in PAM. + data_type: u8, + }, +} + +impl Message<'_> { + /// Copies the contents of this message to the C heap. + fn copy_to_heap(&self) -> StdResult<(Style, *mut c_void), ConversionError> { + let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast())); + match *self { + Self::MaskedPrompt(text) => alloc(Style::PromptEchoOff, text), + Self::Prompt(text) => alloc(Style::PromptEchoOn, text), + Self::RadioPrompt(text) => alloc(Style::RadioType, text), + Self::Error(text) => alloc(Style::ErrorMsg, text), + Self::Info(text) => alloc(Style::TextInfo, text), + Self::BinaryPrompt { data, data_type } => Ok(( + Style::BinaryPrompt, + (CBinaryData::alloc(data, data_type)?).cast(), + )), + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error("error creating PAM message: {0}")] +enum ConversionError { + InvalidEnum(#[from] InvalidEnum<Style>), + Utf8Error(#[from] Utf8Error), + NulError(#[from] NulError), + TooBigError(#[from] TooBigError), +} + +/// The C enum values for messages shown to the user. +#[derive(Debug, PartialEq, FromPrimitive)] +pub enum Style { + /// Requests information from the user; will be masked when typing. + PromptEchoOff = 1, + /// Requests information from the user; will not be masked. + PromptEchoOn = 2, + /// An error message. + ErrorMsg = 3, + /// An informational message. + TextInfo = 4, + /// Yes/No/Maybe conditionals. A Linux-PAM extension. + RadioType = 5, + /// For server–client non-human interaction. + /// + /// NOT part of the X/Open PAM specification. + /// A Linux-PAM extension. + BinaryPrompt = 7, +} + +impl TryFrom<c_int> for Style { + type Error = InvalidEnum<Self>; + fn try_from(value: c_int) -> StdResult<Self, Self::Error> { + Self::from_i32(value).ok_or(value.into()) + } +} + +impl From<Style> for c_int { + fn from(val: Style) -> Self { + val as Self + } +} + +/// A message sent by PAM or a module to an application. +/// This message, and its internal data, is owned by the creator +/// (either the module or PAM itself). +#[repr(C)] +pub struct RawMessage { + /// The style of message to request. + style: c_int, + /// A description of the data requested. + /// + /// For most requests, this will be an owned [`CStr`], but for requests + /// with [`Style::BinaryPrompt`], this will be [`BinaryData`] + /// (a Linux-PAM extension). + data: *mut c_void, +} + +impl RawMessage { + fn set(&mut self, msg: &Message) -> StdResult<(), ConversionError> { + let (style, data) = msg.copy_to_heap()?; + self.clear(); + // SAFETY: We allocated this ourselves or were given it by PAM. + // Otherwise, it's null, but free(null) is fine. + unsafe { libc::free(self.data) }; + self.style = style as c_int; + self.data = data; + Ok(()) + } + + /// Retrieves the data stored in this message. + fn data(&self) -> StdResult<Message, ConversionError> { + let style: Style = self.style.try_into()?; + // SAFETY: We either allocated this message ourselves or were provided it by PAM. + let result = unsafe { + match style { + Style::PromptEchoOff => Message::MaskedPrompt(self.string_data()?), + Style::PromptEchoOn => Message::Prompt(self.string_data()?), + Style::TextInfo => Message::Info(self.string_data()?), + Style::ErrorMsg => Message::Error(self.string_data()?), + Style::RadioType => Message::Error(self.string_data()?), + Style::BinaryPrompt => (self.data as *const CBinaryData).as_ref().map_or_else( + || Message::BinaryPrompt { + data_type: 0, + data: &[], + }, + |data| Message::BinaryPrompt { + data_type: data.data_type(), + data: data.contents(), + }, + ), + } + }; + Ok(result) + } + + /// Gets this message's data pointer as a string. + /// + /// # Safety + /// + /// It's up to you to pass this only on types with a string value. + unsafe fn string_data(&self) -> StdResult<&str, Utf8Error> { + if self.data.is_null() { + Ok("") + } else { + CStr::from_ptr(self.data as *const c_char).to_str() + } + } + + /// Zeroes out the data stored here. + fn clear(&mut self) { + // SAFETY: We either created this data or we got it from PAM. + // After this function is done, it will be zeroed out. + unsafe { + if let Ok(style) = Style::try_from(self.style) { + match style { + Style::BinaryPrompt => { + if let Some(d) = (self.data as *mut CBinaryData).as_mut() { + d.zero_contents() + } + } + Style::TextInfo + | Style::RadioType + | Style::ErrorMsg + | Style::PromptEchoOff + | Style::PromptEchoOn => memory::zero_c_string(self.data), + } + }; + } + } +} + +/// Abstraction of a list-of-messages to be sent in a PAM conversation. +/// +/// On Linux-PAM and other compatible implementations, `messages` +/// is treated as a pointer-to-pointers, like `int argc, char **argv`. +/// (In this situation, the value of `OwnedMessages.indirect` is +/// the pointer passed to `pam_conv`.) +/// +/// ```text +/// ╔═ OwnedMsgs ═╗ points to ┌─ Indirect ─┐ ╔═ Message ═╗ +/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base[0] ┄┄┄┼┄┄┄┄┄> ║ style ║ +/// ║ count ║ │ base[1] ┄┄┄┼┄┄┄╮ ║ data ║ +/// ╚═════════════╝ │ ... │ ┆ ╚═══════════╝ +/// ┆ +/// ┆ ╔═ Message ═╗ +/// ╰┄┄> ║ style ║ +/// ║ data ║ +/// ╚═══════════╝ +/// ``` +/// +/// On OpenPAM and other compatible implementations (like Solaris), +/// `messages` is a pointer-to-pointer-to-array. This appears to be +/// the correct implementation as required by the XSSO specification. +/// +/// ```text +/// ╔═ OwnedMsgs ═╗ points to ┌─ Indirect ─┐ ╔═ Message[] ═╗ +/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base ┄┄┄┄┄┄┼┄┄┄┄┄> ║ style ║ +/// ║ count ║ └────────────┘ ║ data ║ +/// ╚═════════════╝ ╟─────────────╢ +/// ║ style ║ +/// ║ data ║ +/// ╟─────────────╢ +/// ║ ... ║ +/// ``` +/// +/// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.*** +#[repr(C)] +pub struct OwnedMessages { + /// An indirection to the messages themselves, stored on the C heap. + indirect: *mut Indirect<RawMessage>, + /// The number of messages in the list. + count: usize, +} + +impl OwnedMessages { + /// Allocates data to store messages on the C heap. + pub fn alloc(count: usize) -> Self { + // SAFETY: We're allocating. That's safe. + unsafe { + // Since this is Linux-PAM, the indirect is a list of pointers. + let indirect = + libc::calloc(count, size_of::<Indirect<RawMessage>>()) as *mut Indirect<RawMessage>; + let indir_ptrs = slice::from_raw_parts_mut(indirect, count); + for ptr in indir_ptrs { + ptr.base = libc::calloc(1, size_of::<RawMessage>()) as *mut RawMessage; + } + Self { indirect, count } + } + } + + /// Gets a reference to the message at the given index. + pub fn get(&self, index: usize) -> Option<&RawMessage> { + (index < self.count).then(|| unsafe { (*self.indirect).at(index) }) + } + + /// Gets a mutable reference to the message at the given index. + pub fn get_mut(&mut self, index: usize) -> Option<&mut RawMessage> { + (index < self.count).then(|| unsafe { (*self.indirect).at_mut(index) }) + } +} + +#[repr(transparent)] +struct Indirect<T> { + /// The starting address for the T. + base: *mut T, +} + +impl<T> Indirect<T> { + /// Gets a mutable reference to the element at the given index. + /// + /// # Safety + /// + /// We don't check `index`. + unsafe fn at_mut(&mut self, index: usize) -> &mut T { + &mut *self.base.add(index) + } + + unsafe fn at(&self, index: usize) -> &T { + &*self.base.add(index) + } +}