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