# HG changeset patch # User Paul Fisher # Date 1749109298 14400 # Node ID ac6881304c7841a7395b1836c400d1a2c0f13319 # Parent 47eb242a4f8815be3698353b2126b7b33fbf51cc Do conversations, along with way too much stuff. This implements conversations, along with all the memory management brouhaha that goes along with it. The conversation now lives directly on the handle rather than being a thing you have to get from it and then call manually. It Turns Out this makes things a lot easier! I guess we reorganized things again. For the last time. For real. I promise. This all passes ASAN, so it seems Pretty Good! diff -r 47eb242a4f88 -r ac6881304c78 src/conv.rs --- a/src/conv.rs Wed Jun 04 03:53:36 2025 -0400 +++ b/src/conv.rs Thu Jun 05 03:41:38 2025 -0400 @@ -4,8 +4,7 @@ #![allow(dead_code)] use crate::constants::Result; -use crate::pam_ffi::LibPamConversation; -use crate::pam_ffi::Message; +use crate::ErrorCode; use secure_string::SecureString; // TODO: In most cases, we should be passing around references to strings // or binary data. Right now we don't because that turns type inference and @@ -15,6 +14,47 @@ // associated types in the various Conversation traits to avoid copying // when unnecessary. +/// The types of message and request that can be sent to a user. +/// +/// The data within each enum value is the prompt (or other information) +/// that will be presented to the user. +#[derive(Clone, Copy, Debug)] +pub enum Message<'a> { + /// Requests information from the user; will be masked when typing. + /// + /// Response: [`MaskedText`](Response::MaskedText) + MaskedPrompt(&'a str), + /// Requests information from the user; will not be masked. + /// + /// Response: [`Text`](Response::Text) + Prompt(&'a str), + /// "Yes/No/Maybe conditionals" (a Linux-PAM extension). + /// + /// Response: [`Text`](Response::Text) + /// (Linux-PAM documentation doesn't define its contents.) + RadioPrompt(&'a str), + /// Raises an error message to the user. + /// + /// Response: [`NoResponse`](Response::NoResponse) + Error(&'a str), + /// Sends an informational message to the user. + /// + /// Response: [`NoResponse`](Response::NoResponse) + Info(&'a str), + /// Requests binary data from the client (a Linux-PAM extension). + /// + /// This is used for non-human or non-keyboard prompts (security key?). + /// NOT part of the X/Open PAM specification. + /// + /// Response: [`Binary`](Response::Binary) + BinaryPrompt { + /// Some binary data. + data: &'a [u8], + /// A "type" that you can use for signalling. Has no strict definition in PAM. + data_type: u8, + }, +} + /// The responses that PAM will return from a request. #[derive(Debug, PartialEq, derive_more::From)] pub enum Response { @@ -46,6 +86,15 @@ Binary(BinaryData), } +/// The function type for a conversation. +/// +/// A macro to save typing `FnMut(&[Message]) -> Result>`. +#[macro_export] +macro_rules! conv_type { + () => {FnMut(&[Message]) -> Result>}; + (impl) => { impl FnMut(&[Message]) -> Result> } +} + /// A channel for PAM modules to request information from the user. /// /// This trait is used by both applications and PAM modules: @@ -59,7 +108,102 @@ /// /// The returned Vec of messages always contains exactly as many entries /// as there were messages in the request; one corresponding to each. - fn send(&mut self, messages: &[Message]) -> Result>; + fn converse(&mut self, messages: &[Message]) -> Result>; +} + +fn conversation_func(func: conv_type!(impl)) -> impl Conversation { + Convo(func) +} + +struct Convo Result>>(C); + +impl Result>> Conversation for Convo { + fn converse(&mut self, messages: &[Message]) -> Result> { + self.0(messages) + } +} + +/// Provides methods to make it easier to send exactly one message. +/// +/// This is primarily used by PAM modules, so that a module that only needs +/// one piece of information at a time doesn't have a ton of boilerplate. +/// You may also find it useful for testing PAM application libraries. +/// +/// ``` +/// # use nonstick::{PamHandleModule, Conversation, Result}; +/// # fn _do_test(mut pam_handle: impl PamHandleModule) -> Result<()> { +/// use nonstick::ConversationMux; +/// +/// let token = pam_handle.masked_prompt("enter your one-time token")?; +/// # Ok(()) +/// # } +pub trait ConversationMux { + /// Prompts the user for something. + fn prompt(&mut self, request: &str) -> Result; + /// Prompts the user for something, but hides what the user types. + fn masked_prompt(&mut self, request: &str) -> Result; + /// Prompts the user for a yes/no/maybe conditional (a Linux-PAM extension). + /// + /// PAM documentation doesn't define the format of the response. + fn radio_prompt(&mut self, request: &str) -> Result; + /// Alerts the user to an error. + fn error(&mut self, message: &str); + /// Sends an informational message to the user. + fn info(&mut self, message: &str); + /// Requests binary data from the user (a Linux-PAM extension). + fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result; +} + +impl ConversationMux for C { + /// Prompts the user for something. + fn prompt(&mut self, request: &str) -> Result { + let resp = self.converse(&[Message::Prompt(request)])?.pop(); + match resp { + Some(Response::Text(s)) => Ok(s), + _ => Err(ErrorCode::ConversationError), + } + } + + /// Prompts the user for something, but hides what the user types. + fn masked_prompt(&mut self, request: &str) -> Result { + let resp = self.converse(&[Message::MaskedPrompt(request)])?.pop(); + match resp { + Some(Response::MaskedText(s)) => Ok(s), + _ => Err(ErrorCode::ConversationError), + } + } + + /// Prompts the user for a yes/no/maybe conditional (a Linux-PAM extension). + /// + /// PAM documentation doesn't define the format of the response. + fn radio_prompt(&mut self, request: &str) -> Result { + let resp = self.converse(&[Message::RadioPrompt(request)])?.pop(); + match resp { + Some(Response::Text(s)) => Ok(s), + _ => Err(ErrorCode::ConversationError), + } + } + + /// Alerts the user to an error. + fn error(&mut self, message: &str) { + let _ = self.converse(&[Message::Error(message)]); + } + + /// Sends an informational message to the user. + fn info(&mut self, message: &str) { + let _ = self.converse(&[Message::Info(message)]); + } + + /// Requests binary data from the user (a Linux-PAM extension). + fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result { + let resp = self + .converse(&[Message::BinaryPrompt { data, data_type }])? + .pop(); + match resp { + Some(Response::Binary(d)) => Ok(d), + _ => Err(ErrorCode::ConversationError), + } + } } /// Trait that an application can implement if they want to handle messages @@ -81,14 +225,11 @@ fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result; } -impl Conversation for LibPamConversation { - fn send(&mut self, _: &[Message]) -> Result> { - todo!() - } -} - -impl Conversation for D { - fn send(&mut self, messages: &[Message]) -> Result> { +impl Conversation for DM +where + DM: DemuxedConversation, +{ + fn converse(&mut self, messages: &[Message]) -> Result> { messages .iter() .map(|msg| match *msg { @@ -195,7 +336,7 @@ Response::NoResponse, ], tester - .send(&[ + .converse(&[ Message::Prompt("what"), Message::MaskedPrompt("reveal"), Message::Error("whoopsie"), @@ -208,7 +349,7 @@ assert_eq!( ErrorCode::PermissionDenied, - tester.send(&[Message::Prompt("give_err")]).unwrap_err(), + tester.converse(&[Message::Prompt("give_err")]).unwrap_err(), ); // Test the Linux-PAM extensions separately. @@ -219,7 +360,7 @@ Response::Binary(super::BinaryData::new(vec![5, 5, 5], 5)), ], tester - .send(&[ + .converse(&[ Message::RadioPrompt("channel?"), Message::BinaryPrompt { data: &[10, 9, 8], @@ -229,4 +370,80 @@ .unwrap() ); } + + #[test] + fn test_mux() { + use super::ConversationMux; + struct MuxTester; + + impl Conversation for MuxTester { + fn converse(&mut self, messages: &[Message]) -> crate::Result> { + if let [msg] = messages { + match msg { + Message::Info(info) => { + assert_eq!("let me tell you", *info); + Ok(vec![Response::NoResponse]) + } + Message::Error(error) => { + assert_eq!("oh no", *error); + Ok(vec![Response::NoResponse]) + } + Message::Prompt("should_error") => Err(ErrorCode::BufferError), + Message::Prompt(ask) => { + assert_eq!("question", *ask); + Ok(vec![Response::Text("answer".to_owned())]) + } + Message::MaskedPrompt("return_wrong_type") => { + Ok(vec![Response::NoResponse]) + } + Message::MaskedPrompt(ask) => { + assert_eq!("password!", *ask); + Ok(vec![Response::MaskedText(SecureString::from( + "open sesame", + ))]) + } + Message::BinaryPrompt { data, data_type } => { + assert_eq!(&[1, 2, 3], data); + assert_eq!(69, *data_type); + Ok(vec![Response::Binary(super::BinaryData::new( + vec![3, 2, 1], + 42, + ))]) + } + Message::RadioPrompt(ask) => { + assert_eq!("radio?", *ask); + Ok(vec![Response::Text("yes".to_owned())]) + } + } + } else { + panic!("messages is the wrong size ({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("oh no"); + tester.info("let me tell you"); + { + assert_eq!("yes", tester.radio_prompt("radio?").unwrap()); + assert_eq!( + super::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() + ) + } } diff -r 47eb242a4f88 -r ac6881304c78 src/handle.rs --- a/src/handle.rs Wed Jun 04 03:53:36 2025 -0400 +++ b/src/handle.rs Thu Jun 05 03:41:38 2025 -0400 @@ -1,12 +1,6 @@ //! The wrapper types and traits for handles into the PAM library. -use crate::constants::{ErrorCode, Result}; +use crate::constants::Result; use crate::conv::Conversation; -use crate::items::ItemType; -use crate::module::ConversationMux; -use crate::pam_ffi; -use crate::pam_ffi::{memory, LibPamConversation, LibPamHandle}; -use std::ffi::{c_char, c_int}; -use std::{mem, ptr}; macro_rules! trait_item { (get = $getter:ident, item = $item:literal $(, see = $see:path)? $(, $($doc:literal)*)?) => { @@ -57,12 +51,23 @@ }; } -/// Features of a PAM handle that are available to applications and modules. +/// All-in-one trait for what you should expect from PAM as an application. +pub trait PamHandleApplication: PamApplicationOnly + PamShared {} +impl PamHandleApplication for T where T: PamApplicationOnly + PamShared {} + +/// All-in-one trait for what you should expect from PAM as a module. +pub trait PamHandleModule: PamModuleOnly + PamShared {} +impl PamHandleModule for T where T: PamModuleOnly + PamShared {} + +/// Functionality for both PAM applications and PAM modules. /// -/// You probably want [`LibPamHandle`]. This trait is intended to allow creating -/// mock PAM handle types used for testing PAM modules and applications. -pub trait PamHandle { - type Conv: Conversation; +/// This base trait includes features of a PAM handle that are available +/// to both applications and modules. +/// +/// You probably want [`LibPamHandle`](crate::pam_ffi::OwnedLibPamHandle). +/// This trait is intended to allow creating mock PAM handle types +/// to test PAM modules and applications. +pub trait PamShared { /// Retrieves the name of the user who is authenticating or logging in. /// /// If the username has previously been obtained, this uses that username; @@ -70,7 +75,7 @@ /// /// 1. The prompt string passed to this function. /// 2. The string returned by `get_user_prompt_item`. - /// 3. The default prompt, `login: ` + /// 3. The default prompt, `login: `. /// /// See the [`pam_get_user` manual page][man] /// or [`pam_get_user` in the Module Writer's Guide][mwg]. @@ -78,8 +83,8 @@ /// # Example /// /// ```no_run - /// # use nonstick::PamModuleHandle; - /// # fn _doc(handle: &mut impl PamModuleHandle) -> Result<(), Box> { + /// # use nonstick::PamShared; + /// # fn _doc(handle: &mut impl PamShared) -> Result<(), Box> { /// // Get the username using the default prompt. /// let user = handle.get_user(None)?; /// // Get the username using a custom prompt. @@ -92,16 +97,18 @@ /// /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_user.3.html /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_user - fn get_user(&mut self, prompt: Option<&str>) -> Result>; + fn get_user(&mut self, prompt: Option<&str>) -> Result<&str>; trait_item!( get = user_item, item = "PAM_USER", + see = Self::get_user, "The identity of the user for whom service is being requested." "" - "While PAM usually sets this automatically during the course of " - "a [`get_user`](Self::get_user) call, it may be changed by a module " - "over the course of the PAM transaction." + "Unlike [`get_user`](Self::get_user), this will simply get" + "the current state of the user item, and not request the username. " + "While PAM usually sets this automatically in the `get_user` call, " + "it may be changed by a module during the PAM transaction. " "Applications should check it after each step of the PAM process." ); trait_item!( @@ -225,9 +232,9 @@ /// If you are not writing a PAM client application (e.g., you are writing /// a module), you should not use the functionality exposed by this trait. /// -/// Like [`PamHandle`], this is intended to allow creating mock implementations +/// Like [`PamShared`], this is intended to allow creating mock implementations /// of PAM for testing PAM applications. -pub trait PamApplicationHandle: PamHandle { +pub trait PamApplicationOnly { /// Closes the PAM session on an owned PAM handle. /// /// This should be called with the result of the application's last call @@ -238,9 +245,9 @@ /// See the [`pam_end` manual page][man] for more information. /// /// ```no_run - /// # use nonstick::PamApplicationHandle; + /// # use nonstick::handle::PamApplicationOnly; /// # use std::error::Error; - /// # fn _doc(handle: impl PamApplicationHandle, auth_result: nonstick::Result<()>) -> Result<(), Box> { + /// # fn _doc(handle: impl PamApplicationOnly, auth_result: nonstick::Result<()>) -> Result<(), Box> { /// // Earlier: authentication was performed and the result was stored /// // into auth_result. /// handle.close(auth_result)?; @@ -250,9 +257,6 @@ /// /// [man]: https://www.man7.org/linux/man-pages/man3/pam_end.3.html fn close(self, status: Result<()>) -> Result<()>; - - /// Uses a new PAM conversation. - fn set_conversation(&mut self, conversation: Self::Conv) -> Result<()>; } /// Functionality of a PAM handle that can be expected by a PAM module. @@ -260,15 +264,9 @@ /// If you are not writing a PAM module (e.g., you are writing an application), /// you should not use any of the functionality exposed by this trait. /// -/// Like [`PamHandle`], this is intended to allow creating mock implementations +/// Like [`PamShared`], this is intended to allow creating mock implementations /// of PAM for testing PAM modules. -pub trait PamModuleHandle: PamHandle { - /// Gets a channel for communication with the user. - /// - /// The Conversation is the conduit which you use for all communication - /// with the user. - fn conversation(&mut self) -> Result>; - +pub trait PamModuleOnly: Conversation { /// Retrieves the authentication token from the user. /// /// This should only be used by *authentication* and *password-change* @@ -280,8 +278,8 @@ /// # Example /// /// ```no_run - /// # use nonstick::PamModuleHandle; - /// # fn _doc(handle: &mut impl PamModuleHandle) -> Result<(), Box> { + /// # use nonstick::handle::PamModuleOnly; + /// # fn _doc(handle: &mut impl PamModuleOnly) -> Result<(), Box> { /// // Get the user's password using the default prompt. /// let pass = handle.get_authtok(None)?; /// // Get the user's password using a custom prompt. @@ -292,7 +290,7 @@ /// /// [man]: https://www.man7.org/linux/man-pages/man3/pam_get_authtok.3.html /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-by-module-item.html#mwg-pam_get_item - fn get_authtok(&mut self, prompt: Option<&str>) -> Result>; + fn get_authtok(&mut self, prompt: Option<&str>) -> Result<&str>; trait_item!( get = authtok_item, @@ -351,135 +349,3 @@ fn set_data(&mut self, key: &str, data: Box) -> Result<()>; */ } - - -impl LibPamHandle { - /// Gets a C string item. - /// - /// # Safety - /// - /// You better be requesting an item which is a C string. - unsafe fn get_cstr_item(&mut self, item_type: ItemType) -> Result> { - let mut output = ptr::null(); - let ret = unsafe { pam_ffi::pam_get_item(self, item_type as c_int, &mut output) }; - ErrorCode::result_from(ret)?; - memory::wrap_string(output.cast()) - } - - /// Sets a C string item. - /// - /// # Safety - /// - /// You better be setting an item which is a C string. - unsafe fn set_cstr_item(&mut self, item_type: ItemType, data: Option<&str>) -> Result<()> { - let data_str = memory::option_cstr(data)?; - let ret = unsafe { - pam_ffi::pam_set_item( - self, - item_type as c_int, - memory::prompt_ptr(data_str.as_ref()).cast(), - ) - }; - ErrorCode::result_from(ret) - } -} - -impl Drop for LibPamHandle { - /// Ends the PAM session with a zero error code. - /// You probably want to call [`close`](Self::close) instead of - /// letting this drop by itself. - fn drop(&mut self) { - unsafe { - pam_ffi::pam_end(self, 0); - } - } -} - -macro_rules! cstr_item { - (get = $getter:ident, item = $item_type:path) => { - fn $getter(&mut self) -> Result> { - unsafe { self.get_cstr_item($item_type) } - } - }; - (set = $setter:ident, item = $item_type:path) => { - fn $setter(&mut self, value: Option<&str>) -> Result<()> { - unsafe { self.set_cstr_item($item_type, value) } - } - }; -} - -impl PamHandle for LibPamHandle { - type Conv = LibPamConversation; - fn get_user(&mut self, prompt: Option<&str>) -> Result> { - let prompt = memory::option_cstr(prompt)?; - let mut output: *const c_char = ptr::null(); - let ret = unsafe { - pam_ffi::pam_get_user(self, &mut output, memory::prompt_ptr(prompt.as_ref())) - }; - ErrorCode::result_from(ret)?; - unsafe { memory::wrap_string(output) } - } - - cstr_item!(get = user_item, item = ItemType::User); - cstr_item!(set = set_user_item, item = ItemType::User); - cstr_item!(get = service, item = ItemType::Service); - cstr_item!(set = set_service, item = ItemType::Service); - cstr_item!(get = user_prompt, item = ItemType::UserPrompt); - cstr_item!(set = set_user_prompt, item = ItemType::UserPrompt); - cstr_item!(get = tty_name, item = ItemType::Tty); - cstr_item!(set = set_tty_name, item = ItemType::Tty); - cstr_item!(get = remote_user, item = ItemType::RemoteUser); - cstr_item!(set = set_remote_user, item = ItemType::RemoteUser); - cstr_item!(get = remote_host, item = ItemType::RemoteHost); - cstr_item!(set = set_remote_host, item = ItemType::RemoteHost); - cstr_item!(set = set_authtok_item, item = ItemType::AuthTok); - cstr_item!(set = set_old_authtok_item, item = ItemType::OldAuthTok); -} - -impl PamApplicationHandle for LibPamHandle { - fn close(mut self, status: Result<()>) -> Result<()> { - let result = unsafe { pam_ffi::pam_end(&mut self, ErrorCode::result_to_c(status)) }; - // Since we've already `pam_end`ed this session, we don't want it to be - // double-freed on drop. - mem::forget(self); - ErrorCode::result_from(result) - } - - fn set_conversation(&mut self, conversation: Self::Conv) -> Result<()> { - todo!() - } -} - -impl PamModuleHandle for LibPamHandle { - fn conversation(&mut self) -> Result> { - todo!() - } - - fn get_authtok(&mut self, prompt: Option<&str>) -> Result> { - let prompt = memory::option_cstr(prompt)?; - let mut output: *const c_char = ptr::null_mut(); - let res = unsafe { - pam_ffi::pam_get_authtok( - self, - ItemType::AuthTok.into(), - &mut output, - memory::prompt_ptr(prompt.as_ref()), - ) - }; - ErrorCode::result_from(res)?; - unsafe { memory::wrap_string(output) } - } - - cstr_item!(get = authtok_item, item = ItemType::AuthTok); - cstr_item!(get = old_authtok_item, item = ItemType::OldAuthTok); -} - -/// Function called at the end of a PAM session that is called to clean up -/// a value previously provided to PAM in a `pam_set_data` call. -/// -/// You should never call this yourself. -extern "C" fn set_data_cleanup(_: *const libc::c_void, c_data: *mut libc::c_void, _: c_int) { - unsafe { - let _data: Box = Box::from_raw(c_data.cast()); - } -} diff -r 47eb242a4f88 -r ac6881304c78 src/items.rs --- a/src/items.rs Wed Jun 04 03:53:36 2025 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,53 +0,0 @@ -//! Things that can be gotten with the `pam_get_item` function. - -use crate::constants::InvalidEnum; -use num_derive::FromPrimitive; -use num_traits::FromPrimitive; -use std::ffi::c_int; - -/// Identifies what is being gotten or set with `pam_get_item` -/// or `pam_set_item`. -#[derive(FromPrimitive)] -#[repr(i32)] -#[non_exhaustive] // because C could give us anything! -pub enum ItemType { - /// The PAM service name. - Service = 1, - /// The user's login name. - User = 2, - /// The TTY name. - Tty = 3, - /// The remote host (if applicable). - RemoteHost = 4, - /// The conversation struct (not a CStr-based item). - Conversation = 5, - /// The authentication token (password). - AuthTok = 6, - /// The old authentication token (when changing passwords). - OldAuthTok = 7, - /// The remote user's name. - RemoteUser = 8, - /// The prompt shown when requesting a username. - UserPrompt = 9, - /// App-supplied function to override failure delays. - FailDelay = 10, - /// X display name. - XDisplay = 11, - /// X server authentication data. - XAuthData = 12, - /// The type of `pam_get_authtok`. - AuthTokType = 13, -} - -impl TryFrom for ItemType { - type Error = InvalidEnum; - fn try_from(value: c_int) -> Result { - Self::from_i32(value).ok_or(value.into()) - } -} - -impl From for c_int { - fn from(val: ItemType) -> Self { - val as Self - } -} diff -r 47eb242a4f88 -r ac6881304c78 src/lib.rs --- a/src/lib.rs Wed Jun 04 03:53:36 2025 -0400 +++ b/src/lib.rs Thu Jun 05 03:41:38 2025 -0400 @@ -27,17 +27,16 @@ pub mod constants; mod conv; -mod items; pub mod module; -mod handle; +pub mod handle; mod pam_ffi; #[doc(inline)] pub use crate::{ constants::{ErrorCode, Flags, Result}, - conv::{Conversation, DemuxedConversation, Response}, - handle::{PamApplicationHandle, PamHandle, PamModuleHandle}, + conv::{Conversation, ConversationMux, DemuxedConversation, Response}, + handle::{PamHandleApplication, PamHandleModule, PamShared}, module::PamModule, - pam_ffi::{LibPamHandle, Message}, + pam_ffi::{LibPamHandle, OwnedLibPamHandle}, }; diff -r 47eb242a4f88 -r ac6881304c78 src/module.rs --- a/src/module.rs Wed Jun 04 03:53:36 2025 -0400 +++ b/src/module.rs Thu Jun 05 03:41:38 2025 -0400 @@ -4,11 +4,7 @@ #![allow(dead_code)] use crate::constants::{ErrorCode, Flags, Result}; -use crate::conv::BinaryData; -use crate::conv::{Conversation, Response}; -use crate::handle::PamModuleHandle; -use crate::pam_ffi::Message; -use secure_string::SecureString; +use crate::handle::PamHandleModule; use std::ffi::CStr; /// A trait for a PAM module to implement. @@ -25,14 +21,14 @@ /// [manpage]: https://www.man7.org/linux/man-pages/man3/pam.3.html /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/Linux-PAM_MWG.html #[allow(unused_variables)] -pub trait PamModule { +pub trait PamModule { // Functions for auth modules. /// Authenticate the user. /// /// This is probably the first thing you want to implement. /// In most cases, you will want to get the user and password, - /// using [`PamHandle::get_user`] and [`PamModuleHandle::get_authtok`], + /// using [`PamHandle::get_user`] and [`PamModuleOnly::get_authtok`], /// and verify them against something. /// /// See [the Module Writer's Guide entry for `pam_sm_authenticate`][mwg] @@ -82,7 +78,6 @@ /// See [the Module Writer's Guide entry for `pam_sm_acct_mgmt`][mwg] /// for more information. /// - /// /// # Valid flags /// /// This function may be called with the following flags set: @@ -240,81 +235,6 @@ } } -/// Provides methods to make it easier to send exactly one message. -/// -/// This is primarily used by PAM modules, so that a module that only needs -/// one piece of information at a time doesn't have a ton of boilerplate. -/// You may also find it useful for testing PAM application libraries. -/// -/// ``` -/// # use nonstick::Result; -/// # use nonstick::conv::Conversation; -/// # use nonstick::module::ConversationMux; -/// # fn _do_test(conv: impl Conversation) -> Result<()> { -/// let mut mux = ConversationMux(conv); -/// let token = mux.masked_prompt("enter your one-time token")?; -/// # Ok(()) -/// # } -pub struct ConversationMux<'a, C: Conversation>(pub &'a mut C); - -impl Conversation for ConversationMux<'_, C> { - fn send(&mut self, messages: &[Message]) -> Result> { - self.0.send(messages) - } -} - -impl ConversationMux<'_, C> { - /// Prompts the user for something. - pub fn prompt(&mut self, request: &str) -> Result { - let resp = self.send(&[Message::Prompt(request)])?.pop(); - match resp { - Some(Response::Text(s)) => Ok(s), - _ => Err(ErrorCode::ConversationError), - } - } - - /// Prompts the user for something, but hides what the user types. - pub fn masked_prompt(&mut self, request: &str) -> Result { - let resp = self.send(&[Message::MaskedPrompt(request)])?.pop(); - match resp { - Some(Response::MaskedText(s)) => Ok(s), - _ => Err(ErrorCode::ConversationError), - } - } - - /// Prompts the user for a yes/no/maybe conditional (a Linux-PAM extension). - /// - /// PAM documentation doesn't define the format of the response. - pub fn radio_prompt(&mut self, request: &str) -> Result { - let resp = self.send(&[Message::RadioPrompt(request)])?.pop(); - match resp { - Some(Response::Text(s)) => Ok(s), - _ => Err(ErrorCode::ConversationError), - } - } - - /// Alerts the user to an error. - pub fn error(&mut self, message: &str) { - let _ = self.send(&[Message::Error(message)]); - } - - /// Sends an informational message to the user. - pub fn info(&mut self, message: &str) { - let _ = self.send(&[Message::Info(message)]); - } - - /// Requests binary data from the user (a Linux-PAM extension). - pub fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result { - let resp = self - .send(&[Message::BinaryPrompt { data, data_type }])? - .pop(); - match resp { - Some(Response::Binary(d)) => Ok(d), - _ => Err(ErrorCode::ConversationError), - } - } -} - /// Generates the dynamic library entry points for a [PamModule] implementation. /// /// Calling `pam_hooks!(SomeType)` on a type that implements [PamModule] will @@ -326,25 +246,25 @@ /// Here is full example of a PAM module that would authenticate and authorize everybody: /// /// ```no_run -/// use nonstick::{Flags, LibPamHandle, PamModule, PamModuleHandle, Result as PamResult, pam_hooks}; +/// use nonstick::{Flags, OwnedLibPamHandle, PamModule, PamHandleModule, Result as PamResult, pam_hooks}; /// use std::ffi::CStr; /// # fn main() {} /// /// struct MyPamModule; /// pam_hooks!(MyPamModule); /// -/// impl PamModule for MyPamModule { +/// impl PamModule for MyPamModule { /// fn authenticate(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> { /// let password = handle.get_authtok(Some("what's your password?"))?; -/// eprintln!("If you say your password is {:?}, who am I to disagree!", password.unsecure()); +/// // You should use a Conversation to communicate with the user +/// // instead of writing to the console, but this is just an example. +/// eprintln!("If you say your password is {password:?}, who am I to disagree?"); /// Ok(()) /// } /// /// fn account_management(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> { /// let username = handle.get_user(None)?; -/// // You should use a Conversation to communicate with the user -/// // instead of writing to the console, but this is just an example. -/// eprintln!("Hello {username}! I trust you unconditionally!"); +/// eprintln!("Hello {username:?}! I trust you unconditionally!"); /// Ok(()) /// } /// } @@ -462,92 +382,10 @@ #[cfg(test)] mod tests { - use super::{ - Conversation, ConversationMux, ErrorCode, Message, Response, Result, SecureString, - }; - - /// Compile-time test that the `pam_hooks` macro compiles. - mod hooks { - use super::super::{PamModule, PamModuleHandle}; - struct Foo; - impl PamModule for Foo {} - - pam_hooks!(Foo); - } - - #[test] - fn test_mux() { - struct MuxTester; + // Compile-time test that the `pam_hooks` macro compiles. + use super::super::{PamHandleModule, PamModule}; + struct Foo; + impl PamModule for Foo {} - impl Conversation for MuxTester { - fn send(&mut self, messages: &[Message]) -> Result> { - if let [msg] = messages { - match msg { - Message::Info(info) => { - assert_eq!("let me tell you", *info); - Ok(vec![Response::NoResponse]) - } - Message::Error(error) => { - assert_eq!("oh no", *error); - Ok(vec![Response::NoResponse]) - } - Message::Prompt("should_error") => Err(ErrorCode::BufferError), - Message::Prompt(ask) => { - assert_eq!("question", *ask); - Ok(vec![Response::Text("answer".to_owned())]) - } - Message::MaskedPrompt("return_wrong_type") => { - Ok(vec![Response::NoResponse]) - } - Message::MaskedPrompt(ask) => { - assert_eq!("password!", *ask); - Ok(vec![Response::MaskedText(SecureString::from( - "open sesame", - ))]) - } - Message::BinaryPrompt { data, data_type } => { - assert_eq!(&[1, 2, 3], data); - assert_eq!(69, *data_type); - Ok(vec![Response::Binary(super::BinaryData::new( - vec![3, 2, 1], - 42, - ))]) - } - Message::RadioPrompt(ask) => { - assert_eq!("radio?", *ask); - Ok(vec![Response::Text("yes".to_owned())]) - } - } - } else { - panic!("messages is the wrong size ({len})", len = messages.len()) - } - } - } - - let mut tester = MuxTester; - - let mut mux = ConversationMux(&mut tester); - assert_eq!("answer", mux.prompt("question").unwrap()); - assert_eq!( - SecureString::from("open sesame"), - mux.masked_prompt("password!").unwrap() - ); - mux.error("oh no"); - mux.info("let me tell you"); - { - assert_eq!("yes", mux.radio_prompt("radio?").unwrap()); - assert_eq!( - super::BinaryData::new(vec![3, 2, 1], 42), - mux.binary_prompt(&[1, 2, 3], 69).unwrap(), - ) - } - assert_eq!( - ErrorCode::BufferError, - mux.prompt("should_error").unwrap_err(), - ); - assert_eq!( - ErrorCode::ConversationError, - mux.masked_prompt("return_wrong_type").unwrap_err() - ) - } + pam_hooks!(Foo); } diff -r 47eb242a4f88 -r ac6881304c78 src/pam_ffi/conversation.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pam_ffi/conversation.rs Thu Jun 05 03:41:38 2025 -0400 @@ -0,0 +1,162 @@ +use crate::constants::Result; +use crate::conv::{Conversation, Message, Response}; +use crate::pam_ffi::memory::Immovable; +use crate::pam_ffi::message::{MessageIndirector, OwnedMessages}; +use crate::pam_ffi::response::{OwnedResponses, RawBinaryResponse, RawResponse, RawTextResponse}; +use crate::ErrorCode; +use crate::ErrorCode::ConversationError; +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)] +pub struct AppData { + _data: (), + _marker: Immovable, +} + +/// The callback that PAM uses to get information in a conversation. +/// +/// - `num_msg` is the number of messages in the `pam_message` array. +/// - `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, +/// 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, + appdata: *mut AppData, +) -> c_int; + +/// The type used by PAM to call back into a conversation. +#[repr(C)] +pub struct LibPamConversation<'a> { + /// The function that is called to get information from the user. + callback: ConversationCallback, + /// The pointer that will be passed as the last parameter + /// to the conversation callback. + appdata: *mut AppData, + life: PhantomData<&'a mut ()>, + _marker: Immovable, +} + +impl LibPamConversation<'_> { + fn wrap(conv: &mut C) -> Self { + Self { + callback: Self::wrapper_callback::, + appdata: (conv as *mut C).cast(), + life: PhantomData, + _marker: Immovable(PhantomData), + } + } + + unsafe extern "C" fn wrapper_callback( + count: c_int, + messages: *const MessageIndirector, + responses: *mut *mut RawResponse, + me: *mut AppData, + ) -> c_int { + let call = || { + let conv = me + .cast::() + .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 = indir + .iter(count as usize) + .map(Message::try_from) + .collect::>() + .map_err(|_| ErrorCode::ConversationError)?; + let responses = conv.converse(&messages)?; + let owned = + OwnedResponses::build(&responses).map_err(|_| ErrorCode::ConversationError)?; + *response_ptr = owned.into_ptr(); + Ok(()) + }; + ErrorCode::result_to_c(call()) + } +} + +impl Conversation for LibPamConversation<'_> { + fn converse(&mut self, messages: &[Message]) -> Result> { + 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)? + } + 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> { + 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() +} + +/// Converts one message-to-raw pair to a Response. +fn convert((sent, received): (&Message, &mut RawResponse)) -> Result { + 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 + } + 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::Error(_) | Message::Info(_) => 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 + } + }) +} diff -r 47eb242a4f88 -r ac6881304c78 src/pam_ffi/handle.rs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pam_ffi/handle.rs Thu Jun 05 03:41:38 2025 -0400 @@ -0,0 +1,231 @@ +use super::conversation::LibPamConversation; +use crate::constants::{ErrorCode, InvalidEnum, Result}; +use crate::conv::Message; +use crate::handle::{PamApplicationOnly, PamModuleOnly, PamShared}; +use crate::pam_ffi::memory; +use crate::pam_ffi::memory::Immovable; +use crate::{Conversation, Response}; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; +use std::ffi::{c_char, c_int}; +use std::ops::{Deref, DerefMut}; +use std::result::Result as StdResult; +use std::{mem, ptr}; + +/// An owned PAM handle. +#[repr(transparent)] +pub struct OwnedLibPamHandle(*mut LibPamHandle); + +/// An opaque structure that a PAM handle points to. +#[repr(C)] +pub struct LibPamHandle { + _data: (), + _marker: Immovable, +} + +impl LibPamHandle { + /// Gets a C string item. + /// + /// # Safety + /// + /// You better be requesting an item which is a C string. + unsafe fn get_cstr_item(&mut self, item_type: ItemType) -> Result> { + let mut output = ptr::null(); + let ret = unsafe { super::pam_get_item(self, item_type as c_int, &mut output) }; + ErrorCode::result_from(ret)?; + memory::wrap_string(output.cast()) + } + + /// Sets a C string item. + /// + /// # Safety + /// + /// You better be setting an item which is a C string. + unsafe fn set_cstr_item(&mut self, item_type: ItemType, data: Option<&str>) -> Result<()> { + let data_str = memory::option_cstr(data)?; + let ret = unsafe { + super::pam_set_item( + self, + item_type as c_int, + memory::prompt_ptr(data_str.as_ref()).cast(), + ) + }; + ErrorCode::result_from(ret) + } + + /// Gets the `PAM_CONV` item from the handle. + fn conversation_item(&mut self) -> Result<&mut LibPamConversation> { + let output: *mut LibPamConversation = ptr::null_mut(); + let result = unsafe { + super::pam_get_item( + self, + ItemType::Conversation.into(), + &mut output.cast_const().cast(), + ) + }; + ErrorCode::result_from(result)?; + // SAFETY: We got this result from PAM, and we're checking if it's null. + unsafe { output.as_mut() }.ok_or(ErrorCode::ConversationError) + } +} + +impl PamApplicationOnly for OwnedLibPamHandle { + fn close(self, status: Result<()>) -> Result<()> { + let ret = unsafe { super::pam_end(self.0, ErrorCode::result_to_c(status)) }; + // Forget rather than dropping, since dropping also calls pam_end. + mem::forget(self); + ErrorCode::result_from(ret) + } +} + +impl Deref for OwnedLibPamHandle { + type Target = LibPamHandle; + fn deref(&self) -> &Self::Target { + unsafe { &*self.0 } + } +} + +impl DerefMut for OwnedLibPamHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { &mut *self.0 } + } +} + +impl Drop for OwnedLibPamHandle { + /// Ends the PAM session with a zero error code. + /// You probably want to call [`close`](Self::close) instead of + /// letting this drop by itself. + fn drop(&mut self) { + unsafe { + super::pam_end(self.0, 0); + } + } +} + +macro_rules! cstr_item { + (get = $getter:ident, item = $item_type:path) => { + fn $getter(&mut self) -> Result> { + unsafe { self.get_cstr_item($item_type) } + } + }; + (set = $setter:ident, item = $item_type:path) => { + fn $setter(&mut self, value: Option<&str>) -> Result<()> { + unsafe { self.set_cstr_item($item_type, value) } + } + }; +} + +impl PamShared for LibPamHandle { + fn get_user(&mut self, prompt: Option<&str>) -> Result<&str> { + let prompt = memory::option_cstr(prompt)?; + let mut output: *const c_char = ptr::null(); + let ret = + unsafe { super::pam_get_user(self, &mut output, memory::prompt_ptr(prompt.as_ref())) }; + ErrorCode::result_from(ret)?; + unsafe { memory::wrap_string(output) } + .transpose() + .unwrap_or(Err(ErrorCode::ConversationError)) + } + + cstr_item!(get = user_item, item = ItemType::User); + cstr_item!(set = set_user_item, item = ItemType::User); + cstr_item!(get = service, item = ItemType::Service); + cstr_item!(set = set_service, item = ItemType::Service); + cstr_item!(get = user_prompt, item = ItemType::UserPrompt); + cstr_item!(set = set_user_prompt, item = ItemType::UserPrompt); + cstr_item!(get = tty_name, item = ItemType::Tty); + cstr_item!(set = set_tty_name, item = ItemType::Tty); + cstr_item!(get = remote_user, item = ItemType::RemoteUser); + cstr_item!(set = set_remote_user, item = ItemType::RemoteUser); + cstr_item!(get = remote_host, item = ItemType::RemoteHost); + cstr_item!(set = set_remote_host, item = ItemType::RemoteHost); + cstr_item!(set = set_authtok_item, item = ItemType::AuthTok); + cstr_item!(set = set_old_authtok_item, item = ItemType::OldAuthTok); +} + +impl Conversation for LibPamHandle { + fn converse(&mut self, messages: &[Message]) -> Result> { + self.conversation_item()?.converse(messages) + } +} + +impl PamModuleOnly for LibPamHandle { + fn get_authtok(&mut self, prompt: Option<&str>) -> Result<&str> { + let prompt = memory::option_cstr(prompt)?; + let mut output: *const c_char = ptr::null_mut(); + // SAFETY: We're calling this with known-good values. + let res = unsafe { + super::pam_get_authtok( + self, + ItemType::AuthTok.into(), + &mut output, + memory::prompt_ptr(prompt.as_ref()), + ) + }; + ErrorCode::result_from(res)?; + // SAFETY: We got this string from PAM. + unsafe { memory::wrap_string(output) } + .transpose() + .unwrap_or(Err(ErrorCode::ConversationError)) + } + + cstr_item!(get = authtok_item, item = ItemType::AuthTok); + cstr_item!(get = old_authtok_item, item = ItemType::OldAuthTok); +} + +/// Function called at the end of a PAM session that is called to clean up +/// a value previously provided to PAM in a `pam_set_data` call. +/// +/// You should never call this yourself. +extern "C" fn set_data_cleanup(_: *const libc::c_void, c_data: *mut libc::c_void, _: c_int) { + unsafe { + let _data: Box = Box::from_raw(c_data.cast()); + } +} + +/// Identifies what is being gotten or set with `pam_get_item` +/// or `pam_set_item`. +#[derive(FromPrimitive)] +#[repr(i32)] +#[non_exhaustive] // because C could give us anything! +pub enum ItemType { + /// The PAM service name. + Service = 1, + /// The user's login name. + User = 2, + /// The TTY name. + Tty = 3, + /// The remote host (if applicable). + RemoteHost = 4, + /// The conversation struct (not a CStr-based item). + Conversation = 5, + /// The authentication token (password). + AuthTok = 6, + /// The old authentication token (when changing passwords). + OldAuthTok = 7, + /// The remote user's name. + RemoteUser = 8, + /// The prompt shown when requesting a username. + UserPrompt = 9, + /// App-supplied function to override failure delays. + FailDelay = 10, + /// X display name. + XDisplay = 11, + /// X server authentication data. + XAuthData = 12, + /// The type of `pam_get_authtok`. + AuthTokType = 13, +} + +impl TryFrom for ItemType { + type Error = InvalidEnum; + fn try_from(value: c_int) -> StdResult { + Self::from_i32(value).ok_or(value.into()) + } +} + +impl From for c_int { + fn from(val: ItemType) -> Self { + val as Self + } +} diff -r 47eb242a4f88 -r ac6881304c78 src/pam_ffi/memory.rs --- a/src/pam_ffi/memory.rs Wed Jun 04 03:53:36 2025 -0400 +++ b/src/pam_ffi/memory.rs Thu Jun 05 03:41:38 2025 -0400 @@ -8,7 +8,9 @@ use std::{ptr, slice}; /// Makes whatever it's in not [`Send`], [`Sync`], or [`Unpin`]. -pub type Immovable = PhantomData<(*mut u8, PhantomPinned)>; +#[repr(C)] +#[derive(Debug)] +pub struct Immovable(pub PhantomData<(*mut u8, PhantomPinned)>); /// Safely converts a `&str` option to a `CString` option. pub fn option_cstr(prompt: Option<&str>) -> Result> { @@ -71,7 +73,7 @@ } unsafe { let data_alloc = libc::calloc(data.len() + 1, 1); - libc::memcpy(data_alloc, data.as_ptr() as *const c_void, data.len()); + libc::memcpy(data_alloc, data.as_ptr().cast(), data.len()); Ok(data_alloc.cast()) } } @@ -85,7 +87,7 @@ /// It's up to you to provide a valid C string. pub unsafe fn zero_c_string(cstr: *mut c_void) { if !cstr.is_null() { - libc::memset(cstr, 0, libc::strlen(cstr as *const c_char)); + libc::memset(cstr, 0, libc::strlen(cstr.cast())); } } @@ -113,17 +115,14 @@ max: (u32::MAX - 5) as usize, actual: source.len(), })?; + // SAFETY: We're only allocating here. let data = unsafe { - let dest_buffer = libc::malloc(buffer_size as usize) as *mut CBinaryData; + let dest_buffer: *mut CBinaryData = libc::malloc(buffer_size as usize).cast(); let data = &mut *dest_buffer; data.total_length = buffer_size.to_be_bytes(); data.data_type = data_type; let dest = data.data.as_mut_ptr(); - libc::memcpy( - dest as *mut c_void, - source.as_ptr() as *const c_void, - source.len(), - ); + libc::memcpy(dest.cast(), source.as_ptr().cast(), source.len()); dest_buffer }; Ok(data) diff -r 47eb242a4f88 -r ac6881304c78 src/pam_ffi/message.rs --- a/src/pam_ffi/message.rs Wed Jun 04 03:53:36 2025 -0400 +++ b/src/pam_ffi/message.rs Thu Jun 05 03:41:38 2025 -0400 @@ -1,77 +1,19 @@ //! Data and types dealing with PAM messages. use crate::constants::InvalidEnum; +use crate::conv::Message; use crate::pam_ffi::memory; -use crate::pam_ffi::memory::{CBinaryData, NulError, TooBigError}; +use crate::pam_ffi::memory::{CBinaryData, Immovable, NulError, TooBigError}; use num_derive::FromPrimitive; use num_traits::FromPrimitive; -use std::ffi::{c_char, c_int, c_void, CStr}; +use std::ffi::{c_int, c_void, CStr}; use std::result::Result as StdResult; -use std::slice; use std::str::Utf8Error; - -/// The types of message and request that can be sent to a user. -/// -/// The data within each enum value is the prompt (or other information) -/// that will be presented to the user. -#[derive(Debug)] -pub enum Message<'a> { - /// Requests information from the user; will be masked when typing. - /// - /// Response: [`MaskedText`](crate::conv::Response::MaskedText) - MaskedPrompt(&'a str), - /// Requests information from the user; will not be masked. - /// - /// Response: [`Text`](crate::conv::Response::Text) - Prompt(&'a str), - /// "Yes/No/Maybe conditionals" (a Linux-PAM extension). - /// - /// Response: [`Text`](crate::conv::Response::Text) - /// (Linux-PAM documentation doesn't define its contents.) - RadioPrompt(&'a str), - /// Raises an error message to the user. - /// - /// Response: [`NoResponse`](crate::conv::Response::NoResponse) - Error(&'a str), - /// Sends an informational message to the user. - /// - /// Response: [`NoResponse`](crate::conv::Response::NoResponse) - Info(&'a str), - /// Requests binary data from the client (a Linux-PAM extension). - /// - /// This is used for non-human or non-keyboard prompts (security key?). - /// NOT part of the X/Open PAM specification. - /// - /// Response: [`Binary`](crate::conv::Response::Binary) - BinaryPrompt { - /// Some binary data. - data: &'a [u8], - /// A "type" that you can use for signalling. Has no strict definition in PAM. - data_type: u8, - }, -} - -impl Message<'_> { - /// Copies the contents of this message to the C heap. - fn copy_to_heap(&self) -> StdResult<(Style, *mut c_void), ConversionError> { - let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast())); - match *self { - Self::MaskedPrompt(text) => alloc(Style::PromptEchoOff, text), - Self::Prompt(text) => alloc(Style::PromptEchoOn, text), - Self::RadioPrompt(text) => alloc(Style::RadioType, text), - Self::Error(text) => alloc(Style::ErrorMsg, text), - Self::Info(text) => alloc(Style::TextInfo, text), - Self::BinaryPrompt { data, data_type } => Ok(( - Style::BinaryPrompt, - (CBinaryData::alloc(data, data_type)?).cast(), - )), - } - } -} +use std::{ptr, slice}; #[derive(Debug, thiserror::Error)] #[error("error creating PAM message: {0}")] -enum ConversionError { +pub enum ConversionError { InvalidEnum(#[from] InvalidEnum