Mercurial > crates > nonstick
comparison src/pam_ffi/message.rs @ 71:58f9d2a4df38
Reorganize everything again???
- Splits ffi/memory stuff into a bunch of stuff in the pam_ffi module.
- Builds infrastructure for passing Messages and Responses.
- Adds tests for some things at least.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Tue, 03 Jun 2025 21:54:58 -0400 |
parents | src/pam_ffi.rs@9f8381a1c09c |
children | 47eb242a4f88 |
comparison
equal
deleted
inserted
replaced
70:9f8381a1c09c | 71:58f9d2a4df38 |
---|---|
1 //! Data and types dealing with PAM messages. | |
2 | |
3 use crate::constants::InvalidEnum; | |
4 use crate::pam_ffi::memory; | |
5 use crate::pam_ffi::memory::{CBinaryData, NulError, TooBigError}; | |
6 use num_derive::FromPrimitive; | |
7 use num_traits::FromPrimitive; | |
8 use std::ffi::{c_char, c_int, c_void, CStr}; | |
9 use std::result::Result as StdResult; | |
10 use std::slice; | |
11 use std::str::Utf8Error; | |
12 | |
13 /// The types of message and request that can be sent to a user. | |
14 /// | |
15 /// The data within each enum value is the prompt (or other information) | |
16 /// that will be presented to the user. | |
17 #[derive(Debug)] | |
18 pub enum Message<'a> { | |
19 /// Requests information from the user; will be masked when typing. | |
20 /// | |
21 /// Response: [`Response::MaskedText`] | |
22 MaskedPrompt(&'a str), | |
23 /// Requests information from the user; will not be masked. | |
24 /// | |
25 /// Response: [`Response::Text`] | |
26 Prompt(&'a str), | |
27 /// "Yes/No/Maybe conditionals" (a Linux-PAM extension). | |
28 /// | |
29 /// Response: [`Response::Text`] | |
30 /// (Linux-PAM documentation doesn't define its contents.) | |
31 RadioPrompt(&'a str), | |
32 /// Raises an error message to the user. | |
33 /// | |
34 /// Response: [`Response::NoResponse`] | |
35 Error(&'a str), | |
36 /// Sends an informational message to the user. | |
37 /// | |
38 /// Response: [`Response::NoResponse`] | |
39 Info(&'a str), | |
40 /// Requests binary data from the client (a Linux-PAM extension). | |
41 /// | |
42 /// This is used for non-human or non-keyboard prompts (security key?). | |
43 /// NOT part of the X/Open PAM specification. | |
44 /// | |
45 /// Response: [`Response::Binary`] | |
46 BinaryPrompt { | |
47 /// Some binary data. | |
48 data: &'a [u8], | |
49 /// A "type" that you can use for signalling. Has no strict definition in PAM. | |
50 data_type: u8, | |
51 }, | |
52 } | |
53 | |
54 impl Message<'_> { | |
55 /// Copies the contents of this message to the C heap. | |
56 fn copy_to_heap(&self) -> StdResult<(Style, *mut c_void), ConversionError> { | |
57 let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast())); | |
58 match *self { | |
59 Self::MaskedPrompt(text) => alloc(Style::PromptEchoOff, text), | |
60 Self::Prompt(text) => alloc(Style::PromptEchoOn, text), | |
61 Self::RadioPrompt(text) => alloc(Style::RadioType, text), | |
62 Self::Error(text) => alloc(Style::ErrorMsg, text), | |
63 Self::Info(text) => alloc(Style::TextInfo, text), | |
64 Self::BinaryPrompt { data, data_type } => Ok(( | |
65 Style::BinaryPrompt, | |
66 (CBinaryData::alloc(data, data_type)?).cast(), | |
67 )), | |
68 } | |
69 } | |
70 } | |
71 | |
72 #[derive(Debug, thiserror::Error)] | |
73 #[error("error creating PAM message: {0}")] | |
74 enum ConversionError { | |
75 InvalidEnum(#[from] InvalidEnum<Style>), | |
76 Utf8Error(#[from] Utf8Error), | |
77 NulError(#[from] NulError), | |
78 TooBigError(#[from] TooBigError), | |
79 } | |
80 | |
81 /// The C enum values for messages shown to the user. | |
82 #[derive(Debug, PartialEq, FromPrimitive)] | |
83 pub enum Style { | |
84 /// Requests information from the user; will be masked when typing. | |
85 PromptEchoOff = 1, | |
86 /// Requests information from the user; will not be masked. | |
87 PromptEchoOn = 2, | |
88 /// An error message. | |
89 ErrorMsg = 3, | |
90 /// An informational message. | |
91 TextInfo = 4, | |
92 /// Yes/No/Maybe conditionals. A Linux-PAM extension. | |
93 RadioType = 5, | |
94 /// For server–client non-human interaction. | |
95 /// | |
96 /// NOT part of the X/Open PAM specification. | |
97 /// A Linux-PAM extension. | |
98 BinaryPrompt = 7, | |
99 } | |
100 | |
101 impl TryFrom<c_int> for Style { | |
102 type Error = InvalidEnum<Self>; | |
103 fn try_from(value: c_int) -> StdResult<Self, Self::Error> { | |
104 Self::from_i32(value).ok_or(value.into()) | |
105 } | |
106 } | |
107 | |
108 impl From<Style> for c_int { | |
109 fn from(val: Style) -> Self { | |
110 val as Self | |
111 } | |
112 } | |
113 | |
114 /// A message sent by PAM or a module to an application. | |
115 /// This message, and its internal data, is owned by the creator | |
116 /// (either the module or PAM itself). | |
117 #[repr(C)] | |
118 pub struct RawMessage { | |
119 /// The style of message to request. | |
120 style: c_int, | |
121 /// A description of the data requested. | |
122 /// | |
123 /// For most requests, this will be an owned [`CStr`], but for requests | |
124 /// with [`Style::BinaryPrompt`], this will be [`BinaryData`] | |
125 /// (a Linux-PAM extension). | |
126 data: *mut c_void, | |
127 } | |
128 | |
129 impl RawMessage { | |
130 fn set(&mut self, msg: &Message) -> StdResult<(), ConversionError> { | |
131 let (style, data) = msg.copy_to_heap()?; | |
132 self.clear(); | |
133 // SAFETY: We allocated this ourselves or were given it by PAM. | |
134 // Otherwise, it's null, but free(null) is fine. | |
135 unsafe { libc::free(self.data) }; | |
136 self.style = style as c_int; | |
137 self.data = data; | |
138 Ok(()) | |
139 } | |
140 | |
141 /// Retrieves the data stored in this message. | |
142 fn data(&self) -> StdResult<Message, ConversionError> { | |
143 let style: Style = self.style.try_into()?; | |
144 // SAFETY: We either allocated this message ourselves or were provided it by PAM. | |
145 let result = unsafe { | |
146 match style { | |
147 Style::PromptEchoOff => Message::MaskedPrompt(self.string_data()?), | |
148 Style::PromptEchoOn => Message::Prompt(self.string_data()?), | |
149 Style::TextInfo => Message::Info(self.string_data()?), | |
150 Style::ErrorMsg => Message::Error(self.string_data()?), | |
151 Style::RadioType => Message::Error(self.string_data()?), | |
152 Style::BinaryPrompt => (self.data as *const CBinaryData).as_ref().map_or_else( | |
153 || Message::BinaryPrompt { | |
154 data_type: 0, | |
155 data: &[], | |
156 }, | |
157 |data| Message::BinaryPrompt { | |
158 data_type: data.data_type(), | |
159 data: data.contents(), | |
160 }, | |
161 ), | |
162 } | |
163 }; | |
164 Ok(result) | |
165 } | |
166 | |
167 /// Gets this message's data pointer as a string. | |
168 /// | |
169 /// # Safety | |
170 /// | |
171 /// It's up to you to pass this only on types with a string value. | |
172 unsafe fn string_data(&self) -> StdResult<&str, Utf8Error> { | |
173 if self.data.is_null() { | |
174 Ok("") | |
175 } else { | |
176 CStr::from_ptr(self.data as *const c_char).to_str() | |
177 } | |
178 } | |
179 | |
180 /// Zeroes out the data stored here. | |
181 fn clear(&mut self) { | |
182 // SAFETY: We either created this data or we got it from PAM. | |
183 // After this function is done, it will be zeroed out. | |
184 unsafe { | |
185 if let Ok(style) = Style::try_from(self.style) { | |
186 match style { | |
187 Style::BinaryPrompt => { | |
188 if let Some(d) = (self.data as *mut CBinaryData).as_mut() { | |
189 d.zero_contents() | |
190 } | |
191 } | |
192 Style::TextInfo | |
193 | Style::RadioType | |
194 | Style::ErrorMsg | |
195 | Style::PromptEchoOff | |
196 | Style::PromptEchoOn => memory::zero_c_string(self.data), | |
197 } | |
198 }; | |
199 } | |
200 } | |
201 } | |
202 | |
203 /// Abstraction of a list-of-messages to be sent in a PAM conversation. | |
204 /// | |
205 /// On Linux-PAM and other compatible implementations, `messages` | |
206 /// is treated as a pointer-to-pointers, like `int argc, char **argv`. | |
207 /// (In this situation, the value of `OwnedMessages.indirect` is | |
208 /// the pointer passed to `pam_conv`.) | |
209 /// | |
210 /// ```text | |
211 /// ╔═ OwnedMsgs ═╗ points to ┌─ Indirect ─┐ ╔═ Message ═╗ | |
212 /// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base[0] ┄┄┄┼┄┄┄┄┄> ║ style ║ | |
213 /// ║ count ║ │ base[1] ┄┄┄┼┄┄┄╮ ║ data ║ | |
214 /// ╚═════════════╝ │ ... │ ┆ ╚═══════════╝ | |
215 /// ┆ | |
216 /// ┆ ╔═ Message ═╗ | |
217 /// ╰┄┄> ║ style ║ | |
218 /// ║ data ║ | |
219 /// ╚═══════════╝ | |
220 /// ``` | |
221 /// | |
222 /// On OpenPAM and other compatible implementations (like Solaris), | |
223 /// `messages` is a pointer-to-pointer-to-array. This appears to be | |
224 /// the correct implementation as required by the XSSO specification. | |
225 /// | |
226 /// ```text | |
227 /// ╔═ OwnedMsgs ═╗ points to ┌─ Indirect ─┐ ╔═ Message[] ═╗ | |
228 /// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base ┄┄┄┄┄┄┼┄┄┄┄┄> ║ style ║ | |
229 /// ║ count ║ └────────────┘ ║ data ║ | |
230 /// ╚═════════════╝ ╟─────────────╢ | |
231 /// ║ style ║ | |
232 /// ║ data ║ | |
233 /// ╟─────────────╢ | |
234 /// ║ ... ║ | |
235 /// ``` | |
236 /// | |
237 /// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.*** | |
238 #[repr(C)] | |
239 pub struct OwnedMessages { | |
240 /// An indirection to the messages themselves, stored on the C heap. | |
241 indirect: *mut Indirect<RawMessage>, | |
242 /// The number of messages in the list. | |
243 count: usize, | |
244 } | |
245 | |
246 impl OwnedMessages { | |
247 /// Allocates data to store messages on the C heap. | |
248 pub fn alloc(count: usize) -> Self { | |
249 // SAFETY: We're allocating. That's safe. | |
250 unsafe { | |
251 // Since this is Linux-PAM, the indirect is a list of pointers. | |
252 let indirect = | |
253 libc::calloc(count, size_of::<Indirect<RawMessage>>()) as *mut Indirect<RawMessage>; | |
254 let indir_ptrs = slice::from_raw_parts_mut(indirect, count); | |
255 for ptr in indir_ptrs { | |
256 ptr.base = libc::calloc(1, size_of::<RawMessage>()) as *mut RawMessage; | |
257 } | |
258 Self { indirect, count } | |
259 } | |
260 } | |
261 | |
262 /// Gets a reference to the message at the given index. | |
263 pub fn get(&self, index: usize) -> Option<&RawMessage> { | |
264 (index < self.count).then(|| unsafe { (*self.indirect).at(index) }) | |
265 } | |
266 | |
267 /// Gets a mutable reference to the message at the given index. | |
268 pub fn get_mut(&mut self, index: usize) -> Option<&mut RawMessage> { | |
269 (index < self.count).then(|| unsafe { (*self.indirect).at_mut(index) }) | |
270 } | |
271 } | |
272 | |
273 #[repr(transparent)] | |
274 struct Indirect<T> { | |
275 /// The starting address for the T. | |
276 base: *mut T, | |
277 } | |
278 | |
279 impl<T> Indirect<T> { | |
280 /// Gets a mutable reference to the element at the given index. | |
281 /// | |
282 /// # Safety | |
283 /// | |
284 /// We don't check `index`. | |
285 unsafe fn at_mut(&mut self, index: usize) -> &mut T { | |
286 &mut *self.base.add(index) | |
287 } | |
288 | |
289 unsafe fn at(&self, index: usize) -> &T { | |
290 &*self.base.add(index) | |
291 } | |
292 } |