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 }