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 } |