Mercurial > crates > nonstick
diff src/conv.rs @ 70:9f8381a1c09c
Implement low-level conversation primitives.
This change does two primary things:
1. Introduces new Conversation traits, to be implemented both
by the library and by PAM client applications.
2. Builds the memory-management infrastructure for passing messages
through the conversation.
...and it adds tests for both of the above, including ASAN tests.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Tue, 03 Jun 2025 01:21:59 -0400 |
parents | 8f3ae0c7ab92 |
children | 58f9d2a4df38 |
line wrap: on
line diff
--- a/src/conv.rs Sun Jun 01 01:15:04 2025 -0400 +++ b/src/conv.rs Tue Jun 03 01:21:59 2025 -0400 @@ -1,9 +1,156 @@ //! The PAM conversation and associated Stuff. -use crate::pam_ffi::{BinaryResponseInner, NulError, TextResponseInner}; -use std::num::TryFromIntError; -use std::ops::Deref; +// Temporarily allowed until we get the actual conversation functions hooked up. +#![allow(dead_code)] + +use crate::constants::{NulError, Result, TooBigError}; +use crate::pam_ffi::{BinaryResponseInner, GenericResponse, TextResponseInner}; +use secure_string::SecureString; +use std::mem; use std::result::Result as StdResult; +use std::str::Utf8Error; + +// 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 +// trait definitions/implementations into a HUGE MESS. +// +// Ideally, we would be using some kind of `TD: TextData` and `BD: BinaryData` +// 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(Debug)] +pub enum Message<'a> { + /// Requests information from the user; will be masked when typing. + /// + /// Response: [`Response::MaskedText`] + MaskedPrompt(&'a str), + /// Requests information from the user; will not be masked. + /// + /// Response: [`Response::Text`] + Prompt(&'a str), + /// "Yes/No/Maybe conditionals" (a Linux-PAM extension). + /// + /// Response: [`Response::Text`] + /// (Linux-PAM documentation doesn't define its contents.) + RadioPrompt(&'a str), + /// Raises an error message to the user. + /// + /// Response: [`Response::NoResponse`] + Error(&'a str), + /// Sends an informational message to the user. + /// + /// Response: [`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: [`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 { + /// Used to fill in list entries where there is no response expected. + /// + /// Used in response to: + /// + /// - [`Error`](Message::Error) + /// - [`Info`](Message::Info) + NoResponse, + /// A response with text data from the user. + /// + /// Used in response to: + /// + /// - [`Prompt`](Message::Prompt) + /// - [`RadioPrompt`](Message::RadioPrompt) (a Linux-PAM extension) + Text(String), + /// A response to a masked request with text data from the user. + /// + /// Used in response to: + /// + /// - [`MaskedPrompt`](Message::MaskedPrompt) + MaskedText(SecureString), + /// A response to a binary request (a Linux-PAM extension). + /// + /// Used in response to: + /// + /// - [`BinaryPrompt`](Message::BinaryPrompt) + Binary(BinaryData), +} + +/// A channel for PAM modules to request information from the user. +/// +/// This trait is used by both applications and PAM modules: +/// +/// - Applications implement Conversation and provide a user interface +/// to allow the user to respond to PAM questions. +/// - Modules call a Conversation implementation to request information +/// or send information to the user. +pub trait Conversation { + /// Sends messages to the user. + /// + /// The returned Vec of messages always contains exactly as many entries + /// as there were messages in the request; one corresponding to each. + /// + /// Messages with no response (e.g. [info](Message::Info) and + /// [error](Message::Error)) will have a `None` entry instead of a `Response`. + fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>>; +} + +/// Trait that an application can implement if they want to handle messages +/// one at a time. +pub trait DemuxedConversation { + /// Prompts the user for some text. + fn prompt(&mut self, request: &str) -> Result<String>; + /// Prompts the user for some text, but hides their typing. + fn masked_prompt(&mut self, request: &str) -> Result<SecureString>; + /// Prompts the user for a radio option (a Linux-PAM extension). + /// + /// The Linux-PAM documentation doesn't give the format of the response. + fn radio_prompt(&mut self, request: &str) -> Result<String>; + /// 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<BinaryData>; +} + +impl<D: DemuxedConversation> Conversation for D { + fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>> { + messages + .iter() + .map(|msg| match *msg { + Message::Prompt(prompt) => self.prompt(prompt).map(Response::from), + Message::MaskedPrompt(prompt) => self.masked_prompt(prompt).map(Response::from), + Message::RadioPrompt(prompt) => self.radio_prompt(prompt).map(Response::from), + Message::Info(message) => { + self.info(message); + Ok(Response::NoResponse) + } + Message::Error(message) => { + self.error(message); + Ok(Response::NoResponse) + } + Message::BinaryPrompt { data_type, data } => { + self.binary_prompt(data, data_type).map(Response::from) + } + }) + .collect() + } +} /// An owned text response to a PAM conversation. /// @@ -12,17 +159,24 @@ struct TextResponse(*mut TextResponseInner); impl TextResponse { - /// Creates a text response. + /// Allocates a new response with the given text. + /// + /// A copy of the provided text will be allocated on the C heap. pub fn new(text: impl AsRef<str>) -> StdResult<Self, NulError> { TextResponseInner::alloc(text).map(Self) } -} -impl Deref for TextResponse { - type Target = TextResponseInner; - fn deref(&self) -> &Self::Target { - // SAFETY: We allocated this ourselves, or it was provided by PAM. - unsafe { &*self.0 } + /// Converts this into a GenericResponse. + fn generic(self) -> *mut GenericResponse { + let ret = self.0 as *mut GenericResponse; + mem::forget(self); + ret + } + + /// Gets the string data, if possible. + pub fn as_str(&self) -> StdResult<&str, Utf8Error> { + // SAFETY: We allocated this ourselves or got it back from PAM. + unsafe { &*self.0 }.contents().to_str() } } @@ -38,20 +192,33 @@ /// /// It points to a value on the C heap. #[repr(C)] -struct BinaryResponse(*mut BinaryResponseInner); +pub struct BinaryResponse(pub(super) *mut BinaryResponseInner); impl BinaryResponse { /// Creates a binary response with the given data. - pub fn new(data: impl AsRef<[u8]>, data_type: u8) -> StdResult<Self, TryFromIntError> { + /// + /// A copy of the data will be made and allocated on the C heap. + pub fn new(data: &[u8], data_type: u8) -> StdResult<Self, TooBigError> { BinaryResponseInner::alloc(data, data_type).map(Self) } -} + + /// Converts this into a GenericResponse. + fn generic(self) -> *mut GenericResponse { + let ret = self.0 as *mut GenericResponse; + mem::forget(self); + ret + } -impl Deref for BinaryResponse { - type Target = BinaryResponseInner; - fn deref(&self) -> &Self::Target { - // SAFETY: We allocated this ourselves, or it was provided by PAM. - unsafe { &*self.0 } + /// The data type we point to. + pub fn data_type(&self) -> u8 { + // SAFETY: We allocated this ourselves or got it back from PAM. + unsafe { &*self.0 }.data_type() + } + + /// The data we point to. + pub fn data(&self) -> &[u8] { + // SAFETY: We allocated this ourselves or got it back from PAM. + unsafe { &*self.0 }.contents() } } @@ -63,19 +230,165 @@ } } +/// Owned binary data. +#[derive(Debug, PartialEq)] +pub struct BinaryData { + data: Vec<u8>, + data_type: u8, +} + +impl BinaryData { + pub fn new(data: Vec<u8>, data_type: u8) -> Self { + Self { data, data_type } + } + pub fn data(&self) -> &[u8] { + &self.data + } + pub fn data_type(&self) -> u8 { + self.data_type + } +} + +impl From<BinaryResponse> for BinaryData { + /// Copies the data onto the Rust heap. + fn from(value: BinaryResponse) -> Self { + Self { + data: value.data().to_vec(), + data_type: value.data_type(), + } + } +} + +impl From<BinaryData> for Vec<u8> { + /// Extracts the inner vector from the BinaryData. + fn from(value: BinaryData) -> Self { + value.data + } +} + #[cfg(test)] mod test { - use super::{BinaryResponse, TextResponse}; + use super::{ + BinaryResponse, Conversation, DemuxedConversation, Message, Response, SecureString, + TextResponse, + }; + use crate::constants::ErrorCode; + use crate::pam_ffi::GenericResponse; + + #[test] + fn test_demux() { + #[derive(Default)] + struct DemuxTester { + error_ran: bool, + info_ran: bool, + } + + impl DemuxedConversation for DemuxTester { + fn prompt(&mut self, request: &str) -> crate::Result<String> { + match request { + "what" => Ok("whatwhat".to_owned()), + "give_err" => Err(ErrorCode::PermissionDenied), + _ => panic!("unexpected prompt!"), + } + } + fn masked_prompt(&mut self, request: &str) -> crate::Result<SecureString> { + assert_eq!("reveal", request); + Ok(SecureString::from("my secrets")) + } + fn radio_prompt(&mut self, request: &str) -> crate::Result<String> { + assert_eq!("channel?", request); + Ok("zero".to_owned()) + } + fn error(&mut self, message: &str) { + self.error_ran = true; + assert_eq!("whoopsie", message); + } + fn info(&mut self, message: &str) { + self.info_ran = true; + assert_eq!("did you know", message); + } + fn binary_prompt( + &mut self, + data: &[u8], + data_type: u8, + ) -> crate::Result<super::BinaryData> { + assert_eq!(&[10, 9, 8], data); + assert_eq!(66, data_type); + Ok(super::BinaryData::new(vec![5, 5, 5], 5)) + } + } + + let mut tester = DemuxTester::default(); + + assert_eq!( + vec![ + Response::Text("whatwhat".to_owned()), + Response::MaskedText("my secrets".into()), + Response::NoResponse, + Response::NoResponse, + ], + tester + .send(&[ + Message::Prompt("what"), + Message::MaskedPrompt("reveal"), + Message::Error("whoopsie"), + Message::Info("did you know"), + ]) + .unwrap() + ); + assert!(tester.error_ran); + assert!(tester.info_ran); + + assert_eq!( + ErrorCode::PermissionDenied, + tester.send(&[Message::Prompt("give_err")]).unwrap_err(), + ); + + // Test the Linux-PAM extensions separately. + + assert_eq!( + vec![ + Response::Text("zero".to_owned()), + Response::Binary(super::BinaryData::new(vec![5, 5, 5], 5)), + ], + tester + .send(&[ + Message::RadioPrompt("channel?"), + Message::BinaryPrompt { + data: &[10, 9, 8], + data_type: 66 + }, + ]) + .unwrap() + ); + } + + // The below tests are used in conjunction with ASAN to verify + // that we correctly clean up all our memory. #[test] fn test_text_response() { let resp = TextResponse::new("it's a-me!").unwrap(); - assert_eq!("it's a-me!", resp.contents().to_str().unwrap()); + assert_eq!("it's a-me!", resp.as_str().unwrap()); } + #[test] fn test_binary_response() { let data = [123, 210, 55]; let resp = BinaryResponse::new(&data, 99).unwrap(); - assert_eq!(&data, resp.contents()); + assert_eq!(data, resp.data()); + assert_eq!(99, resp.data_type()); + } + + #[test] + fn test_to_generic() { + let text = TextResponse::new("oh no").unwrap(); + let text = text.generic(); + let binary = BinaryResponse::new(&[], 33).unwrap(); + let binary = binary.generic(); + unsafe { + GenericResponse::free(text); + GenericResponse::free(binary); + } } }