view src/libpam/question.rs @ 90:f6186e41399b

Miscellaneous fixes and cleanup: - Rename `get_user` to `username` and `get_authtok` to `authtok`. - Use pam_strerror for error messages. - Add library linkage to build.rs (it was missing???).
author Paul Fisher <paul@pfish.zone>
date Sat, 14 Jun 2025 09:30:16 -0400
parents dd3e9c4bcde3
children efc2b56c8928
line wrap: on
line source

//! Data and types dealing with PAM messages.

#[cfg(feature = "linux-pam-extensions")]
use crate::conv::{BinaryQAndA, RadioQAndA};
use crate::conv::{ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA};
use crate::libpam::conversation::OwnedMessage;
use crate::libpam::memory::{CBinaryData, Immovable};
pub use crate::libpam::pam_ffi::Question;
use crate::libpam::{memory, pam_ffi};
use crate::ErrorCode;
use crate::Result;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use std::ffi::{c_void, CStr};
use std::{ptr, slice};

/// Abstraction of a collection of questions to be sent in a PAM conversation.
///
/// The PAM C API conversation function looks like this:
///
/// ```c
/// int pam_conv(
///     int count,
///     const struct pam_message **questions,
///     struct pam_response **answers,
///     void *appdata_ptr,
/// )
/// ```
///
/// On Linux-PAM and other compatible implementations, `questions`
/// is treated as a pointer-to-pointers, like `int argc, char **argv`.
/// (In this situation, the value of `Questions.indirect` is
/// the pointer passed to `pam_conv`.)
///
/// ```text
///            points to  ┌───────────────┐      ╔═ Question ═╗
/// questions ┄┄┄┄┄┄┄┄┄┄> │ questions[0] ┄┼┄┄┄┄> ║ style      ║
///                       │ questions[1] ┄┼┄┄┄╮  ║ data ┄┄┄┄┄┄╫┄┄> ...
///                       │ ...           │   ┆  ╚════════════╝
///                                           ┆
///                                           ┆    ╔═ Question ═╗
///                                           ╰┄┄> ║ 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
///            points to  ┌─────────────┐       ╔═ Question[] ═╗
/// questions ┄┄┄┄┄┄┄┄┄┄> │ *questions ┄┼┄┄┄┄┄> ║ style        ║
///                       └─────────────┘       ║ data ┄┄┄┄┄┄┄┄╫┄┄> ...
///                                             ╟──────────────╢
///                                             ║ style        ║
///                                             ║ data ┄┄┄┄┄┄┄┄╫┄┄> ...
///                                             ╟──────────────╢
///                                             ║ ...          ║
/// ```
pub trait QuestionsTrait {
    /// Allocates memory for this indirector and all its members.
    fn new(messages: &[Message]) -> Result<Self>
    where
        Self: Sized;

    /// Gets the pointer that is passed .
    fn ptr(&self) -> *const *const Question;

    /// Converts a pointer into a borrowed list of Questions.
    ///
    /// # Safety
    ///
    /// You have to provide a valid pointer.
    unsafe fn borrow_ptr<'a>(
        ptr: *const *const Question,
        count: usize,
    ) -> impl Iterator<Item = &'a Question>;
}

#[cfg(pam_impl = "linux-pam")]
pub type Questions = LinuxPamQuestions;

#[cfg(not(pam_impl = "linux-pam"))]
pub type Questions = XSsoQuestions;

/// The XSSO standard version of the pointer train to questions.
#[derive(Debug)]
#[repr(C)]
pub struct XSsoQuestions {
    /// Points to the memory address where the meat of `questions` is.
    /// **The memory layout of Vec is not specified**, and we need to return
    /// a pointer to the pointer, hence we have to store it here.
    pointer: *const Question,
    questions: Vec<Question>,
    _marker: Immovable,
}

impl XSsoQuestions {
    fn len(&self) -> usize {
        self.questions.len()
    }
    fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> {
        self.questions.iter_mut()
    }
}

impl QuestionsTrait for XSsoQuestions {
    fn new(messages: &[Message]) -> Result<Self> {
        let questions: Result<Vec<_>> = messages.iter().map(Question::try_from).collect();
        let questions = questions?;
        Ok(Self {
            pointer: questions.as_ptr(),
            questions,
            _marker: Default::default(),
        })
    }

    fn ptr(&self) -> *const *const Question {
        &self.pointer as *const *const Question
    }

