aboutsummaryrefslogtreecommitdiff
path: root/server/src/auth.rs
blob: 0e523ed1581e9fa004201c889ba8f4cf3ef0b9e1 (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
/*
    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 <metamuffin.org>
*/

use crate::State;
use anyhow::{Result, anyhow, bail};
use argon2::{Argon2, PasswordHasher, password_hash::Salt};
use jellycommon::{
    USER_LOGIN, USER_PASSWORD,
    jellyobject::{ObjectBuffer, Path},
};
use jellydb::query::{Filter, Query, Sort};

pub fn token_to_user(state: &State, token: &str) -> Result<ObjectBuffer> {
    let user_row = token::validate(&state.session_key, token)?;

    let mut user = None;
    state.database.read_transaction(&mut |txn| {
        user = state.users.get(txn, user_row)?;
        Ok(())
    })?;

    user.ok_or(anyhow!("user was deleted"))
}

pub fn login(state: &State, username: &str, password: &str, expire: Option<i64>) -> Result<String> {
    let password = hash_password(username, password);

    let mut user_row = None;
    let mut user = None;
    state.database.read_transaction(&mut |txn| {
        user_row = state.users.query_single(
            txn,
            Query {
                filter: Filter::Match(Path(vec![USER_LOGIN.0]), username.as_bytes().to_vec()),
                sort: Sort::None,
            },
        )?;
        if let Some(ur) = user_row {
            user = state.users.get(txn, ur)?;
        }
        Ok(())
    });

    let (Some(user_row), Some(user)) = (user_row, user) else {
        bail!("unknown user");
    };
    let Some(correct_pw) = user.as_object().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),
    ))
}

pub fn hash_password(username: &str, password: &str) -> Vec<u8> {
    Argon2::default()
        .hash_password(
            format!("{username}\0{password}").as_bytes(),
            <&str as TryInto<Salt>>::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::table::RowNum;

    pub struct SessionKey(Aes256GcmSiv);

    impl SessionKey {
        pub fn parse(s: &str) -> Result<Self> {
            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<RowNum> {
        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)
        }
    }
}