libpwquality/
lib.rs

1//! libpwquality bindings for Rust
2//!
3//! ## Example
4//!
5//! ```
6#![doc = include_str!("../examples/example.rs")]
7//! ```
8//!
9//! ## Cargo features
10//!
11//! * `vX_Y_Z`:  Build with system libpwquality version X.Y.Z.
12//!   v1_0 *Enabled by default*
13//!
14//! * `vendored`: Build with vendored libpwquality.
15//!   This requires cracklib to be installed.
16//!   You can also set `CRACKLIB_INCLUDE_PATH` and `CRACKLIB_LIBRARY_PATH`
17//!   environment variables to specify the include path and library path.
18//!   *Disabled by default*
19//!
20//! * `vendored-cracklib`: Build with vendored libpwquality and cracklib.
21//!   The build script will try to guess the path of cracklib dictionaries,
22//!   but you can set `DEFAULT_CRACKLIB_DICT` environment variable to override it.
23//!   *Disabled by default*
24
25#![cfg_attr(docsrs, feature(doc_auto_cfg))]
26
27use libpwquality_sys as sys;
28use paste::paste;
29use std::os::raw::{c_char, c_int, c_void};
30use std::{
31    ffi::{CStr, CString},
32    path::Path,
33    ptr::{null, null_mut},
34};
35
36macro_rules! define_settings {
37    ($($(#[$meta:meta])? $setting:ident),* $(,)?) => {
38        paste! {
39            /// `PWQuality` Setting.
40            #[derive(Copy, Clone, Debug)]
41            #[non_exhaustive]
42            enum Setting {
43                $(
44                    $(#[$meta])?
45                    $setting = sys::[<PWQ_SETTING_ $setting:snake:upper>] as isize,
46                )*
47            }
48        }
49    };
50}
51
52define_settings! {
53    DiffOk,
54    MinLength,
55    DigCredit,
56    UpCredit,
57    LowCredit,
58    OthCredit,
59    MinClass,
60    MaxRepeat,
61    DictPath,
62    MaxClassRepeat,
63    GecosCheck,
64    BadWords,
65    #[cfg(any(feature = "v1_2", feature = "vendored", feature = "vendored-cracklib"))]
66    MaxSequence,
67    #[cfg(any(feature = "v1_3", feature = "vendored", feature = "vendored-cracklib"))]
68    DictCheck,
69    #[cfg(any(feature = "v1_4", feature = "vendored", feature = "vendored-cracklib"))]
70    UserCheck,
71    #[cfg(any(feature = "v1_4", feature = "vendored", feature = "vendored-cracklib"))]
72    Enforcing,
73    #[cfg(any(feature = "v1_4_1", feature = "vendored", feature = "vendored-cracklib"))]
74    RetryTimes,
75    #[cfg(any(feature = "v1_4_1", feature = "vendored", feature = "vendored-cracklib"))]
76    EnforceRoot,
77    #[cfg(any(feature = "v1_4_1", feature = "vendored", feature = "vendored-cracklib"))]
78    LocalUsers,
79    #[cfg(any(feature = "v1_4_3", feature = "vendored", feature = "vendored-cracklib"))]
80    UserSubstr,
81}
82
83/// `PWQuality` Error.
84pub struct PWQError(String);
85
86impl PWQError {
87    fn new_aux(error_code: i32, aux_error: Option<*mut c_void>) -> Self {
88        unsafe {
89            let s =
90                sys::pwquality_strerror(null_mut(), 0, error_code, aux_error.unwrap_or(null_mut()))
91                    .as_ref()
92                    .map(|p| CStr::from_ptr(p).to_string_lossy().to_string())
93                    .unwrap_or(format!("Unknown error: errcode={error_code}"));
94
95            Self(s)
96        }
97    }
98
99    fn new(error_code: i32) -> Self {
100        Self::new_aux(error_code, None)
101    }
102}
103
104impl std::error::Error for PWQError {}
105
106impl std::fmt::Debug for PWQError {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        write!(f, "PWQError: {}", self.0)
109    }
110}
111
112impl std::fmt::Display for PWQError {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        write!(f, "{}", self.0)
115    }
116}
117
118type Result<T> = std::result::Result<T, PWQError>;
119
120macro_rules! define_getseters {
121    (#[$doc_meta:meta] $(#[$feat_meta:meta])? $func:ident, $setting:ident) => {
122        paste! {
123            $(#[$feat_meta])?
124            #[doc = " Get"]
125            #[$doc_meta]
126            pub fn [<get_ $func>] (&self) -> i32 {
127                self.get_int_value($crate::Setting::$setting)
128            }
129
130            $(#[$feat_meta])?
131            #[doc = " Set"]
132            #[$doc_meta]
133            pub fn $func(&self, value: i32) -> &Self {
134                self.set_int_value($crate::Setting::$setting, value)
135            }
136        }
137    };
138    (#[$doc_meta:meta] $(#[$feat_meta:meta])? $func:ident, $setting:ident, bool) => {
139        paste! {
140            $(#[$feat_meta])?
141            #[doc = " Get"]
142            #[$doc_meta]
143            pub fn [<get_ $func>] (&self) -> bool {
144                self.get_int_value($crate::Setting::$setting) != 0
145            }
146
147            $(#[$feat_meta])?
148            #[doc = " Set"]
149            #[$doc_meta]
150            pub fn $func(&self, value: bool) -> &Self {
151                self.set_int_value($crate::Setting::$setting, i32::from(value))
152            }
153        }
154    };
155}
156
157/// `PWQuality` instance that holds the underlying [pwquality_settings_t](sys::pwquality_settings_t).
158pub struct PWQuality {
159    pwq: *mut sys::pwquality_settings_t,
160}
161
162impl PWQuality {
163    /// Create a new `PWQuality` instance.
164    pub fn new() -> Result<Self> {
165        unsafe {
166            let ptr = sys::pwquality_default_settings();
167
168            ptr.as_ref()
169                .ok_or(PWQError::new(sys::PWQ_ERROR_MEM_ALLOC))
170                .map(|_| Self { pwq: ptr })
171        }
172    }
173
174    /// Parse the default configuration file.
175    pub fn read_default_config(&self) -> Result<&Self> {
176        self.read_optional_config::<&str>(None)
177    }
178
179    /// Parse the given configuration file.
180    pub fn read_config<P: AsRef<Path>>(&self, path: P) -> Result<&Self> {
181        self.read_optional_config(Some(path))
182    }
183
184    /// Parse the configuration file.
185    fn read_optional_config<P: AsRef<Path>>(&self, path: Option<P>) -> Result<&Self> {
186        let mut aux_error = null_mut();
187        let c_path = path
188            .map(|p| CString::new(p.as_ref().to_string_lossy().to_string()))
189            .transpose()
190            .unwrap();
191
192        let ret = unsafe {
193            sys::pwquality_read_config(
194                self.pwq,
195                c_path.as_ref().map(|s| s.as_ptr()).unwrap_or(null()),
196                &mut aux_error,
197            )
198        };
199
200        if ret == 0 {
201            Ok(self)
202        } else {
203            Err(PWQError::new_aux(ret, Some(aux_error)))
204        }
205    }
206
207    /// Set value of an integer setting.
208    fn set_int_value(&self, setting: Setting, value: i32) -> &Self {
209        let ret = unsafe { sys::pwquality_set_int_value(self.pwq, setting as c_int, value) };
210
211        debug_assert!(ret == 0);
212
213        self
214    }
215
216    /// Get value of an integer setting.
217    fn get_int_value(&self, setting: Setting) -> i32 {
218        let mut value: i32 = 0;
219        let ret = unsafe { sys::pwquality_get_int_value(self.pwq, setting as c_int, &mut value) };
220
221        debug_assert!(ret == 0);
222
223        value
224    }
225
226    /// Set value of a string setting.
227    fn set_str_value(&self, setting: Setting, value: &str) -> Result<&Self> {
228        let value = CString::new(value).unwrap();
229        let ret =
230            unsafe { sys::pwquality_set_str_value(self.pwq, setting as c_int, value.as_ptr()) };
231
232        if ret == 0 {
233            Ok(self)
234        } else {
235            Err(PWQError::new(ret))
236        }
237    }
238
239    /// Get value of a string setting.
240    fn get_str_value(&self, setting: Setting) -> Result<String> {
241        let mut ptr: *const c_char = null();
242
243        let ret = unsafe { sys::pwquality_get_str_value(self.pwq, setting as c_int, &mut ptr) };
244        if ret == 0 {
245            let s = unsafe {
246                ptr.as_ref()
247                    .map(|p| CStr::from_ptr(p).to_string_lossy().to_string())
248                    .unwrap_or_default()
249            };
250
251            Ok(s)
252        } else {
253            Err(PWQError::new(ret))
254        }
255    }
256
257    /// Generate a random password of entropy_bits entropy and check it according to the settings.
258    pub fn generate(&self, bits: i32) -> Result<String> {
259        let mut ptr: *mut c_char = null_mut();
260        unsafe {
261            let ret = sys::pwquality_generate(self.pwq, bits, &mut ptr);
262
263            ptr.as_ref().ok_or(PWQError::new(ret)).map(|p| {
264                let s = CStr::from_ptr(p).to_string_lossy().to_string();
265
266                // free the memory allocated in the C library
267                libc::free(ptr.cast());
268
269                s
270            })
271        }
272    }
273
274    /// Check the password according to the settings.
275    pub fn check(
276        &self,
277        password: &str,
278        old_password: Option<&str>,
279        user: Option<&str>,
280    ) -> Result<i32> {
281        let c_password = CString::new(password).unwrap();
282        let mut aux_error = null_mut();
283
284        let c_old_password = old_password.map(CString::new).transpose().unwrap();
285        let c_user = user.map(CString::new).transpose().unwrap();
286
287        let ret = unsafe {
288            sys::pwquality_check(
289                self.pwq,
290                c_password.as_ptr(),
291                c_old_password
292                    .as_ref()
293                    .map(|s| s.as_ptr())
294                    .unwrap_or(null()),
295                c_user.as_ref().map(|s| s.as_ptr()).unwrap_or(null()),
296                &mut aux_error,
297            )
298        };
299
300        if ret < 0 {
301            Err(PWQError::new_aux(ret, Some(aux_error)))
302        } else {
303            Ok(ret)
304        }
305    }
306
307    define_getseters! {
308        #[doc = " the minimum number of characters in the new password that must not be present in the old password."]
309        min_diff,
310        DiffOk
311    }
312
313    define_getseters! {
314        #[doc = " the minimum acceptable size for the new password."]
315        min_length,
316        MinLength
317    }
318
319    define_getseters! {
320        #[doc = " the maximum credit for having digits in the new password."]
321        digit_credit,
322        DigCredit
323    }
324
325    define_getseters! {
326        #[doc = " the maximum credit for having uppercase characters in the new password."]
327        uppercase_credit,
328        UpCredit
329    }
330
331    define_getseters! {
332        #[doc = " the maximum credit for having lowercase characters in the new password."]
333        lowercase_credit,
334        LowCredit
335    }
336
337    define_getseters! {
338        #[doc = " the maximum credit for having other characters in the new password."]
339        other_credit,
340        OthCredit
341    }
342
343    define_getseters! {
344        #[doc = " the minimum number of required classes of characters for the new password."]
345        min_class,
346        MinClass
347    }
348
349    define_getseters! {
350        #[doc = " the maximum number of allowed same consecutive characters in the new password."]
351        max_repeat,
352        MaxRepeat
353    }
354
355    define_getseters! {
356        #[doc = " the maximum number of allowed consecutive characters of the same class in the new password."]
357        max_class_repeat,
358        MaxClassRepeat
359    }
360
361    define_getseters! {
362        #[doc = " the maximum length of monotonic character sequences in the new password."]
363        #[cfg(any(feature = "v1_2", feature = "vendored", feature = "vendored-cracklib"))]
364        max_sequence,
365        MaxSequence
366    }
367
368    define_getseters! {
369        #[doc = " whether to perform the passwd GECOS field check."]
370        gecos_check,
371        GecosCheck,
372        bool
373    }
374
375    define_getseters! {
376        #[doc = " whether to perform the dictionary check."]
377        #[cfg(any(feature = "v1_3", feature = "vendored", feature = "vendored-cracklib"))]
378        dict_check,
379        DictCheck,
380        bool
381    }
382
383    define_getseters! {
384        #[doc = " whether to perform the user name check."]
385        #[cfg(any(feature = "v1_4", feature = "vendored", feature = "vendored-cracklib"))]
386        user_check,
387        UserCheck,
388        bool
389    }
390
391    define_getseters! {
392        #[doc = " whether the check is enforced."]
393        #[cfg(any(feature = "v1_4", feature = "vendored", feature = "vendored-cracklib"))]
394        enforcing,
395        Enforcing,
396        bool
397    }
398
399    define_getseters! {
400        #[doc = " maximum retries for the password change should be allowed."]
401        #[cfg(any(feature = "v1_4_1", feature = "vendored", feature = "vendored-cracklib"))]
402        retry_times,
403        RetryTimes
404    }
405
406    define_getseters! {
407        #[doc = " whether the check is enforced for root."]
408        #[cfg(any(feature = "v1_4_1", feature = "vendored", feature = "vendored-cracklib"))]
409        enforce_for_root,
410        EnforceRoot,
411        bool
412    }
413
414    define_getseters! {
415        #[doc = " whether to check local users only."]
416        #[cfg(any(feature = "v1_4_1", feature = "vendored", feature = "vendored-cracklib"))]
417        local_users_only,
418        LocalUsers,
419        bool
420    }
421
422    #[cfg(any(
423        feature = "v1_4_5",
424        feature = "vendored",
425        feature = "vendored-cracklib"
426    ))]
427    /// Get the length of substrings of the user name to check.
428    pub fn get_user_substr(&self) -> i32 {
429        self.get_int_value(crate::Setting::UserSubstr)
430    }
431
432    #[cfg(any(
433        feature = "v1_4_3",
434        feature = "vendored",
435        feature = "vendored-cracklib"
436    ))]
437    /// Set the length of substrings of the user name to check.
438    pub fn user_substr(&self, value: i32) -> &Self {
439        self.set_int_value(crate::Setting::UserSubstr, value)
440    }
441
442    /// Set the list of words more than 3 characters long that are forbidden.
443    pub fn bad_words<W>(&self, words: W) -> Result<&Self>
444    where
445        W: IntoIterator,
446        W::Item: ToString,
447    {
448        let s = words
449            .into_iter()
450            .map(|s| s.to_string())
451            .collect::<Vec<String>>()
452            .join(" ");
453
454        self.set_str_value(Setting::BadWords, &s)
455    }
456
457    /// Get the list of words more than 3 characters long that are forbidden.
458    pub fn get_bad_words(&self) -> Result<Vec<String>> {
459        self.get_str_value(Setting::BadWords)
460            .map(|s| s.split_whitespace().map(String::from).collect())
461    }
462
463    /// Set the path to the cracklib dictionaries.
464    pub fn dict_path(&self, path: &str) -> Result<&Self> {
465        self.set_str_value(Setting::DictPath, path)
466    }
467
468    /// Get the path to the cracklib dictionaries.
469    pub fn get_dict_path(&self) -> Result<String> {
470        self.get_str_value(Setting::DictPath)
471    }
472}
473
474impl Drop for PWQuality {
475    /// Free pwquality settings data.
476    fn drop(&mut self) {
477        unsafe { sys::pwquality_free_settings(self.pwq) }
478    }
479}