1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
//! PAM authentication for srdwm's own session-lock UI
//! (`crates/wayland/src/native_lock.rs`) - the one piece of that feature
//! that must never be "close enough": this is what stands between a typed
//! password and actually unlocking the session.
//!
//! Uses `pam_client`'s application-side API (the same shape swaylock,
//! gtklock, and every other real screen locker use) rather than reading
//! `/etc/shadow` by hand - PAM already handles the privilege boundary
//! correctly (on a normal distro, `pam_unix.so` shells out to the setuid
//! `unix_chkpwd` helper to compare the password, so this process never
//! needs elevated privileges or shadow-file access itself) and honors
//! whatever the system's actual auth policy is (a YubiKey module, an
//! account lockout policy, anything `/etc/pam.d/srdwm` enables), not just
//! a plain password check.
//!
//! Needs `/etc/pam.d/srdwm` to exist (any distro's default `login`-derived
//! policy works, e.g. `auth include login`) - a service with no PAM
//! config file at all fails every authentication attempt, not falls back
//! to some default. That file is a root-owned system config change,
//! deliberately not written by this code.
use pam_client::conv_mock::Conversation;
use pam_client::{Context, Flag};
/// The PAM service name - see `Context::new`'s own docs: this is what
/// selects the policy from `/etc/pam.d/<service>`.
const SERVICE: &str = "srdwm";
/// Verifies `password` for `username` against the system's real PAM
/// policy. `true` only for a genuine, complete authentication success
/// (both `authenticate` *and* `acct_mgmt`, so a correct password on a
/// locked or expired account still correctly fails) - every other
/// outcome, including a PAM setup problem that has nothing to do with the
/// password itself, resolves to `false`. Deliberately no distinction
/// between "wrong password" and "something is broken" in the return value
/// - fail secure means every non-success path stays locked, not just the
/// ones that are the user's own fault. Logged at `warn` for whoever's
/// debugging a setup problem, never at a level that would put the
/// password itself in a log.
pub fn authenticate(username: &str, password: &str) -> bool {
let conversation = Conversation::with_credentials(username, password);
let mut context = match Context::new(SERVICE, Some(username), conversation) {
Ok(ctx) => ctx,
Err(e) => {
log::warn!("session lock: failed to start PAM context for service '{SERVICE}': {e} ({:?})", e.code());
return false;
}
};
if let Err(e) = context.authenticate(Flag::NONE) {
log::warn!("session lock: PAM authentication failed: {e} ({:?})", e.code());
return false;
}
if let Err(e) = context.acct_mgmt(Flag::NONE) {
log::warn!("session lock: PAM account check failed: {e} ({:?})", e.code());
return false;
}
true
}
|