view src/pam_ffi/message.rs @ 72:47eb242a4f88

Fill out the PamHandle trait. This updates the PamHandle trait to have methods for each Item, and implements them on the LibPamHandle.
author Paul Fisher <paul@pfish.zone>
date Wed, 04 Jun 2025 03:53:36 -0400
parents 58f9d2a4df38
children ac6881304c78
line wrap: on
line source

//! 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: [`MaskedText`](crate::conv::Response::MaskedText)
    MaskedPrompt(&'a str),
    /// Requests information from the user; will not be masked.
    ///
    /// Response: [`Text`](crate::conv::Response::Text)
    Prompt(&'a str),
    /// "Yes/No/Maybe conditionals" (a Linux-PAM extension).
    ///
    /// Response: [`Text`](crate::conv::Response::Text)
    /// (Linux-PAM documentation doesn't define its contents.)
    RadioPrompt(&'a str),
    /// Raises an error message to the user.
    ///
    /// Response: [`NoResponse`](crate::conv::Response::NoResponse)
    Error(&'a str),
    /// Sends an informational message to the user.
    ///
    /// Response: [`NoResponse`](crate::conv::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: [`Binary`](crate::conv::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)
    }
}