view src/conv.rs @ 87:05291b601f0a

Well and truly separate the Linux extensions. This separates the Linux extensions on the libpam side, and disables the two enums on the interface side. Users can still call the Linux extensions from non-Linux PAM impls, but they'll get a conversation error back.
author Paul Fisher <paul@pfish.zone>
date Tue, 10 Jun 2025 04:40:01 -0400
parents 2128123b9406
children
line wrap: on
line source

//! The PAM conversation and associated Stuff.

// Temporarily allowed until we get the actual conversation functions hooked up.
#![allow(dead_code)]

use crate::constants::{ErrorCode, Result};
use secure_string::SecureString;
use std::cell::Cell;
use std::fmt;
use std::result::Result as StdResult;

/// 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.
#[non_exhaustive]
pub enum Message<'a> {
    Prompt(&'a QAndA<'a>),
    MaskedPrompt(&'a MaskedQAndA<'a>),
    Error(&'a ErrorMsg<'a>),
    Info(&'a InfoMsg<'a>),
    RadioPrompt(&'a RadioQAndA<'a>),
    BinaryPrompt(&'a BinaryQAndA<'a>),
}

impl Message<'_> {
    /// Sets an error answer on this question, without having to inspect it.
    ///
    /// Use this as a default match case:
    ///
    /// ```
    /// use nonstick::conv::{Message, QAndA};
    /// use nonstick::ErrorCode;
    ///
    /// fn cant_respond(message: Message) {
    ///     match message {
    ///         Message::Info(i) => {
    ///             eprintln!("fyi, {}", i.question());
    ///             i.set_answer(Ok(()))
    ///         }
    ///         Message::Error(e) => {
    ///             eprintln!("ERROR: {}", e.question());
    ///             e.set_answer(Ok(()))
    ///         }
    ///         // We can't answer any questions.
    ///         other => other.set_error(ErrorCode::ConversationError),
    ///     }
    /// }
    pub fn set_error(&self, err: ErrorCode) {
        match *self {
            Message::Prompt(m) => m.set_answer(Err(err)),
            Message::MaskedPrompt(m) => m.set_answer(Err(err)),
            Message::Error(m) => m.set_answer(Err(err)),
            Message::Info(m) => m.set_answer(Err(err)),
            Message::RadioPrompt(m) => m.set_answer(Err(err)),
            Message::BinaryPrompt(m) => m.set_answer(Err(err)),
        }
    }
}

macro_rules! q_and_a {
    ($(#[$m:meta])* $name:ident<'a, Q=$qt:ty, A=$at:ty>, $val:path) => {
        $(#[$m])*
        pub struct $name<'a> {
            q: $qt,
            a: Cell<Result<$at>>,
        }

        $(#[$m])*
        impl<'a> $name<'a> {
            #[doc = concat!("Creates a `", stringify!($t), "` to be sent to the user.")]
            pub fn new(question: $qt) -> Self {
                Self {
                    q: question,
                    a: Cell::new(Err(ErrorCode::ConversationError)),
                }
            }

            /// Converts this Q&A into a [`Message`] for the [`Conversation`].
            pub fn message(&self) -> Message<'_> {
                $val(self)
            }

            /// The contents of the question being asked.
            ///
            /// For instance, this might say `"Username:"` to prompt the user
            /// for their name, or the text of an error message.
            pub fn question(&self) -> $qt {
                self.q
            }

            /// Sets the answer to the question.
            ///
            /// The [`Conversation`] implementation calls this to set the answer.
            /// The conversation should *always call this function*,
            /// even for Q&A messages that don't have "an answer"
            /// (like error or info messages).
            pub fn set_answer(&self, answer: Result<$at>) {
                self.a.set(answer)
            }

            /// Gets the answer to the question.
            pub fn answer(self) -> Result<$at> {
                self.a.into_inner()
            }
        }

        // shout out to stackoverflow user ballpointben for this lazy impl:
        // https://stackoverflow.com/a/78871280/39808
        $(#[$m])*
        impl fmt::Debug for $name<'_> {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> StdResult<(), fmt::Error> {
                #[derive(Debug)]
                struct $name<'a> { q: $qt }
                fmt::Debug::fmt(&$name { q: self.q }, f)
            }
        }
    };
}

