Mercurial > crates > nonstick
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 69:8f3ae0c7ab92 | 70:9f8381a1c09c |
|---|---|
| 1 //! The PAM conversation and associated Stuff. | 1 //! The PAM conversation and associated Stuff. |
| 2 | 2 |
| 3 use crate::pam_ffi::{BinaryResponseInner, NulError, TextResponseInner}; | 3 // Temporarily allowed until we get the actual conversation functions hooked up. |
| 4 use std::num::TryFromIntError; | 4 #![allow(dead_code)] |
| 5 use std::ops::Deref; | 5 |
| 6 use crate::constants::{NulError, Result, TooBigError}; | |
| 7 use crate::pam_ffi::{BinaryResponseInner, GenericResponse, TextResponseInner}; | |
| 8 use secure_string::SecureString; | |
| 9 use std::mem; | |
| 6 use std::result::Result as StdResult; | 10 use std::result::Result as StdResult; |
| 11 use std::str::Utf8Error; | |
| 12 | |
| 13 // TODO: In most cases, we should be passing around references to strings | |
| 14 // or binary data. Right now we don't because that turns type inference and | |
| 15 // trait definitions/implementations into a HUGE MESS. | |
| 16 // | |
| 17 // Ideally, we would be using some kind of `TD: TextData` and `BD: BinaryData` | |
| 18 // associated types in the various Conversation traits to avoid copying | |
| 19 // when unnecessary. | |
| 20 | |
| 21 /// The types of message and request that can be sent to a user. | |
| 22 /// | |
| 23 /// The data within each enum value is the prompt (or other information) | |
| 24 /// that will be presented to the user. | |
| 25 #[derive(Debug)] | |
| 26 pub enum Message<'a> { | |
| 27 /// Requests information from the user; will be masked when typing. | |
| 28 /// | |
| 29 /// Response: [`Response::MaskedText`] | |
| 30 MaskedPrompt(&'a str), | |
| 31 /// Requests information from the user; will not be masked. | |
| 32 /// | |
| 33 /// Response: [`Response::Text`] | |
| 34 Prompt(&'a str), | |
| 35 /// "Yes/No/Maybe conditionals" (a Linux-PAM extension). | |
| 36 /// | |
| 37 /// Response: [`Response::Text`] | |
| 38 /// (Linux-PAM documentation doesn't define its contents.) | |
| 39 RadioPrompt(&'a str), | |
| 40 /// Raises an error message to the user. | |
| 41 /// | |
| 42 /// Response: [`Response::NoResponse`] | |
| 43 Error(&'a str), | |
| 44 /// Sends an informational message to the user. | |
| 45 /// | |
| 46 /// Response: [`Response::NoResponse`] | |
| 47 Info(&'a str), | |
| 48 /// Requests binary data from the client (a Linux-PAM extension). | |
| 49 /// | |
| 50 /// This is used for non-human or non-keyboard prompts (security key?). | |
| 51 /// NOT part of the X/Open PAM specification. | |
| 52 /// | |
| 53 /// Response: [`Response::Binary`] | |
| 54 BinaryPrompt { | |
| 55 /// Some binary data. | |
| 56 data: &'a [u8], | |
| 57 /// A "type" that you can use for signalling. Has no strict definition in PAM. | |
| 58 data_type: u8, | |
| 59 }, | |
| 60 } | |
| 61 | |
| 62 /// The responses that PAM will return from a request. | |
| 63 #[derive(Debug, PartialEq, derive_more::From)] | |
| 64 pub enum Response { | |
| 65 /// Used to fill in list entries where there is no response expected. | |
| 66 /// | |
| 67 /// Used in response to: | |
| 68 /// | |
| 69 /// - [`Error`](Message::Error) | |
| 70 /// - [`Info`](Message::Info) | |
| 71 NoResponse, | |
| 72 /// A response with text data from the user. | |
| 73 /// | |
| 74 /// Used in response to: | |
| 75 /// | |
| 76 /// - [`Prompt`](Message::Prompt) | |
| 77 /// - [`RadioPrompt`](Message::RadioPrompt) (a Linux-PAM extension) | |
| 78 Text(String), | |
| 79 /// A response to a masked request with text data from the user. | |
| 80 /// | |
| 81 /// Used in response to: | |
| 82 /// | |
| 83 /// - [`MaskedPrompt`](Message::MaskedPrompt) | |
| 84 MaskedText(SecureString), | |
| 85 /// A response to a binary request (a Linux-PAM extension). | |
| 86 /// | |
| 87 /// Used in response to: | |
| 88 /// | |
| 89 /// - [`BinaryPrompt`](Message::BinaryPrompt) | |
| 90 Binary(BinaryData), | |
| 91 } | |
| 92 | |
| 93 /// A channel for PAM modules to request information from the user. | |
| 94 /// | |
| 95 /// This trait is used by both applications and PAM modules: | |
| 96 /// | |
| 97 /// - Applications implement Conversation and provide a user interface | |
| 98 /// to allow the user to respond to PAM questions. | |
| 99 /// - Modules call a Conversation implementation to request information | |
| 100 /// or send information to the user. | |
| 101 pub trait Conversation { | |
| 102 /// Sends messages to the user. | |
| 103 /// | |
| 104 /// The returned Vec of messages always contains exactly as many entries | |
| 105 /// as there were messages in the request; one corresponding to each. | |
| 106 /// | |
| 107 /// Messages with no response (e.g. [info](Message::Info) and | |
| 108 /// [error](Message::Error)) will have a `None` entry instead of a `Response`. | |
| 109 fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>>; | |
| 110 } | |
| 111 | |
| 112 /// Trait that an application can implement if they want to handle messages | |
| 113 /// one at a time. | |
| 114 pub trait DemuxedConversation { | |
| 115 /// Prompts the user for some text. | |
| 116 fn prompt(&mut self, request: &str) -> Result<String>; | |
| 117 /// Prompts the user for some text, but hides their typing. | |
| 118 fn masked_prompt(&mut self, request: &str) -> Result<SecureString>; | |
| 119 /// Prompts the user for a radio option (a Linux-PAM extension). | |
| 120 /// | |
| 121 /// The Linux-PAM documentation doesn't give the format of the response. | |
| 122 fn radio_prompt(&mut self, request: &str) -> Result<String>; | |
| 123 /// Alerts the user to an error. | |
| 124 fn error(&mut self, message: &str); | |
| 125 /// Sends an informational message to the user. | |
| 126 fn info(&mut self, message: &str); | |
| 127 /// Requests binary data from the user (a Linux-PAM extension). | |
| 128 fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData>; | |
| 129 } | |
| 130 | |
| 131 impl<D: DemuxedConversation> Conversation for D { | |
| 132 fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>> { | |
| 133 messages | |
| 134 .iter() | |
| 135 .map(|msg| match *msg { | |
| 136 Message::Prompt(prompt) => self.prompt(prompt).map(Response::from), | |
| 137 Message::MaskedPrompt(prompt) => self.masked_prompt(prompt).map(Response::from), | |
| 138 Message::RadioPrompt(prompt) => self.radio_prompt(prompt).map(Response::from), | |
| 139 Message::Info(message) => { | |
| 140 self.info(message); | |
| 141 Ok(Response::NoResponse) | |
| 142 } | |
| 143 Message::Error(message) => { | |
| 144 self.error(message); | |
| 145 Ok(Response::NoResponse) | |
| 146 } | |
| 147 Message::BinaryPrompt { data_type, data } => { | |
| 148 self.binary_prompt(data, data_type).map(Response::from) | |
| 149 } | |
| 150 }) | |
| 151 .collect() | |
| 152 } | |
| 153 } | |
| 7 | 154 |
| 8 /// An owned text response to a PAM conversation. | 155 /// An owned text response to a PAM conversation. |
| 9 /// | 156 /// |
| 10 /// It points to a value on the C heap. | 157 /// It points to a value on the C heap. |
| 11 #[repr(C)] | 158 #[repr(C)] |
| 12 struct TextResponse(*mut TextResponseInner); | 159 struct TextResponse(*mut TextResponseInner); |
| 13 | 160 |
| 14 impl TextResponse { | 161 impl TextResponse { |
| 15 /// Creates a text response. | 162 /// Allocates a new response with the given text. |
| 163 /// | |
| 164 /// A copy of the provided text will be allocated on the C heap. | |
| 16 pub fn new(text: impl AsRef<str>) -> StdResult<Self, NulError> { | 165 pub fn new(text: impl AsRef<str>) -> StdResult<Self, NulError> { |
| 17 TextResponseInner::alloc(text).map(Self) | 166 TextResponseInner::alloc(text).map(Self) |
| 18 } | 167 } |
| 19 } | 168 |
| 20 | 169 /// Converts this into a GenericResponse. |
| 21 impl Deref for TextResponse { | 170 fn generic(self) -> *mut GenericResponse { |
| 22 type Target = TextResponseInner; | 171 let ret = self.0 as *mut GenericResponse; |
| 23 fn deref(&self) -> &Self::Target { | 172 mem::forget(self); |
| 24 // SAFETY: We allocated this ourselves, or it was provided by PAM. | 173 ret |
| 25 unsafe { &*self.0 } | 174 } |
| 175 | |
| 176 /// Gets the string data, if possible. | |
| 177 pub fn as_str(&self) -> StdResult<&str, Utf8Error> { | |
| 178 // SAFETY: We allocated this ourselves or got it back from PAM. | |
| 179 unsafe { &*self.0 }.contents().to_str() | |
| 26 } | 180 } |
| 27 } | 181 } |
| 28 | 182 |
| 29 impl Drop for TextResponse { | 183 impl Drop for TextResponse { |
| 30 /// Frees an owned response. | 184 /// Frees an owned response. |
| 36 | 190 |
| 37 /// An owned binary response to a PAM conversation. | 191 /// An owned binary response to a PAM conversation. |
| 38 /// | 192 /// |
| 39 /// It points to a value on the C heap. | 193 /// It points to a value on the C heap. |
| 40 #[repr(C)] | 194 #[repr(C)] |
| 41 struct BinaryResponse(*mut BinaryResponseInner); | 195 pub struct BinaryResponse(pub(super) *mut BinaryResponseInner); |
| 42 | 196 |
| 43 impl BinaryResponse { | 197 impl BinaryResponse { |
| 44 /// Creates a binary response with the given data. | 198 /// Creates a binary response with the given data. |
| 45 pub fn new(data: impl AsRef<[u8]>, data_type: u8) -> StdResult<Self, TryFromIntError> { | 199 /// |
| 200 /// A copy of the data will be made and allocated on the C heap. | |
| 201 pub fn new(data: &[u8], data_type: u8) -> StdResult<Self, TooBigError> { | |
| 46 BinaryResponseInner::alloc(data, data_type).map(Self) | 202 BinaryResponseInner::alloc(data, data_type).map(Self) |
| 47 } | 203 } |
| 48 } | 204 |
| 49 | 205 /// Converts this into a GenericResponse. |
| 50 impl Deref for BinaryResponse { | 206 fn generic(self) -> *mut GenericResponse { |
| 51 type Target = BinaryResponseInner; | 207 let ret = self.0 as *mut GenericResponse; |
| 52 fn deref(&self) -> &Self::Target { | 208 mem::forget(self); |
| 53 // SAFETY: We allocated this ourselves, or it was provided by PAM. | 209 ret |
| 54 unsafe { &*self.0 } | 210 } |
| 211 | |
| 212 /// The data type we point to. | |
| 213 pub fn data_type(&self) -> u8 { | |
| 214 // SAFETY: We allocated this ourselves or got it back from PAM. | |
| 215 unsafe { &*self.0 }.data_type() | |
| 216 } | |
| 217 | |
| 218 /// The data we point to. | |
| 219 pub fn data(&self) -> &[u8] { | |
| 220 // SAFETY: We allocated this ourselves or got it back from PAM. | |
| 221 unsafe { &*self.0 }.contents() | |
| 55 } | 222 } |
| 56 } | 223 } |
| 57 | 224 |
| 58 impl Drop for BinaryResponse { | 225 impl Drop for BinaryResponse { |
| 59 /// Frees an owned response. | 226 /// Frees an owned response. |
| 61 // SAFETY: We allocated this ourselves, or it was provided by PAM. | 228 // SAFETY: We allocated this ourselves, or it was provided by PAM. |
| 62 unsafe { BinaryResponseInner::free(self.0) } | 229 unsafe { BinaryResponseInner::free(self.0) } |
| 63 } | 230 } |
| 64 } | 231 } |
| 65 | 232 |
| 233 /// Owned binary data. | |
| 234 #[derive(Debug, PartialEq)] | |
| 235 pub struct BinaryData { | |
| 236 data: Vec<u8>, | |
| 237 data_type: u8, | |
| 238 } | |
| 239 | |
| 240 impl BinaryData { | |
| 241 pub fn new(data: Vec<u8>, data_type: u8) -> Self { | |
| 242 Self { data, data_type } | |
| 243 } | |
| 244 pub fn data(&self) -> &[u8] { | |
| 245 &self.data | |
| 246 } | |
| 247 pub fn data_type(&self) -> u8 { | |
| 248 self.data_type | |
| 249 } | |
| 250 } | |
| 251 | |
| 252 impl From<BinaryResponse> for BinaryData { | |
| 253 /// Copies the data onto the Rust heap. | |
| 254 fn from(value: BinaryResponse) -> Self { | |
| 255 Self { | |
| 256 data: value.data().to_vec(), | |
| 257 data_type: value.data_type(), | |
| 258 } | |
| 259 } | |
| 260 } | |
| 261 | |
| 262 impl From<BinaryData> for Vec<u8> { | |
| 263 /// Extracts the inner vector from the BinaryData. | |
| 264 fn from(value: BinaryData) -> Self { | |
| 265 value.data | |
| 266 } | |
| 267 } | |
| 268 | |
| 66 #[cfg(test)] | 269 #[cfg(test)] |
| 67 mod test { | 270 mod test { |
| 68 use super::{BinaryResponse, TextResponse}; | 271 use super::{ |
| 272 BinaryResponse, Conversation, DemuxedConversation, Message, Response, SecureString, | |
| 273 TextResponse, | |
| 274 }; | |
| 275 use crate::constants::ErrorCode; | |
| 276 use crate::pam_ffi::GenericResponse; | |
| 277 | |
| 278 #[test] | |
| 279 fn test_demux() { | |
| 280 #[derive(Default)] | |
| 281 struct DemuxTester { | |
| 282 error_ran: bool, | |
| 283 info_ran: bool, | |
| 284 } | |
| 285 | |
| 286 impl DemuxedConversation for DemuxTester { | |
| 287 fn prompt(&mut self, request: &str) -> crate::Result<String> { | |
| 288 match request { | |
| 289 "what" => Ok("whatwhat".to_owned()), | |
| 290 "give_err" => Err(ErrorCode::PermissionDenied), | |
| 291 _ => panic!("unexpected prompt!"), | |
| 292 } | |
| 293 } | |
| 294 fn masked_prompt(&mut self, request: &str) -> crate::Result<SecureString> { | |
| 295 assert_eq!("reveal", request); | |
| 296 Ok(SecureString::from("my secrets")) | |
| 297 } | |
| 298 fn radio_prompt(&mut self, request: &str) -> crate::Result<String> { | |
| 299 assert_eq!("channel?", request); | |
| 300 Ok("zero".to_owned()) | |
| 301 } | |
| 302 fn error(&mut self, message: &str) { | |
| 303 self.error_ran = true; | |
| 304 assert_eq!("whoopsie", message); | |
| 305 } | |
| 306 fn info(&mut self, message: &str) { | |
| 307 self.info_ran = true; | |
| 308 assert_eq!("did you know", message); | |
| 309 } | |
| 310 fn binary_prompt( | |
| 311 &mut self, | |
| 312 data: &[u8], | |
| 313 data_type: u8, | |
| 314 ) -> crate::Result<super::BinaryData> { | |
| 315 assert_eq!(&[10, 9, 8], data); | |
| 316 assert_eq!(66, data_type); | |
| 317 Ok(super::BinaryData::new(vec![5, 5, 5], 5)) | |
| 318 } | |
| 319 } | |
| 320 | |
| 321 let mut tester = DemuxTester::default(); | |
| 322 | |
| 323 assert_eq!( | |
| 324 vec![ | |
| 325 Response::Text("whatwhat".to_owned()), | |
| 326 Response::MaskedText("my secrets".into()), | |
| 327 Response::NoResponse, | |
| 328 Response::NoResponse, | |
| 329 ], | |
| 330 tester | |
| 331 .send(&[ | |
| 332 Message::Prompt("what"), | |
| 333 Message::MaskedPrompt("reveal"), | |
| 334 Message::Error("whoopsie"), | |
| 335 Message::Info("did you know"), | |
| 336 ]) | |
| 337 .unwrap() | |
| 338 ); | |
| 339 assert!(tester.error_ran); | |
| 340 assert!(tester.info_ran); | |
| 341 | |
| 342 assert_eq!( | |
| 343 ErrorCode::PermissionDenied, | |
| 344 tester.send(&[Message::Prompt("give_err")]).unwrap_err(), | |
| 345 ); | |
| 346 | |
| 347 // Test the Linux-PAM extensions separately. | |
| 348 | |
| 349 assert_eq!( | |
| 350 vec![ | |
| 351 Response::Text("zero".to_owned()), | |
| 352 Response::Binary(super::BinaryData::new(vec![5, 5, 5], 5)), | |
| 353 ], | |
| 354 tester | |
| 355 .send(&[ | |
| 356 Message::RadioPrompt("channel?"), | |
| 357 Message::BinaryPrompt { | |
| 358 data: &[10, 9, 8], | |
| 359 data_type: 66 | |
| 360 }, | |
| 361 ]) | |
| 362 .unwrap() | |
| 363 ); | |
| 364 } | |
| 365 | |
| 366 // The below tests are used in conjunction with ASAN to verify | |
| 367 // that we correctly clean up all our memory. | |
| 69 | 368 |
| 70 #[test] | 369 #[test] |
| 71 fn test_text_response() { | 370 fn test_text_response() { |
| 72 let resp = TextResponse::new("it's a-me!").unwrap(); | 371 let resp = TextResponse::new("it's a-me!").unwrap(); |
| 73 assert_eq!("it's a-me!", resp.contents().to_str().unwrap()); | 372 assert_eq!("it's a-me!", resp.as_str().unwrap()); |
| 74 } | 373 } |
| 374 | |
| 75 #[test] | 375 #[test] |
| 76 fn test_binary_response() { | 376 fn test_binary_response() { |
| 77 let data = [123, 210, 55]; | 377 let data = [123, 210, 55]; |
| 78 let resp = BinaryResponse::new(&data, 99).unwrap(); | 378 let resp = BinaryResponse::new(&data, 99).unwrap(); |
| 79 assert_eq!(&data, resp.contents()); | 379 assert_eq!(data, resp.data()); |
| 80 } | 380 assert_eq!(99, resp.data_type()); |
| 81 } | 381 } |
| 382 | |
| 383 #[test] | |
| 384 fn test_to_generic() { | |
| 385 let text = TextResponse::new("oh no").unwrap(); | |
| 386 let text = text.generic(); | |
| 387 let binary = BinaryResponse::new(&[], 33).unwrap(); | |
| 388 let binary = binary.generic(); | |
| 389 unsafe { | |
| 390 GenericResponse::free(text); | |
| 391 GenericResponse::free(binary); | |
| 392 } | |
| 393 } | |
| 394 } |
