bsd_auth/lib.rs
1use std::ffi::{CStr, CString};
2
3use bsd_auth_sys as ffi;
4
5pub use ffi::AuthItem;
6
7/// Error type wrapping various error conditions
8#[derive(Clone, Debug, Eq, PartialEq)]
9pub enum Error {
10 Challenge,
11 Close,
12 Utf8(std::str::Utf8Error),
13 Nul(std::ffi::NulError),
14 GetHostName(i32),
15 NullSession,
16 SetEnv,
17 ClrEnv,
18 GetItem,
19 SetItem,
20 SetOption,
21 ClrOption,
22 ClrOptions,
23 SetData,
24 UserChallenge,
25}
26
27impl From<std::str::Utf8Error> for Error {
28 fn from(e: std::str::Utf8Error) -> Self {
29 Self::Utf8(e)
30 }
31}
32
33impl From<std::ffi::NulError> for Error {
34 fn from(e: std::ffi::NulError) -> Self {
35 Self::Nul(e)
36 }
37}
38
39/// BSD Authentication session
40pub struct Session {
41 ptr: *mut ffi::auth_session_t,
42}
43
44impl Session {
45 /// Open a new BSD Authentication session with the default service
46 /// (which can be changed later).
47 pub fn new() -> Result<Self, Error> {
48 // safety: creates an authentication session,
49 // and initializes all members with default values
50 //
51 // auth_open returns a null pointer on failure
52 let ptr = unsafe { ffi::auth_open() };
53
54 if ptr == std::ptr::null_mut() {
55 Err(Error::NullSession)
56 } else {
57 Ok(Self { ptr })
58 }
59 }
60
61 /// Create a Session from a raw `auth_session_t` pointer
62 pub fn from_raw(ptr: *mut ffi::auth_session_t) -> Result<Self, Error> {
63 if ptr == std::ptr::null_mut() {
64 Err(Error::NullSession)
65 } else {
66 Ok(Self { ptr })
67 }
68 }
69
70 /// Convert the Session into a raw `auth_session_t` pointer
71 ///
72 /// Consumes the Session
73 pub fn into_raw(mut self) -> Result<*mut ffi::auth_session_t, Error> {
74 self.check_ptr()?;
75 let ret_ptr = self.ptr;
76 self.ptr = std::ptr::null_mut();
77 Ok(ret_ptr)
78 }
79
80 /// Request a challenge for the session
81 ///
82 /// The name and style must have already been specified
83 ///
84 /// Call is not thread-safe
85 pub fn auth_challenge(&self) -> Result<String, Error> {
86 // safety: auth_challenge performs null check for the session
87 // So, safe to just pass the ptr pointer without a check
88 let c_res = unsafe { ffi::auth_challenge(self.ptr) };
89 if c_res == std::ptr::null_mut() {
90 Err(Error::Challenge)
91 } else {
92 // safety: auth_challenge returns challenge C string on success
93 //
94 // The string should be a valid UTF-8 string pointing to
95 // valid memory, so converting to a Rust String should be safe
96 let res = unsafe { CString::from_raw(c_res) };
97 Ok(res.to_str()?.into())
98 }
99 }
100
101 /// Close the specified BSD Authentication session
102 ///
103 /// Frees the ptr pointer to the session
104 /// future calls with the Session will all return Error
105 ///
106 /// Inner pointer can be reset with calls that open a new session
107 ///
108 /// Consumes the Session
109 ///
110 /// Call is not thread-safe
111 pub fn auth_close(self) -> Result<(), Error> {
112 self.check_ptr()?;
113 let res = unsafe { ffi::auth_close(self.ptr) };
114 if res == 0 {
115 Err(Error::Close)
116 } else {
117 Ok(())
118 }
119 }
120
121 /// Get the BSD Authentication session state
122 /// (0 = unauth, 1 = auth)
123 ///
124 /// Call is not thread-safe
125 pub fn auth_getstate(&self) -> Result<i32, Error> {
126 self.check_ptr()?;
127 Ok(unsafe { ffi::auth_getstate(self.ptr) })
128 }
129
130 /// Set/unset the requested environment variables.
131 /// Mark the variables as set so they will not be set a second time.
132 ///
133 /// Environment variables are requested via the spool
134 /// of the `auth_session_t` struct
135 ///
136 /// Call is not thread-safe
137 pub fn auth_setenv(&self) -> Result<(), Error> {
138 self.check_ptr()?;
139 // safety: auth_setenv does not check the validity of the spool
140 // There is no way to check for an error
141 //
142 // auth_session_t is an opaque type, so we can't check either
143 unsafe { ffi::auth_setenv(self.ptr) };
144 Ok(())
145 }
146
147 /// Clear out any of the requested environment variables.
148 ///
149 /// Call is not thread-safe
150 pub fn auth_clrenv(&self) -> Result<(), Error> {
151 self.check_ptr()?;
152 // safety: auth_setenv does not check the validity of the spool
153 // There is no way to check for an error
154 //
155 // auth_session_t is an opaque type, so we can't check either
156 unsafe { ffi::auth_clrenv(self.ptr) };
157 Ok(())
158 }
159
160 /// Get the item value
161 ///
162 /// Call is not thread-safe
163 pub fn auth_getitem(&self, item: AuthItem) -> Result<String, Error> {
164 self.check_ptr()?;
165 // safety: auth_getitem also checks for null, so the call is safe
166 let c_res = unsafe { ffi::auth_getitem(self.ptr, item as u32) };
167 if c_res == std::ptr::null_mut() {
168 // return error if the value is unset
169 Err(Error::GetItem)
170 } else {
171 // safety: at this point, the value returned should be a
172 // pointer to a valid UTF-8 C string
173 let c_res = unsafe { CString::from_raw(c_res) };
174 let res = c_res.to_str()?.into();
175 let _ = c_res.into_raw();
176 Ok(res)
177 }
178 }
179
180 /// Set an item value
181 ///
182 /// Value must be a valid UTF-8 string
183 ///
184 /// Call is not thread-safe
185 pub fn auth_setitem(&self, item: AuthItem, value: &str) -> Result<(), Error> {
186 self.check_ptr()?;
187 let c_str = CString::new(value)?;
188 // safety: auth_setitem checks for null, and sets errno to EINVAL
189 // if the auth_session_t* or members are null (so let it do the checks).
190 let c_res = match item {
191 AuthItem::All => unsafe {
192 // pass null to clear all member items, non-null errors
193 ffi::auth_setitem(self.ptr, item as u32, std::ptr::null_mut())
194 }
195 AuthItem::Interactive => unsafe {
196 let ptr = if value.len() == 0 {
197 // set to null to unset the interactive flag
198 std::ptr::null_mut()
199 } else {
200 // set to non-null to set the interactive flag
201 c_str.into_raw()
202 };
203 ffi::auth_setitem(self.ptr, item as u32, ptr)
204 }
205 _ => unsafe {
206 ffi::auth_setitem(self.ptr, item as u32, c_str.into_raw())
207 }
208 };
209
210 if c_res == -1 {
211 Err(Error::SetItem)
212 } else {
213 Ok(())
214 }
215 }
216
217 /// Set an option name and value
218 ///
219 /// Returns error if:
220 ///
221 /// - session is null
222 /// - option allocation fails
223 /// - name is too long
224 ///
225 /// Call is not thread-safe
226 pub fn auth_setoption(&self, name: &str, value: &str) -> Result<(), Error> {
227 self.check_ptr()?;
228 let n = CString::new(name)?;
229 let v = CString::new(value)?;
230 // safety: auth_setoption checks for null arguments, and argument validity
231 let c_res = unsafe { ffi::auth_setoption(self.ptr, n.into_raw(), v.into_raw()) };
232
233 if c_res == -1 {
234 Err(Error::SetOption)
235 } else {
236 Ok(())
237 }
238 }
239
240 /// Clear all set options in the BSD Authentication session
241 ///
242 /// Call is not thread-safe
243 pub fn auth_clroptions(&self) -> Result<(), Error> {
244 self.check_ptr()?;
245 // safety: auth_clroptions checks for null
246 // in the optlist member, call is safe with the call above
247 unsafe { ffi::auth_clroptions(self.ptr) };
248 Ok(())
249 }
250
251 /// Clear the option matching the specified name
252 ///
253 /// Call is not thread-safe
254 pub fn auth_clroption(&self, option: &str) -> Result<(), Error> {
255 self.check_ptr()?;
256 let opt = CString::new(option)?;
257 // safety: auth_clroption checks for null
258 // in the optlist member, call is safe with the call above
259 unsafe { ffi::auth_clroption(self.ptr, opt.into_raw()) };
260 Ok(())
261 }
262
263 /// Set BSD Authentication session data to be read into the spool.
264 ///
265 /// Data is not mutated, but needs to be a mutable reference
266 /// to satisfy the borrow checker.
267 ///
268 /// Call is not thread-safe
269 pub fn auth_setdata(&self, data: &mut [u8]) -> Result<(), Error> {
270 self.check_ptr()?;
271 let d = data.as_mut_ptr() as *mut _;
272 let len = data.len() as u64;
273 // safety: auth_setdata checks for nulls of members. With the null
274 // check for the session above, the call is safe.
275 let c_res = unsafe { ffi::auth_setdata(self.ptr, d, len) };
276
277 if c_res == -1 {
278 Err(Error::SetData)
279 } else {
280 Ok(())
281 }
282 }
283
284 /// Single function interface to a BSD Authentication session
285 ///
286 /// Functions similarly to a auth_userokay, but does not close the session.
287 ///
288 /// Example:
289 ///
290 /// ```rust
291 /// # use bsd_auth::Session;
292 /// let name = "nobody".to_string();
293 /// let mut passwd = "some_passwd".to_string();
294 ///
295 /// let _session = Session::auth_usercheck(name.as_str(), None, None, Some(&mut passwd)).unwrap();
296 /// ```
297 ///
298 /// From `man 3 auth_approval`:
299 ///
300 /// ```no_build
301 /// The auth_usercheck() function operates the same as the auth_userokay()
302 /// function except that it does not close the BSD Authentication session
303 /// created. Rather than returning the status of the session, it returns a
304 /// pointer to the newly created BSD Authentication session.
305 ///
306 /// If authentication fails, a null pointer is returned, which results in
307 /// an error in the Rust API.
308 /// ```
309 ///
310 /// For more details see `man 3 auth_approval`
311 pub fn auth_usercheck(
312 name: &str,
313 style: Option<&str>,
314 auth_type: Option<&str>,
315 password: Option<&mut str>,
316 ) -> Result<Self, Error> {
317 let c_name = CString::new(name)?;
318
319 let style_ptr = match style {
320 Some(s) => CString::new(s)?.into_raw(),
321 None => std::ptr::null_mut(),
322 };
323
324 let type_ptr = match auth_type {
325 Some(t) => CString::new(t)?.into_raw(),
326 None => std::ptr::null_mut(),
327 };
328
329 let passwd_ptr = match password {
330 Some(p) => {
331 let ptr = CString::new(&*p)?.into_raw();
332 // safety: pointer is guaranteed non-null, and points to valid memory
333 unsafe { libc::explicit_bzero(p.as_mut_ptr() as *mut _, p.len()) };
334 ptr
335 }
336 None => std::ptr::null_mut(),
337 };
338
339 // safety: auth_usercheck performs null checks on all the arguments
340 //
341 // If the user name is invalid, or authentication fails, a null pointer
342 // is returned
343 let ses_ptr = unsafe { ffi::auth_usercheck(c_name.into_raw(), style_ptr, type_ptr, passwd_ptr) };
344
345 Self::from_raw(ses_ptr)
346 }
347
348 /// Single function call interface for a BSD Authentication session
349 ///
350 /// Provide a name, and optional style, type and password.
351 ///
352 /// If style or type are not provided, the default values will be used.
353 ///
354 /// Supplying a password uses the non-interactive version of the authentication.
355 /// Not supplying a password uses an interactive authentication mode.
356 ///
357 /// Example:
358 ///
359 /// ```rust
360 /// # use bsd_auth::Session;
361 /// let name = "nobody".to_string();
362 /// let mut passwd = "some_passwd".to_string();
363 ///
364 /// assert!(!Session::auth_userokay(name.as_str(), None, None, Some(&mut passwd)).unwrap());
365 /// ```
366 ///
367 /// From `man 3 auth_approval`:
368 ///
369 /// ```no_build
370 /// Provides a single function call interface.
371 ///
372 /// Provided with a user's name in name, and an optional style, type, and password, the auth_userokay() function returns a simple yes/no response.
373 ///
374 /// A return value of true implies failure; a false return value implies success.
375 /// Other error conditions result in Error.
376 ///
377 /// If style is not NULL, it specifies the desired style of authentication to be used.
378 /// If it is NULL then the default style for the user is used.
379 /// In this case, name may include the desired style by appending it to the user's name with a single colon (`:') as a separator.
380 /// If type is not NULL then it is used as the authentication type (such as "auth-myservice").
381 /// If password is NULL then auth_userokay() operates in an interactive mode with the user on standard input, output, and error.
382 /// If password is specified, auth_userokay() operates in a non-interactive mode and only tests the specified passwords.
383 /// This non-interactive method does not work with challenge-response authentication styles.
384 ///
385 /// For security reasons, when a password is specified, auth_userokay() will zero out its value before it returns.
386 /// ```
387 ///
388 /// For more details see `man 3 auth_approval`
389 pub fn auth_userokay(
390 name: &str,
391 style: Option<&str>,
392 auth_type: Option<&str>,
393 password: Option<&mut str>,
394 ) -> Result<bool, Error> {
395 let c_name = CString::new(name)?;
396
397 let style_ptr = match style {
398 Some(s) => CString::new(s)?.into_raw(),
399 None => std::ptr::null_mut(),
400 };
401
402 let type_ptr = match auth_type {
403 Some(t) => CString::new(t)?.into_raw(),
404 None => std::ptr::null_mut(),
405 };
406
407 let passwd_ptr = match password {
408 Some(p) => {
409 let ptr = CString::new(&*p)?.into_raw();
410 // safety: password guaranteed non-null, and points to valid
411 // memory
412 unsafe { libc::explicit_bzero(p.as_mut_ptr() as *mut _, p.len()) };
413 ptr
414 }
415 None => std::ptr::null_mut(),
416 };
417
418 // safety: auth_userokay performs null checks on all the arguments
419 //
420 // If the user name is invalid, or authentication fails, a null pointer
421 // is returned
422 let ret = unsafe { ffi::auth_userokay(c_name.into_raw(), style_ptr, type_ptr, passwd_ptr) };
423
424 Ok(ret != 0)
425 }
426
427 /// Get an authentication challenge for the user, with optional style and type
428 /// Example:
429 ///
430 /// ```rust
431 /// # use bsd_auth::Session;
432 /// /* Create the session and get the challenge */
433 /// let (session, _chal) = Session::auth_userchallenge("nobody", Some("passwd"), Some("auth_doas")).unwrap();
434 ///
435 /// /* Prompt the user for a response */
436 /// let mut response = String::from_utf8([1; 32].to_vec()).unwrap();
437 /// session.auth_userresponse(&mut response, 0).unwrap();
438 /// ```
439 ///
440 /// From `man 3 auth_approval`:
441 ///
442 /// The auth_userchallenge() function takes the same name, style, and type arguments as does auth_userokay().
443 ///
444 /// However, rather than authenticating the user, it returns a possible challenge in the pointer pointed to by challengep.
445 ///
446 /// To provide a safe Rust API the challenge pointer is converted to a string.
447 ///
448 /// The memory pointed to by challengep is cleared for security.
449 ///
450 /// The return value of the function is a pointer to a newly created BSD Authentication session.
451 ///
452 /// This challenge, if not NULL, should be displayed to the user.
453 ///
454 /// In any case, the user should provide a password which is the response in a call to auth_userresponse().
455 ///
456 /// For more information, see `man 3 auth_approval`
457 pub fn auth_userchallenge(
458 name: &str,
459 style: Option<&str>,
460 auth_type: Option<&str>,
461 ) -> Result<(Self, String), Error> {
462 let name_ptr = CString::new(name)?.into_raw();
463
464 let style_ptr = match style {
465 Some(s) => CString::new(s)?.into_raw(),
466 None => std::ptr::null_mut(),
467 };
468
469 let type_ptr = match auth_type {
470 Some(t) => CString::new(t)?.into_raw(),
471 None => std::ptr::null_mut(),
472 };
473
474 let mut challenge_ptr = CString::new("")?.into_raw();
475
476 // safety: auth_userchallenge performs null checks on all of the
477 // arguments. If the user name is invalid, or authentication fails,
478 // a null pointer is returned
479 let ses_ptr = unsafe { ffi::auth_userchallenge(name_ptr, style_ptr, type_ptr, &mut challenge_ptr) };
480
481 let challenge = if challenge_ptr == std::ptr::null_mut() {
482 let mut buf = [0i8; (libc::_SC_HOST_NAME_MAX + 1) as usize];
483 let host = gethostname(&mut buf)?;
484 format!("doas ({}@{}) password: ", name, host.to_str()?)
485 } else {
486 // safety: with the null check above, the challenge pointer should
487 // point to a valid C string
488 unsafe {
489 let cstr = CString::from_raw(challenge_ptr);
490 let c = cstr.to_str()?.to_string();
491 // release ownership of challenge pointer
492 // to let auth_userchallenge handle the memory
493 let _ = cstr.into_raw();
494 c
495 }
496 };
497
498 Ok((Self::from_raw(ses_ptr)?, challenge))
499 }
500
501 /// Provide a user response for a BSD Authentication session
502 ///
503 /// Consumes the Session due to the FFI call closing the session
504 ///
505 /// If `more` is non-zero, the session is returned
506 ///
507 /// Example:
508 ///
509 /// ```rust
510 /// # use bsd_auth::Session;
511 /// let name = "nobody".to_string();
512 /// let style = Some("passwd");
513 /// let mut passwd = "some_passwd".to_string();
514 ///
515 /// let session = Session::auth_usercheck(name.as_str(), style, None, Some(&mut passwd.clone())).unwrap();
516 ///
517 /// let mut res = "some_passwd".to_string();
518 /// let (ses, success) = session.auth_userresponse(&mut res.clone(), 0).unwrap();
519 /// assert!(ses.is_none());
520 /// assert_eq!(success, false);
521 ///
522 /// let session = Session::auth_usercheck(name.as_str(), style, None, Some(&mut passwd)).unwrap();
523 ///
524 /// let (ses, success) = session.auth_userresponse(&mut res, 1).unwrap();
525 /// assert!(ses.is_some());
526 /// assert_eq!(success, false);
527 /// ```
528 ///
529 /// From `man 3 auth_approval`:
530 ///
531 ///```no_build
532 /// In addition to the password, the pointer returned by auth_userchallenge()
533 /// should be passed in as as and the value of more should be non-zero if the
534 /// program wishes to allow more attempts.
535 ///
536 /// If more is zero then the session will be closed.
537 ///
538 /// The auth_userresponse() function closes the BSD Authentication session and has the same return value as auth_userokay().
539 ///
540 /// For security reasons, when a response is specified, auth_userresponse() will zero out its value before it returns.
541 /// ```
542 pub fn auth_userresponse(
543 mut self,
544 response: &mut str,
545 more: u32,
546 ) -> Result<(Option<Session>, bool), Error> {
547 self.check_ptr()?;
548
549 let res_ptr = CString::new(&*response)?.into_raw();
550
551 // safety: auth_userresponse checks arguments for null, and clears the
552 // memory pointed to by the response pointer
553 let res = unsafe { ffi::auth_userresponse(self.ptr, res_ptr, more as i32) };
554
555 let ses = match more {
556 0 => {
557 self.ptr = std::ptr::null_mut();
558 None
559 }
560 _ => Some(self),
561 };
562
563 Ok((ses, res != 0))
564 }
565
566 fn check_ptr(&self) -> Result<(), Error> {
567 if self.ptr == std::ptr::null_mut() {
568 Err(Error::NullSession)
569 } else {
570 Ok(())
571 }
572 }
573}
574
575impl Drop for Session {
576 fn drop(&mut self) {
577 if self.ptr != std::ptr::null_mut() {
578 // safety: auth_clean performs null checks
579 // on ptr members before freeing
580 unsafe { ffi::auth_close(self.ptr); }
581 }
582 }
583}
584
585fn gethostname(buf: &mut [i8]) -> Result<&CStr, Error> {
586 // This is basically what nix does, but don't want to add the extra dep
587 // for one function
588 // safety: pointer is guaranteed non-null, and points to valid memory
589 // write into the buffer is guaranteed in-bounds
590 let ret = unsafe { libc::gethostname(buf.as_mut_ptr(), buf.len()) };
591 if ret == -1 {
592 Err(Error::GetHostName(std::io::Error::last_os_error().raw_os_error().unwrap()))
593 } else {
594 // safety: pointer is guaranteed non-null and points to valid memory
595 // May or may not be a valid UTF8 string
596 Ok(unsafe { CStr::from_ptr(buf.as_ptr()) })
597 }
598}
599
600#[cfg(test)]
601mod tests {
602 use super::*;
603
604 #[test]
605 fn test_session() {
606 assert!(Session::new().is_ok());
607 }
608
609 #[test]
610 fn test_usercheck() {
611 let name = "nobody".to_string();
612 let mut passwd = "some_password".to_string();
613 {
614 let session = Session::auth_usercheck(name.as_str(), None, None, Some(&mut passwd.clone())).unwrap();
615 assert_eq!(session.auth_getitem(AuthItem::Name).unwrap(), name);
616 }
617 {
618 let session = Session::auth_usercheck(name.as_str(), Some("passwd"), None, Some(&mut passwd.clone())).unwrap();
619 assert_eq!(session.auth_getitem(AuthItem::Name).unwrap(), name);
620 }
621 {
622 let session = Session::auth_usercheck(name.as_str(), Some("passwd"), Some("type"), Some(&mut passwd.clone())).unwrap();
623 assert_eq!(session.auth_getitem(AuthItem::Name).unwrap(), name);
624 }
625 {
626 let session = Session::auth_usercheck(name.as_str(), Some("passwd"), None, Some(&mut passwd)).unwrap();
627 assert_eq!(session.auth_getitem(AuthItem::Name).unwrap(), name);
628 }
629 }
630
631 #[test]
632 fn test_authuserresponse() {
633 let name = "nobody".to_string();
634 let style = Some("passwd");
635 let mut passwd = "some_passwd".to_string();
636 let session = Session::auth_usercheck(name.as_str(), style, None, Some(&mut passwd)).unwrap();
637 let mut res = String::from_utf8([1u8; 1024].to_vec()).unwrap();
638 assert!(session.auth_userresponse(&mut res, 0).is_ok());
639 }
640
641 #[test]
642 fn test_gethostname() {
643 let mut buf = [0i8; (libc::_SC_HOST_NAME_MAX + 1) as usize];
644 let host = gethostname(&mut buf).unwrap();
645 println!("{}", host.to_str().unwrap());
646 assert!(host.to_str().is_ok());
647 }
648
649 #[test]
650 fn test_auth_userchallenge() {
651 let _ = unsafe { libc::setresuid(1000, 1000, 1000) };
652 let path = CString::new("/usr/libexec/auth").unwrap();
653 let perm = CString::new("rx").unwrap();
654 let _ = unsafe { libc::unveil(path.as_ptr(), perm.as_ptr()) };
655 let (_session, _challenge) = Session::auth_userchallenge("user", None, Some("auth-doas")).unwrap();
656 }
657}