q_and_a!(
    /// A Q&A that asks the user for text and does not show it while typing.
    ///
    /// In other words, a password entry prompt.
    MaskedQAndA<'a, Q=&'a str, A=SecureString>,
    Message::MaskedPrompt
);

q_and_a!(
    /// A standard Q&A prompt that asks the user for text.
    ///
    /// This is the normal "ask a person a question" prompt.
    /// When the user types, their input will be shown to them.
    /// It can be used for things like usernames.
    QAndA<'a, Q=&'a str, A=String>,
    Message::Prompt
);

q_and_a!(
    /// A Q&A for "radio button"–style data. (Linux-PAM extension)
    ///
    /// This message type is theoretically useful for "yes/no/maybe"
    /// questions, but nowhere in the documentation is it specified
    /// what the format of the answer will be, or how this should be shown.
    RadioQAndA<'a, Q=&'a str, A=String>,
    Message::RadioPrompt
);

q_and_a!(
    /// Asks for binary data. (Linux-PAM extension)
    ///
    /// This sends a binary message to the client application.
    /// It can be used to communicate with non-human logins,
    /// or to enable things like security keys.
    ///
    /// The `data_type` tag is a value that is simply passed through
    /// to the application. PAM does not define any meaning for it.
    BinaryQAndA<'a, Q=(&'a [u8], u8), A=BinaryData>,
    Message::BinaryPrompt
);

/// Owned binary data.
#[derive(Debug, Default, PartialEq)]
pub struct BinaryData {
    /// The data.
    pub data: Vec<u8>,
    /// A tag describing the type of the data, to use how you please.
    pub data_type: u8,
}

impl BinaryData {
    /// Creates a `BinaryData` with the given contents and type.
    pub fn new(data: impl Into<Vec<u8>>, data_type: u8) -> Self {
        Self {
            data: data.into(),
            data_type,
        }
    }
}

impl<IV: Into<Vec<u8>>> From<(IV, u8)> for BinaryData {
    /// Makes a new BinaryData from borrowed data.
    fn from((data, data_type): (IV, u8)) -> Self {
        Self {
            data: data.into(),
            data_type,
        }
    }
}

impl From<BinaryData> for (Vec<u8>, u8) {
    /// Easy destructuring.
    fn from(value: BinaryData) -> Self {
        (value.data, value.data_type)
    }
}

impl<'a> From<&'a BinaryData> for (&'a [u8], u8) {
    fn from(value: &'a BinaryData) -> Self {
        (&value.data, value.data_type)
    }
}

q_and_a!(
    /// A message containing information to be passed to the user.
    ///
    /// While this does not have an answer, [`Conversation`] implementations
    /// should still call [`set_answer`][`QAndA::set_answer`] to verify that
    /// the message has been displayed (or actively discarded).
    InfoMsg<'a, Q = &'a str, A = ()>,
    Message::Info
);

q_and_a!(
    /// An error message to be passed to the user.
    ///
    /// While this does not have an answer, [`Conversation`] implementations
    /// should still call [`set_answer`][`QAndA::set_answer`] to verify that
    /// the message has been displayed (or actively discarded).
    ErrorMsg<'a, Q = &'a str, A = ()>,
    Message::Error
);

/// A channel for PAM modules to request information from the user.
///
/// This trait is used by both applications and PAM modules:
///
/// - Applications implement Conversation and provide a user interface
///   to allow the user to respond to PAM questions.
/// - Modules call a Conversation implementation to request information
///   or send information to the user.
pub trait Conversation {
    /// Sends messages to the user.
    ///
    /// The returned Vec of messages always contains exactly as many entries
    /// as there were messages in the request; one corresponding to each.
    ///
    /// TODO: write detailed documentation about how to use this.
    fn communicate(&mut self, messages: &[Message]);
}

