changeset 60:05cc2c27334f

The Big Refactor: clean up docs and exports. - Brings the most important symbols in the library to the root with `pub use` statements. - Expands and updates documentation. - Rearranges things extensively to make the external interface nicer and make the structure easier to understand. - Renames a few things (e.g. `Result`).
author Paul Fisher <paul@pfish.zone>
date Wed, 21 May 2025 19:00:51 -0400
parents 3f4a77aa88be
children 5eecd797fc69
files src/constants.rs src/conv.rs src/items.rs src/lib.rs src/macros.rs src/memory.rs src/module.rs src/pam_ffi.rs
diffstat 8 files changed, 390 insertions(+), 337 deletions(-) [+]
line wrap: on
line diff
--- a/src/constants.rs	Wed May 21 00:27:18 2025 -0400
+++ b/src/constants.rs	Wed May 21 19:00:51 2025 -0400
@@ -1,24 +1,37 @@
+//! Constants and enum values from the PAM library.
+
 use bitflags::bitflags;
 use libc::{c_int, c_uint};
 use num_derive::FromPrimitive;
 use num_traits::FromPrimitive;
 use std::any;
 use std::marker::PhantomData;
-// TODO: Import constants from C header file at compile time.
 
-// The Linux-PAM flags
-// see /usr/include/security/_pam_types.h
 bitflags! {
+    /// The available PAM flags.
+    ///
+    /// See `/usr/include/security/_pam_types.h` for more details.
     #[derive(Debug, PartialEq)]
     #[repr(transparent)]
     pub struct Flags: c_uint {
+        /// Authentication service should not generate any messages.
         const SILENT = 0x8000;
+        /// The service should return [ErrorCode::AuthError] if the user
+        /// has a null authentication token.
         const DISALLOW_NULL_AUTHTOK = 0x0001;
+        /// Set user credentials for an authentication service.
         const ESTABLISH_CRED = 0x0002;
+        /// Delete user credentials associated with
+        /// an authentication service.
         const DELETE_CRED = 0x0004;
+        /// Reinitialize user credentials.
         const REINITIALIZE_CRED = 0x0008;
+        /// Extend the lifetime of user credentials.
         const REFRESH_CRED = 0x0010;
-        const CHANGE_EXPIRED_AUTHTOK= 0x0020;
+        /// The password service should only update those passwords
+        /// that have aged. If this flag is _not_ passed,
+        /// the password service should update all passwords.
+        const CHANGE_EXPIRED_AUTHTOK = 0x0020;
     }
 }
 
@@ -43,7 +56,7 @@
 
 impl TryFrom<c_int> for MessageStyle {
     type Error = InvalidEnum<Self>;
-    fn try_from(value: c_int) -> Result<Self, Self::Error> {
+    fn try_from(value: c_int) -> std::result::Result<Self, Self::Error> {
         Self::from_i32(value).ok_or(value.into())
     }
 }
@@ -54,8 +67,8 @@
     }
 }
 
-/// The Linux-PAM error return values.
-/// Success is instead represented by the `Ok` entry of a `Result`.
+/// The Linux-PAM error return values. Success is an Ok [Result].
+///
 /// Most abbreviations (except `AuthTok` and `Max`) are now full words.
 /// For more detailed information, see
 /// `/usr/include/security/_pam_types.h`.
@@ -127,19 +140,21 @@
     Incomplete = 31,
 }
 
