/* 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 */ use crate::State; use anyhow::{Result, anyhow, bail}; use argon2::{Argon2, PasswordHasher, password_hash::Salt}; use jellycommon::{jellyobject::Path, *}; use jellydb::{Filter, Query}; pub fn token_to_user(state: &State, token: &str) -> Result> { let user_row = token::validate(&state.session_key, token)?; let mut user = None; state.database.transaction(&mut |txn| { user = txn.get(user_row)?; Ok(()) })?; user.ok_or(anyhow!("user was deleted")) } pub fn login( state: &State, username: &str, password: &str, expire: Option, ) -> Result<(String, bool)> { let password = hash_password(username, password); let mut user_row = None; let mut user = None; state.database.transaction(&mut |txn| { user_row = txn.query_single(Query { filter: Filter::Match(Path(vec![USER_LOGIN.0]), username.into()), ..Default::default() })?; if let Some(ur) = user_row { user = txn.get(ur)?; } Ok(()) })?; let (Some(user_row), Some(user)) = (user_row, user) else { bail!("unknown user"); }; let Some(correct_pw) = user.get(USER_PASSWORD) else { bail!("password login is disabled") }; if password != correct_pw { bail!("incorrect password") } Ok(( token::create( &state.session_key, user_row, expire.unwrap_or(60 * 60 * 24 * 30), ), user.has(USER_PASSWORD_REQUIRE_CHANGE.0), )) } pub fn hash_password(username: &str, password: &str) -> Vec { Argon2::default() .hash_password( format!("{username}\0{password}").as_bytes(), <&str as TryInto>::try_into("IYMa13osbNeLJKnQ1T8LlA").unwrap(), ) .unwrap() .hash .unwrap() .as_bytes() .to_vec() } 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::RowNum; pub struct SessionKey(Aes256GcmSiv); impl SessionKey { pub fn parse(s: &str) -> Result { let k = STANDARD.decode(s)?; Ok(Self(Aes256GcmSiv::new_from_slice(&k)?)) } } pub fn create(sk: &SessionKey, user: RowNum, expire: i64) -> String { let expire_ts = Utc::now().timestamp() + expire; let mut plain = Vec::new(); plain.extend(user.to_be_bytes()); plain.extend(expire_ts.to_be_bytes()); 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 { 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"))?; 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()); if Utc::now().timestamp() > expire_ts { bail!("session expired") } else { Ok(user) } } }