    unsafe fn borrow_ptr<'a>(
        ptr: *const *const Question,
        count: usize,
    ) -> impl Iterator<Item = &'a Question> {
        slice::from_raw_parts(*ptr, count).iter()
    }
}

/// The Linux version of the pointer train to questions.
#[derive(Debug)]
#[repr(C)]
pub struct LinuxPamQuestions {
    #[allow(clippy::vec_box)] // we need to do this.
    /// The place where the questions are.
    questions: Vec<Box<Question>>,
    _marker: Immovable,
}

impl LinuxPamQuestions {
    fn len(&self) -> usize {
        self.questions.len()
    }

    fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> {
        self.questions.iter_mut().map(AsMut::as_mut)
    }
}

impl QuestionsTrait for LinuxPamQuestions {
    fn new(messages: &[Message]) -> Result<Self> {
        let questions: Result<_> = messages
            .iter()
            .map(|msg| Question::try_from(msg).map(Box::new))
            .collect();
        Ok(Self {
            questions: questions?,
            _marker: Default::default(),
        })
    }

    fn ptr(&self) -> *const *const Question {
        self.questions.as_ptr().cast()
    }

    unsafe fn borrow_ptr<'a>(
        ptr: *const *const Question,
        count: usize,
    ) -> impl Iterator<Item = &'a Question> {
        slice::from_raw_parts(ptr.cast::<&Question>(), count)
            .iter()
            .copied()
    }
}

/// The C enum values for messages shown to the user.
#[derive(Debug, PartialEq, TryFromPrimitive, IntoPrimitive)]
#[repr(u32)]
enum Style {
    /// Requests information from the user; will be masked when typing.
    PromptEchoOff = pam_ffi::PAM_PROMPT_ECHO_OFF,
    /// Requests information from the user; will not be masked.
    PromptEchoOn = pam_ffi::PAM_PROMPT_ECHO_ON,
    /// An error message.
    ErrorMsg = pam_ffi::PAM_ERROR_MSG,
    /// An informational message.
    TextInfo = pam_ffi::PAM_TEXT_INFO,
    /// Yes/No/Maybe conditionals. A Linux-PAM extension.
    #[cfg(feature = "linux-pam-extensions")]
    RadioType = pam_ffi::PAM_RADIO_TYPE,
    /// For server–client non-human interaction.
    ///
    /// NOT part of the X/Open PAM specification.
    /// A Linux-PAM extension.
    #[cfg(feature = "linux-pam-extensions")]
    BinaryPrompt = pam_ffi::PAM_BINARY_PROMPT,
}

impl Question {
    /// 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) -> Result<&str> {
        if self.data.is_null() {
            Ok("")
        } else {
            CStr::from_ptr(self.data.cast())
                .to_str()
                .map_err(|_| ErrorCode::ConversationError)
        }
    }

    /// Gets this message's data pointer as borrowed binary data.
    unsafe fn binary_data(&self) -> (&[u8], u8) {
        self.data
            .cast::<CBinaryData>()
            .as_ref()
            .map(Into::into)
            .unwrap_or_default()
    }
}

impl TryFrom<&Message<'_>> for Question {
    type Error = ErrorCode;
    fn try_from(msg: &Message) -> Result<Self> {
        let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast()));
        // We will only allocate heap data if we have a valid input.
        let (style, data): (_, *mut c_void) = match *msg {
            Message::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()),
            Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()),
            Message::Error(p) => alloc(Style::ErrorMsg, p.question()),
            Message::Info(p) => alloc(Style::TextInfo, p.question()),
            #[cfg(feature = "linux-pam-extensions")]
            Message::RadioPrompt(p) => alloc(Style::RadioType, p.question()),
            #[cfg(feature = "linux-pam-extensions")]
            Message::BinaryPrompt(p) => Ok((
                Style::BinaryPrompt,
                CBinaryData::alloc(p.question())?.cast(),
            )),
            #[cfg(not(feature = "linux-pam-extensions"))]
            Message::RadioPrompt(_) | Message::BinaryPrompt(_) => Err(ErrorCode::ConversationError),
        }?;
        Ok(Self {
            style: style.into(),
            data,
            _marker: Default::default(),
        })
    }
}

