Skip to content

Commit 9f84881

Browse files
pam: Support changing user secret
By changing keyring secrets as well if we can unlock them using the previous secret
1 parent 9a41573 commit 9f84881

9 files changed

Lines changed: 353 additions & 46 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pam/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ crate-type = ["cdylib"]
1717
libc = "0.2"
1818
tokio = { workspace = true, features = ["rt", "rt-multi-thread", "net", "io-util", "sync", "time"] }
1919
serde = { workspace = true }
20+
serde_repr = "0.1"
2021
zvariant = { workspace = true }
2122
tracing = { workspace = true }
2223
tracing-subscriber = { workspace = true, features = ["env-filter"] }

pam/README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,25 @@ auth required pam_unix.so
5959
auth optional pam_oo7.so
6060
account required pam_unix.so
6161
password required pam_unix.so
62+
password optional pam_oo7.so
6263
session required pam_unix.so
6364
session optional pam_oo7.so auto_start
6465
session optional pam_systemd.so
6566
```
6667

67-
**Important**: The module must be added to both `auth` and `session` stacks:
68-
- `auth` stack: Captures and stashes the password
69-
- `session` stack: Retrieves the password and sends it to the daemon
68+
**Important**: The module should be added to three PAM stacks:
69+
- `auth` stack: Captures and stashes the password during authentication
70+
- `session` stack: Retrieves the stashed password and sends it to the daemon for keyring unlocking
71+
- `password` stack: Intercepts password changes and updates the keyring password to match
72+
73+
#### Password Change Support
74+
75+
When added to the `password` stack, the module will automatically update your keyring passwords when you change your user password (e.g., using `passwd` command). This ensures your keyrings remain accessible after password changes.
76+
77+
The module intercepts the password change operation:
78+
1. Captures both the old and new passwords
79+
2. Sends them to the daemon
80+
3. The daemon validates the old password and re-encrypts all matching keyrings with the new password
7081

7182
## Configuration
7283

pam/src/ffi.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@ use std::os::raw::{c_char, c_int, c_void};
44
pub const PAM_SUCCESS: c_int = 0;
55
pub const PAM_SYSTEM_ERR: c_int = 4;
66
pub const PAM_AUTHTOK_RECOVER_ERR: c_int = 20;
7+
pub const PAM_IGNORE: c_int = 25;
78

89
// PAM item types
910
pub const PAM_AUTHTOK: c_int = 6;
11+
pub const PAM_OLDAUTHTOK: c_int = 7;
12+
13+
// PAM chauthtok flags
14+
pub const PAM_PRELIM_CHECK: c_int = 0x1;
15+
pub const PAM_UPDATE_AUTHTOK: c_int = 0x2;
1016

1117
// Opaque PAM handle type
1218
#[repr(C)]

pam/src/lib.rs

Lines changed: 116 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ use std::{
1515
use zeroize::Zeroizing;
1616

1717
use crate::{
18-
ffi::{PAM_AUTHTOK, PAM_AUTHTOK_RECOVER_ERR, PAM_SUCCESS, PAM_SYSTEM_ERR, pam_handle_t},
18+
ffi::{
19+
PAM_AUTHTOK, PAM_AUTHTOK_RECOVER_ERR, PAM_IGNORE, PAM_OLDAUTHTOK, PAM_PRELIM_CHECK,
20+
PAM_SUCCESS, PAM_SYSTEM_ERR, PAM_UPDATE_AUTHTOK, pam_handle_t,
21+
},
1922
protocol::PamMessage,
2023
socket::send_secret_to_daemon,
2124
};
@@ -42,18 +45,30 @@ unsafe fn get_user(pamh: *mut pam_handle_t) -> Result<String, c_int> {
4245

4346
/// Get the authentication token from PAM
4447
unsafe fn get_auth_token(pamh: *mut pam_handle_t) -> Result<Zeroizing<Vec<u8>>, c_int> {
48+
unsafe { get_auth_token_internal(pamh, PAM_AUTHTOK, "PAM_AUTHTOK") }
49+
}
50+
51+
/// Get the old authentication token from PAM (for password changes)
52+
unsafe fn get_old_auth_token(pamh: *mut pam_handle_t) -> Result<Zeroizing<Vec<u8>>, c_int> {
53+
unsafe { get_auth_token_internal(pamh, PAM_OLDAUTHTOK, "PAM_OLDAUTHTOK") }
54+
}
55+
56+
unsafe fn get_auth_token_internal(
57+
pamh: *mut pam_handle_t,
58+
item_type: c_int,
59+
item_name: &str,
60+
) -> Result<Zeroizing<Vec<u8>>, c_int> {
4561
let mut authtok_ptr: *const std::os::raw::c_void = std::ptr::null();
4662

47-
// Use pam_get_item to get PAM_AUTHTOK (just like pam_gnome_keyring does)
48-
let ret = unsafe { ffi::pam_get_item(pamh, PAM_AUTHTOK, &mut authtok_ptr) };
63+
let ret = unsafe { ffi::pam_get_item(pamh, item_type, &mut authtok_ptr) };
4964

5065
if ret != PAM_SUCCESS {
51-
tracing::debug!("pam_get_item returned error: {}", ret);
66+
tracing::debug!("pam_get_item({}) returned error: {}", item_name, ret);
5267
return Err(ret);
5368
}
5469

5570
if authtok_ptr.is_null() {
56-
tracing::debug!("PAM_AUTHTOK is null (password not available)");
71+
tracing::debug!("{} is null (password not available)", item_name);
5772
return Err(PAM_SYSTEM_ERR);
5873
}
5974

@@ -62,7 +77,8 @@ unsafe fn get_auth_token(pamh: *mut pam_handle_t) -> Result<Zeroizing<Vec<u8>>,
6277
let password_bytes = password_cstr.to_bytes().to_vec();
6378

6479
tracing::debug!(
65-
"Captured auth token of length {} bytes",
80+
"Captured {} of length {} bytes",
81+
item_name,
6682
password_bytes.len()
6783
);
6884

@@ -258,10 +274,7 @@ pub unsafe extern "C" fn pam_sm_open_session(
258274
}
259275
};
260276

261-
let message = PamMessage {
262-
username: username.clone(),
263-
secret: password.to_vec(),
264-
};
277+
let message = PamMessage::unlock(username.clone(), password.to_vec());
265278

266279
// Send the secret to the oo7 daemon
267280
std::thread::spawn(
@@ -294,11 +307,100 @@ pub extern "C" fn pam_sm_close_session(
294307

295308
/// PAM password change entry point
296309
#[unsafe(no_mangle)]
297-
pub extern "C" fn pam_sm_chauthtok(
298-
_pamh: *mut pam_handle_t,
299-
_flags: c_int,
310+
#[allow(clippy::missing_safety_doc)]
311+
pub unsafe extern "C" fn pam_sm_chauthtok(
312+
pamh: *mut pam_handle_t,
313+
flags: c_int,
300314
_argc: c_int,
301315
_argv: *mut *const c_char,
302316
) -> c_int {
303-
PAM_SUCCESS
317+
if let Ok(layer) = tracing_journald::layer() {
318+
use tracing_subscriber::layer::SubscriberExt;
319+
let subscriber = tracing_subscriber::registry().with(layer);
320+
let _ = tracing::subscriber::set_global_default(subscriber);
321+
}
322+
323+
if flags & PAM_PRELIM_CHECK != 0 {
324+
tracing::debug!("PAM_PRELIM_CHECK phase for password change");
325+
return PAM_IGNORE;
326+
}
327+
328+
if flags & PAM_UPDATE_AUTHTOK != 0 {
329+
tracing::debug!("PAM_UPDATE_AUTHTOK phase for password change");
330+
331+
let username = match unsafe { get_user(pamh) } {
332+
Ok(user) => user,
333+
Err(ret) => {
334+
tracing::error!("Failed to get username during password change");
335+
return ret;
336+
}
337+
};
338+
339+
let user_uid = match get_user_uid(&username) {
340+
Some(uid) => uid,
341+
None => {
342+
tracing::error!("Failed to get UID for user: {}", username);
343+
return PAM_SYSTEM_ERR;
344+
}
345+
};
346+
347+
let old_password = match unsafe { get_old_auth_token(pamh) } {
348+
Ok(pass) => pass,
349+
Err(_) => {
350+
tracing::warn!(
351+
"No old password available for user {}, cannot update keyring password",
352+
username
353+
);
354+
return PAM_SUCCESS;
355+
}
356+
};
357+
358+
let new_password = match unsafe { get_auth_token(pamh) } {
359+
Ok(pass) => pass,
360+
Err(_) => {
361+
tracing::warn!(
362+
"No new password available for user {}, cannot update keyring password",
363+
username
364+
);
365+
return PAM_SUCCESS;
366+
}
367+
};
368+
369+
if old_password.is_empty() || new_password.is_empty() {
370+
tracing::debug!("Old or new password is empty, skipping keyring password change");
371+
return PAM_SUCCESS;
372+
}
373+
374+
tracing::info!(
375+
"Password change for user {}: old={} bytes, new={} bytes",
376+
username,
377+
old_password.len(),
378+
new_password.len()
379+
);
380+
381+
let message = PamMessage::change_password(
382+
username.clone(),
383+
old_password.to_vec(),
384+
new_password.to_vec(),
385+
);
386+
387+
std::thread::spawn(
388+
move || match send_secret_to_daemon(message, user_uid, false) {
389+
Ok(_) => {
390+
tracing::info!(
391+
"Successfully sent password change request to oo7 daemon for user: {}",
392+
username
393+
);
394+
}
395+
Err(e) => {
396+
tracing::error!("Failed to send password change to oo7 daemon: {}", e);
397+
}
398+
},
399+
);
400+
401+
return PAM_SUCCESS;
402+
}
403+
404+
tracing::warn!("pam_sm_chauthtok called with unknown flags: {}", flags);
405+
PAM_IGNORE
304406
}

pam/src/protocol.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,45 @@
11
use serde::{Deserialize, Serialize};
2+
use serde_repr::{Deserialize_repr, Serialize_repr};
23
use zeroize::{Zeroize, ZeroizeOnDrop};
34
use zvariant::{Type, serialized::Context, to_bytes};
45

6+
#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, Type, PartialEq, Eq)]
7+
#[repr(u8)]
8+
pub enum PamOperation {
9+
Unlock = 0,
10+
ChangePassword = 1,
11+
}
12+
513
#[derive(Debug, Serialize, Deserialize, Type, Zeroize, ZeroizeOnDrop)]
614
pub struct PamMessage {
15+
#[zeroize(skip)]
16+
operation: PamOperation,
717
pub username: String,
8-
pub secret: Vec<u8>,
18+
old_secret: Vec<u8>,
19+
new_secret: Vec<u8>,
920
}
1021

1122
impl PamMessage {
23+
/// Create an unlock message
24+
pub fn unlock(username: String, secret: Vec<u8>) -> Self {
25+
Self {
26+
operation: PamOperation::Unlock,
27+
username,
28+
old_secret: Vec::new(),
29+
new_secret: secret,
30+
}
31+
}
32+
33+
/// Create a password change message
34+
pub fn change_password(username: String, old_secret: Vec<u8>, new_secret: Vec<u8>) -> Self {
35+
Self {
36+
operation: PamOperation::ChangePassword,
37+
username,
38+
old_secret,
39+
new_secret,
40+
}
41+
}
42+
1243
pub fn to_bytes(&self) -> Result<Vec<u8>, zvariant::Error> {
1344
let ctxt = Context::new_dbus(zvariant::LE, 0);
1445
to_bytes(ctxt, self).map(|data| data.to_vec())

pam/src/socket.rs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,7 @@ async fn send_secret_to_daemon_async(
156156

157157
tracing::debug!("Connected to daemon socket");
158158

159-
tracing::debug!(
160-
"Sending secret of length {} bytes for user {}",
161-
message.secret.len(),
162-
message.username
163-
);
159+
tracing::debug!("Sending message for user {}", message.username);
164160
let message_bytes = Zeroizing::new(message.to_bytes().map_err(SocketError::Serialize)?);
165161

166162
let length = message_bytes.len() as u32;
@@ -212,15 +208,12 @@ mod tests {
212208

213209
let message = PamMessage::from_bytes(&message_bytes).unwrap();
214210
assert_eq!(message.username, "testuser");
215-
assert_eq!(message.secret, b"testpassword");
211+
assert_eq!(message.new_secret, b"testpassword");
216212
});
217213

218214
tokio::time::sleep(Duration::from_millis(100)).await;
219215

220-
let message = PamMessage {
221-
username: "testuser".to_string(),
222-
secret: b"testpassword".to_vec(),
223-
};
216+
let message = PamMessage::unlock("testuser".to_string(), b"testpassword".to_vec());
224217

225218
let result = send_secret_to_daemon_async(message, 1000, false).await;
226219
assert!(result.is_ok());

server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ openssl = { version = "0.10", optional = true }
2525
oo7 = { workspace = true, features = ["tokio"] }
2626
rpassword = "7.4"
2727
serde.workspace = true
28+
serde_repr = "0.1"
2829
sha2 = { version = "0.10", optional = true }
2930
tokio = { workspace = true, features = ["full"] }
3031
tokio-stream = "0.1"

0 commit comments

Comments
 (0)