changeset 77:351bdc13005e

Update the libpam module to work with the new structure.
author Paul Fisher <paul@pfish.zone>
date Sun, 08 Jun 2025 01:03:46 -0400
parents e58d24849e82
children 002adfb98c5c
files Cargo.toml src/constants.rs src/conv.rs src/libpam/conversation.rs src/libpam/handle.rs src/libpam/memory.rs src/libpam/message.rs src/libpam/module.rs src/libpam/response.rs
diffstat 9 files changed, 527 insertions(+), 479 deletions(-) [+]
line wrap: on
line diff
--- a/Cargo.toml	Sat Jun 07 18:55:27 2025 -0400
+++ b/Cargo.toml	Sun Jun 08 01:03:46 2025 -0400
@@ -10,6 +10,7 @@
 edition = "2021"
 
 [features]
+default = ["link"]
 # Enable this to actually link against your system's PAM library.
 link = []
 
--- a/src/constants.rs	Sat Jun 07 18:55:27 2025 -0400
+++ b/src/constants.rs	Sun Jun 08 01:03:46 2025 -0400
@@ -174,9 +174,8 @@
 }
 
 /// Error returned when attempting to coerce an invalid C integer into an enum.
-#[derive(thiserror::Error)]
+#[derive(Debug, PartialEq, thiserror::Error)]
 #[error("{0} is not a valid {type}", type = any::type_name::<T>())]
-#[derive(Debug, PartialEq)]
 pub struct InvalidEnum<T>(c_int, PhantomData<T>);
 
 impl<T> From<InvalidEnum<T>> for c_int {
--- a/src/conv.rs	Sat Jun 07 18:55:27 2025 -0400
+++ b/src/conv.rs	Sun Jun 08 01:03:46 2025 -0400
@@ -14,12 +14,12 @@
 /// that will be presented to the user.
 #[non_exhaustive]
 pub enum Message<'a> {
-    MaskedPrompt(&'a MaskedPrompt<'a>),
-    Prompt(&'a Prompt<'a>),
-    RadioPrompt(&'a RadioPrompt<'a>),
-    BinaryPrompt(&'a BinaryPrompt<'a>),
-    InfoMsg(&'a InfoMsg<'a>),
-    ErrorMsg(&'a ErrorMsg<'a>),
+    Prompt(&'a QAndA<'a>),
+    MaskedPrompt(&'a MaskedQAndA<'a>),
+    RadioPrompt(&'a RadioQAndA<'a>),
+    BinaryPrompt(&'a BinaryQAndA<'a>),
+    Error(&'a ErrorMsg<'a>),
+    Info(&'a InfoMsg<'a>),
 }
 
 impl Message<'_> {
@@ -33,11 +33,11 @@
     ///
     /// fn cant_respond(message: Message) {
     ///     match message {
-    ///         Message::InfoMsg(i) => {
+    ///         Message::Info(i) => {
     ///             eprintln!("fyi, {}", i.question());
     ///             i.set_answer(Ok(()))
     ///         }
-    ///         Message::ErrorMsg(e) => {
+    ///         Message::Error(e) => {
     ///             eprintln!("ERROR: {}", e.question());
     ///             e.set_answer(Ok(()))
     ///         }
@@ -46,55 +46,19 @@
     ///     }
     /// }
     pub fn set_error(&self, err: ErrorCode) {
-        match self {
+        match *self {
+            Message::Prompt(m) => m.set_answer(Err(err)),
             Message::MaskedPrompt(m) => m.set_answer(Err(err)),
-            Message::Prompt(m) => m.set_answer(Err(err)),
             Message::RadioPrompt(m) => m.set_answer(Err(err)),
             Message::BinaryPrompt(m) => m.set_answer(Err(err)),
-            Message::InfoMsg(m) => m.set_answer(Err(err)),
-            Message::ErrorMsg(m) => m.set_answer(Err(err)),
+            Message::Error(m) => m.set_answer(Err(err)),
+            Message::Info(m) => m.set_answer(Err(err)),
         }
     }
 }
 
