diff options
Diffstat (limited to 'server/src/auth.rs')
| -rw-r--r-- | server/src/auth.rs | 171 |
1 files changed, 56 insertions, 115 deletions
diff --git a/server/src/auth.rs b/server/src/auth.rs index 03ff3a3..e84c4d1 100644 --- a/server/src/auth.rs +++ b/server/src/auth.rs @@ -3,132 +3,73 @@ 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()) - } -}); +use crate::State; +use anyhow::{Result, anyhow}; +use jellycommon::jellyobject::ObjectBuffer; -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); - } +pub fn token_to_user(state: &State, token: &str) -> Result<ObjectBuffer> { + let user_row = token::validate(&state.session_key, token)?; - 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); + let mut user = None; + state.database.read_transaction(&mut |txn| { + user = state.users.get(txn, user_row)?; + Ok(()) + })?; - base64::engine::general_purpose::URL_SAFE.encode(&ciphertext) + user.ok_or(anyhow!("user was deleted")) } -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:?}"))?; +pub mod token { + use aes_gcm_siv::{ + Aes256GcmSiv, KeyInit, + aead::{Aead, generic_array::GenericArray}, + }; + use anyhow::{Result, anyhow, bail}; + use base64::{ + Engine, + engine::general_purpose::{STANDARD, URL_SAFE}, + }; + use chrono::Utc; + use jellydb::table::RowNum; + use std::time::Duration; - let (session_data, _): (SessionData, _) = - bincode::serde::decode_from_slice(&plaintext, bincode::config::standard())?; + pub struct SessionKey(Aes256GcmSiv); - if session_data.expire < Utc::now() { - Err(anyhow!("session expired"))? + impl SessionKey { + pub fn parse(s: &str) -> Result<Self> { + let k = STANDARD.decode(s)?; + Ok(Self(Aes256GcmSiv::new_from_slice(&k)?)) + } } - 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; + pub fn create(sk: &SessionKey, user: RowNum, expire: Duration) -> String { + let expire_ts = Utc::now().timestamp() + expire.as_secs() as i64; + let mut plain = Vec::new(); + plain.extend(user.to_be_bytes()); + plain.extend(expire_ts.to_be_bytes()); - 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, - }); -} + let nonce = [(); 12].map(|_| rand::random()); + let mut ciper = + sk.0.encrypt(&GenericArray::from(nonce), plain.as_slice()) + .unwrap(); + ciper.extend(nonce); + URL_SAFE.encode(&ciper) + } + pub fn validate(sk: &SessionKey, token: &str) -> Result<RowNum> { + let cipher = URL_SAFE.decode(token)?; + let (cipher, nonce) = cipher.split_at(cipher.len() - 12); + let plain = + sk.0.decrypt(nonce.into(), cipher) + .map_err(|_| anyhow!("invalid session"))?; -#[test] -fn test() { - load_test_config(); - let tok = create( - "blub".to_string(), - jellycommon::user::PermissionSet::default(), - Duration::from_days(1), - ); - validate(&tok).unwrap(); -} + let user = RowNum::from_be_bytes(plain[0..8].try_into().unwrap()); + let expire_ts = i64::from_be_bytes(plain[8..16].try_into().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()); + if Utc::now().timestamp() > expire_ts { + bail!("session expired") + } else { + Ok(user) + } + } } |