diff --git a/Cargo.lock b/Cargo.lock index 90b8a76..acee835 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -377,6 +377,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + [[package]] name = "chrono" version = "0.4.44" @@ -449,6 +460,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -462,7 +482,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -484,7 +504,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -601,7 +621,7 @@ dependencies = [ "hkdf", "pem-rfc7468", "pkcs8", - "rand_core", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -683,7 +703,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -815,6 +835,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.0", "wasip2", "wasip3", ] @@ -832,7 +853,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -1068,7 +1089,7 @@ dependencies = [ "p256", "p384", "pem", - "rand", + "rand 0.8.5", "rsa", "serde", "serde_json", @@ -1206,7 +1227,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] @@ -1510,7 +1531,18 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.0", ] [[package]] @@ -1520,7 +1552,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1532,6 +1564,12 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "regex" version = "1.12.3" @@ -1598,7 +1636,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -1789,6 +1827,7 @@ dependencies = [ "chrono", "jsonwebtoken", "pam", + "rand 0.10.0", "rustls", "serde", "serde_json", @@ -1804,7 +1843,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -1831,7 +1870,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1b86679..a295173 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ bcrypt = "0.19.0" chrono = "0.4.44" jsonwebtoken = { version = "10.3.0", features = ["rust_crypto"] } pam = { version = "0.8.0", features = ["client"] } +rand = "0.10.0" rustls = { version = "0.23.37", features = ["ring"] } serde = { version = "1", features = ["derive"] } serde_json = "1.0.149" diff --git a/src/auth.rs b/src/auth.rs index 68a1517..0fb89d0 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,18 +1,66 @@ -use axum::response::Redirect; -use axum::{ - Router, extract::Path, http::HeaderMap, http::StatusCode, response::IntoResponse, - response::Json, routing::get, routing::post, -}; +use axum::{http::HeaderMap, http::StatusCode, response::IntoResponse, response::Json}; use base64::{Engine, engine::general_purpose}; use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode}; use pam::Client; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use sysinfo::{Components, Disks, Networks, System}; -use tokio::process::Command; -use zbus::Connection; +use std::path::PathBuf; +use std::sync::OnceLock; +use std::time::{SystemTime, UNIX_EPOCH}; -const JWT_SECRET: &str = "change-me-to-a-long-random-string"; +static JWT_SECRET: OnceLock = OnceLock::new(); + +const ROTATION_DAYS: u64 = 7; + +fn secret_path() -> PathBuf { + PathBuf::from("/home/jack/.local/share/sysapi/jwt_secret") +} + +fn generate_secret() -> String { + format!( + "{:016x}{:016x}", + rand::random::(), + rand::random::() + ) +} + +fn current_timestamp() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() +} + +pub fn jwt_secret() -> &'static str { + JWT_SECRET.get_or_init(|| { + let path = secret_path(); + std::fs::create_dir_all(path.parent().unwrap()).ok(); + + // file format: "timestamp:secret" + if let Ok(contents) = std::fs::read_to_string(&path) { + if let Some((ts_str, secret)) = contents.trim().split_once(':') { + if let Ok(ts) = ts_str.parse::() { + if current_timestamp() - ts < ROTATION_DAYS * 86400 { + return secret.to_string(); + } + println!("JWT secret expired, rotating..."); + } + } + } + + let secret = generate_secret(); + let contents = format!("{}:{}", current_timestamp(), secret); + std::fs::write(&path, &contents).ok(); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).ok(); + } + + println!("Generated new JWT secret"); + secret + }) +} #[derive(Serialize, Deserialize)] struct Claims { @@ -28,7 +76,7 @@ pub fn create_token(username: &str) -> String { encode( &Header::default(), &claims, - &EncodingKey::from_secret(JWT_SECRET.as_bytes()), + &EncodingKey::from_secret(jwt_secret().as_bytes()), ) .unwrap() } @@ -40,7 +88,7 @@ pub fn verify_token(headers: &HeaderMap) -> bool { let token = val.to_str().unwrap_or("").replace("Bearer ", ""); decode::( &token, - &DecodingKey::from_secret(JWT_SECRET.as_bytes()), + &DecodingKey::from_secret(jwt_secret().as_bytes()), &Validation::default(), ) .is_ok() diff --git a/src/config.rs b/src/config.rs index fdeeb64..ede6401 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,6 +2,8 @@ pub const ALLOWED_SERVICES: &[&str] = &[ "syncthing", "caddy", "sshd", + "dashboard", + "sysapi", "cloudflare-dyndns.timer", "cloudflare-dyndns", "docker", diff --git a/src/main.rs b/src/main.rs index 0da9a03..8913c00 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,5 @@ use axum::response::Redirect; -use axum::{ - Router, extract::Path, http::HeaderMap, http::StatusCode, response::IntoResponse, - response::Json, routing::get, routing::post, -}; -use tokio::process::Command; +use axum::{Router, routing::get, routing::post}; mod auth; mod config; diff --git a/src/routes/services.rs b/src/routes/services.rs index be53fd8..06e140f 100644 --- a/src/routes/services.rs +++ b/src/routes/services.rs @@ -1,14 +1,7 @@ -use axum::response::Redirect; use axum::{ - Router, extract::Path, http::HeaderMap, http::StatusCode, response::IntoResponse, - response::Json, routing::get, routing::post, + extract::Path, http::HeaderMap, http::StatusCode, response::IntoResponse, + response::Json, }; -use base64::{Engine, engine::general_purpose}; -use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode}; -use pam::Client; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use sysinfo::{Components, Disks, Networks, System}; use tokio::process::Command; use zbus::Connection; diff --git a/src/routes/stats.rs b/src/routes/stats.rs index f675731..215b00d 100644 --- a/src/routes/stats.rs +++ b/src/routes/stats.rs @@ -1,16 +1,7 @@ -use axum::response::Redirect; -use axum::{ - Router, extract::Path, http::HeaderMap, http::StatusCode, response::IntoResponse, - response::Json, routing::get, routing::post, -}; -use base64::{Engine, engine::general_purpose}; -use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode}; -use pam::Client; -use serde::{Deserialize, Serialize}; +use axum::response::Json; use std::collections::HashMap; use sysinfo::{Components, Disks, Networks, System}; use tokio::process::Command; -use zbus::Connection; use crate::models; diff --git a/src/routes/system.rs b/src/routes/system.rs index 6e7ccce..a7d945a 100644 --- a/src/routes/system.rs +++ b/src/routes/system.rs @@ -1,6 +1,5 @@ use axum::{ - Router, extract::Path, http::HeaderMap, http::StatusCode, response::IntoResponse, - response::Json, routing::get, routing::post, + http::HeaderMap, http::StatusCode, response::IntoResponse, }; use zbus::Connection;