diff options
| author | metamuffin <metamuffin@disroot.org> | 2026-01-25 01:21:24 +0100 |
|---|---|---|
| committer | metamuffin <metamuffin@disroot.org> | 2026-01-25 01:21:24 +0100 |
| commit | e3708145efd0bbed5d10320ee0a0deff0f38479e (patch) | |
| tree | 877ace344bc78c9fc11c3418f8361de660b594a5 /server | |
| parent | 2bcccb18a6cb8bf836f57c3d86f759b19699def2 (diff) | |
| download | jellything-e3708145efd0bbed5d10320ee0a0deff0f38479e.tar jellything-e3708145efd0bbed5d10320ee0a0deff0f38479e.tar.bz2 jellything-e3708145efd0bbed5d10320ee0a0deff0f38479e.tar.zst | |
start moving logic code; less arcing in stream
Diffstat (limited to 'server')
| -rw-r--r-- | server/src/auth.rs | 134 | ||||
| -rw-r--r-- | server/src/main.rs | 1 | ||||
| -rw-r--r-- | server/src/request_info.rs | 4 |
3 files changed, 137 insertions, 2 deletions
diff --git a/server/src/auth.rs b/server/src/auth.rs new file mode 100644 index 0000000..03ff3a3 --- /dev/null +++ b/server/src/auth.rs @@ -0,0 +1,134 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2026 metamuffin <metamuffin.org> +*/ +use crate::{CONF, DATABASE}; +use aes_gcm_siv::{ + KeyInit, + aead::{Aead, generic_array::GenericArray}, +}; +use anyhow::anyhow; +use base64::Engine; +use log::warn; +use serde::{Deserialize, Serialize}; +use std::{sync::LazyLock, time::Duration}; + +pub struct Session { + pub user: User, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionData { + username: String, + expire: DateTime<Utc>, + permissions: PermissionSet, +} + +static SESSION_KEY: LazyLock<[u8; 32]> = LazyLock::new(|| { + if let Some(sk) = &CONF.session_key { + let r = base64::engine::general_purpose::STANDARD + .decode(sk) + .expect("key invalid; should be valid base64"); + r.try_into() + .expect("key has the wrong length; should be 32 bytes") + } else { + warn!("session_key not configured; generating a random one."); + [(); 32].map(|_| rand::random()) + } +}); + +pub fn create(username: String, permissions: PermissionSet, expire: Duration) -> String { + let session_data = SessionData { + expire: Utc::now() + expire, + username: username.to_owned(), + permissions, + }; + let mut plaintext = + bincode::serde::encode_to_vec(&session_data, bincode::config::standard()).unwrap(); + + while plaintext.len().is_multiple_of(16) { + plaintext.push(0); + } + + let cipher = aes_gcm_siv::Aes256GcmSiv::new_from_slice(&*SESSION_KEY).unwrap(); + let nonce = [(); 12].map(|_| rand::random()); + let mut ciphertext = cipher + .encrypt(&GenericArray::from(nonce), plaintext.as_slice()) + .unwrap(); + ciphertext.extend(nonce); + + base64::engine::general_purpose::URL_SAFE.encode(&ciphertext) +} + +pub fn validate(token: &str) -> anyhow::Result<String> { + let ciphertext = base64::engine::general_purpose::URL_SAFE.decode(token)?; + let cipher = aes_gcm_siv::Aes256GcmSiv::new_from_slice(&*SESSION_KEY).unwrap(); + let (ciphertext, nonce) = ciphertext.split_at(ciphertext.len() - 12); + let plaintext = cipher + .decrypt(nonce.into(), ciphertext) + .map_err(|e| anyhow!("decryption failed: {e:?}"))?; + + let (session_data, _): (SessionData, _) = + bincode::serde::decode_from_slice(&plaintext, bincode::config::standard())?; + + if session_data.expire < Utc::now() { + Err(anyhow!("session expired"))? + } + + Ok(session_data.username) +} + +pub fn token_to_session(token: &str) -> anyhow::Result<Session> { + let username = validate(token)?; + let user = DATABASE + .get_user(&username)? + .ok_or(anyhow!("user does not exist"))?; + Ok(Session { user }) +} +pub fn bypass_auth_session() -> anyhow::Result<Session> { + let user = DATABASE + .get_user(CONF.admin_username.as_ref().unwrap())? + .ok_or(anyhow!("user does not exist"))?; + Ok(Session { user }) +} + +#[cfg(test)] +fn load_test_config() { + use std::path::PathBuf; + + use crate::{CONF_PRELOAD, Config}; + *CONF_PRELOAD.lock().unwrap() = Some(Config { + database_path: PathBuf::default(), + login_expire: 10, + session_key: None, + admin_password: None, + admin_username: None, + }); +} + +#[test] +fn test() { + load_test_config(); + let tok = create( + "blub".to_string(), + jellycommon::user::PermissionSet::default(), + Duration::from_days(1), + ); + validate(&tok).unwrap(); +} + +#[test] +fn test_crypto() { + load_test_config(); + let nonce = [(); 12].map(|_| rand::random()); + let cipher = aes_gcm_siv::Aes256GcmSiv::new_from_slice(&*SESSION_KEY).unwrap(); + let plaintext = b"testing stuff---"; + let ciphertext = cipher + .encrypt(&GenericArray::from(nonce), plaintext.as_slice()) + .unwrap(); + let plaintext2 = cipher + .decrypt((&nonce).into(), ciphertext.as_slice()) + .unwrap(); + assert_eq!(plaintext, plaintext2.as_slice()); +} diff --git a/server/src/main.rs b/server/src/main.rs index be1aba4..9b6463f 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -22,6 +22,7 @@ pub mod request_info; pub mod responders; pub mod routes; pub mod ui; +pub mod auth; #[rocket::main] async fn main() { diff --git a/server/src/request_info.rs b/server/src/request_info.rs index 779b4e1..3a1023f 100644 --- a/server/src/request_info.rs +++ b/server/src/request_info.rs @@ -93,7 +93,7 @@ pub(super) fn accept_language<'a>(request: &'a Request<'_>) -> &'a str { .unwrap_or("en") } -pub(super) async fn session_from_request(req: &Request<'_>) -> Result<Session, MyError> { +pub(super) async fn user_from_request(req: &Request<'_>) -> Result<Session, MyError> { if cfg!(feature = "bypass-auth") { Ok(bypass_auth_session()?) } else { @@ -112,7 +112,7 @@ pub(super) async fn session_from_request(req: &Request<'_>) -> Result<Session, M // jellyfin urlescapes the token for *some* requests let token = token.replace("%3D", "="); - Ok(token_to_session(&token)?) + Ok(token_to_user(&token)?) } } |