-/// A question-and-answer pair that can be communicated in a [`Conversation`].
-///
-/// The asking side creates a `QAndA`, then converts it to a [`Message`]
-/// and sends it via a [`Conversation`]. The Conversation then retrieves
-/// the answer to the question (if needed) and sets the response.
-/// Once control returns to the asker, the asker gets the answer from this
-/// `QAndA` and uses it however it wants.
-///
-/// For a more detailed explanation of how this works,
-/// see [`Conversation::communicate`].
-pub trait QAndA<'a> {
-    /// The type of the content of the question.
-    type Question: Copy;
-    /// The type of the answer to the question.
-    type Answer;
-
-    /// Converts this Q-and-A pair into a [`Message`] for the [`Conversation`].
-    fn message(&self) -> Message;
-
-    /// The contents of the question being asked.
-    ///
-    /// For instance, this might say `"Username:"` to prompt the user
-    /// for their name.
-    fn question(&self) -> Self::Question;
-
-    /// Sets the answer to the question.
-    ///
-    /// The [`Conversation`] implementation calls this to set the answer.
-    /// The conversation should *always call this function*, even for messages
-    /// that don't have "an answer" (like error or info messages).
-    fn set_answer(&self, answer: Result<Self::Answer>);
-
-    /// Gets the answer to the question.
-    fn answer(self) -> Result<Self::Answer>;
-}
-
 macro_rules! q_and_a {
-    ($name:ident<'a, Q=$qt:ty, A=$at:ty>, $($doc:literal)*) => {
+    ($name:ident<'a, Q=$qt:ty, A=$at:ty>, $val:path, $($doc:literal)*) => {
         $(
             #[doc = $doc]
         )*
@@ -103,25 +67,34 @@
             a: Cell<Result<$at>>,
         }
 
-        impl<'a> QAndA<'a> for $name<'a> {
-            type Question = $qt;
-            type Answer = $at;
+        impl<'a> $name<'a> {
+            /// Converts this Q&A into a [`Message`] for the [`Conversation`].
+            pub fn message(&self) -> Message {
+                $val(self)
+            }
 
-            fn question(&self) -> Self::Question {
+            /// 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
             }
 
-            fn set_answer(&self, answer: Result<Self::Answer>) {
+            /// 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)
             }
 
-            fn answer(self) -> Result<Self::Answer> {
+            /// Gets the answer to the question.
+            pub fn answer(self) -> Result<$at> {
                 self.a.into_inner()
             }
-
-            fn message(&self) -> Message {
-                Message::$name(self)
-            }
         }
     };
 }
@@ -130,7 +103,7 @@
     ($t:ident) => {
         impl<'a> $t<'a> {
             #[doc = concat!("Creates a `", stringify!($t), "` to be sent to the user.")]
-            fn ask(question: &'a str) -> Self {
+            pub fn new(question: &'a str) -> Self {
                 Self {
                     q: question,
                     a: Cell::new(Err(ErrorCode::ConversationError)),
@@ -141,35 +114,39 @@
 }
 
 q_and_a!(
-    MaskedPrompt<'a, Q=&'a str, A=SecureString>,
-    "Asks the user for data and does not echo it back while being entered."
+    MaskedQAndA<'a, Q=&'a str, A=SecureString>,
+    Message::MaskedPrompt,
+    "A Q&A that asks the user for text and does not show it while typing."
     ""
     "In other words, a password entry prompt."
 );
-ask!(MaskedPrompt);
+ask!(MaskedQAndA);
 
 q_and_a!(
-    Prompt<'a, Q=&'a str, A=String>,
-    "Asks the user for data."
+    QAndA<'a, Q=&'a str, A=String>,
+    Message::Prompt,
+    "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."
 );
-ask!(Prompt);
+ask!(QAndA);
 
 q_and_a!(
-    RadioPrompt<'a, Q=&'a str, A=String>,
-    "Asks the user for \"radio button\"–style data. (Linux-PAM extension)"
+    RadioQAndA<'a, Q=&'a str, A=String>,
+    Message::RadioPrompt,
+    "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."
 );
-ask!(RadioPrompt);
+ask!(RadioQAndA);
 
 q_and_a!(
-    BinaryPrompt<'a, Q=BinaryQuestion<'a>, A=BinaryData>,
+    BinaryQAndA<'a, Q=BorrowedBinaryData<'a>, A=BinaryData>,
+    Message::BinaryPrompt,
     "Asks for binary data. (Linux-PAM extension)"
     ""
     "This sends a binary message to the client application."
@@ -179,29 +156,44 @@
     "The `data_type` tag is a value that is simply passed through"
     "to the application. PAM does not define any meaning for it."
 );
-impl<'a> BinaryPrompt<'a> {
+impl<'a> BinaryQAndA<'a> {
     /// Creates a prompt for the given binary data.
     ///
     /// The `data_type` is a tag you can use for communication between
     /// the module and the application. Its meaning is undefined by PAM.
-    fn ask(data: &'a [u8], data_type: u8) -> Self {
+    pub fn new(data: &'a [u8], data_type: u8) -> Self {
+        BorrowedBinaryData { data, data_type }.into()
+    }
+}
+
+impl<'a> From<BorrowedBinaryData<'a>> for BinaryQAndA<'a> {
+    fn from(q: BorrowedBinaryData<'a>) -> Self {
         Self {
-            q: BinaryQuestion { data, data_type },
+            q,
             a: Cell::new(Err(ErrorCode::ConversationError)),
         }
     }
 }
 
-/// The contents of a question requesting binary data.
-///
-/// A borrowed version of [`BinaryData`].
-#[derive(Copy, Clone, Debug)]
-pub struct BinaryQuestion<'a> {
+impl<'a> From<&'a BinaryData> for BinaryQAndA<'a> {
+    fn from(src: &'a BinaryData) -> Self {
+        BorrowedBinaryData::from(src).into()
+    }
+}
+
+/// A version of [`BinaryData`] where the `data` is borrowed.
+#[derive(Copy, Clone, Debug, Default)]
+pub struct BorrowedBinaryData<'a> {
     data: &'a [u8],
     data_type: u8,
 }
 
-impl BinaryQuestion<'_> {
+impl<'a> BorrowedBinaryData<'a> {
+    /// Creates a new BinaryQuestion as a view over the given data.
+    pub fn new(data: &'a [u8], data_type: u8) -> Self {
+        Self { data, data_type }
+    }
+
     /// Gets the data of this question.
     pub fn data(&self) -> &[u8] {
         self.data
@@ -213,11 +205,17 @@
     }
 }
 
+impl<'a> From<&'a BinaryData> for BorrowedBinaryData<'a> {
+    fn from(value: &'a BinaryData) -> Self {
+        Self::new(&value.data, value.data_type)
+    }
+}
+
 /// Owned binary data.
 ///
-/// For borrowed data, see [`BinaryQuestion`].
+/// For borrowed data, see [`BorrowedBinaryData`].
 /// You can take ownership of the stored data with `.into::<Vec<u8>>()`.
-#[derive(Debug, PartialEq)]
+#[derive(Debug, Default, PartialEq)]
 pub struct BinaryData {
     data: Vec<u8>,
     data_type: u8,
@@ -238,6 +236,15 @@
     }
 }
 
+impl<'a> From<BorrowedBinaryData<'a>> for BinaryData {
+    fn from(value: BorrowedBinaryData) -> Self {
+        Self {
+            data: value.data.to_vec(),
+            data_type: value.data_type,
+        }
+    }
+}
+
 impl From<BinaryData> for Vec<u8> {
     /// Takes ownership of the data stored herein.
     fn from(value: BinaryData) -> Self {
@@ -247,6 +254,7 @@
 
 q_and_a!(
     InfoMsg<'a, Q = &'a str, A = ()>,
+    Message::Info,
     "A message containing information to be passed to the user."
     ""
     "While this does not have an answer, [`Conversation`] implementations"
@@ -255,7 +263,7 @@
 );
 impl<'a> InfoMsg<'a> {
     /// Creates an informational message to send to the user.
-    fn new(message: &'a str) -> Self {
+    pub fn new(message: &'a str) -> Self {
         Self {
             q: message,
             a: Cell::new(Err(ErrorCode::ConversationError)),
@@ -265,6 +273,7 @@
 
 q_and_a!(
     ErrorMsg<'a, Q = &'a str, A = ()>,
+    Message::Error,
     "An error message to be passed to the user."
     ""
     "While this does not have an answer, [`Conversation`] implementations"
@@ -274,7 +283,7 @@
 );
 impl<'a> ErrorMsg<'a> {
     /// Creates an error message to send to the user.
-    fn new(message: &'a str) -> Self {
+    pub fn new(message: &'a str) -> Self {
         Self {
             q: message,
             a: Cell::new(Err(ErrorCode::ConversationError)),
@@ -427,27 +436,27 @@
 }
 
 macro_rules! conv_fn {
-    ($fn_name:ident($($param:ident: $pt:ty),+) -> $resp_type:ty { $ask:path }) => {
+    ($fn_name:ident($($param:ident: $pt:ty),+) -> $resp_type:ty { $msg:ty }) => {
         fn $fn_name(&mut self, $($param: $pt),*) -> Result<$resp_type> {
-            let prompt = $ask($($param),*);
+            let prompt = <$msg>::new($($param),*);
             self.communicate(&[prompt.message()]);
             prompt.answer()
         }
     };
-    ($fn_name:ident($($param:ident: $pt:ty),+) { $ask:path }) => {
+    ($fn_name:ident($($param:ident: $pt:ty),+) { $msg:ty }) => {
         fn $fn_name(&mut self, $($param: $pt),*) {
-            self.communicate(&[$ask($($param),*).message()]);
+            self.communicate(&[<$msg>::new($($param),*).message()]);
         }
     };
 }
 
 impl<C: Conversation> SimpleConversation for C {
-    conv_fn!(prompt(message: &str) -> String { Prompt::ask });
-    conv_fn!(masked_prompt(message: &str) -> SecureString { MaskedPrompt::ask });
-    conv_fn!(radio_prompt(message: &str) -> String { RadioPrompt::ask });
-    conv_fn!(error_msg(message: &str) { ErrorMsg::new });
-    conv_fn!(info_msg(message: &str) { InfoMsg::new });
-    conv_fn!(binary_prompt(data: &[u8], data_type: u8) -> BinaryData { BinaryPrompt::ask });
+    conv_fn!(prompt(message: &str) -> String { QAndA });
+    conv_fn!(masked_prompt(message: &str) -> SecureString { MaskedQAndA } );
+    conv_fn!(radio_prompt(message: &str) -> String { RadioQAndA });
+    conv_fn!(error_msg(message: &str) { ErrorMsg });
+    conv_fn!(info_msg(message: &str) { InfoMsg });
+    conv_fn!(binary_prompt(data: &[u8], data_type: u8) -> BinaryData { BinaryQAndA });
 }
 
 /// A [`Conversation`] which asks the questions one at a time.
@@ -466,11 +475,11 @@
                 Message::RadioPrompt(prompt) => {
                     prompt.set_answer(self.0.radio_prompt(prompt.question()))
                 }
-                Message::InfoMsg(prompt) => {
+                Message::Info(prompt) => {
                     self.0.info_msg(prompt.question());
                     prompt.set_answer(Ok(()))
                 }
-                Message::ErrorMsg(prompt) => {
+                Message::Error(prompt) => {
                     self.0.error_msg(prompt.question());
                     prompt.set_answer(Ok(()))
                 }
@@ -486,8 +495,8 @@
 #[cfg(test)]
 mod tests {
     use super::{
-        BinaryPrompt, Conversation, ErrorMsg, InfoMsg, MaskedPrompt, Message, Prompt, QAndA,
-        RadioPrompt, Result, SecureString, SimpleConversation,
+        BinaryQAndA, Conversation, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA,
+        RadioQAndA, Result, SecureString, SimpleConversation,
     };
     use crate::constants::ErrorCode;
     use crate::BinaryData;
@@ -533,11 +542,11 @@
 
         let mut tester = DemuxTester::default();
 
-        let what = Prompt::ask("what");
-        let pass = MaskedPrompt::ask("reveal");
+        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 = Prompt::ask("give_err");
+        let has_err = QAndA::new("give_err");
 
         let mut conv = tester.as_conversation();
 
@@ -563,8 +572,8 @@
 
         let mut conv = tester.as_conversation();
 
-        let radio = RadioPrompt::ask("channel?");
-        let bin = BinaryPrompt::ask(&[10, 9, 8], 66);
+        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());
@@ -578,11 +587,11 @@
             fn communicate(&mut self, messages: &[Message]) {
                 if let [msg] = messages {
                     match *msg {
-                        Message::InfoMsg(info) => {
+                        Message::Info(info) => {
                             assert_eq!("let me tell you", info.question());
                             info.set_answer(Ok(()))
                         }
-                        Message::ErrorMsg(error) => {
+                        Message::Error(error) => {
                             assert_eq!("oh no", error.question());
                             error.set_answer(Ok(()))
                         }
--- a/src/libpam/conversation.rs	Sat Jun 07 18:55:27 2025 -0400
+++ b/src/libpam/conversation.rs	Sun Jun 08 01:03:46 2025 -0400
@@ -1,14 +1,15 @@
-use crate::constants::Result;
-use crate::conv::{Conversation, Message, Response};
+use crate::conv::{
+    BinaryQAndA, Conversation, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA,
+    RadioQAndA,
+};
 use crate::libpam::memory::Immovable;
-use crate::libpam::message::{MessageIndirector, OwnedMessages};
-use crate::libpam::response::{OwnedResponses, RawBinaryResponse, RawResponse, RawTextResponse};
+use crate::libpam::message::{Indirect, Questions};
+use crate::libpam::response::{Answer, Answers, BinaryAnswer, TextAnswer};
 use crate::ErrorCode;
-use crate::ErrorCode::ConversationError;
+use crate::Result;
 use std::ffi::c_int;
 use std::iter;
 use std::marker::PhantomData;
-use std::result::Result as StdResult;
 
 /// An opaque structure that is passed through PAM in a conversation.
 #[repr(C)]
@@ -23,15 +24,15 @@
 /// - `messages` is a pointer to the messages being sent to the user.
 ///   For details about its structure, see the documentation of
 ///   [`OwnedMessages`](super::OwnedMessages).
-/// - `responses` is a pointer to an array of [`RawResponse`]s,
+/// - `responses` is a pointer to an array of [`Answer`]s,
 ///   which PAM sets in response to a module's request.
 ///   This is an array of structs, not an array of pointers to a struct.
 ///   There should always be exactly as many `responses` as `num_msg`.
 /// - `appdata` is the `appdata` field of the [`LibPamConversation`] we were passed.
 pub type ConversationCallback = unsafe extern "C" fn(
     num_msg: c_int,
-    messages: *const MessageIndirector,
-    responses: *mut *mut RawResponse,
+    messages: *const Indirect,
+    responses: *mut *mut Answer,
     appdata: *mut AppData,
 ) -> c_int;
 
@@ -59,104 +60,116 @@
 
     unsafe extern "C" fn wrapper_callback<C: Conversation>(
         count: c_int,
-        messages: *const MessageIndirector,
-        responses: *mut *mut RawResponse,
+        questions: *const Indirect,
+        answers: *mut *mut Answer,
         me: *mut AppData,
     ) -> c_int {
-        let call = || {
+        let internal = || {
+            // Collect all our pointers
             let conv = me
                 .cast::<C>()
                 .as_mut()
                 .ok_or(ErrorCode::ConversationError)?;
-            let indir = messages.as_ref().ok_or(ErrorCode::ConversationError)?;
-            let response_ptr = responses.as_mut().ok_or(ErrorCode::ConversationError)?;
-            let messages: Vec<Message> = indir
+            let indirect = questions.as_ref().ok_or(ErrorCode::ConversationError)?;
+            let answers_ptr = answers.as_mut().ok_or(ErrorCode::ConversationError)?;
+
+            // Build our owned list of Q&As from the questions we've been asked
+            let messages: Vec<OwnedMessage> = indirect
                 .iter(count as usize)
-                .map(Message::try_from)
-                .collect::<StdResult<_, _>>()
+                .map(OwnedMessage::try_from)
+                .collect::<Result<_>>()
                 .map_err(|_| ErrorCode::ConversationError)?;
-            let responses = conv.communicate(&messages)?;
-            let owned =
-                OwnedResponses::build(&responses).map_err(|_| ErrorCode::ConversationError)?;
-            *response_ptr = owned.into_ptr();
+            // Borrow all those Q&As and ask them
+            let borrowed: Vec<Message> = messages.iter().map(Into::into).collect();
+            conv.communicate(&borrowed);
+
+            // Send our answers back
+            let owned = Answers::build(messages).map_err(|_| ErrorCode::ConversationError)?;
+            *answers_ptr = owned.into_ptr();
             Ok(())
         };
-        ErrorCode::result_to_c(call())
+        ErrorCode::result_to_c(internal())
     }
 }
 
 impl Conversation for LibPamConversation<'_> {
-    fn communicate(&mut self, messages: &[Message]) -> Result<Vec<Response>> {
-        let mut msgs_to_send = OwnedMessages::alloc(messages.len());
-        for (dst, src) in iter::zip(msgs_to_send.iter_mut(), messages.iter()) {
-            dst.set(*src).map_err(|_| ErrorCode::ConversationError)?
+    fn communicate(&mut self, messages: &[Message]) {
+        let internal = || {
+            let mut msgs_to_send = Questions::alloc(messages.len());
+            for (dst, src) in iter::zip(msgs_to_send.iter_mut(), messages.iter()) {
+                dst.fill(src).map_err(|_| ErrorCode::ConversationError)?
+            }
+            let mut response_pointer = std::ptr::null_mut();
+            // SAFETY: We're calling into PAM with valid everything.
+            let result = unsafe {
+                (self.callback)(
+                    messages.len() as c_int,
+                    msgs_to_send.indirect(),
+                    &mut response_pointer,
+                    self.appdata,
+                )
+            };
+            ErrorCode::result_from(result)?;
+            // SAFETY: This is a pointer we just got back from PAM.
+            // We have to trust that the responses from PAM match up
+            // with the questions we sent.
+            unsafe {
+                let mut owned_responses = Answers::from_c_heap(response_pointer, messages.len());
+                for (msg, response) in iter::zip(messages, owned_responses.iter_mut()) {
+                    convert(msg, response);
+                }
+            };
+            Ok(())
+        };
+        if let Err(e) = internal() {
+            messages.iter().for_each(|m| m.set_error(e))
         }
-        let mut response_pointer = std::ptr::null_mut();
-        // SAFETY: We're calling into PAM with valid everything.
-        let result = unsafe {
-            (self.callback)(
-                messages.len() as c_int,
-                msgs_to_send.indirector(),
-                &mut response_pointer,
-                self.appdata,
-            )
-        };
-        ErrorCode::result_from(result)?;
-        // SAFETY: This is a pointer we just got back from PAM.
-        let owned_responses =
-            unsafe { OwnedResponses::from_c_heap(response_pointer, messages.len()) };
-        convert_responses(messages, owned_responses)
     }
 }
 
-fn convert_responses(
-    messages: &[Message],
-    mut raw_responses: OwnedResponses,
-) -> Result<Vec<Response>> {
-    let pairs = iter::zip(messages.iter(), raw_responses.iter_mut());
-    // We first collect into a Vec of Results so that we always process
-    // every single entry, which may involve freeing it.
-    let responses: Vec<_> = pairs.map(convert).collect();
-    // Only then do we return the first error, if present.
-    responses.into_iter().collect()
+/// Like [`Message`], but this time we own the contents.
+pub enum OwnedMessage<'a> {
+    MaskedPrompt(MaskedQAndA<'a>),
+    Prompt(QAndA<'a>),
+    RadioPrompt(RadioQAndA<'a>),
+    BinaryPrompt(BinaryQAndA<'a>),
+    Info(InfoMsg<'a>),
+    Error(ErrorMsg<'a>),
+}
+
+impl<'a> From<&'a OwnedMessage<'a>> for Message<'a> {
+    fn from(src: &'a OwnedMessage) -> Self {
+        match src {
+            OwnedMessage::MaskedPrompt(m) => Message::MaskedPrompt(m),
+            OwnedMessage::Prompt(m) => Message::Prompt(m),
+            OwnedMessage::RadioPrompt(m) => Message::RadioPrompt(m),
+            OwnedMessage::BinaryPrompt(m) => Message::BinaryPrompt(m),
+            OwnedMessage::Info(m) => Message::Info(m),
+            OwnedMessage::Error(m) => Message::Error(m),
+        }
+    }
 }
 
-/// Converts one message-to-raw pair to a Response.
-fn convert((sent, received): (&Message, &mut RawResponse)) -> Result<Response> {
-    Ok(match sent {
-        Message::MaskedPrompt(_) => {
-            // SAFETY: Since this is a response to a text message,
-            // we know it is text.
-            let text_resp = unsafe { RawTextResponse::upcast(received) };
-            let ret = Response::MaskedText(
-                text_resp
-                    .contents()
-                    .map_err(|_| ErrorCode::ConversationError)?
-                    .into(),
-            );
-            // SAFETY: We're the only ones using this,
-            // and we haven't freed it.
-            text_resp.free_contents();
-            ret
+/// Fills in the answer of the Message with the given response.
+///
+/// # Safety
+///
+/// You are responsible for ensuring that the src-dst pair matches.
+unsafe fn convert(msg: &Message, resp: &mut Answer) {
+    macro_rules! fill_text {
+    ($dst:ident, $src:ident) => {{let text_resp = unsafe {TextAnswer::upcast($src)};
+    $dst.set_answer(text_resp.contents().map(Into::into));}}
+}
+    match *msg {
+        Message::MaskedPrompt(qa) => fill_text!(qa, resp),
+        Message::Prompt(qa) => fill_text!(qa, resp),
+        Message::RadioPrompt(qa) => fill_text!(qa, resp),
+        Message::Error(m) => m.set_answer(Ok(())),
+        Message::Info(m) => m.set_answer(Ok(())),
+        Message::BinaryPrompt(qa) => {
+            let bin_resp = unsafe { BinaryAnswer::upcast(resp) };
+            qa.set_answer(Ok(bin_resp.data().into()));
+            bin_resp.zero_contents()
         }
-        Message::Prompt(_) | Message::RadioPrompt(_) => {
-            // SAFETY: Since this is a response to a text message,
-            // we know it is text.
-            let text_resp = unsafe { RawTextResponse::upcast(received) };
-            let ret = Response::Text(text_resp.contents().map_err(|_| ConversationError)?.into());
-            // SAFETY: We're the only ones using this,
-            // and we haven't freed it.
-            text_resp.free_contents();
-            ret
-        }
-        Message::ErrorMsg(_) | Message::InfoMsg(_) => Response::NoResponse,
-        Message::BinaryPrompt { .. } => {
-            let bin_resp = unsafe { RawBinaryResponse::upcast(received) };
-            let ret = Response::Binary(bin_resp.to_owned());
-            // SAFETY: We're the only ones using this,
-            // and we haven't freed it.
-            bin_resp.free_contents();
-            ret
-        }
-    })
+    }
 }
--- a/src/libpam/handle.rs	Sat Jun 07 18:55:27 2025 -0400
+++ b/src/libpam/handle.rs	Sun Jun 08 01:03:46 2025 -0400
@@ -4,7 +4,7 @@
 use crate::handle::{PamApplicationOnly, PamModuleOnly, PamShared};
 use crate::libpam::memory;
 use crate::libpam::memory::Immovable;
-use crate::{Conversation, Response};
+use crate::Conversation;
 use num_derive::FromPrimitive;
 use num_traits::FromPrimitive;
 use std::ffi::{c_char, c_int};
@@ -144,8 +144,15 @@
 }
 
 impl Conversation for LibPamHandle {
-    fn communicate(&mut self, messages: &[Message]) -> Result<Vec<Response>> {
-        self.conversation_item()?.communicate(messages)
+    fn communicate(&mut self, messages: &[Message]) {
+        match self.conversation_item() {
+            Ok(conv) => conv.communicate(messages),
+            Err(e) => {
+                for msg in messages {
+                    msg.set_error(e)
+                }
+            }
+        }
     }
 }
 
--- a/src/libpam/memory.rs	Sat Jun 07 18:55:27 2025 -0400
+++ b/src/libpam/memory.rs	Sun Jun 08 01:03:46 2025 -0400
@@ -1,10 +1,10 @@
 //! Things for dealing with memory.
 
-use crate::ErrorCode;
+use crate::conv::BorrowedBinaryData;
 use crate::Result;
+use crate::{BinaryData, ErrorCode};
 use std::ffi::{c_char, c_void, CStr, CString};
 use std::marker::{PhantomData, PhantomPinned};
-use std::result::Result as StdResult;
 use std::{ptr, slice};
 
 /// Makes whatever it's in not [`Send`], [`Sync`], or [`Unpin`].
@@ -66,10 +66,10 @@
 ///
 /// - it allocates data on the C heap with [`libc::malloc`].
 /// - it doesn't take ownership of the data passed in.
-pub fn malloc_str(text: impl AsRef<str>) -> StdResult<*mut c_char, NulError> {
-    let data = text.as_ref().as_bytes();
-    if let Some(nul) = data.iter().position(|x| *x == 0) {
-        return Err(NulError(nul));
+pub fn malloc_str(text: &str) -> Result<*mut c_char> {
+    let data = text.as_bytes();
+    if data.contains(&0) {
+        return Err(ErrorCode::ConversationError);
     }
     unsafe {
         let data_alloc = libc::calloc(data.len() + 1, 1);
@@ -110,11 +110,9 @@
 
 impl CBinaryData {
     /// Copies the given data to a new BinaryData on the heap.
-    pub fn alloc(source: &[u8], data_type: u8) -> StdResult<*mut CBinaryData, TooBigError> {
-        let buffer_size = u32::try_from(source.len() + 5).map_err(|_| TooBigError {
-            max: (u32::MAX - 5) as usize,
-            actual: source.len(),
-        })?;
+    pub fn alloc(source: &[u8], data_type: u8) -> Result<*mut CBinaryData> {
+        let buffer_size =
+            u32::try_from(source.len() + 5).map_err(|_| ErrorCode::ConversationError)?;
         // SAFETY: We're only allocating here.
         let data = unsafe {
             let dest_buffer: *mut CBinaryData = libc::malloc(buffer_size as usize).cast();
@@ -132,13 +130,6 @@
         u32::from_be_bytes(self.total_length).saturating_sub(5) as usize
     }
 
-    pub fn contents(&self) -> &[u8] {
-        unsafe { slice::from_raw_parts(self.data.as_ptr(), self.length()) }
-    }
-    pub fn data_type(&self) -> u8 {
-        self.data_type
-    }
-
     /// Clears this data and frees it.
     pub unsafe fn zero_contents(&mut self) {
         let contents = slice::from_raw_parts_mut(self.data.as_mut_ptr(), self.length());
@@ -150,16 +141,19 @@
     }
 }
 
-#[derive(Debug, thiserror::Error)]
-#[error("null byte within input at byte {0}")]
-pub struct NulError(pub usize);
+impl<'a> From<&'a CBinaryData> for BorrowedBinaryData<'a> {
+    fn from(value: &'a CBinaryData) -> Self {
+        BorrowedBinaryData::new(
+            unsafe { slice::from_raw_parts(value.data.as_ptr(), value.length()) },
+            value.data_type,
+        )
+    }
+}
 
-/// Returned when trying to fit too much data into a binary message.
-#[derive(Debug, thiserror::Error)]
-#[error("cannot create a message of {actual} bytes; maximum is {max}")]
-pub struct TooBigError {
-    pub actual: usize,
-    pub max: usize,
+impl From<Option<&'_ CBinaryData>> for BinaryData {
+    fn from(value: Option<&CBinaryData>) -> Self {
+        value.map(BorrowedBinaryData::from).map(Into::into).unwrap_or_default()
+    }
 }
 
 #[cfg(test)]
--- a/src/libpam/message.rs	Sat Jun 07 18:55:27 2025 -0400
+++ b/src/libpam/message.rs	Sun Jun 08 01:03:46 2025 -0400
@@ -1,24 +1,17 @@
 //! Data and types dealing with PAM messages.
 
 use crate::constants::InvalidEnum;
-use crate::conv::Message;
+use crate::libpam::conversation::OwnedMessage;
 use crate::libpam::memory;
-use crate::libpam::memory::{CBinaryData, Immovable, NulError, TooBigError};
+use crate::libpam::memory::{CBinaryData, Immovable};
+use crate::ErrorCode;
+use crate::Result;
 use num_derive::FromPrimitive;
 use num_traits::FromPrimitive;
 use std::ffi::{c_int, c_void, CStr};
 use std::result::Result as StdResult;
-use std::str::Utf8Error;
 use std::{ptr, slice};
-
-#[derive(Debug, thiserror::Error)]
-#[error("error creating PAM message: {0}")]
-pub enum ConversionError {
-    InvalidEnum(#[from] InvalidEnum<Style>),
-    Utf8Error(#[from] Utf8Error),
-    NulError(#[from] NulError),
-    TooBigError(#[from] TooBigError),
-}
+use crate::conv::{BorrowedBinaryData, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, RadioQAndA};
 
 /// The C enum values for messages shown to the user.
 #[derive(Debug, PartialEq, FromPrimitive)]
@@ -53,11 +46,15 @@
     }
 }
 
-/// A message sent by PAM or a module to an application.
-/// This message, and its internal data, is owned by the creator
+/// 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`].
+///
+/// This question, and its internal data, is owned by its creator
 /// (either the module or PAM itself).
 #[repr(C)]
-pub struct RawMessage {
+pub struct Question {
     /// The style of message to request.
     style: c_int,
     /// A description of the data requested.
@@ -69,8 +66,8 @@
     _marker: Immovable,
 }
 
-impl RawMessage {
-    pub fn set(&mut self, msg: Message) -> StdResult<(), ConversionError> {
+impl Question {
+    pub fn fill(&mut self, msg: &Message) -> Result<()> {
         let (style, data) = copy_to_heap(msg)?;
         self.clear();
         // SAFETY: We allocated this ourselves or were given it by PAM.
@@ -86,14 +83,25 @@
     /// # Safety
     ///
     /// It's up to you to pass this only on types with a string value.
-    unsafe fn string_data(&self) -> StdResult<&str, Utf8Error> {
+    unsafe fn string_data(&self) -> Result<&str> {
         if self.data.is_null() {
             Ok("")
         } else {
-            CStr::from_ptr(self.data.cast()).to_str()
+            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) -> BorrowedBinaryData {
+        self.data
+            .cast::<CBinaryData>()
+            .as_ref()
+            .map(Into::into)
+            .unwrap_or_default()
+    }
+
     /// Zeroes out the data stored here.
     fn clear(&mut self) {
         // SAFETY: We either created this data or we got it from PAM.
@@ -119,23 +127,61 @@
     }
 }
 
-/// Copies the contents of this message to the C heap.
-fn copy_to_heap(msg: Message) -> StdResult<(Style, *mut c_void), ConversionError> {
-    let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast()));
-    match msg {
-        Message::MaskedPrompt(text) => alloc(Style::PromptEchoOff, text),
-        Message::Prompt(text) => alloc(Style::PromptEchoOn, text),
-        Message::RadioPrompt(text) => alloc(Style::RadioType, text),
-        Message::ErrorMsg(text) => alloc(Style::ErrorMsg, text),
-        Message::InfoMsg(text) => alloc(Style::TextInfo, text),
-        Message::BinaryPrompt { data, data_type } => Ok((
-            Style::BinaryPrompt,
-            (CBinaryData::alloc(data, data_type)?).cast(),
-        )),
+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 matching the
+        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()?)),
+                Style::RadioType => Self::RadioPrompt(RadioQAndA::new(question.string_data()?)),
+                Style::BinaryPrompt => Self::BinaryPrompt(question.binary_data().into()),
+            }
+        };
+        Ok(prompt)
     }
 }
 
-/// Abstraction of a list-of-messages to be sent in a PAM conversation.
+/// Copies the contents of this message to the C heap.
+fn copy_to_heap(msg: &Message) -> Result<(Style, *mut c_void)> {
+    let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast()));
+    match *msg {
+        Message::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()),
+        Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()),
+        Message::RadioPrompt(p) => alloc(Style::RadioType, p.question()),
+        Message::Error(p) => alloc(Style::ErrorMsg, p.question()),
+        Message::Info(p) => alloc(Style::TextInfo, p.question()),
+        Message::BinaryPrompt(p) => {
+            let q = p.question();
+            Ok((
+                Style::BinaryPrompt,
+                CBinaryData::alloc(q.data(), q.data_type())?.cast(),
+            ))
+        }
+    }
+}
+
+/// 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, `messages`
 /// is treated as a pointer-to-pointers, like `int argc, char **argv`.
@@ -170,39 +216,39 @@
 /// ```
 ///
 /// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.***
-pub struct OwnedMessages {
-    /// An indirection to the messages themselves, stored on the C heap.
-    indirect: *mut MessageIndirector,
-    /// The number of messages in the list.
+pub struct Questions {
+    /// An indirection to the questions themselves, stored on the C heap.
+    indirect: *mut Indirect,
+    /// The number of questions.
     count: usize,
 }
 
-impl OwnedMessages {
-    /// Allocates data to store messages on the C heap.
+impl Questions {
+    /// Allocates data to store questions on the C heap.
     pub fn alloc(count: usize) -> Self {
         Self {
-            indirect: MessageIndirector::alloc(count),
+            indirect: Indirect::alloc(count),
             count,
         }
     }
 
     /// The pointer to the thing with the actual list.
-    pub fn indirector(&self) -> *const MessageIndirector {
+    pub fn indirect(&self) -> *const Indirect {
         self.indirect
     }
 
-    pub fn iter(&self) -> impl Iterator<Item = &RawMessage> {
+    pub fn iter(&self) -> impl Iterator<Item = &Question> {
         // SAFETY: we're iterating over an amount we know.
         unsafe { (*self.indirect).iter(self.count) }
     }
 
-    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut RawMessage> {
+    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> {
         // SAFETY: we're iterating over an amount we know.
         unsafe { (*self.indirect).iter_mut(self.count) }
     }
 }
 
-impl Drop for OwnedMessages {
+impl Drop for Questions {
     fn drop(&mut self) {
         // SAFETY: We are valid and have a valid pointer.
         // Once we're done, everything will be safe.
@@ -221,23 +267,22 @@
 /// This is kept separate to provide a place where we can separate
 /// the pointer-to-pointer-to-list from pointer-to-list-of-pointers.
 #[repr(transparent)]
-pub struct MessageIndirector {
-    base: [*mut RawMessage; 0],
+pub struct Indirect {
+    base: [*mut Question; 0],
     _marker: Immovable,
 }
 
-impl MessageIndirector {
+impl Indirect {
     /// Allocates memory for this indirector and all its members.
     fn alloc(count: usize) -> *mut Self {
         // SAFETY: We're only allocating, and when we're done,
         // everything will be in a known-good state.
         unsafe {
-            let me_ptr: *mut MessageIndirector =
-                libc::calloc(count, size_of::<*mut RawMessage>()).cast();
+            let me_ptr: *mut Indirect = libc::calloc(count, size_of::<*mut Question>()).cast();
             let me = &mut *me_ptr;
             let ptr_list = slice::from_raw_parts_mut(me.base.as_mut_ptr(), count);
             for entry in ptr_list {
-                *entry = libc::calloc(1, size_of::<RawMessage>()).cast();
+                *entry = libc::calloc(1, size_of::<Question>()).cast();
             }
             me
         }
@@ -248,7 +293,7 @@
     /// # Safety
     ///
     /// You have to provide the right count.
-    pub unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &RawMessage> {
+    pub unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> {
         (0..count).map(|idx| &**self.base.as_ptr().add(idx))
     }
 
@@ -257,7 +302,7 @@
     /// # Safety
     ///
     /// You have to provide the right count.
-    pub unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut RawMessage> {
+    pub unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question> {
         (0..count).map(|idx| &mut **self.base.as_mut_ptr().add(idx))
     }
 
@@ -278,57 +323,26 @@
     }
 }
 
-impl<'a> TryFrom<&'a RawMessage> for Message<'a> {
-    type Error = ConversionError;
-
-    /// Retrieves the data stored in this message.
-    fn try_from(input: &RawMessage) -> StdResult<Message, ConversionError> {
-        let style: Style = input.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(input.string_data()?),
-                Style::PromptEchoOn => Message::Prompt(input.string_data()?),
-                Style::TextInfo => Message::InfoMsg(input.string_data()?),
-                Style::ErrorMsg => Message::ErrorMsg(input.string_data()?),
-                Style::RadioType => Message::ErrorMsg(input.string_data()?),
-                Style::BinaryPrompt => input.data.cast::<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)
-    }
-}
-
 #[cfg(test)]
 mod tests {
-    use crate::conv::Message;
-    use crate::libpam::message::OwnedMessages;
+    use super::{MaskedQAndA, Questions};
+    use crate::conv::{BinaryQAndA, QAndA};
 
     #[test]
     fn test_owned_messages() {
-        let mut tons_of_messages = OwnedMessages::alloc(10);
+        let mut tons_of_messages = Questions::alloc(10);
         let mut msgs: Vec<_> = tons_of_messages.iter_mut().collect();
         assert!(msgs.get(10).is_none());
         let last_msg = &mut msgs[9];
-        last_msg.set(Message::MaskedPrompt("hocus pocus")).unwrap();
+        last_msg
+            .fill(&MaskedQAndA::new("hocus pocus").message())
+            .unwrap();
         let another_msg = &mut msgs[0];
         another_msg
-            .set(Message::BinaryPrompt {
-                data: &[5, 4, 3, 2, 1],
-                data_type: 99,
-            })
+            .fill(&BinaryQAndA::new(&[5, 4, 3, 2, 1], 66).message())
             .unwrap();
         let overwrite = &mut msgs[3];
-        overwrite.set(Message::Prompt("what")).unwrap();
-        overwrite.set(Message::Prompt("who")).unwrap();
+        overwrite.fill(&QAndA::new("what").message()).unwrap();
+        overwrite.fill(&QAndA::new("who").message()).unwrap();
     }
 }
--- a/src/libpam/module.rs	Sat Jun 07 18:55:27 2025 -0400
+++ b/src/libpam/module.rs	Sun Jun 08 01:03:46 2025 -0400
@@ -1,5 +1,3 @@
-use std::ffi::CStr;
-
 /// Generates the dynamic library entry points for a [PamModule] implementation.
 ///
 /// Calling `pam_hooks!(SomeType)` on a type that implements [PamModule] will
@@ -10,8 +8,8 @@
 ///
 /// Here is full example of a PAM module that would authenticate and authorize everybody:
 ///
-/// ```
-/// use nonstick::{Flags, OwnedLibPamHandle, PamModule, PamHandleModule, Result as PamResult, pam_hooks};
+/// ```no_run
+/// use nonstick::{Flags, SimpleConversation, OwnedLibPamHandle, PamModule, PamHandleModule, Result as PamResult, pam_hooks};
 /// use std::ffi::CStr;
 /// # fn main() {}
 ///
@@ -21,12 +19,15 @@
 /// impl<T: PamHandleModule> PamModule<T> for MyPamModule {
 ///     fn authenticate(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
 ///         let password = handle.get_authtok(Some("what's your password?"))?;
-///         handle.info_msg(fmt!("If you say your password is {password:?}, who am I to disagree?"));
+///         let response = format!("If you say your password is {password:?}, who am I to disagree?");
+///         handle.info_msg(&response);
+///         Ok(())
 ///     }
 ///
 ///     fn account_management(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
 ///         let username = handle.get_user(None)?;
-///         handle.info_msg(fmt!("Hello {username}! I trust you unconditionally."))
+///         let response = format!("Hello {username}! I trust you unconditionally.");
+///         handle.info_msg(&response);
 ///         Ok(())
 ///     }
 /// }
--- a/src/libpam/response.rs	Sat Jun 07 18:55:27 2025 -0400
+++ b/src/libpam/response.rs	Sun Jun 08 01:03:46 2025 -0400
@@ -1,55 +1,59 @@
-//! Types used when dealing with PAM conversations.
+//! Types used to communicate data from the application to the module.
 
-use crate::conv::BinaryData;
+use crate::conv::BorrowedBinaryData;
+use crate::libpam::conversation::OwnedMessage;
 use crate::libpam::memory;
-use crate::libpam::memory::{CBinaryData, Immovable, NulError, TooBigError};
-use crate::Response;
+use crate::libpam::memory::{CBinaryData, Immovable};
+use crate::{ErrorCode, Result};
 use std::ffi::{c_int, c_void, CStr};
 use std::ops::{Deref, DerefMut};
-use std::result::Result as StdResult;
-use std::str::Utf8Error;
 use std::{iter, mem, ptr, slice};
 
 #[repr(transparent)]
 #[derive(Debug)]
-pub struct RawTextResponse(RawResponse);
+pub struct TextAnswer(Answer);
 
-impl RawTextResponse {
-    /// Interprets the provided `RawResponse` as a text response.
+impl TextAnswer {
+    /// Interprets the provided `Answer` as a text answer.
     ///
     /// # Safety
     ///
-    /// It's up to you to provide a response that is a `RawTextResponse`.
-    pub unsafe fn upcast(from: &mut RawResponse) -> &mut Self {
+    /// It's up to you to provide an answer that is a `TextAnswer`.
+    pub unsafe fn upcast(from: &mut Answer) -> &mut Self {
         // SAFETY: We're provided a valid reference.
-        &mut *(from as *mut RawResponse).cast::<Self>()
+        &mut *(from as *mut Answer).cast::<Self>()
     }
 
-    /// Fills in the provided `RawResponse` with the given text.
-    ///
-    /// You are responsible for calling [`free`](Self::free_contents)
-    /// on the pointer you get back when you're done with it.
-    pub fn fill(dest: &mut RawResponse, text: impl AsRef<str>) -> StdResult<&mut Self, NulError> {
-        dest.data = memory::malloc_str(text)?.cast();
-        // SAFETY: We just filled this in so we know it's a text response.
-        Ok(unsafe { Self::upcast(dest) })
+    /// Converts the `Answer` to a `TextAnswer` with the given text.
+    fn fill(dest: &mut Answer, text: &str) -> Result<()> {
+        let allocated = memory::malloc_str(text)?;
+        dest.free_contents();
+        dest.data = allocated.cast();
+        Ok(())
     }
 
-    /// Gets the string stored in this response.
-    pub fn contents(&self) -> StdResult<&str, Utf8Error> {
+    /// Gets the string stored in this answer.
+    pub fn contents(&self) -> Result<&str> {
         if self.0.data.is_null() {
             Ok("")
         } else {
-            // SAFETY: This data is either passed from PAM (so we are forced to
-            // trust it) or was created by us in TextResponseInner::alloc.
+            // SAFETY: This data is either passed from PAM (so we are forced
+            // to trust it) or was created by us in TextAnswerInner::alloc.
             // In either case, it's going to be a valid null-terminated string.
-            unsafe { CStr::from_ptr(self.0.data.cast()) }.to_str()
+            unsafe { CStr::from_ptr(self.0.data.cast()) }
+                .to_str()
+                .map_err(|_| ErrorCode::ConversationError)
         }
     }
 
-    /// Releases memory owned by this response.
+    /// Zeroes out the answer data, frees it, and points our data to `null`.
+    ///
+    /// When this `TextAnswer` is part of an [`Answers`],
+    /// this is optional (since that will perform the `free`),
+    /// but it will clear potentially sensitive data.
     pub fn free_contents(&mut self) {
-        // SAFETY: We know we own this data.
+        // SAFETY: We own this data and know it's valid.
+        // If it's null, this is a no-op.
         // After we're done, it will be null.
         unsafe {
             memory::zero_c_string(self.0.data);
@@ -59,65 +63,51 @@
     }
 }
 
-/// A [`RawResponse`] with [`CBinaryData`] in it.
+/// A [`Answer`] with [`CBinaryData`] in it.
 #[repr(transparent)]
 #[derive(Debug)]
-pub struct RawBinaryResponse(RawResponse);
+pub struct BinaryAnswer(Answer);
 
-impl RawBinaryResponse {
-    /// Interprets the provided `RawResponse` as a binary response.
+impl BinaryAnswer {
+    /// Interprets the provided [`Answer`] as a binary answer.
     ///
     /// # Safety
     ///
-    /// It's up to you to provide a response that is a `RawBinaryResponse`.
-    pub unsafe fn upcast(from: &mut RawResponse) -> &mut Self {
+    /// It's up to you to provide an answer that is a `BinaryAnswer`.
+    pub unsafe fn upcast(from: &mut Answer) -> &mut Self {
         // SAFETY: We're provided a valid reference.
-        &mut *(from as *mut RawResponse).cast::<Self>()
+        &mut *(from as *mut Answer).cast::<Self>()
     }
 
-    /// Fills in a `RawResponse` with the provided binary data.
+    /// Fills in a [`Answer`] with the provided binary data.
     ///
     /// The `data_type` is a tag you can use for whatever.
     /// It is passed through PAM unchanged.
     ///
-    /// The referenced data is copied to the C heap. We do not take ownership.
-    /// You are responsible for calling [`free`](Self::free_contents)
-    /// on the pointer you get back when you're done with it.
-    pub fn fill<'a>(
-        dest: &'a mut RawResponse,
-        data: &[u8],
-        data_type: u8,
-    ) -> StdResult<&'a mut Self, TooBigError> {
-        dest.data = CBinaryData::alloc(data, data_type)?.cast();
-        // SAFETY: We just filled this in, so we know it's binary.
-        Ok(unsafe { Self::upcast(dest) })
+    /// The referenced data is copied to the C heap.
+    /// We do not take ownership of the original data.
+    pub fn fill(dest: &mut Answer, data: BorrowedBinaryData) -> Result<()> {
+        let allocated = CBinaryData::alloc(data.data(), data.data_type())?;
+        dest.free_contents();
+        dest.data = allocated.cast();
+        Ok(())
     }
 
-    /// Gets the binary data in this response.
-    pub fn data(&self) -> &[u8] {
-        self.contents().map(CBinaryData::contents).unwrap_or(&[])
-    }
-
-    /// Gets the `data_type` tag that was embedded with the message.
-    pub fn data_type(&self) -> u8 {
-        self.contents().map(CBinaryData::data_type).unwrap_or(0)
-    }
-
-    fn contents(&self) -> Option<&CBinaryData> {
-        // SAFETY: This was either something we got from PAM (in which case
-        // we trust it), or something that was created with
-        // BinaryResponseInner::alloc. In both cases, it points to valid data.
+    /// Gets the binary data in this answer.
+    pub fn data(&self) -> Option<&CBinaryData> {
+        // SAFETY: We either got this data from PAM or allocated it ourselves.
+        // Either way, we trust that it is either valid data or null.
         unsafe { self.0.data.cast::<CBinaryData>().as_ref() }
     }
 
-    pub fn to_owned(&self) -> BinaryData {
-        BinaryData::new(self.data().into(), self.data_type())
-    }
-
-    /// Releases memory owned by this response.
-    pub fn free_contents(&mut self) {
+    /// Zeroes out the answer data, frees it, and points our data to `null`.
+    ///
+    /// When this `TextAnswer` is part of an [`Answers`],
+    /// this is optional (since that will perform the `free`),
+    /// but it will clear potentially sensitive data.
+    pub fn zero_contents(&mut self) {
         // SAFETY: We know that our data pointer is either valid or null.
-        // Once we're done, it's null and the response is safe.
+        // Once we're done, it's null and the answer is safe.
         unsafe {
             let data_ref = self.0.data.cast::<CBinaryData>().as_mut();
             if let Some(d) = data_ref {
@@ -129,15 +119,15 @@
     }
 }
 
-/// Generic version of response data.
+/// Generic version of answer data.
 ///
-/// This has the same structure as [`RawBinaryResponse`]
-/// and [`RawTextResponse`].
+/// This has the same structure as [`BinaryAnswer`]
+/// and [`TextAnswer`].
 #[repr(C)]
 #[derive(Debug)]
-pub struct RawResponse {
-    /// Pointer to the data returned in a response.
-    /// For most responses, this will be a [`CStr`], but for responses to
+pub struct Answer {
+    /// Pointer to the data returned in an answer.
+    /// For most answers, this will be a [`CStr`], but for answers to
     /// [`MessageStyle::BinaryPrompt`]s, this will be [`CBinaryData`]
     /// (a Linux-PAM extension).
     data: *mut c_void,
@@ -146,94 +136,95 @@
     _marker: Immovable,
 }
 
-/// A contiguous block of responses.
+impl Answer {
+    /// Frees the contents of this answer.
+    ///
+    /// After this is done, this answer's `data` will be `null`,
+    /// which is a valid (empty) state.
+    fn free_contents(&mut self) {
+        // SAFETY: We have either an owned valid pointer, or null.
+        // We can free our owned pointer, and `free(null)` is a no-op.
+        unsafe {
+            libc::free(self.data);
+            self.data = ptr::null_mut();
+        }
+    }
+}
+
+/// An owned, contiguous block of [`Answer`]s.
 #[derive(Debug)]
-pub struct OwnedResponses {
-    base: *mut RawResponse,
+pub struct Answers {
+    base: *mut Answer,
     count: usize,
 }
 
-impl OwnedResponses {
-    /// Allocates an owned list of responses on the C heap.
+impl Answers {
+    /// Allocates an owned list of answers on the C heap.
     fn alloc(count: usize) -> Self {
-        OwnedResponses {
+        Answers {
             // SAFETY: We are doing allocation here.
-            base: unsafe { libc::calloc(count, size_of::<RawResponse>()) }.cast(),
+            base: unsafe { libc::calloc(count, size_of::<Answer>()) }.cast(),
             count,
         }
     }
 
-    pub fn build(value: &[Response]) -> StdResult<Self, FillError> {
-        let mut outputs = OwnedResponses::alloc(value.len());
-        // If we fail in here after allocating OwnedResponses,
-        // we still free all memory, even though we don't zero it first.
-        // This is an acceptable level of risk.
-        for (input, output) in iter::zip(value.iter(), outputs.iter_mut()) {
+    pub fn build(value: Vec<OwnedMessage>) -> Result<Self> {
+        let mut outputs = Answers::alloc(value.len());
+        // Even if we fail during this process, we still end up freeing
+        // all allocated answer memory.
+        for (input, output) in iter::zip(value, outputs.iter_mut()) {
             match input {
-                Response::NoResponse => {
-                    RawTextResponse::fill(output, "")?;
-                }
-                Response::Text(data) => {
-                    RawTextResponse::fill(output, data)?;
-                }
-                Response::MaskedText(data) => {
-                    RawTextResponse::fill(output, data.unsecure())?;
-                }
-                Response::Binary(data) => {
-                    RawBinaryResponse::fill(output, data.data(), data.data_type())?;
-                }
+                OwnedMessage::MaskedPrompt(p) => TextAnswer::fill(output, p.answer()?.unsecure())?,
+                OwnedMessage::Prompt(p) => TextAnswer::fill(output, &(p.answer()?))?,
+                OwnedMessage::BinaryPrompt(p) => BinaryAnswer::fill(output, (&p.answer()?).into())?,
+                OwnedMessage::Error(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?,
+                OwnedMessage::Info(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?,
+                OwnedMessage::RadioPrompt(p) => TextAnswer::fill(output, &(p.answer()?))?,
             }
         }
         Ok(outputs)
     }
 
-    /// Converts this into a `*RawResponse` for passing to PAM.
+    /// Converts this into a `*Answer` for passing to PAM.
     ///
     /// The pointer "owns" its own data (i.e., this will not be dropped).
-    pub fn into_ptr(self) -> *mut RawResponse {
+    pub fn into_ptr(self) -> *mut Answer {
         let ret = self.base;
         mem::forget(self);
         ret
     }
 
-    /// Takes ownership of a list of responses allocated on the C heap.
+    /// Takes ownership of a list of answers allocated on the C heap.
     ///
     /// # Safety
     ///
     /// It's up to you to make sure you pass a valid pointer.
-    pub unsafe fn from_c_heap(base: *mut RawResponse, count: usize) -> Self {
-        OwnedResponses { base, count }
+    pub unsafe fn from_c_heap(base: *mut Answer, count: usize) -> Self {
+        Answers { base, count }
     }
 }
 
-#[derive(Debug, thiserror::Error)]
-#[error("error converting responses: {0}")]
-pub enum FillError {
-    NulError(#[from] NulError),
-    TooBigError(#[from] TooBigError),
-}
-
-impl Deref for OwnedResponses {
-    type Target = [RawResponse];
+impl Deref for Answers {
+    type Target = [Answer];
     fn deref(&self) -> &Self::Target {
         // SAFETY: This is the memory we manage ourselves.
         unsafe { slice::from_raw_parts(self.base, self.count) }
     }
 }
 
-impl DerefMut for OwnedResponses {
+impl DerefMut for Answers {
     fn deref_mut(&mut self) -> &mut Self::Target {
         // SAFETY: This is the memory we manage ourselves.
         unsafe { slice::from_raw_parts_mut(self.base, self.count) }
     }
 }
 
-impl Drop for OwnedResponses {
+impl Drop for Answers {
     fn drop(&mut self) {
         // SAFETY: We allocated this ourselves, or it was provided to us by PAM.
         unsafe {
-            for resp in self.iter_mut() {
-                libc::free(resp.data)
+            for answer in self.iter_mut() {
+                answer.free_contents()
             }
             libc::free(self.base.cast())
         }
@@ -242,76 +233,95 @@
 
 #[cfg(test)]
 mod tests {
-    use super::{BinaryData, OwnedResponses, RawBinaryResponse, RawTextResponse, Response};
+    use super::{Answers, BinaryAnswer, TextAnswer, BorrowedBinaryData};
+    use crate::BinaryData;
+    use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, MaskedQAndA, QAndA, RadioQAndA};
+    use crate::libpam::conversation::OwnedMessage;
 
     #[test]
     fn test_round_trip() {
-        let responses = [
-            Response::Binary(BinaryData::new(vec![1, 2, 3], 99)),
-            Response::Text("whats going on".to_owned()),
-            Response::MaskedText("well then".into()),
-            Response::NoResponse,
-            Response::Text("bogus".to_owned()),
+        let binary_msg = {
+            let qa = BinaryQAndA::new(&[], 0);
+            qa.set_answer(Ok(BinaryData::new(vec![1, 2, 3], 99)));
+            OwnedMessage::BinaryPrompt(qa)
+        };
+
+        macro_rules! answered {
+        ($typ:ty, $msg:path, $data:expr) => {
+            {let qa = <$typ>::new("");
+            qa.set_answer(Ok($data)); $msg(qa)}
+        }
+    }
+
+
+        let answers = vec![
+            binary_msg,
+            answered!(QAndA, OwnedMessage::Prompt, "whats going on".to_owned()),
+            answered!(MaskedQAndA, OwnedMessage::MaskedPrompt, "well then".into()),
+            answered!(ErrorMsg, OwnedMessage::Error, ()),
+            answered!(InfoMsg, OwnedMessage::Info, ()),
+            answered!(RadioQAndA, OwnedMessage::RadioPrompt, "beep boop".to_owned()),
         ];
-        let sent = OwnedResponses::build(&responses).unwrap();
-        let heap_resps = sent.into_ptr();
-        let mut received = unsafe { OwnedResponses::from_c_heap(heap_resps, 5) };
+        let n = answers.len();
+        let sent = Answers::build(answers).unwrap();
+        let heap_answers = sent.into_ptr();
+        let mut received = unsafe { Answers::from_c_heap(heap_answers, n) };
 
         let assert_text = |want, raw| {
-            let up = unsafe { RawTextResponse::upcast(raw) };
+            let up = unsafe { TextAnswer::upcast(raw) };
             assert_eq!(want, up.contents().unwrap());
             up.free_contents();
             assert_eq!("", up.contents().unwrap());
         };
         let assert_bin = |want_data: &[u8], want_type, raw| {
-            let up = unsafe { RawBinaryResponse::upcast(raw) };
-            assert_eq!(want_data, up.data());
-            assert_eq!(want_type, up.data_type());
-            up.free_contents();
-            let empty: [u8; 0] = [];
-            assert_eq!(&empty, up.data());
-            assert_eq!(0, up.data_type());
+            let up = unsafe { BinaryAnswer::upcast(raw) };
+            assert_eq!(BinaryData::new(want_data.into(), want_type), up.data().into());
+            up.zero_contents();
+            assert_eq!(BinaryData::default(), up.data().into());
         };
-        if let [zero, one, two, three, four] = &mut received[..] {
+        if let [zero, one, two, three, four, five] = &mut received[..] {
             assert_bin(&[1, 2, 3], 99, zero);
             assert_text("whats going on", one);
             assert_text("well then", two);
             assert_text("", three);
-            assert_text("bogus", four);
+            assert_text("", four);
+            assert_text("beep boop", five);
         } else {
-            panic!("wrong size!")
+            panic!("received wrong size {len}!", len = received.len())
         }
     }
 
     #[test]
-    fn test_text_response() {
-        let mut responses = OwnedResponses::alloc(2);
-        let text = RawTextResponse::fill(&mut responses[0], "hello").unwrap();
-        let data = text.contents().expect("valid");
+    fn test_text_answer() {
+        let mut answers = Answers::alloc(2);
+        let zeroth = &mut answers[0];
+        TextAnswer::fill(zeroth, "hello").unwrap();
+        let zeroth_text = unsafe { TextAnswer::upcast(zeroth) };
+        let data = zeroth_text.contents().expect("valid");
         assert_eq!("hello", data);
-        text.free_contents();
-        text.free_contents();
-        RawTextResponse::fill(&mut responses[1], "hell\0").expect_err("should error; contains nul");
+        zeroth_text.free_contents();
+        zeroth_text.free_contents();
+        TextAnswer::fill(&mut answers[1], "hell\0").expect_err("should error; contains nul");
     }
 
     #[test]
-    fn test_binary_response() {
-        let mut responses = OwnedResponses::alloc(1);
-        let real_data = [1, 2, 3, 4, 5, 6, 7, 8];
-        let resp = RawBinaryResponse::fill(&mut responses[0], &real_data, 7)
-            .expect("alloc should succeed");
-        let data = resp.data();
-        assert_eq!(&real_data, data);
-        assert_eq!(7, resp.data_type());
-        resp.free_contents();
-        resp.free_contents();
+    fn test_binary_answer() {
+        let mut answers = Answers::alloc(1);
+        let real_data = BinaryData::new(vec![1, 2, 3, 4, 5, 6, 7, 8], 9);
+        let answer = &mut answers[0];
+        BinaryAnswer::fill(answer, (&real_data).into()).expect("alloc should succeed");
+        let bin_answer = unsafe { BinaryAnswer::upcast(answer) };
+        assert_eq!(real_data, bin_answer.data().into());
+        answer.free_contents();
+        answer.free_contents();
     }
 
     #[test]
     #[ignore]
-    fn test_binary_response_too_big() {
+    fn test_binary_answer_too_big() {
         let big_data: Vec<u8> = vec![0xFFu8; 10_000_000_000];
-        let mut responses = OwnedResponses::alloc(1);
-        RawBinaryResponse::fill(&mut responses[0], &big_data, 0).expect_err("this is too big!");
+        let mut answers = Answers::alloc(1);
+        BinaryAnswer::fill(&mut answers[0], BorrowedBinaryData::new(&big_data, 100))
+            .expect_err("this is too big!");
     }
 }