Watch
1
0
Fork
You've already forked souveraine
0
souveraine/src/sessiond/auth.rs
Fimeg b4b30b124d Unified device state machine + security hardening
sessiond:
- device_state.rs: 8-state unified device state machine with legal
  transition table, sensor evidence model (proximity/accel/light/touch),
  confidence scoring, cross-sensor disagreement detection, and forensic
  logging with full state snapshots at every decision point.
- protocol.rs: extended with DeviceState, SensorInput, and ForensicLog
  IPC requests. SensorSource/SensorValue types for proximity, accel,
  light, touch.
- server.rs: DeviceStateMachine integrated into Daemon struct. Handlers
  for DeviceState (full state query), SensorInput (sensor evidence +
  Observed transitions), ForensicLog (recent entries query).
- lock.rs: Failed attempt counter on PIN surface (advisory, PAM owns
  lockout policy). Rendered as red digit glyphs below PIN dots.
- draw.rs: Attempt counter rendering + test.
- auth.rs: PAM config docs reference.
- mod.rs: device_state module added.

shell:
- IdleCoordinator.qml: Legal transition table with runtime enforcement.
  setState() refuses illegal transitions with warning. returnActive()
  explicitly only allows Dimmed/Waking.
- GlobalStates.qml: Write authority comments on every property (// WRITER:).
- SessionAudit.qml: SHA-256 replaces MD5 for hash chain. Forensic event
  wiring (device-state-transition, device-error, sensor-input, wake-event).
  logDeviceError/logSensorInput/logWakeEvent functions for QML callers.

Design doc: SouveraineOS/docs/DEVICE-STATE-MACHINE.md (separate repo).

Tests: 22 passing (was 7). Full lifecycle test exercises Active → Dimmed →
Locked → Observed → DozeLight → DozeDeep → Suspending → Asleep → Locked
with 23 forensic entries.
2026-07-24 16:29:04 -04:00

183 lines
6.4 KiB
Rust

//! Minimal PAM client for the fallback unlock surface.
//!
//! Hand-rolled FFI against libpam rather than a wrapper crate: the daemon
//! asks exactly one question ("does this PIN authenticate this user") and a
//! dependency is a bigger liability than 100 lines of well-understood
//! unsafe. pam_unix answers via the setuid unix_chkpwd helper, so this works
//! from an unprivileged session process — the same mechanism hyprlock and
//! the shell's PamContext rely on.
//!
//! Doctrine: auth goes through PAM, always (SESSION-AUTHORITY-DOCTRINE §4).
//! This module never inspects credentials itself; it carries the PIN to the
//! stack configured for the service and reports the verdict.
//!
//! The expected PAM service config is documented in
//! docs/pam-souveraine-sessiond.md. The file `/etc/pam.d/souveraine-sessiond`
//! is shipped as root-owned system config by the OS overlay, never by this
//! crate. Without it, pam_start fails and the lock session does not start.
use std::ffi::{c_char, c_int, c_void, CStr, CString};
const PAM_SUCCESS: c_int = 0;
const PAM_CONV_ERR: c_int = 19;
const PAM_PROMPT_ECHO_OFF: c_int = 1;
const PAM_PROMPT_ECHO_ON: c_int = 2;
const PAM_ERROR_MSG: c_int = 3;
const PAM_TEXT_INFO: c_int = 4;
#[repr(C)]
struct PamMessage {
msg_style: c_int,
msg: *const c_char,
}
#[repr(C)]
struct PamResponse {
resp: *mut c_char,
resp_retcode: c_int,
}
#[repr(C)]
struct PamConv {
conv: unsafe extern "C" fn(
num_msg: c_int,
msg: *mut *const PamMessage,
resp: *mut *mut PamResponse,
appdata_ptr: *mut c_void,
) -> c_int,
appdata_ptr: *mut c_void,
}
#[link(name = "pam")]
extern "C" {
fn pam_start(
service: *const c_char,
user: *const c_char,
conv: *const PamConv,
handle: *mut *mut c_void,
) -> c_int;
fn pam_authenticate(handle: *mut c_void, flags: c_int) -> c_int;
fn pam_end(handle: *mut c_void, status: c_int) -> c_int;
fn pam_strerror(handle: *mut c_void, errnum: c_int) -> *const c_char;
}
/// The conversation only ever answers prompts with the PIN. Info/error
/// messages are acknowledged and dropped — the fallback surface has nowhere
/// to render a module's prose and must not block on it.
unsafe extern "C" fn conv_fn(
num_msg: c_int,
msg: *mut *const PamMessage,
resp: *mut *mut PamResponse,
appdata_ptr: *mut c_void,
) -> c_int {
if num_msg <= 0 || msg.is_null() || resp.is_null() || appdata_ptr.is_null() {
return PAM_CONV_ERR;
}
let pin = &*(appdata_ptr as *const CString);
// Linux-PAM passes an array of message pointers. Responses are freed by
// the module with free(), so they must come from malloc/strdup.
let responses =
libc::calloc(num_msg as usize, std::mem::size_of::<PamResponse>()) as *mut PamResponse;
if responses.is_null() {
return PAM_CONV_ERR;
}
// Linux-PAM convention: *msg points at a contiguous array of messages
// (modules there prompt one message at a time anyway).
for i in 0..num_msg as usize {
let m = (*msg).add(i);
if m.is_null() {
libc::free(responses as *mut c_void);
return PAM_CONV_ERR;
}
let slot = &mut *responses.add(i);
match (*m).msg_style {
PAM_PROMPT_ECHO_OFF | PAM_PROMPT_ECHO_ON => {
slot.resp = libc::strdup(pin.as_ptr());
if slot.resp.is_null() {
libc::free(responses as *mut c_void);
return PAM_CONV_ERR;
}
}
PAM_ERROR_MSG | PAM_TEXT_INFO => {
slot.resp = std::ptr::null_mut();
}
_ => {
libc::free(responses as *mut c_void);
return PAM_CONV_ERR;
}
}
slot.resp_retcode = 0;
}
*resp = responses;
PAM_SUCCESS
}
/// Run one authenticate conversation. Blocking (pam_unix sleeps ~2s on a
/// wrong answer); callers own threading. Returns Ok(()) on success and the
/// stack's complaint otherwise.
pub fn authenticate(service: &str, user: &str, pin: &str) -> Result<(), String> {
let service = CString::new(service).map_err(|_| "service contains NUL".to_string())?;
let user_c = CString::new(user).map_err(|_| "user contains NUL".to_string())?;
let pin = CString::new(pin).map_err(|_| "pin contains NUL".to_string())?;
// Boxed so the conversation callback has a stable pointer for the whole
// transaction; dropped after pam_end.
let pin = Box::new(pin);
let conv = PamConv {
conv: conv_fn,
appdata_ptr: &*pin as *const CString as *mut c_void,
};
let mut handle: *mut c_void = std::ptr::null_mut();
// SAFETY: all pointers live across the calls; handle is only used
// between a successful pam_start and pam_end.
unsafe {
let rc = pam_start(service.as_ptr(), user_c.as_ptr(), &conv, &mut handle);
if rc != PAM_SUCCESS {
return Err(format!("pam_start failed (code {rc})"));
}
let rc = pam_authenticate(handle, 0);
let verdict = if rc == PAM_SUCCESS {
Ok(())
} else {
let msg = pam_strerror(handle, rc);
Err(if msg.is_null() {
format!("authentication failed (code {rc})")
} else {
CStr::from_ptr(msg).to_string_lossy().into_owned()
})
};
pam_end(handle, rc);
verdict
}
}
/// The user this session belongs to, from the process's own identity —
/// sessiond authenticates the seat owner, never a name a caller supplies.
pub fn session_user() -> Result<String, String> {
// SAFETY: getpwuid_r with a stack buffer; result pointer checked.
unsafe {
let uid = libc::getuid();
let mut pwd: libc::passwd = std::mem::zeroed();
let mut buf = [0u8; 1024];
let mut result: *mut libc::passwd = std::ptr::null_mut();
// libc's `c_char` is i8 on some targets and u8 on others (aarch64),
// so the buffer is `u8` and we cast through `c_char` to satisfy
// whichever signature the target's libc crate exposes.
let rc = libc::getpwuid_r(
uid,
&mut pwd,
buf.as_mut_ptr() as *mut libc::c_char,
buf.len(),
&mut result,
);
if rc != 0 || result.is_null() {
return Err(format!("getpwuid_r failed for uid {uid}"));
}
Ok(CStr::from_ptr(pwd.pw_name).to_string_lossy().into_owned())
}
}