Remove pam and implement totp in rust
This commit is contained in:
parent
043ab278f5
commit
256546164f
4 changed files with 850 additions and 36 deletions
785
Cargo.lock
generated
785
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -8,12 +8,18 @@ axum = "0.8.8"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
bcrypt = "0.19.0"
|
bcrypt = "0.19.0"
|
||||||
chrono = "0.4.44"
|
chrono = "0.4.44"
|
||||||
|
crypt = "0.1.0"
|
||||||
|
yescrypt = { version = "0.1.0-pre.4", features = ["password-hash"] }
|
||||||
|
password-hash = { version = "0.6", features = ["phc"] }
|
||||||
jsonwebtoken = { version = "10.3.0", features = ["rust_crypto"] }
|
jsonwebtoken = { version = "10.3.0", features = ["rust_crypto"] }
|
||||||
|
libc = "0.2.183"
|
||||||
pam = { version = "0.8.0", features = ["client"] }
|
pam = { version = "0.8.0", features = ["client"] }
|
||||||
|
pwhash = "1.0.0"
|
||||||
rand = "0.10.0"
|
rand = "0.10.0"
|
||||||
rustls = { version = "0.23.37", features = ["ring"] }
|
rustls = { version = "0.23.37", features = ["ring"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
|
shadow-rs = "1.7.1"
|
||||||
sysinfo = "0.38.4"
|
sysinfo = "0.38.4"
|
||||||
tokio = { version = "1.50.0", features = [
|
tokio = { version = "1.50.0", features = [
|
||||||
"macros",
|
"macros",
|
||||||
|
|
@ -21,3 +27,5 @@ tokio = { version = "1.50.0", features = [
|
||||||
"process",
|
"process",
|
||||||
] }
|
] }
|
||||||
zbus = "5.14.0"
|
zbus = "5.14.0"
|
||||||
|
totp-rs = { version = "5.7", features = ["gen_secret", "otpauth"] }
|
||||||
|
base32 = "0.5.1"
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
"d /var/lib/server-dash-api 0750 server-dash-api server-dash-api -"
|
"d /var/lib/server-dash-api 0750 server-dash-api server-dash-api -"
|
||||||
"d /var/lib/server-dash-api/google-auth 0750 server-dash-api server-dash-api -"
|
"d /var/lib/server-dash-api/google-auth 0755 server-dash-api server-dash-api -"
|
||||||
];
|
];
|
||||||
|
|
||||||
security.polkit.extraConfig = ''
|
security.polkit.extraConfig = ''
|
||||||
|
|
|
||||||
91
src/auth.rs
91
src/auth.rs
|
|
@ -5,15 +5,17 @@ use axum::{
|
||||||
};
|
};
|
||||||
use base64::{Engine, engine::general_purpose};
|
use base64::{Engine, engine::general_purpose};
|
||||||
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
|
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode};
|
||||||
use pam::Client;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
use totp_rs::{Algorithm, Secret, TOTP};
|
||||||
|
use yescrypt::{PasswordHash, PasswordVerifier, Yescrypt};
|
||||||
|
|
||||||
static JWT_SECRET: OnceLock<String> = OnceLock::new();
|
static JWT_SECRET: OnceLock<String> = OnceLock::new();
|
||||||
|
|
||||||
const ROTATION_DAYS: u64 = 7;
|
const ROTATION_DAYS: u64 = 7;
|
||||||
|
const TOTP_SECRET_PATH: &str = "/var/lib/server-dash-api/google-auth/jack";
|
||||||
|
|
||||||
fn secret_path() -> PathBuf {
|
fn secret_path() -> PathBuf {
|
||||||
PathBuf::from("/var/lib/server-dash-api/jwt_secret")
|
PathBuf::from("/var/lib/server-dash-api/jwt_secret")
|
||||||
|
|
@ -39,7 +41,6 @@ pub fn jwt_secret() -> &'static str {
|
||||||
let path = secret_path();
|
let path = secret_path();
|
||||||
std::fs::create_dir_all(path.parent().unwrap()).ok();
|
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 Ok(contents) = std::fs::read_to_string(&path) {
|
||||||
if let Some((ts_str, secret)) = contents.trim().split_once(':') {
|
if let Some((ts_str, secret)) = contents.trim().split_once(':') {
|
||||||
if let Ok(ts) = ts_str.parse::<u64>() {
|
if let Ok(ts) = ts_str.parse::<u64>() {
|
||||||
|
|
@ -98,24 +99,86 @@ pub fn verify_token(headers: &HeaderMap) -> bool {
|
||||||
.is_ok()
|
.is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode_basic_auth(headers: &HeaderMap) -> Option<(String, String)> {
|
pub fn decode_basic_auth(headers: &HeaderMap) -> Option<(String, String, String)> {
|
||||||
let val = headers.get("Authorization")?.to_str().ok()?;
|
let val = headers.get("Authorization")?.to_str().ok()?;
|
||||||
let encoded = val.strip_prefix("Basic ")?;
|
let encoded = val.strip_prefix("Basic ")?;
|
||||||
let decoded = general_purpose::STANDARD.decode(encoded).ok()?;
|
let decoded = general_purpose::STANDARD.decode(encoded).ok()?;
|
||||||
let s = String::from_utf8(decoded).ok()?;
|
let s = String::from_utf8(decoded).ok()?;
|
||||||
let (user, pass) = s.split_once(':')?;
|
let (user, rest) = s.split_once(':')?;
|
||||||
Some((user.to_string(), pass.to_string()))
|
if rest.len() < 6 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (password, totp) = rest.split_at(rest.len() - 6);
|
||||||
|
Some((user.to_string(), password.to_string(), totp.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verify_system_credentials(username: &str, password: &str) -> bool {
|
fn verify_password(username: &str, password: &str) -> bool {
|
||||||
let mut client = match Client::with_password("server-dash-api") {
|
let shadow_content = match std::fs::read_to_string("/etc/shadow") {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(_) => return false,
|
Err(e) => {
|
||||||
|
println!("Failed to read /etc/shadow: {}", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
client
|
for line in shadow_content.lines() {
|
||||||
.conversation_mut()
|
let fields: Vec<&str> = line.split(':').collect();
|
||||||
.set_credentials(username, password);
|
if fields.len() < 2 {
|
||||||
client.authenticate().is_ok()
|
continue;
|
||||||
|
}
|
||||||
|
if fields[0] != username {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return verify_shadow_hash(password, fields[1]);
|
||||||
|
}
|
||||||
|
println!("User not found in shadow");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_shadow_hash(password: &str, hash: &str) -> bool {
|
||||||
|
let parsed_hash = match PasswordHash::new(hash) {
|
||||||
|
Ok(h) => h,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to parse hash: {:?}", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Yescrypt::default()
|
||||||
|
.verify_password(password.as_bytes(), &parsed_hash)
|
||||||
|
.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_totp(totp_code: &str) -> bool {
|
||||||
|
let secret_file = match std::fs::read_to_string(TOTP_SECRET_PATH) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to read TOTP secret: {}", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let secret_b32 = secret_file.lines().next().unwrap_or("").trim().to_string();
|
||||||
|
|
||||||
|
let totp = match TOTP::new(
|
||||||
|
Algorithm::SHA1,
|
||||||
|
6,
|
||||||
|
1,
|
||||||
|
30,
|
||||||
|
Secret::Encoded(secret_b32).to_bytes().unwrap(),
|
||||||
|
None,
|
||||||
|
"jack".to_string(),
|
||||||
|
) {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to create TOTP: {:?}", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
totp.check_current(totp_code).unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_system_credentials(username: &str, password: &str, totp: &str) -> bool {
|
||||||
|
verify_password(username, password) && verify_totp(totp)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn require_auth(headers: HeaderMap, request: Request<Body>, next: Next) -> Response {
|
pub async fn require_auth(headers: HeaderMap, request: Request<Body>, next: Next) -> Response {
|
||||||
|
|
@ -128,7 +191,7 @@ pub async fn require_auth(headers: HeaderMap, request: Request<Body>, next: Next
|
||||||
|
|
||||||
// POST /auth/login
|
// POST /auth/login
|
||||||
pub async fn post_login(headers: HeaderMap) -> impl IntoResponse {
|
pub async fn post_login(headers: HeaderMap) -> impl IntoResponse {
|
||||||
let (username, password_and_totp) = match decode_basic_auth(&headers) {
|
let (username, password, totp) = match decode_basic_auth(&headers) {
|
||||||
Some(c) => c,
|
Some(c) => c,
|
||||||
None => {
|
None => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -139,7 +202,7 @@ pub async fn post_login(headers: HeaderMap) -> impl IntoResponse {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if !verify_system_credentials(&username, &password_and_totp) {
|
if !verify_system_credentials(&username, &password, &totp) {
|
||||||
return (StatusCode::UNAUTHORIZED, "Invalid credentials").into_response();
|
return (StatusCode::UNAUTHORIZED, "Invalid credentials").into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue