Skip to main content

lean_ctx/core/
session_token.rs

1/// Auto-generated session tokens for proxy/HTTP server security.
2///
3/// Security principle: Least Action — minimize attack surface by always having
4/// a token, even when the user hasn't explicitly configured one. The token is
5/// written to a file with restrictive permissions so authorized local clients
6/// can discover it.
7use std::path::PathBuf;
8
9const TOKEN_BYTES: usize = 32;
10const TOKEN_FILE: &str = "session_token";
11
12/// Generate a cryptographically random hex token.
13pub fn generate_token() -> String {
14    let mut buf = [0u8; TOKEN_BYTES];
15    getrandom::fill(&mut buf).expect("getrandom failed");
16    bytes_to_hex(&buf)
17}
18
19fn bytes_to_hex(bytes: &[u8]) -> String {
20    let mut s = String::with_capacity(bytes.len() * 2);
21    for &b in bytes {
22        s.push(char::from_digit((b >> 4) as u32, 16).unwrap_or('0'));
23        s.push(char::from_digit((b & 0xf) as u32, 16).unwrap_or('0'));
24    }
25    s
26}
27
28/// Resolve or generate the proxy/HTTP session token.
29///
30/// Priority:
31/// 1. Explicit env var (user override)
32/// 2. Existing token file (persistence across restarts)
33/// 3. Generate new + write to file
34pub fn resolve_proxy_token(env_var: &str) -> String {
35    if let Ok(val) = std::env::var(env_var) {
36        if !val.trim().is_empty() {
37            return val.trim().to_string();
38        }
39    }
40
41    let token_path = token_file_path();
42    if let Ok(existing) = std::fs::read_to_string(&token_path) {
43        let t = existing.trim().to_string();
44        if !t.is_empty() {
45            return t;
46        }
47    }
48
49    let token = generate_token();
50    write_token_file(&token_path, &token);
51    token
52}
53
54/// Write token to file with restrictive permissions (0600 on Unix).
55fn write_token_file(path: &PathBuf, token: &str) {
56    if let Some(parent) = path.parent() {
57        let _ = std::fs::create_dir_all(parent);
58    }
59    let _ = std::fs::write(path, token);
60    set_restrictive_permissions(path);
61}
62
63fn token_file_path() -> PathBuf {
64    crate::core::data_dir::lean_ctx_data_dir()
65        .unwrap_or_else(|_| PathBuf::from(".lean-ctx"))
66        .join(TOKEN_FILE)
67}
68
69#[cfg(unix)]
70fn set_restrictive_permissions(path: &PathBuf) {
71    use std::os::unix::fs::PermissionsExt;
72    let perms = std::fs::Permissions::from_mode(0o600);
73    let _ = std::fs::set_permissions(path, perms);
74}
75
76#[cfg(not(unix))]
77fn set_restrictive_permissions(_path: &PathBuf) {
78    // Windows: rely on user-scoped directories
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn generated_token_is_64_hex_chars() {
87        let token = generate_token();
88        assert_eq!(token.len(), 64);
89        assert!(token.chars().all(|c| c.is_ascii_hexdigit()));
90    }
91
92    #[test]
93    fn generated_tokens_are_unique() {
94        let a = generate_token();
95        let b = generate_token();
96        assert_ne!(a, b);
97    }
98}