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
|
/*
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::{
jellyobject::{ObjectBuffer, Path},
*,
};
use jellydb::{Filter, Query};
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.transaction(&mut |txn| {
user = txn.get(user_row)?;
Ok(())
})?;
user.ok_or(anyhow!("user was deleted"))
}
pub fn login(
state: &State,
username: &str,
password: &str,
expire: Option<i64>,
) -> Result<(String, bool)> {
let password = hash_password(username, password);
let mut user_row = None;
let mut user = None;
state.database.transaction(&mut |txn| {
user_row = txn.query_single(Query {
filter: Filter::Match(Path(vec![USER_LOGIN.0]), username.into()),
..Default::default()
})?;
if let Some(ur) = user_row {
user = txn.get(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),
),
user.as_object().has(USER_PASSWORD_REQUIRE_CHANGE.0),
))
}
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::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)
}
}
}
|