/// Turns a simple function into a [`Conversation`].
///
/// This can be used to wrap a free-floating function for use as a
/// Conversation:
///
/// ```
/// use nonstick::conv::{conversation_func, Conversation, Message};
/// mod some_library {
/// #    use nonstick::Conversation;
///     pub fn get_auth_data(conv: &mut impl Conversation) {
///         /* ... */
///     }
/// }
///
/// fn my_terminal_prompt(messages: &[Message]) {
///     // ...
/// #    todo!()
/// }
///
/// fn main() {
///     some_library::get_auth_data(&mut conversation_func(my_terminal_prompt));
/// }
/// ```
pub fn conversation_func(func: impl FnMut(&[Message])) -> impl Conversation {
    Convo(func)
}

struct Convo<C: FnMut(&[Message])>(C);

impl<C: FnMut(&[Message])> Conversation for Convo<C> {
    fn communicate(&mut self, messages: &[Message]) {
        self.0(messages)
    }
}

/// A conversation trait for asking or answering one question at a time.
///
/// An implementation of this is provided for any [`Conversation`],
/// or a PAM application can implement this trait and handle messages
/// one at a time.
///
/// For example, to use a `Conversation` as a `SimpleConversation`:
///
/// ```
/// # use nonstick::{Conversation, Result};
/// # use secure_string::SecureString;
/// // Bring this trait into scope to get `masked_prompt`, among others.
/// use nonstick::SimpleConversation;
///
/// fn ask_for_token(convo: &mut impl Conversation) -> Result<SecureString> {
///     convo.masked_prompt("enter your one-time token")
/// }
/// ```
///
/// or to use a `SimpleConversation` as a `Conversation`:
///
/// ```
/// use nonstick::{Conversation, SimpleConversation};
/// use secure_string::SecureString;
/// # use nonstick::{BinaryData, Result};
/// mod some_library {
/// #    use nonstick::Conversation;
///     pub fn get_auth_data(conv: &mut impl Conversation) { /* ... */
///     }
/// }
///
/// struct MySimpleConvo {/* ... */}
/// # impl MySimpleConvo { fn new() -> Self { Self{} } }
///
/// impl SimpleConversation for MySimpleConvo {
///     // ...
/// # fn prompt(&mut self, request: &str) -> Result<String> {
/// #     todo!()
/// # }
/// #
/// # fn masked_prompt(&mut self, request: &str) -> Result<SecureString> {
/// #     todo!()
/// # }
/// #
/// # fn error_msg(&mut self, message: &str) {
/// #     todo!()
/// # }
/// #
/// # fn info_msg(&mut self, message: &str) {
/// #     todo!()
/// # }
/// #
/// # fn radio_prompt(&mut self, request: &str) -> Result<String> {
/// #     todo!()
/// # }
/// #
/// # fn binary_prompt(&mut self, (data, data_type): (&[u8], u8)) -> Result<BinaryData> {
/// #     todo!()
/// # }
/// }
///
/// fn main() {
///     let mut simple = MySimpleConvo::new();
///     some_library::get_auth_data(&mut simple.as_conversation())
/// }
/// ```
pub trait SimpleConversation {
    /// Lets you use this simple conversation as a full [Conversation].
    ///
    /// The wrapper takes each message received in [`Conversation::communicate`]
    /// and passes them one-by-one to the appropriate method,
    /// then collects responses to return.
    fn as_conversation(&mut self) -> Demux<'_, Self>
    where
        Self: Sized,
    {
        Demux(self)
    }
    /// Prompts the user for something.
    fn prompt(&mut self, request: &str) -> Result<String>;
    /// Prompts the user for something, but hides what the user types.
    fn masked_prompt(&mut self, request: &str) -> Result<SecureString>;
    /// Alerts the user to an error.
    fn error_msg(&mut self, message: &str);
    /// Sends an informational message to the user.
    fn info_msg(&mut self, message: &str);
    /// \[Linux extension] Prompts the user for a yes/no/maybe conditional.
    ///
    /// PAM documentation doesn't define the format of the response.
    ///
    /// When called on an implementation that doesn't support radio prompts,
    /// this will return [`ErrorCode::ConversationError`].
    /// If implemented on an implementation that doesn't support radio prompts,
    /// this will never be called.
    fn radio_prompt(&mut self, request: &str) -> Result<String> {
        let _ = request;
        Err(ErrorCode::ConversationError)
    }
    /// \[Linux extension] Requests binary data from the user.
    ///
    /// When called on an implementation that doesn't support radio prompts,
    /// this will return [`ErrorCode::ConversationError`].
    /// If implemented on an implementation that doesn't support radio prompts,
    /// this will never be called.
    fn binary_prompt(&mut self, data_and_type: (&[u8], u8)) -> Result<BinaryData> {
        let _ = data_and_type;
        Err(ErrorCode::ConversationError)
    }
}

