aboutsummaryrefslogtreecommitdiff
path: root/logic/src/session.rs
blob: 6f168e31258e29b86b7f97c650293a52196e79ba (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/*
    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) 2025 metamuffin <metamuffin.org>
*/
use crate::{CONF, DATABASE};
use aes_gcm_siv::{
    KeyInit,
    aead::{Aead, generic_array::GenericArray},
};
use anyhow::anyhow;
use base64::Engine;
use jellycommon::{
    chrono::{DateTime, Utc},
    user::{PermissionSet, User},
};
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())
    }
});

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<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:?}"))?;

    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)
}

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;

    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,
    });
}

#[test]
fn test() {
    load_test_config();
    let tok = create(
        "blub".to_string(),
        jellycommon::user::PermissionSet::default(),
        Duration::from_days(1),
    );
    validate(&tok).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());
}