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}