/* 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) 2024 metamuffin */ use super::SessionData; use aes_gcm_siv::{ aead::{generic_array::GenericArray, Aead}, KeyInit, }; use anyhow::anyhow; use base64::Engine; use chrono::{Duration, Utc}; use jellybase::SECRETS; use jellycommon::user::PermissionSet; use log::warn; use std::sync::LazyLock; static SESSION_KEY: LazyLock<[u8; 32]> = LazyLock::new(|| { if let Some(sk) = &SECRETS.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() % 16 == 0 { 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 { 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) } #[test] fn test() { let tok = create( "blub".to_string(), jellycommon::user::PermissionSet::default(), Duration::days(1), ); validate(&tok).unwrap(); } #[test] fn test_crypto() { 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()); }