macro_rules! conv_fn {
    ($(#[$m:meta])* $fn_name:ident($($param:tt: $pt:ty),+) -> $resp_type:ty { $msg:ty }) => {
        $(#[$m])*
        fn $fn_name(&mut self, $($param: $pt),*) -> Result<$resp_type> {
            let prompt = <$msg>::new($($param),*);
            self.communicate(&[prompt.message()]);
            prompt.answer()
        }
    };
    ($(#[$m:meta])*$fn_name:ident($($param:tt: $pt:ty),+) { $msg:ty }) => {
        $(#[$m])*
        fn $fn_name(&mut self, $($param: $pt),*) {
            self.communicate(&[<$msg>::new($($param),*).message()]);
        }
    };
}

impl<C: Conversation> SimpleConversation for C {
    conv_fn!(prompt(message: &str) -> String { QAndA });
    conv_fn!(masked_prompt(message: &str) -> SecureString { MaskedQAndA } );
    conv_fn!(error_msg(message: &str) { ErrorMsg });
    conv_fn!(info_msg(message: &str) { InfoMsg });
    conv_fn!(radio_prompt(message: &str) -> String { RadioQAndA });
    conv_fn!(binary_prompt((data, data_type): (&[u8], u8)) -> BinaryData { BinaryQAndA });
}

/// A [`Conversation`] which asks the questions one at a time.
///
/// This is automatically created by [`SimpleConversation::as_conversation`].
pub struct Demux<'a, SC: SimpleConversation>(&'a mut SC);

impl<SC: SimpleConversation> Conversation for Demux<'_, SC> {
    fn communicate(&mut self, messages: &[Message]) {
        for msg in messages {
            match msg {
                Message::Prompt(prompt) => prompt.set_answer(self.0.prompt(prompt.question())),
                Message::MaskedPrompt(prompt) => {
                    prompt.set_answer(self.0.masked_prompt(prompt.question()))
                }
                Message::RadioPrompt(prompt) => {
                    prompt.set_answer(self.0.radio_prompt(prompt.question()))
                }
                Message::Info(prompt) => {
                    self.0.info_msg(prompt.question());
                    prompt.set_answer(Ok(()))
                }
                Message::Error(prompt) => {
                    self.0.error_msg(prompt.question());
                    prompt.set_answer(Ok(()))
                }
                Message::BinaryPrompt(prompt) => {
                    let q = prompt.question();
                    prompt.set_answer(self.0.binary_prompt(q))
                }
            }
        }
    }
}

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

    #[test]
    fn test_demux() {
        #[derive(Default)]
        struct DemuxTester {
            error_ran: bool,
            info_ran: bool,
        }

        impl SimpleConversation for DemuxTester {
            fn prompt(&mut self, request: &str) -> Result<String> {
                match request {
                    "what" => Ok("whatwhat".to_owned()),
                    "give_err" => Err(ErrorCode::PermissionDenied),
                    _ => panic!("unexpected prompt!"),
                }
            }
            fn masked_prompt(&mut self, request: &str) -> Result<SecureString> {
                assert_eq!("reveal", request);
                Ok(SecureString::from("my secrets"))
            }
            fn error_msg(&mut self, message: &str) {
                self.error_ran = true;
                assert_eq!("whoopsie", message);
            }
            fn info_msg(&mut self, message: &str) {
                self.info_ran = true;
                assert_eq!("did you know", message);
            }
            fn radio_prompt(&mut self, request: &str) -> Result<String> {
                assert_eq!("channel?", request);
                Ok("zero".to_owned())
            }
            fn binary_prompt(&mut self, data_and_type: (&[u8], u8)) -> Result<BinaryData> {
                assert_eq!((&[10, 9, 8][..], 66), data_and_type);
                Ok(BinaryData::new(vec![5, 5, 5], 5))
            }
        }

        let mut tester = DemuxTester::default();

        let what = QAndA::new("what");
        let pass = MaskedQAndA::new("reveal");
        let err = ErrorMsg::new("whoopsie");
        let info = InfoMsg::new("did you know");
        let has_err = QAndA::new("give_err");

        let mut conv = tester.as_conversation();

        // Basic tests.

        conv.communicate(&[
            what.message(),
            pass.message(),
            err.message(),
            info.message(),
            has_err.message(),
        ]);

        assert_eq!("whatwhat", what.answer().unwrap());
        assert_eq!(SecureString::from("my secrets"), pass.answer().unwrap());
        assert_eq!(Ok(()), err.answer());
        assert_eq!(Ok(()), info.answer());
        assert_eq!(ErrorCode::PermissionDenied, has_err.answer().unwrap_err());
        assert!(tester.error_ran);
        assert!(tester.info_ran);

        // Test the Linux extensions separately.
        {
            let mut conv = tester.as_conversation();

            let radio = RadioQAndA::new("channel?");
            let bin = BinaryQAndA::new((&[10, 9, 8], 66));
            conv.communicate(&[radio.message(), bin.message()]);

            assert_eq!("zero", radio.answer().unwrap());
            assert_eq!(BinaryData::from(([5, 5, 5], 5)), bin.answer().unwrap());
        }
    }

    fn test_mux() {
        struct MuxTester;

        impl Conversation for MuxTester {
            fn communicate(&mut self, messages: &[Message]) {
                if let [msg] = messages {
                    match *msg {
                        Message::Info(info) => {
                            assert_eq!("let me tell you", info.question());
                            info.set_answer(Ok(()))
                        }
                        Message::Error(error) => {
                            assert_eq!("oh no", error.question());
                            error.set_answer(Ok(()))
                        }
                        Message::Prompt(prompt) => prompt.set_answer(match prompt.question() {
                            "should_err" => Err(ErrorCode::PermissionDenied),
                            "question" => Ok("answer".to_owned()),
                            other => panic!("unexpected question {other:?}"),
                        }),
                        Message::MaskedPrompt(ask) => {
                            assert_eq!("password!", ask.question());
                            ask.set_answer(Ok("open sesame".into()))
                        }
                        Message::BinaryPrompt(prompt) => {
                            assert_eq!((&[1, 2, 3][..], 69), prompt.question());
                            prompt.set_answer(Ok(BinaryData::from((&[3, 2, 1], 42))))
                        }
                        Message::RadioPrompt(ask) => {
                            assert_eq!("radio?", ask.question());
                            ask.set_answer(Ok("yes".to_owned()))
                        }
                    }
                } else {
                    panic!(
                        "there should only be one message, not {len}",
                        len = messages.len()
                    )
                }
            }
        }

        let mut tester = MuxTester;

        assert_eq!("answer", tester.prompt("question").unwrap());
        assert_eq!(
            SecureString::from("open sesame"),
            tester.masked_prompt("password!").unwrap()
        );
        tester.error_msg("oh no");
        tester.info_msg("let me tell you");
        // Linux-PAM extensions. Always implemented, but separate for clarity.
        {
            assert_eq!("yes", tester.radio_prompt("radio?").unwrap());
            assert_eq!(
                BinaryData::new(vec![3, 2, 1], 42),
                tester.binary_prompt((&[1, 2, 3], 69)).unwrap(),
            )
        }
        assert_eq!(
            ErrorCode::BufferError,
            tester.prompt("should_error").unwrap_err(),
        );
        assert_eq!(
            ErrorCode::ConversationError,
            tester.masked_prompt("return_wrong_type").unwrap_err()
        )
    }
}