Mercurial > crates > nonstick
view 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 source
//! The PAM conversation and associated Stuff. // 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. /// /// It points to a value on the C heap. #[repr(C)] struct TextResponse(*mut TextResponseInner); impl TextResponse { /// 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) } /// 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() } } impl Drop for TextResponse { /// Frees an owned response. fn drop(&mut self) { // SAFETY: We allocated this ourselves, or it was provided by PAM. unsafe { TextResponseInner::free(self.0) } } } /// An owned binary response to a PAM conversation. /// /// It points to a value on the C heap. #[repr(C)] pub struct BinaryResponse(pub(super) *mut BinaryResponseInner); impl BinaryResponse { /// Creates a binary response with the given data. /// /// 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 } /// 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() } } impl Drop for BinaryResponse { /// Frees an owned response. fn drop(&mut self) { // SAFETY: We allocated this ourselves, or it was provided by PAM. unsafe { BinaryResponseInner::free(self.0) } } } /// 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, 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.as_str().unwrap()); } #[test] fn test_binary_response() { let data = [123, 210, 55]; let resp = BinaryResponse::new(&data, 99).unwrap(); 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); } } }