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