view src/libpam/question.rs @ 132:0b6a17f8c894 default tip

Get constant test working again with OpenPAM.
author Paul Fisher <paul@pfish.zone>
date Wed, 02 Jul 2025 02:34:29 -0400
parents 80c07e5ab22f
children
line wrap: on
line source

//! Data and types dealing with PAM messages.

#[cfg(feature = "linux-pam-ext")]
use crate::conv::{BinaryQAndA, RadioQAndA};
use crate::conv::{ErrorMsg, Exchange, InfoMsg, MaskedQAndA, QAndA};
use crate::libpam::conversation::OwnedExchange;
use crate::libpam::memory::{CBinaryData, CHeapBox, CHeapString};
use crate::ErrorCode;
use crate::Result;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use std::ffi::{c_int, c_void, CStr};

mod style_const {
    pub use libpam_sys::*;
    #[cfg(not(feature = "link"))]
    #[cfg_pam_impl(not("LinuxPam"))]
    pub const PAM_RADIO_TYPE: i32 = 897;
    #[cfg(not(feature = "link"))]
    #[cfg_pam_impl(not("LinuxPam"))]
    pub const PAM_BINARY_PROMPT: i32 = 10010101;
}

/// The C enum values for messages shown to the user.
#[derive(Debug, PartialEq, TryFromPrimitive, IntoPrimitive)]
#[repr(i32)]
enum Style {
    /// Requests information from the user; will be masked when typing.
    PromptEchoOff = style_const::PAM_PROMPT_ECHO_OFF,
    /// Requests information from the user; will not be masked.
    PromptEchoOn = style_const::PAM_PROMPT_ECHO_ON,
    /// An error message.
    ErrorMsg = style_const::PAM_ERROR_MSG,
    /// An informational message.
    TextInfo = style_const::PAM_TEXT_INFO,
    /// Yes/No/Maybe conditionals. A Linux-PAM extension.
    #[cfg(feature = "linux-pam-ext")]
    RadioType = style_const::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-ext")]
    BinaryPrompt = style_const::PAM_BINARY_PROMPT,
}

/// A question sent by PAM or a module to an application.
///
/// PAM refers to this as a "message", but we call it a question
/// to avoid confusion with [`Message`](crate::conv::Exchange).
///
/// This question, and its internal data, is owned by its creator
/// (either the module or PAM itself).
#[repr(C)]
#[derive(Debug)]
pub struct Question {
    /// The style of message to request.
    pub style: c_int,
    /// A description of the data requested.
    ///
    /// For most requests, this will be an owned [`CStr`],
    /// but for requests with style `PAM_BINARY_PROMPT`,
    /// this will be `CBinaryData` (a Linux-PAM extension).
    pub data: Option<CHeapBox<c_void>>,
}

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> {
        match self.data.as_ref() {
            None => Ok(""),
            Some(data) => CStr::from_ptr(CHeapBox::as_ptr(data).cast().as_ptr())
                .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
            .as_ref()
            .map(|data| CBinaryData::data(CHeapBox::as_ptr(data).cast()))
            .unwrap_or_default()
    }
}

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

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) {
                let _ = match style {
                    #[cfg(feature = "linux-pam-ext")]
                    Style::BinaryPrompt => self
                        .data
                        .as_ref()
                        .map(|p| CBinaryData::zero_contents(CHeapBox::as_ptr(p).cast())),
                    #[cfg(feature = "linux-pam-ext")]
                    Style::RadioType => self
                        .data
                        .as_ref()
                        .map(|p| CHeapString::zero(CHeapBox::as_ptr(p).cast())),
                    Style::TextInfo
                    | Style::ErrorMsg
                    | Style::PromptEchoOff
                    | Style::PromptEchoOn => self
                        .data
                        .as_ref()
                        .map(|p| CHeapString::zero(CHeapBox::as_ptr(p).cast())),
                };
            };
        }
    }
}

impl<'a> TryFrom<&'a Question> for OwnedExchange<'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-ext")]
                Style::RadioType => Self::RadioPrompt(RadioQAndA::new(question.string_data()?)),
                #[cfg(feature = "linux-pam-ext")]
                Style::BinaryPrompt => Self::BinaryPrompt(BinaryQAndA::new(question.binary_data())),
            }
        };
        Ok(prompt)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    macro_rules! assert_matches {
        (($variant:path, $q:expr), $input:expr) => {
            let input = $input;
            let exc = input.exchange();
            if let $variant(msg) = exc {
                assert_eq!($q, msg.question());
            } else {
                panic!(
                    "want enum variant {v}, got {exc:?}",
                    v = stringify!($variant)
                );
            }
        };
    }

    // TODO: Test TryFrom<Exchange> for Question/OwnedQuestion.

    #[test]
    fn standard() {
        assert_matches!(
            (Exchange::MaskedPrompt, "hocus pocus"),
            MaskedQAndA::new("hocus pocus")
        );
        assert_matches!((Exchange::Prompt, "what"), QAndA::new("what"));
        assert_matches!((Exchange::Prompt, "who"), QAndA::new("who"));
        assert_matches!((Exchange::Info, "hey"), InfoMsg::new("hey"));
        assert_matches!((Exchange::Error, "gasp"), ErrorMsg::new("gasp"));
    }

    #[test]
    #[cfg(feature = "linux-pam-ext")]
    fn linux_extensions() {
        assert_matches!(
            (Exchange::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66)),
            BinaryQAndA::new((&[5, 4, 3, 2, 1], 66))
        );
        assert_matches!(
            (Exchange::RadioPrompt, "you must choose"),
            RadioQAndA::new("you must choose")
        );
    }
}