impl Drop for Question {
    fn drop(&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 {
            // This is nice-to-have. We'll try to zero out the data
            // in the Question. If it's not a supported format, we skip it.
            if let Ok(style) = Style::try_from(self.style) {
                match style {
                    #[cfg(feature = "linux-pam-extensions")]
                    Style::BinaryPrompt => {
                        if let Some(d) = self.data.cast::<CBinaryData>().as_mut() {
                            d.zero_contents()
                        }
                    }
                    #[cfg(feature = "linux-pam-extensions")]
                    Style::RadioType => memory::zero_c_string(self.data.cast()),
                    Style::TextInfo
                    | Style::ErrorMsg
                    | Style::PromptEchoOff
                    | Style::PromptEchoOn => memory::zero_c_string(self.data.cast()),
                }
            };
            memory::free(self.data);
            self.data = ptr::null_mut();
        }
    }
}

impl<'a> TryFrom<&'a Question> for OwnedMessage<'a> {
    type Error = ErrorCode;
    fn try_from(question: &'a Question) -> Result<Self> {
        let style: Style = question
            .style
            .try_into()
            .map_err(|_| ErrorCode::ConversationError)?;
        // SAFETY: In all cases below, we're creating questions based on
        // known types that we get from PAM and the inner types it should have.
        let prompt = unsafe {
            match style {
                Style::PromptEchoOff => {
                    Self::MaskedPrompt(MaskedQAndA::new(question.string_data()?))
                }
                Style::PromptEchoOn => Self::Prompt(QAndA::new(question.string_data()?)),
                Style::ErrorMsg => Self::Error(ErrorMsg::new(question.string_data()?)),
                Style::TextInfo => Self::Info(InfoMsg::new(question.string_data()?)),
                #[cfg(feature = "linux-pam-extensions")]
                Style::RadioType => Self::RadioPrompt(RadioQAndA::new(question.string_data()?)),
                #[cfg(feature = "linux-pam-extensions")]
                Style::BinaryPrompt => Self::BinaryPrompt(BinaryQAndA::new(question.binary_data())),
            }
        };
        Ok(prompt)
    }
}

#[cfg(test)]
mod tests {

    macro_rules! assert_matches {
        ($id:ident => $variant:path, $q:expr) => {
            if let $variant($id) = $id {
                assert_eq!($q, $id.question());
            } else {
                panic!("mismatched enum variant {x:?}", x = $id);
            }
        };
    }

    macro_rules! tests { ($fn_name:ident<$typ:ident>) => {
        mod $fn_name {
            use super::super::*;
            #[test]
            fn standard() {
                let interrogation = <$typ>::new(&[
                    MaskedQAndA::new("hocus pocus").message(),
                    QAndA::new("what").message(),
                    QAndA::new("who").message(),
                    InfoMsg::new("hey").message(),
                    ErrorMsg::new("gasp").message(),
                ])
                .unwrap();
                let indirect = interrogation.ptr();

                let remade = unsafe { $typ::borrow_ptr(indirect, interrogation.len()) };
                let messages: Vec<OwnedMessage> = remade
                    .map(TryInto::try_into)
                    .collect::<Result<_>>()
                    .unwrap();
                let [masked, what, who, hey, gasp] = messages.try_into().unwrap();
                assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus");
                assert_matches!(what => OwnedMessage::Prompt, "what");
                assert_matches!(who => OwnedMessage::Prompt, "who");
                assert_matches!(hey => OwnedMessage::Info, "hey");
                assert_matches!(gasp => OwnedMessage::Error, "gasp");
            }

            #[test]
            #[cfg(not(feature = "linux-pam-extensions"))]
            fn no_linux_extensions() {
                use crate::conv::{BinaryQAndA, RadioQAndA};
                <$typ>::new(&[
                    BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(),
                    RadioQAndA::new("you must choose").message(),
                ]).unwrap_err();
            }

            #[test]
            #[cfg(feature = "linux-pam-extensions")]
            fn linux_extensions() {
                let interrogation = <$typ>::new(&[
                    BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(),
                    RadioQAndA::new("you must choose").message(),
                ]).unwrap();
                let indirect = interrogation.ptr();

                let remade = unsafe { $typ::borrow_ptr(indirect, interrogation.len()) };
                let messages: Vec<OwnedMessage> = unsafe { remade }
                    .map(TryInto::try_into)
                    .collect::<Result<_>>()
                    .unwrap();
                let [bin, choose] = messages.try_into().unwrap();
                assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66));
                assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose");
            }
        }
    }}

    tests!(test_xsso<XSsoQuestions>);
    tests!(test_linux<LinuxPamQuestions>);
}