diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/Cargo.toml | 6 | ||||
-rw-r--r-- | server/src/main.rs | 2 | ||||
-rw-r--r-- | server/src/routes/ui/account/mod.rs | 10 | ||||
-rw-r--r-- | server/src/routes/ui/account/session/guard.rs (renamed from server/src/routes/ui/account/session.rs) | 64 | ||||
-rw-r--r-- | server/src/routes/ui/account/session/mod.rs | 17 | ||||
-rw-r--r-- | server/src/routes/ui/account/session/token.rs | 72 | ||||
-rw-r--r-- | server/src/routes/ui/style/transition.js | 1 |
7 files changed, 123 insertions, 49 deletions
diff --git a/server/Cargo.toml b/server/Cargo.toml index b73841e..13b7aef 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -8,6 +8,7 @@ jellycommon = { path = "../common" } jellyremuxer = { path = "../remuxer" } serde = { version = "1.0.163", features = ["derive"] } +bincode = { version = "2.0.0-rc.3", features = ["serde", "derive"] } serde_json = "1.0.96" log = "0.4.18" @@ -15,10 +16,13 @@ env_logger = "0.10.0" anyhow = "1.0.71" once_cell = "1.17.2" chashmap = "2.2.2" -argon2 = "0.5.0" rand = "0.8.5" +base64 = "0.21.2" chrono = { version = "0.4.26", features = ["serde"] } +argon2 = "0.5.0" +aes-gcm-siv = "0.11.1" + async-std = "1.12.0" rocket = { version = "0.5.0-rc.3", features = ["secrets", "json"] } tokio = { version = "1.28.2", features = ["io-util"] } diff --git a/server/src/main.rs b/server/src/main.rs index a9a22cf..eba9532 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -3,6 +3,8 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2023 metamuffin <metamuffin.org> */ +#![feature(lazy_cell)] + use config::{load_global_config, GlobalConfig}; use database::Database; use jellyremuxer::RemuxerContext; diff --git a/server/src/routes/ui/account/mod.rs b/server/src/routes/ui/account/mod.rs index 0e4e0cc..79fa652 100644 --- a/server/src/routes/ui/account/mod.rs +++ b/server/src/routes/ui/account/mod.rs @@ -7,7 +7,6 @@ pub mod admin; pub mod session; pub mod settings; -use self::session::SessionCookie; use super::{error::MyError, layout::LayoutPage}; use crate::{ database::{Database, User}, @@ -16,6 +15,7 @@ use crate::{ }; use anyhow::anyhow; use argon2::{password_hash::Salt, Argon2, PasswordHasher}; +use chrono::Duration; use rocket::{ form::{Contextual, Form}, get, @@ -159,7 +159,7 @@ pub fn r_account_login_post( #[post("/account/logout")] pub fn r_account_logout_post(jar: &CookieJar) -> MyResult<Redirect> { - jar.remove_private(Cookie::named("user")); + jar.remove_private(Cookie::named("session")); Ok(Redirect::found(uri!(r_home()))) } @@ -181,10 +181,10 @@ pub fn login_logic( Err(anyhow!("invalid password"))? } - jar.add_private( + jar.add( Cookie::build( - "user", - serde_json::to_string(&SessionCookie::new(user.name)).unwrap(), + "session", + session::token::create(user.name, Duration::days(CONF.login_expire)), ) .permanent() .finish(), diff --git a/server/src/routes/ui/account/session.rs b/server/src/routes/ui/account/session/guard.rs index c41c968..58dfe01 100644 --- a/server/src/routes/ui/account/session.rs +++ b/server/src/routes/ui/account/session/guard.rs @@ -3,63 +3,38 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2023 metamuffin <metamuffin.org> */ -use crate::{ - database::{Database, User}, - routes::ui::error::MyError, - CONF, -}; +use super::{token, Session}; +use crate::{database::Database, routes::ui::error::MyError}; use anyhow::anyhow; -use chrono::{DateTime, Duration, Utc}; +use log::warn; use rocket::{ outcome::Outcome, request::{self, FromRequest}, Request, State, }; -use serde::{Deserialize, Serialize}; - -pub struct Session { - pub user: User, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SessionCookie { - name: String, - expire: DateTime<Utc>, -} - -impl SessionCookie { - pub fn new(name: String) -> Self { - Self { - name, - expire: Utc::now() + Duration::days(CONF.login_expire), - } - } -} impl Session { pub async fn from_request_ut(req: &Request<'_>) -> Result<Self, MyError> { + let username; + #[cfg(not(feature = "bypass-auth"))] - let cookie = req - .cookies() - .get_private("user") - .ok_or(anyhow!("login required"))?; - #[cfg(not(feature = "bypass-auth"))] - let cookie = serde_json::from_str::<SessionCookie>(cookie.value())?; - #[cfg(feature = "bypass-auth")] - let cookie = SessionCookie { - name: crate::CONF.admin_username.to_string(), - expire: Utc::now() + Duration::days(CONF.login_expire), + { + let token = req + .query_value("session") + .map(|e| e.expect("str parse should not fail, right?")) + .or(req.cookies().get("session").map(|cookie| cookie.value())) + .ok_or(anyhow!("not logged in"))?; + + username = token::validate(token)?; }; - if cookie.expire < Utc::now() { - Err(anyhow!("cookie expired"))?; + #[cfg(feature = "bypass-auth")] + { + username = "admin".to_string(); } let db = req.guard::<&State<Database>>().await.unwrap(); - let user = db - .users - .get(&cookie.name.to_string())? - .ok_or(anyhow!("user not found"))?; + let user = db.users.get(&username)?.ok_or(anyhow!("user not found"))?; Ok(Session { user }) } @@ -85,7 +60,10 @@ impl<'r> FromRequest<'r> for Session { Box::pin(async move { match Self::from_request_ut(request).await { Ok(x) => Outcome::Success(x), - Err(_) => Outcome::Forward(()), + Err(e) => { + warn!("authentificated route rejected: {e:?}"); + Outcome::Forward(()) + } } }) } diff --git a/server/src/routes/ui/account/session/mod.rs b/server/src/routes/ui/account/session/mod.rs new file mode 100644 index 0000000..1546ee7 --- /dev/null +++ b/server/src/routes/ui/account/session/mod.rs @@ -0,0 +1,17 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::database::User; + +pub mod guard; +pub mod token; + +pub struct Session { + pub user: User, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionData { + username: String, + expire: DateTime<Utc>, +} diff --git a/server/src/routes/ui/account/session/token.rs b/server/src/routes/ui/account/session/token.rs new file mode 100644 index 0000000..d4546aa --- /dev/null +++ b/server/src/routes/ui/account/session/token.rs @@ -0,0 +1,72 @@ +use super::SessionData; +use aes_gcm_siv::{ + aead::{generic_array::GenericArray, Aead}, + KeyInit, +}; +use anyhow::anyhow; +use base64::Engine; +use chrono::{Duration, Utc}; +use std::sync::LazyLock; + +static SESSION_KEY: LazyLock<[u8; 32]> = LazyLock::new(|| [(); 32].map(|_| rand::random())); + +pub fn create(username: String, expire: Duration) -> String { + let session_data = SessionData { + expire: Utc::now() + expire, + username, + }; + 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()); + eprintln!("SESSION_KEY={SESSION_KEY:?}"); + let mut ciphertext = cipher + .encrypt(&GenericArray::from(nonce), plaintext.as_slice()) + .unwrap(); + ciphertext.extend(nonce); + + base64::engine::general_purpose::STANDARD.encode(&ciphertext) +} + +pub fn validate(token: &str) -> anyhow::Result<String> { + let ciphertext = base64::engine::general_purpose::STANDARD.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(), 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()); +} diff --git a/server/src/routes/ui/style/transition.js b/server/src/routes/ui/style/transition.js index c125c42..7d39176 100644 --- a/server/src/routes/ui/style/transition.js +++ b/server/src/routes/ui/style/transition.js @@ -36,6 +36,7 @@ function prepare_load(href, back) { let rt = "" try { const r = await r_promise + if (!r.ok) return document.body.innerHTML = "<h1>error</h1>" rt = await r.text() } catch (e) { console.error(e) |