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)
+    }
+}