Initial public release
This commit is contained in:
commit
c1107147b4
92 files changed
+10562
No files matched your search
@@ -0,0 +1,15 @@
|
||||
use sha2::{Sha256, Digest};
|
||||
|
||||
// Deterministic CSRF token derived from session token
|
||||
pub fn generate_csrf_token(session_id: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(session_id.as_bytes());
|
||||
hasher.update(b"csrf-salt-bzod-2026");
|
||||
hex::encode(hasher.finalize())
|
||||
}
|
||||
|
||||
// Verify CSRF token
|
||||
pub fn verify_csrf(session_id: &str, submitted_token: &str) -> bool {
|
||||
let expected = generate_csrf_token(session_id);
|
||||
expected == submitted_token
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use axum::{
|
||||
extract::{FromRequestParts, FromRef},
|
||||
http::{request::Parts, StatusCode},
|
||||
};
|
||||
use crate::state::AppState;
|
||||
use crate::models::User;
|
||||
use crate::auth::session::authenticate_api_key;
|
||||
|
||||
// Extractor: Authenticate API requests using Bearer token
|
||||
pub struct ApiUser(pub User);
|
||||
|
||||
#[axum::async_trait]
|
||||
impl<S> FromRequestParts<S> for ApiUser
|
||||
where
|
||||
AppState: FromRef<S>,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = (StatusCode, &'static str);
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let app_state = AppState::from_ref(state);
|
||||
let auth_header = parts.headers
|
||||
.get("Authorization")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.ok_or((StatusCode::UNAUTHORIZED, "Missing Authorization header"))?;
|
||||
|
||||
let conn = app_state.admin_db.lock().unwrap();
|
||||
match authenticate_api_key(&conn, auth_header) {
|
||||
Ok(Some(user)) => Ok(ApiUser(user)),
|
||||
Ok(None) => Err((StatusCode::UNAUTHORIZED, "Invalid API token")),
|
||||
Err(_) => Err((StatusCode::INTERNAL_SERVER_ERROR, "Database error")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
pub mod password;
|
||||
pub mod session;
|
||||
pub mod csrf;
|
||||
pub mod middleware;
|
||||
|
||||
pub use password::{hash_password, verify_password, verify_sha256};
|
||||
pub use session::{generate_token, authenticate_session, authenticate_api_key};
|
||||
pub use csrf::{generate_csrf_token, verify_csrf};
|
||||
pub use middleware::ApiUser;
|
||||
@@ -0,0 +1,31 @@
|
||||
use sha2::{Sha256, Digest};
|
||||
use argon2::{
|
||||
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
||||
Argon2,
|
||||
};
|
||||
|
||||
// Hashing password with Argon2id
|
||||
pub fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
let password_hash = argon2.hash_password(password.as_bytes(), &salt)?.to_string();
|
||||
Ok(password_hash)
|
||||
}
|
||||
|
||||
// Verifying Argon2id password hash
|
||||
pub fn verify_password(password: &str, hash: &str) -> bool {
|
||||
if let Ok(parsed_hash) = PasswordHash::new(hash) {
|
||||
Argon2::default().verify_password(password.as_bytes(), &parsed_hash).is_ok()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Verifying SHA-256 bootstrap hash
|
||||
pub fn verify_sha256(password: &str, expected_hex: &str) -> bool {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(password.as_bytes());
|
||||
let result = hasher.finalize();
|
||||
let hex_result = hex::encode(result);
|
||||
hex_result.eq_ignore_ascii_case(expected_hex)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
use sha2::{Sha256, Digest};
|
||||
use rand::{RngCore, thread_rng};
|
||||
use axum_extra::extract::CookieJar;
|
||||
use rusqlite::Connection;
|
||||
use chrono::Utc;
|
||||
use crate::db::admin::{get_session, get_user_by_id, update_api_key_last_used, get_api_key_by_hash};
|
||||
use crate::models::User;
|
||||
|
||||
// Generate a secure random token (hex-encoded)
|
||||
pub fn generate_token(bytes_len: usize) -> String {
|
||||
let mut key = vec![0u8; bytes_len];
|
||||
thread_rng().fill_bytes(&mut key);
|
||||
hex::encode(key)
|
||||
}
|
||||
|
||||
// Authenticate session from cookies
|
||||
pub fn authenticate_session(
|
||||
conn: &Connection,
|
||||
jar: &CookieJar,
|
||||
) -> Result<Option<(User, String)>, rusqlite::Error> {
|
||||
let cookie = match jar.get("bzod_session") {
|
||||
Some(c) => c,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let session_id = cookie.value();
|
||||
let session = match get_session(conn, session_id)? {
|
||||
Some(s) => s,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// Check expiration
|
||||
if let Ok(expires) = chrono::DateTime::parse_from_rfc3339(&session.expires_at) {
|
||||
if expires.with_timezone(&Utc) < Utc::now() {
|
||||
// Expired
|
||||
return Ok(None);
|
||||
}
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Get user
|
||||
if let Some(user) = get_user_by_id(conn, &session.user_id)? {
|
||||
Ok(Some((user, session.id)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticate API key from Authorization header
|
||||
pub fn authenticate_api_key(
|
||||
conn: &Connection,
|
||||
auth_header: &str,
|
||||
) -> Result<Option<User>, rusqlite::Error> {
|
||||
if !auth_header.starts_with("Bearer ") {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let key = auth_header.trim_start_matches("Bearer ").trim();
|
||||
if key.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Hash the API key using SHA-256 to compare with stored hash
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(key.as_bytes());
|
||||
let hashed_key = hex::encode(hasher.finalize());
|
||||
|
||||
if let Some(api_key_rec) = get_api_key_by_hash(conn, &hashed_key)? {
|
||||
// Update last used timestamp
|
||||
update_api_key_last_used(conn, &api_key_rec.id)?;
|
||||
|
||||
// Get user
|
||||
if let Some(user) = get_user_by_id(conn, &api_key_rec.user_id)? {
|
||||
return Ok(Some(user));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
Reference in new issue
Block a user