aboutsummaryrefslogtreecommitdiff
path: root/server/src/routes/ui/account/session
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/routes/ui/account/session')
-rw-r--r--server/src/routes/ui/account/session/guard.rs70
-rw-r--r--server/src/routes/ui/account/session/mod.rs17
-rw-r--r--server/src/routes/ui/account/session/token.rs72
3 files changed, 159 insertions, 0 deletions
diff --git a/server/src/routes/ui/account/session/guard.rs b/server/src/routes/ui/account/session/guard.rs
new file mode 100644
index 0000000..58dfe01
--- /dev/null
+++ b/server/src/routes/ui/account/session/guard.rs
@@ -0,0 +1,70 @@
+/*
+ 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) 2023 metamuffin <metamuffin.org>
+*/
+use super::{token, Session};
+use crate::{database::Database, routes::ui::error::MyError};
+use anyhow::anyhow;
+use log::warn;
+use rocket::{
+ outcome::Outcome,
+ request::{self, FromRequest},
+ Request, State,
+};
+
+impl Session {
+ pub async fn from_request_ut(req: &Request<'_>) -> Result<Self, MyError> {
+ let username;
+
+ #[cfg(not(feature = "bypass-auth"))]
+ {
+ 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)?;
+ };
+
+ #[cfg(feature = "bypass-auth")]
+ {
+ username = "admin".to_string();
+ }
+
+ let db = req.guard::<&State<Database>>().await.unwrap();
+ let user = db.users.get(&username)?.ok_or(anyhow!("user not found"))?;
+
+ Ok(Session { user })
+ }
+}
+
+impl<'r> FromRequest<'r> for Session {
+ type Error = MyError;
+
+ fn from_request<'life0, 'async_trait>(
+ request: &'r Request<'life0>,
+ ) -> core::pin::Pin<
+ Box<
+ dyn core::future::Future<Output = request::Outcome<Self, Self::Error>>
+ + core::marker::Send
+ + 'async_trait,
+ >,
+ >
+ where
+ 'r: 'async_trait,
+ 'life0: 'async_trait,
+ Self: 'async_trait,
+ {
+ Box::pin(async move {
+ match Self::from_request_ut(request).await {
+ Ok(x) => Outcome::Success(x),
+ 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());
+}