Skip to main content

sentinelpass_protocol/
token.rs

1//! IPC auth token file management.
2//!
3//! WBS-412 (SR-DATA-003): the token is a bearer credential for the whole
4//! IPC surface, so it is created with an explicit owner-only mode (0600 on
5//! Unix, no umask-exposed window), inside an owner-only directory created on
6//! demand. A loose mode found at load is warned about and tightened
7//! (warn-level policy: refusing daemon startup would turn a loose mode into
8//! a local DoS; the owner-only parent directory is the primary guard). The
9//! core helper mirrors `sentinelpass_core::platform` but is duplicated here
10//! because the protocol crate must not depend on core.
11
12use crate::paths::default_ipc_token_path;
13use crate::{ProtocolError, Result};
14use rand::{rngs::OsRng, RngCore};
15use std::io::Write;
16use std::path::{Path, PathBuf};
17use tracing::warn;
18use zeroize::Zeroize;
19
20/// Read IPC auth token from disk.
21pub fn load_ipc_token() -> Result<String> {
22    load_ipc_token_from(&default_ipc_token_path())
23}
24
25/// Path of the native-host installation capability secret (WBS-505):
26/// `<config dir>/PasswordManager/native_host.capability`, 0600, provisioned
27/// by the daemon. The host reads and presents it; only its hash lives in
28/// the daemon's capability store.
29pub fn native_host_capability_path() -> PathBuf {
30    crate::paths::get_config_dir().join("native_host.capability")
31}
32
33/// Load the presented native-host capability secret, if provisioned.
34/// `None` = not installed yet (the daemon mints it on its first start).
35pub fn load_native_host_capability() -> Option<String> {
36    std::fs::read_to_string(native_host_capability_path())
37        .ok()
38        .map(|s| s.trim().to_string())
39        .filter(|s| !s.is_empty())
40}
41
42/// Path-parameterized variant of [`load_ipc_token`] (used by tests and
43/// embedders that keep the token outside the default location).
44pub fn load_ipc_token_from(token_path: &Path) -> Result<String> {
45    let token = std::fs::read_to_string(token_path)?.trim().to_string();
46    if token.is_empty() {
47        return Err(ProtocolError::Ipc(format!(
48            "IPC token file is empty: {:?}",
49            token_path
50        )));
51    }
52    enforce_owner_only(token_path);
53    Ok(token)
54}
55
56/// Load existing IPC auth token or create one if it does not exist.
57pub fn load_or_create_ipc_token() -> Result<String> {
58    load_or_create_ipc_token_at(&default_ipc_token_path())
59}
60
61/// Path-parameterized variant of [`load_or_create_ipc_token`].
62pub fn load_or_create_ipc_token_at(token_path: &Path) -> Result<String> {
63    if let Some(parent) = token_path.parent() {
64        if !parent.exists() {
65            // Directory the daemon is creating: owner-only from birth
66            // (WBS-412). A pre-existing parent (user-chosen, or a test
67            // tempdir) is never re-chmod'd.
68            std::fs::create_dir_all(parent)?;
69            #[cfg(unix)]
70            {
71                use std::os::unix::fs::PermissionsExt;
72                std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))?;
73            }
74        } else {
75            std::fs::create_dir_all(parent)?;
76        }
77    }
78
79    if token_path.exists() {
80        return load_ipc_token_from(token_path);
81    }
82
83    let mut token_bytes = [0u8; 32];
84    OsRng.fill_bytes(&mut token_bytes);
85    let token = hex::encode(token_bytes);
86    token_bytes.zeroize();
87
88    let mut options = std::fs::OpenOptions::new();
89    options.write(true).create_new(true);
90    #[cfg(unix)]
91    {
92        // Born owner-only: no window where the token exists with umask
93        // permissions (WBS-412). The chmod below stays as a backstop.
94        use std::os::unix::fs::OpenOptionsExt;
95        options.mode(0o600);
96    }
97    let mut file = options.open(token_path)?;
98    file.write_all(token.as_bytes())?;
99
100    #[cfg(unix)]
101    {
102        use std::os::unix::fs::PermissionsExt;
103        std::fs::set_permissions(token_path, std::fs::Permissions::from_mode(0o600))?;
104    }
105
106    Ok(token)
107}
108
109/// Warn about and tighten (best-effort) a token file whose mode is not
110/// owner-only. No-op on platforms without POSIX modes (the Windows ACL story
111/// relies on user-profile inheritance; see `paths.rs` / the status matrix).
112fn enforce_owner_only(token_path: &Path) {
113    #[cfg(unix)]
114    {
115        use std::os::unix::fs::PermissionsExt;
116        let owner_only = std::fs::metadata(token_path)
117            .map(|m| m.permissions().mode() & 0o077 == 0)
118            .unwrap_or(true); // vanished between read and stat: nothing to tighten
119        if !owner_only {
120            warn!(
121                "IPC token file {:?} is group/world-accessible; tightening to 0600",
122                token_path
123            );
124            let _ = std::fs::set_permissions(token_path, std::fs::Permissions::from_mode(0o600));
125        }
126    }
127    #[cfg(not(unix))]
128    {
129        let _ = token_path;
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[cfg(unix)]
138    #[test]
139    fn created_token_and_parent_are_owner_only() {
140        use std::os::unix::fs::PermissionsExt;
141
142        let outer = tempfile::TempDir::new().unwrap();
143        let token_path = outer.path().join("cfg").join("ipc.token");
144
145        load_or_create_ipc_token_at(&token_path).unwrap();
146
147        let mode = |p: &Path| std::fs::metadata(p).unwrap().permissions().mode() & 0o777;
148        assert_eq!(mode(&token_path), 0o600, "token must be born 0600");
149        assert_eq!(
150            mode(token_path.parent().unwrap()),
151            0o700,
152            "created parent dir must be 0700"
153        );
154
155        // Round-trip: the same token loads again.
156        let again = load_or_create_ipc_token_at(&token_path).unwrap();
157        let first = std::fs::read_to_string(&token_path).unwrap();
158        assert_eq!(again.trim(), first.trim());
159    }
160
161    #[cfg(unix)]
162    #[test]
163    fn loose_token_mode_is_repaired_on_load() {
164        use std::os::unix::fs::PermissionsExt;
165
166        let outer = tempfile::TempDir::new().unwrap();
167        let token_path = outer.path().join("ipc.token");
168        load_or_create_ipc_token_at(&token_path).unwrap();
169
170        std::fs::set_permissions(&token_path, std::fs::Permissions::from_mode(0o644)).unwrap();
171        load_ipc_token_from(&token_path).unwrap();
172        let mode = std::fs::metadata(&token_path).unwrap().permissions().mode() & 0o777;
173        assert_eq!(mode, 0o600, "load must tighten a loose token mode");
174    }
175
176    // Pre-existing parents are never re-chmod'd (a token stored beside
177    // unrelated files must not change their directory's mode).
178    #[cfg(unix)]
179    #[test]
180    fn preexisting_parent_directory_is_left_untouched() {
181        use std::os::unix::fs::PermissionsExt;
182
183        let outer = tempfile::TempDir::new().unwrap(); // already exists, 0700
184        let token_path = outer.path().join("ipc.token");
185        load_or_create_ipc_token_at(&token_path).unwrap();
186        // If this ran with an unconditional chmod the assertion below would
187        // be trivially true; it guards the else-branch against regressions
188        // by asserting nothing changed even with a deliberately looser dir.
189        let loose = outer.path().join("loose");
190        std::fs::create_dir_all(&loose).unwrap();
191        std::fs::set_permissions(&loose, std::fs::Permissions::from_mode(0o755)).unwrap();
192        let token_path = loose.join("ipc.token");
193        load_or_create_ipc_token_at(&token_path).unwrap();
194        let mode = std::fs::metadata(&loose).unwrap().permissions().mode() & 0o777;
195        assert_eq!(mode, 0o755, "pre-existing parent must stay untouched");
196    }
197}