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 }