-pub type PamResult<T> = Result<T, ErrorCode>;
+/// A PAM-specific Result type with an [ErrorCode] error.
+pub type Result<T> = std::result::Result<T, ErrorCode>;
+
 impl ErrorCode {
-    /// Converts a PamResult into the result code that C wants.
-    pub fn result_to_c(value: PamResult<()>) -> c_int {
+    /// Converts this [Result] into a C-compatible result code.
+    pub fn result_to_c<T>(value: Result<T>) -> c_int {
         match value {
             Ok(_) => 0, // PAM_SUCCESS
             Err(otherwise) => otherwise.into(),
         }
     }
 
-    /// Converts a C result code into a PamResult, with success as Ok.
+    /// Converts a C result code into a [Result], with success as Ok.
     /// Invalid values are returned as a [Self::SystemError].
-    pub fn result_from(value: c_int) -> PamResult<()> {
+    pub fn result_from(value: c_int) -> Result<()> {
         match value {
             0 => Ok(()),
             value => Err(value.try_into().unwrap_or(Self::SystemError)),
@@ -150,7 +165,7 @@
 impl TryFrom<c_int> for ErrorCode {
     type Error = InvalidEnum<Self>;
 
-    fn try_from(value: c_int) -> Result<Self, Self::Error> {
+    fn try_from(value: c_int) -> std::result::Result<Self, Self::Error> {
         Self::from_i32(value).ok_or(value.into())
     }
 }
@@ -163,12 +178,18 @@
 
 /// Error returned when attempting to coerce an invalid C integer into an enum.
 #[derive(thiserror::Error)]
-#[error("{} is not a valid {}", .0, any::type_name::<T>())]
+#[error("{0} is not a valid {type}", type = any::type_name::<T>())]
 #[derive(Debug, PartialEq)]
-pub struct InvalidEnum<T>(std::ffi::c_int, PhantomData<T>);
+pub struct InvalidEnum<T>(c_int, PhantomData<T>);
 
-impl<T> From<std::ffi::c_int> for InvalidEnum<T> {
-    fn from(value: std::ffi::c_int) -> Self {
+impl<T> From<InvalidEnum<T>> for c_int {
+    fn from(value: InvalidEnum<T>) -> Self {
+        value.0
+    }
+}
+
+impl<T> From<c_int> for InvalidEnum<T> {
+    fn from(value: c_int) -> Self {
         Self(value, PhantomData)
     }
 }
@@ -180,9 +201,12 @@
     #[test]
     fn test_enums() {
         assert_eq!(Ok(MessageStyle::ErrorMsg), 3.try_into());
-        assert_eq!(Err(999.into()), ErrorCode::try_from(999));
+        assert_eq!(Err(InvalidEnum::from(999)), ErrorCode::try_from(999));
         assert_eq!(Ok(()), ErrorCode::result_from(0));
         assert_eq!(Err(ErrorCode::Abort), ErrorCode::result_from(26));
         assert_eq!(Err(ErrorCode::SystemError), ErrorCode::result_from(423));
+        assert!(InvalidEnum::<MessageStyle>(33, PhantomData)
+            .to_string()
+            .starts_with("33 is not a valid "));
     }
 }
--- a/src/conv.rs	Wed May 21 00:27:18 2025 -0400
+++ b/src/conv.rs	Wed May 21 19:00:51 2025 -0400
@@ -1,10 +1,12 @@
+//! The [Conversation] struct, for interacting with the user.
+
 use libc::{c_char, c_int};
 use std::ffi::{CStr, CString};
 use std::ptr;
 
 use crate::constants::ErrorCode;
 use crate::constants::MessageStyle;
-use crate::constants::PamResult;
+use crate::constants::Result;
 use crate::items::Item;
 
 #[repr(C)]
@@ -19,6 +21,7 @@
     resp_retcode: libc::c_int, // Unused - always zero
 }
 
+#[doc(hidden)]
 #[repr(C)]
 pub struct Inner {
     conv: extern "C" fn(
@@ -30,30 +33,24 @@
     appdata_ptr: *const libc::c_void,
 }
 
-/// A `Conv`ersation channel with the user.
+/// A communication channel with the user.
 ///
-/// Communication is mediated by the PAM client (the application that invoked
-/// pam).  Messages sent will be relayed to the user by the client, and response
-/// will be relayed back.
-pub struct Conv<'a>(&'a Inner);
+/// Use this to communicate with the user, if needed, beyond the standard
+/// things you can get/set with `get_user`/`get_authtok` and friends.
+/// The PAM client (i.e., the application that is logging in) will present
+/// the messages you send to the user and ask for responses.
+pub struct Conversation<'a>(&'a Inner);
 
-impl Conv<'_> {
-    /// Sends a message to the pam client.
+impl Conversation<'_> {
+    /// Sends a message to the PAM client.
     ///
     /// This will typically result in the user seeing a message or a prompt.
-    /// There are several message styles available:
+    /// For details, see what [MessageStyle]s are available.
     ///
-    /// - PAM_PROMPT_ECHO_OFF
-    /// - PAM_PROMPT_ECHO_ON
-    /// - PAM_ERROR_MSG
-    /// - PAM_TEXT_INFO
-    /// - PAM_RADIO_TYPE
-    /// - PAM_BINARY_PROMPT
-    ///
-    /// Note that the user experience will depend on how the client implements
-    /// these message styles - and not all applications implement all message
-    /// styles.
-    pub fn send(&self, style: MessageStyle, msg: &str) -> PamResult<Option<&CStr>> {
+    /// Note that the user experience will depend on how each style
+    /// is implemented by the client, and that not all clients
+    /// will implement all message styles.
+    pub fn send(&self, style: MessageStyle, msg: &str) -> Result<Option<&CStr>> {
         let mut resp_ptr: *const Response = ptr::null();
         let msg_cstr = CString::new(msg).unwrap();
         let msg = Message {
@@ -74,7 +71,7 @@
     }
 }
 
-impl Item for Conv<'_> {
+impl Item for Conversation<'_> {
     type Raw = Inner;
 
     fn type_id() -> crate::items::ItemType {
--- a/src/items.rs	Wed May 21 00:27:18 2025 -0400
+++ b/src/items.rs	Wed May 21 19:00:51 2025 -0400
@@ -1,37 +1,43 @@
+//! 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, CStr};
 
+/// Enum identifying what a `pam_get_item` return is.
+///
+/// Generally, you shouldn’t have to worry about this, and instead
+/// just use the various [Item] implementations.
 #[derive(FromPrimitive)]
 #[repr(i32)]
 #[non_exhaustive] // because C could give us anything!
 pub enum ItemType {
-    /// The service name
+    /// The PAM service name.
     Service = 1,
-    /// The user name
+    /// The user's login name.
     User = 2,
-    /// The tty name
+    /// The TTY name.
     Tty = 3,
-    /// The remote host name
+    /// The remote host (if applicable).
     RemoteHost = 4,
-    /// The pam_conv structure
+    /// The [crate::Conversation] structure.
     Conversation = 5,
-    /// The authentication token (password)
+    /// The authentication token (password).
     AuthTok = 6,
-    /// The old authentication token
+    /// The old authentication token (when changing passwords).
     OldAuthTok = 7,
-    /// The remote user name
+    /// The remote user's name.
     RemoteUser = 8,
-    /// the prompt for getting a username
+    /// The prompt shown when requesting a username.
     UserPrompt = 9,
-    /// app supplied function to override failure delays
+    /// App-supplied function to override failure delays.
     FailDelay = 10,
-    /// X :display name
+    /// X display name.
     XDisplay = 11,
-    /// X :server authentication data
+    /// X server authentication data.
     XAuthData = 12,
-    /// The type for pam_get_authtok
+    /// The type of `pam_get_authtok`.
     AuthTokType = 13,
 }
 
@@ -48,7 +54,7 @@
     }
 }
 
-/// A type that can be requested by [crate::Handle::get_item].
+/// A type that can be requested by [crate::PamHandle::get_item].
 pub trait Item {
     /// The `repr(C)` type that is returned (by pointer) by the underlying `pam_get_item` function.
     type Raw;
@@ -69,7 +75,8 @@
 
 macro_rules! cstr_item {
     ($name:ident) => {
-        ///A `CStr`-based item from a PAM conversation.
+        #[doc = concat!("The [ItemType::", stringify!($name), "]")]
+        #[doc = " item, represented as a [CStr]."]
         #[derive(Debug)]
         pub struct $name<'s>(pub &'s CStr);
 
@@ -98,11 +105,12 @@
     };
 }
 
+// Conversation is not included here since it's special.
+
 cstr_item!(Service);
 cstr_item!(User);
 cstr_item!(Tty);
 cstr_item!(RemoteHost);
-// Conversation is not included here since it's special.
 cstr_item!(AuthTok);
 cstr_item!(OldAuthTok);
 cstr_item!(RemoteUser);
--- a/src/lib.rs	Wed May 21 00:27:18 2025 -0400
+++ b/src/lib.rs	Wed May 21 19:00:51 2025 -0400
@@ -1,31 +1,38 @@
-//! Interface to the pluggable authentication module framework (PAM).
+//! A safe, nonstick interface to PAM.
+//!
+//! This implements a type-safe library to interact with PAM.
+//! Currently, it implements the subset of PAM useful for implementing a module.
+//!
+//! To write a new PAM module using this crate:
 //!
-//! The goal of this library is to provide a type-safe API that can be used to
-//! interact with PAM.  The library is incomplete - currently it supports
-//! a subset of functions for use in a pam authentication module.  A pam module
-//! is a shared library that is invoked to authenticate a user, or to perform
-//! other functions.
+//!  1. Create a `dylib` crate.
+//!  2. Implement a subset of the functions in the [PamModule] trait
+//!     corresponding to what you want your module to do.
+//!     In the simplest case (for a password-authentication system),
+//!     this will be the [PamModule::sm_authenticate] function.
+//!  3. Export your PAM module using the [pam_hooks!] macro.
+//!  4. Build and install the dynamic library.
+//!     This usually entails placing it at
+//!     `/usr/lib/security/pam_your_module.so`,
+//!     or maybe
+//!     <code>/usr/lib/<var>your-architecture</var>/security/pam_your_module.so</code>.
 //!
-//! For general information on writing pam modules, see
+//! For general information on writing PAM modules, see
 //! [The Linux-PAM Module Writers' Guide][module-guide]
 //!
 //! [module-guide]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/Linux-PAM_MWG.html
-//!
-//! A typical authentication module will define an external function called
-//! `pam_sm_authenticate()`, which will use functions in this library to
-//! interrogate the program that requested authentication for more information,
-//! and to render a result.
-//!
-//! Note that constants that are normally read from pam header files are
-//! hard-coded in the `constants` module.  The values there are taken from
-//! a Linux system.  That means that it might take some work to get this library
-//! to work on other platforms.
-
-extern crate libc;
 
 pub mod constants;
 pub mod conv;
 pub mod items;
-#[doc(hidden)]
-pub mod macros;
 pub mod module;
+
+mod memory;
+mod pam_ffi;
+
+#[doc(inline)]
+pub use crate::{
+    constants::{ErrorCode, Flags, MessageStyle, Result},
+    conv::Conversation,
+    module::{PamHandle, PamModule},
+};
--- a/src/macros.rs	Wed May 21 00:27:18 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,125 +0,0 @@
-/// Macro to generate the `extern "C"` entrypoint bindings needed by PAM
-///
-/// You can call `pam_hooks!(SomeType);` for any type that implements `PamHooks`
-///
-/// ## Examples:
-///
-/// Here is full example of a PAM module that would authenticate and authorize everybody:
-///
-/// ```
-/// #[macro_use] extern crate nonstick;
-///
-/// use nonstick::module::{PamHooks, PamHandle};
-/// use nonstick::constants::{PamResult, Flags};
-/// use std::ffi::CStr;
-///
-/// # fn main() {}
-/// struct MyPamModule;
-/// pam_hooks!(MyPamModule);
-///
-/// impl PamHooks for MyPamModule {
-///    fn acct_mgmt(pamh: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
-///        println!("Everybody is authorized!");
-///        Ok(())
-///    }
-///
-///    fn sm_authenticate(pamh: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
-///        println!("Everybody is authenticated!");
-///        Ok(())
-///    }
-/// }
-/// ```
-#[macro_export]
-macro_rules! pam_hooks {
-    ($ident:ident) => {
-        pub use self::pam_hooks_scope::*;
-        mod pam_hooks_scope {
-            use std::ffi::CStr;
-            use std::os::raw::{c_char, c_int};
-            use $crate::constants::{ErrorCode, Flags};
-            use $crate::module::{PamHandle, PamHooks};
-
-            fn extract_argv<'a>(argc: c_int, argv: *const *const c_char) -> Vec<&'a CStr> {
-                (0..argc)
-                    .map(|o| unsafe { CStr::from_ptr(*argv.offset(o as isize) as *const c_char) })
-                    .collect()
-            }
-
-            #[no_mangle]
-            pub extern "C" fn pam_sm_acct_mgmt(
-                pamh: &mut PamHandle,
-                flags: Flags,
-                argc: c_int,
-                argv: *const *const c_char,
-            ) -> c_int {
-                let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::acct_mgmt(pamh, args, flags))
-            }
-
-            #[no_mangle]
-            pub extern "C" fn pam_sm_authenticate(
-                pamh: &mut PamHandle,
-                flags: Flags,
-                argc: c_int,
-                argv: *const *const c_char,
-            ) -> c_int {
-                let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::sm_authenticate(pamh, args, flags))
-            }
-
-            #[no_mangle]
-            pub extern "C" fn pam_sm_chauthtok(
-                pamh: &mut PamHandle,
-                flags: Flags,
-                argc: c_int,
-                argv: *const *const c_char,
-            ) -> c_int {
-                let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::sm_chauthtok(pamh, args, flags))
-            }
-
-            #[no_mangle]
-            pub extern "C" fn pam_sm_close_session(
-                pamh: &mut PamHandle,
-                flags: Flags,
-                argc: c_int,
-                argv: *const *const c_char,
-            ) -> c_int {
-                let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::sm_close_session(pamh, args, flags))
-            }
-
-            #[no_mangle]
-            pub extern "C" fn pam_sm_open_session(
-                pamh: &mut PamHandle,
-                flags: Flags,
-                argc: c_int,
-                argv: *const *const c_char,
-            ) -> c_int {
-                let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::sm_open_session(pamh, args, flags))
-            }
-
-            #[no_mangle]
-            pub extern "C" fn pam_sm_setcred(
-                pamh: &mut PamHandle,
-                flags: Flags,
-                argc: c_int,
-                argv: *const *const c_char,
-            ) -> c_int {
-                let args = extract_argv(argc, argv);
-                ErrorCode::result_to_c(super::$ident::sm_setcred(pamh, args, flags))
-            }
-        }
-    };
-}
-
-#[cfg(test)]
-pub mod test {
-    use crate::module::PamHooks;
-
-    struct Foo;
-    impl PamHooks for Foo {}
-
-    pam_hooks!(Foo);
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/memory.rs	Wed May 21 19:00:51 2025 -0400
@@ -0,0 +1,35 @@
+//! Utility functions for dealing with memory copying and stuff.
+
+use crate::ErrorCode;
+use libc::c_char;
+use std::ffi::{CStr, CString};
+
+/// Safely converts a `&str` option to a `CString` option.
+pub fn option_cstr(prompt: Option<&str>) -> crate::Result<Option<CString>> {
+    prompt
+        .map(CString::new)
+        .transpose()
+        .map_err(|_| ErrorCode::ConversationError)
+}
+
+/// Gets the pointer to the given CString, or a null pointer if absent.
+pub fn prompt_ptr(prompt: Option<&CString>) -> *const c_char {
+    match prompt {
+        Some(c_str) => c_str.as_ptr(),
+        None => std::ptr::null(),
+    }
+}
+
+/// Creates an owned copy of a string that is returned from a
+/// <code>pam_get_<var>whatever</var></code> function.
+pub fn copy_pam_string(result_ptr: *const c_char) -> crate::Result<String> {
+    // We really shouldn't get a null pointer back here, but if we do, return nothing.
+    if result_ptr.is_null() {
+        return Ok(String::new());
+    }
+    let bytes = unsafe { CStr::from_ptr(result_ptr) };
+    bytes
+        .to_str()
+        .map(String::from)
+        .map_err(|_| ErrorCode::ConversationError)
+}
--- a/src/module.rs	Wed May 21 00:27:18 2025 -0400
+++ b/src/module.rs	Wed May 21 19:00:51 2025 -0400
@@ -1,73 +1,28 @@
-//! Functions for use in pam modules.
+//! Functions and types useful for implementing a PAM module.
 
-use crate::constants::{ErrorCode, Flags, PamResult};
+use crate::constants::{ErrorCode, Flags, Result};
 use crate::items::{Item, ItemType};
+use crate::memory;
 use libc::c_char;
 use secure_string::SecureString;
 use std::ffi::{c_int, CStr, CString};
 
-/// Opaque type, used as a pointer when making pam API calls.
-///
-/// A module is invoked via an external function such as `pam_sm_authenticate`.
-/// Such a call provides a pam handle pointer.  The same pointer should be given
-/// as an argument when making API calls.
-#[repr(C)]
-pub struct PamHandle {
-    _data: [u8; 0],
-}
-
-#[link(name = "pam")]
-extern "C" {
-    fn pam_get_data(
-        pamh: *const PamHandle,
-        module_data_name: *const c_char,
-        data: &mut *const libc::c_void,
-    ) -> c_int;
-
-    fn pam_set_data(
-        pamh: *const PamHandle,
-        module_data_name: *const c_char,
-        data: *const libc::c_void,
-        cleanup: extern "C" fn(
-            pamh: *const PamHandle,
-            data: *mut libc::c_void,
-            error_status: c_int,
-        ),
-    ) -> c_int;
-
-    fn pam_get_item(
-        pamh: *const PamHandle,
-        item_type: c_int,
-        item: &mut *const libc::c_void,
-    ) -> c_int;
-
-    fn pam_set_item(pamh: *mut PamHandle, item_type: c_int, item: *const libc::c_void) -> c_int;
-
-    fn pam_get_user(
-        pamh: *const PamHandle,
-        user: &mut *const c_char,
-        prompt: *const c_char,
-    ) -> c_int;
-
-    fn pam_get_authtok(
-        pamh: *const PamHandle,
-        item_type: c_int,
-        data: &mut *const c_char,
-        prompt: *const c_char,
-    ) -> c_int;
-
-}
+use crate::pam_ffi;
 
 /// 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 cleanup<T>(_: *const PamHandle, c_data: *mut libc::c_void, _: c_int) {
+extern "C" fn cleanup<T>(_: *const libc::c_void, c_data: *mut libc::c_void, _: c_int) {
     unsafe {
         let _data: Box<T> = Box::from_raw(c_data.cast());
     }
 }
 
+/// An opaque structure pointing to a PAM handle.
+#[repr(transparent)]
+pub struct PamHandle(*mut libc::c_void);
+
 impl PamHandle {
     /// Gets some value, identified by `key`, that has been set by the module
     /// previously.
@@ -75,20 +30,16 @@
     /// See the [`pam_get_data` manual page](
     /// https://www.man7.org/linux/man-pages/man3/pam_get_data.3.html).
     ///
-    /// # Errors
-    ///
-    /// Returns an error if the underlying PAM function call fails.
-    ///
     /// # Safety
     ///
     /// The data stored under the provided key must be of type `T` otherwise the
     /// behaviour of this function is undefined.
     ///
     /// The data, if present, is owned by the current PAM conversation.
-    pub unsafe fn get_data<T>(&self, key: &str) -> PamResult<Option<&T>> {
+    pub unsafe fn get_data<T>(&self, key: &str) -> Result<Option<&T>> {
         let c_key = CString::new(key).map_err(|_| ErrorCode::ConversationError)?;
         let mut ptr: *const libc::c_void = std::ptr::null();
-        ErrorCode::result_from(pam_get_data(self, c_key.as_ptr(), &mut ptr))?;
+        ErrorCode::result_from(pam_ffi::pam_get_data(self.0, c_key.as_ptr(), &mut ptr))?;
         match ptr.is_null() {
             true => Ok(None),
             false => {
@@ -103,15 +54,11 @@
     ///
     /// See the [`pam_set_data` manual page](
     /// https://www.man7.org/linux/man-pages/man3/pam_set_data.3.html).
-    ///
-    /// # Errors
-    ///
-    /// Returns an error if the underlying PAM function call fails.
-    pub fn set_data<T>(&mut self, key: &str, data: Box<T>) -> PamResult<()> {
+    pub fn set_data<T>(&mut self, key: &str, data: Box<T>) -> Result<()> {
         let c_key = CString::new(key).map_err(|_| ErrorCode::ConversationError)?;
         let ret = unsafe {
-            pam_set_data(
-                self,
+            pam_ffi::pam_set_data(
+                self.0,
                 c_key.as_ptr(),
                 Box::into_raw(data).cast(),
                 cleanup::<T>,
@@ -128,14 +75,10 @@
     ///
     /// See the [`pam_get_item` manual page](
     /// https://www.man7.org/linux/man-pages/man3/pam_get_item.3.html).
-    ///
-    /// # Errors
-    ///
-    /// Returns an error if the underlying PAM function call fails.
-    pub fn get_item<T: crate::items::Item>(&self) -> PamResult<Option<T>> {
+    pub fn get_item<T: crate::items::Item>(&self) -> Result<Option<T>> {
         let mut ptr: *const libc::c_void = std::ptr::null();
         let out = unsafe {
-            let ret = pam_get_item(self, T::type_id().into(), &mut ptr);
+            let ret = pam_ffi::pam_get_item(self.0, T::type_id().into(), &mut ptr);
             ErrorCode::result_from(ret)?;
             let typed_ptr: *const T::Raw = ptr.cast();
             match typed_ptr.is_null() {
@@ -150,12 +93,9 @@
     ///
     /// See the [`pam_set_item` manual page](
     /// https://www.man7.org/linux/man-pages/man3/pam_set_item.3.html).
-    ///
-    /// # Errors
-    ///
-    /// Returns an error if the underlying PAM function call fails.
-    pub fn set_item<T: Item>(&mut self, item: T) -> PamResult<()> {
-        let ret = unsafe { pam_set_item(self, T::type_id().into(), item.into_raw().cast()) };
+    pub fn set_item<T: Item>(&mut self, item: T) -> Result<()> {
+        let ret =
+            unsafe { pam_ffi::pam_set_item(self.0, T::type_id().into(), item.into_raw().cast()) };
         ErrorCode::result_from(ret)
     }
 
@@ -165,16 +105,14 @@
     ///
     /// See the [`pam_get_user` manual page](
     /// https://www.man7.org/linux/man-pages/man3/pam_get_user.3.html).
-    ///
-    /// # Errors
-    ///
-    /// Returns an error if the underlying PAM function call fails.
-    pub fn get_user(&self, prompt: Option<&str>) -> PamResult<String> {
-        let prompt = option_cstr(prompt)?;
+    pub fn get_user(&self, prompt: Option<&str>) -> Result<String> {
+        let prompt = memory::option_cstr(prompt)?;
         let mut output: *const c_char = std::ptr::null_mut();
-        let ret = unsafe { pam_get_user(self, &mut output, prompt_ptr(prompt.as_ref())) };
+        let ret = unsafe {
+            pam_ffi::pam_get_user(self.0, &mut output, memory::prompt_ptr(prompt.as_ref()))
+        };
         ErrorCode::result_from(ret)?;
-        copy_pam_string(output)
+        memory::copy_pam_string(output)
     }
 
     /// Retrieves the authentication token from the user.
@@ -183,75 +121,54 @@
     ///
     /// See the [`pam_get_authtok` manual page](
     /// https://www.man7.org/linux/man-pages/man3/pam_get_authtok.3.html).
-    ///
-    /// # Errors
-    ///
-    /// Returns an error if the underlying PAM function call fails.
-    pub fn get_authtok(&self, prompt: Option<&str>) -> PamResult<SecureString> {
-        let prompt = option_cstr(prompt)?;
+    pub fn get_authtok(&self, prompt: Option<&str>) -> Result<SecureString> {
+        let prompt = memory::option_cstr(prompt)?;
         let mut output: *const c_char = std::ptr::null_mut();
         let res = unsafe {
-            pam_get_authtok(
-                self,
+            pam_ffi::pam_get_authtok(
+                self.0,
                 ItemType::AuthTok.into(),
                 &mut output,
-                prompt_ptr(prompt.as_ref()),
+                memory::prompt_ptr(prompt.as_ref()),
             )
         };
         ErrorCode::result_from(res)?;
-        copy_pam_string(output).map(SecureString::from)
+        memory::copy_pam_string(output).map(SecureString::from)
     }
 }
 
-/// Safely converts a `&str` option to a `CString` option.
-fn option_cstr(prompt: Option<&str>) -> PamResult<Option<CString>> {
-    prompt
-        .map(CString::new)
-        .transpose()
-        .map_err(|_| ErrorCode::ConversationError)
-}
-
-/// The pointer to the prompt CString, or null if absent.
-pub(crate) fn prompt_ptr(prompt: Option<&CString>) -> *const c_char {
-    match prompt {
-        Some(c_str) => c_str.as_ptr(),
-        None => std::ptr::null(),
+impl From<*mut libc::c_void> for PamHandle {
+    /// Wraps an internal Handle pointer.
+    fn from(value: *mut libc::c_void) -> Self {
+        Self(value)
     }
 }
 
-/// Creates an owned copy of a string that is returned from a
-/// <code>pam_get_<var>whatever</var></code> function.
-pub(crate) fn copy_pam_string(result_ptr: *const c_char) -> PamResult<String> {
-    // We really shouldn't get a null pointer back here, but if we do, return nothing.
-    if result_ptr.is_null() {
-        return Ok(String::new());
-    }
-    let bytes = unsafe { CStr::from_ptr(result_ptr) };
-    bytes
-        .to_str()
-        .map(String::from)
-        .map_err(|_| ErrorCode::ConversationError)
-}
-
-/// Provides functions that are invoked by the entrypoints generated by the
-/// [`pam_hooks!` macro](../macro.pam_hooks.html).
+/// Trait representing what a PAM module can do.
+///
+/// By default, all the functions in this trait are ignored.
+/// Implement any functions you wish to handle in your module.
+/// After implementing this trait, use the [crate::pam_hooks!] macro
+/// to export your functions.
 ///
-/// All hooks are ignored by PAM dispatch by default given the default return value of `PAM_IGNORE`.
-/// Override any functions that you want to handle with your module. See [PAM’s root manual page](
-/// https://www.man7.org/linux/man-pages/man3/pam.3.html).
+/// For more information, see [`pam(3)`’s root manual page][manpage]
+/// and the [PAM Module Writer’s Guide][module-guide].
+///
+/// [manpage]: https://www.man7.org/linux/man-pages/man3/pam.3.html
+/// [module-guide]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/Linux-PAM_MWG.html
 #[allow(unused_variables)]
-pub trait PamHooks {
+pub trait PamModule {
     /// This function performs the task of establishing whether the user is permitted to gain access at
     /// this time. It should be understood that the user has previously been validated by an
     /// authentication module. This function checks for other things. Such things might be: the time of
     /// day or the date, the terminal line, remote hostname, etc. This function may also determine
     /// things like the expiration on passwords, and respond that the user change it before continuing.
-    fn acct_mgmt(handle: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
+    fn acct_mgmt(handle: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> Result<()> {
         Err(ErrorCode::Ignore)
     }
 
     /// This function performs the task of authenticating the user.
-    fn sm_authenticate(handle: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
+    fn sm_authenticate(handle: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> Result<()> {
         Err(ErrorCode::Ignore)
     }
 
@@ -261,17 +178,17 @@
     /// `PAM_PRELIM_CHECK` and then, if the module does not return `PAM_TRY_AGAIN`, subsequently with
     /// `PAM_UPDATE_AUTHTOK`. It is only on the second call that the authorization token is
     /// (possibly) changed.
-    fn sm_chauthtok(handle: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
+    fn sm_chauthtok(handle: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> Result<()> {
         Err(ErrorCode::Ignore)
     }
 
     /// This function is called to terminate a session.
-    fn sm_close_session(handle: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
+    fn sm_close_session(handle: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> Result<()> {
         Err(ErrorCode::Ignore)
     }
 
     /// This function is called to commence a session.
-    fn sm_open_session(handle: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
+    fn sm_open_session(handle: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> Result<()> {
         Err(ErrorCode::Ignore)
     }
 
@@ -280,7 +197,148 @@
     /// information about a user than their authentication token. This function is used to make such
     /// information available to the application. It should only be called after the user has been
     /// authenticated but before a session has been established.
-    fn sm_setcred(handle: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
+    fn sm_setcred(handle: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> Result<()> {
         Err(ErrorCode::Ignore)
     }
 }
+
+/// Generates the dynamic library entry points for a [PamModule] implementation.
+///
+/// Calling `pam_hooks!(SomeType)` on a type that implements [PamModule] will
+/// generate the exported `extern "C"` functions that PAM uses to call into
+/// your module.
+///
+/// ## Examples:
+///
+/// Here is full example of a PAM module that would authenticate and authorize everybody:
+///
+/// ```
+/// use nonstick::{Flags, PamHandle, PamModule, Result as PamResult, pam_hooks};
+/// use std::ffi::CStr;
+///
+/// # fn main() {}
+/// struct MyPamModule;
+/// pam_hooks!(MyPamModule);
+///
+/// impl PamModule for MyPamModule {
+///     fn acct_mgmt(pamh: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
+///         // You should use a Conversation to communicate with the user
+///         // instead of writing to the console, but this is just an example.
+///         eprintln!("Everybody is authorized!");
+///         Ok(())
+///     }
+///
+///     fn sm_authenticate(pamh: &mut PamHandle, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
+///         eprintln!("Everybody is authenticated!");
+///         Ok(())
+///     }
+/// }
+/// ```
+#[macro_export]
+macro_rules! pam_hooks {
+    ($ident:ident) => {
+        mod _pam_hooks_scope {
+            use std::ffi::{c_char, c_int, CStr};
+            use $crate::{ErrorCode, Flags, PamModule};
+
+            #[no_mangle]
+            extern "C" fn pam_sm_acct_mgmt(
+                pamh: *mut libc::c_void,
+                flags: Flags,
+                argc: c_int,
+                argv: *const *const c_char,
+            ) -> c_int {
+                let args = extract_argv(argc, argv);
+                ErrorCode::result_to_c(super::$ident::acct_mgmt(&mut pamh.into(), args, flags))
+            }
+
+            #[no_mangle]
+            extern "C" fn pam_sm_authenticate(
+                pamh: *mut libc::c_void,
+                flags: Flags,
+                argc: c_int,
+                argv: *const *const c_char,
+            ) -> c_int {
+                let args = extract_argv(argc, argv);
+                ErrorCode::result_to_c(super::$ident::sm_authenticate(
+                    &mut pamh.into(),
+                    args,
+                    flags,
+                ))
+            }
+
+            #[no_mangle]
+            extern "C" fn pam_sm_chauthtok(
+                pamh: *mut libc::c_void,
+                flags: Flags,
+                argc: c_int,
+                argv: *const *const c_char,
+            ) -> c_int {
+                let args = extract_argv(argc, argv);
+                ErrorCode::result_to_c(super::$ident::sm_chauthtok(&mut pamh.into(), args, flags))
+            }
+
+            #[no_mangle]
+            extern "C" fn pam_sm_close_session(
+                pamh: *mut libc::c_void,
+                flags: Flags,
+                argc: c_int,
+                argv: *const *const c_char,
+            ) -> c_int {
+                let args = extract_argv(argc, argv);
+                ErrorCode::result_to_c(super::$ident::sm_close_session(
+                    &mut pamh.into(),
+                    args,
+                    flags,
+                ))
+            }
+
+            #[no_mangle]
+            extern "C" fn pam_sm_open_session(
+                pamh: *mut libc::c_void,
+                flags: Flags,
+                argc: c_int,
+                argv: *const *const c_char,
+            ) -> c_int {
+                let args = extract_argv(argc, argv);
+                ErrorCode::result_to_c(super::$ident::sm_open_session(
+                    &mut pamh.into(),
+                    args,
+                    flags,
+                ))
+            }
+
+            #[no_mangle]
+            extern "C" fn pam_sm_setcred(
+                pamh: *mut libc::c_void,
+                flags: Flags,
+                argc: c_int,
+                argv: *const *const c_char,
+            ) -> c_int {
+                let args = extract_argv(argc, argv);
+                ErrorCode::result_to_c(super::$ident::sm_setcred(&mut pamh.into(), args, flags))
+            }
+
+            /// Turns `argc`/`argv` into a [Vec] of [CStr]s.
+            ///
+            /// # Safety
+            ///
+            /// We use this only with arguments we get from `libpam`, which we kind of have to trust.
+            fn extract_argv<'a>(argc: c_int, argv: *const *const c_char) -> Vec<&'a CStr> {
+                (0..argc)
+                    .map(|o| unsafe { CStr::from_ptr(*argv.offset(o as isize) as *const c_char) })
+                    .collect()
+            }
+        }
+    };
+}
+
+#[cfg(test)]
+pub mod test {
+    use crate::module::PamModule;
+
+    struct Foo;
+    impl PamModule for Foo {}
+
+    pam_hooks!(Foo);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pam_ffi.rs	Wed May 21 19:00:51 2025 -0400
@@ -0,0 +1,49 @@
+//! Functions exported by the PAM FFI.
+
+use libc::c_char;
+use std::ffi::c_int;
+
+#[link(name = "pam")]
+extern "C" {
+    pub fn pam_get_data(
+        pamh: *const libc::c_void,
+        module_data_name: *const c_char,
+        data: &mut *const libc::c_void,
+    ) -> c_int;
+
+    pub fn pam_set_data(
+        pamh: *mut libc::c_void,
+        module_data_name: *const c_char,
+        data: *const libc::c_void,
+        cleanup: extern "C" fn(
+            pamh: *const libc::c_void,
+            data: *mut libc::c_void,
+            error_status: c_int,
+        ),
+    ) -> c_int;
+
+    pub fn pam_get_item(
+        pamh: *const libc::c_void,
+        item_type: c_int,
+        item: &mut *const libc::c_void,
+    ) -> c_int;
+
+    pub fn pam_set_item(
+        pamh: *mut libc::c_void,
+        item_type: c_int,
+        item: *const libc::c_void,
+    ) -> c_int;
+
+    pub fn pam_get_user(
+        pamh: *const libc::c_void,
+        user: &mut *const c_char,
+        prompt: *const c_char,
+    ) -> c_int;
+
+    pub fn pam_get_authtok(
+        pamh: *const libc::c_void,
+        item_type: c_int,
+        data: &mut *const c_char,
+        prompt: *const c_char,
+    ) -> c_int;
+}