1pub mod err;
2
3use std::{
4 collections::HashMap,
5 ops::DerefMut,
6 path::Path,
7 sync::{Arc, RwLock},
8 time::{Duration, SystemTime},
9};
10
11use async_mutex::Mutex as AsyncMutex;
12use base64::{prelude::BASE64_STANDARD, Engine};
13use sqlx::{query, sqlite::SqliteConnectOptions, Connection, Row, SqliteConnection};
14
15pub use err::Error;
16use tracing::{debug, info, trace};
17
18fn gen_salt() -> [u8; 64] {
19 let mut buf = [0u8; 64];
20 getrandom::fill(&mut buf).unwrap();
21 buf
22}
23
24struct SafeInst {
25 conn: AsyncMutex<SqliteConnection>,
26 argon2: argon2::Config<'static>,
27 token: RwLock<HashMap<String, (String, SystemTime)>>,
28}
29
30#[derive(Clone)]
32pub struct Safe(Arc<SafeInst>);
33
34const Q_INIT: &str = "CREATE TABLE IF NOT EXISTS main (user TEXT PRIMARY KEY, phc TEXT);";
36
37impl Safe {
38 pub async fn new(p: impl AsRef<Path>) -> Result<Self, Error> {
40 let opt = SqliteConnectOptions::default()
41 .filename(&p)
42 .create_if_missing(true);
43 let mut conn = SqliteConnection::connect_with(&opt).await?;
44 info!("connected to {:?}", p.as_ref());
45 query(Q_INIT).execute(&mut conn).await?;
46 trace!("password database initialized");
47 Ok(Self(Arc::new(SafeInst {
48 conn: AsyncMutex::new(conn),
49 argon2: argon2::Config::default(),
50 token: RwLock::new(HashMap::new()),
51 })))
52 }
53
54 pub fn issue_token(&self, user: &str) -> String {
56 let mut buf = [0u8; 64];
57 getrandom::fill(&mut buf).unwrap();
58 let token = BASE64_STANDARD.encode(buf);
59 self.0
60 .token
61 .write()
62 .unwrap()
63 .insert(token.clone(), (user.to_owned(), SystemTime::now()));
64 trace!("issued token '{}**' for '{user}'", &token[0..4]);
65 return token;
66 }
67
68 pub fn invalidate_token(&self, token: &str) {
82 self.0.token.write().unwrap().remove(token);
83 trace!("invalidated token '{}**'", token);
84 }
85
86 pub fn invalidate_user_token(&self, user: &str) {
88 self.0.token.write().unwrap().retain(|_, (u, _)| u != user);
89 trace!("invalidated user session '{user}'")
90 }
91
92 pub fn expire_token(&self, duration: Duration) {
94 let mut token = self.0.token.write().unwrap();
95 let prev = token.len();
96 token.retain(|_, (_, time)| {
97 SystemTime::now()
98 .duration_since(*time)
99 .is_ok_and(|d| d < duration)
100 });
101 let diff = prev - token.len();
102 trace!("expired {diff} tokens");
103 }
104
105 pub async fn user_cnt(&self) -> Result<usize, Error> {
107 let cnt: u64 = query("SELECT COUNT(*) FROM main")
108 .fetch_one(self.0.conn.lock().await.deref_mut())
109 .await?
110 .get(0);
111 Ok(cnt as usize)
112 }
113
114 pub async fn create(&self, user: &str, pass: &str) -> Result<(), Error> {
116 let q = query("SELECT NULL FROM main WHERE user = ?").bind(user);
117 let v = q.fetch_all(self.0.conn.lock().await.deref_mut()).await?;
118 if v.len() > 0 {
119 return Err(Error::UserAlreadyExist(user.to_owned()));
120 }
121 let hashed = argon2::hash_encoded(pass.as_bytes(), &gen_salt(), &self.0.argon2)?;
122 let query = query("INSERT INTO main (user, phc) VALUES (?, ?)")
123 .bind(user)
124 .bind(hashed);
125 query.execute(self.0.conn.lock().await.deref_mut()).await?;
126 info!("created user '{user}'");
127 Ok(())
128 }
129
130 pub async fn verify(&self, user: &str, pass: &str) -> Result<bool, Error> {
133 let query = query("SELECT phc FROM main WHERE user = ?").bind(user);
134 let mut conn = self.0.conn.lock().await;
135 let v = query.fetch_all(conn.deref_mut()).await?;
136 match v.len() {
137 0 => return Err(Error::UserNotExist(user.to_owned())),
138 2.. => return Err(Error::InvalidData(format!("duplicate user '{user}'"))),
139 _ => (),
140 };
141 let p = v[0].try_get("phc")?;
142 let res = argon2::verify_encoded(p, pass.as_bytes())?;
143 if res {
144 debug!("authorized '{user}' with password");
145 }
146 Ok(res)
147 }
148
149 pub fn verify_token(&self, token: &str) -> Option<String> {
152 let map = self.0.token.read().unwrap();
153 map.get(token).map(|(user, _)| user.clone())
154 }
155
156 pub async fn update(&self, user: &str, new_pass: &str) -> Result<(), Error> {
158 self.invalidate_user_token(user);
159 let hashed = argon2::hash_encoded(new_pass.as_bytes(), &gen_salt(), &self.0.argon2)?;
160 let query = query("UPDATE main SET phc = ? WHERE user = ?")
161 .bind(hashed)
162 .bind(user);
163 query.execute(self.0.conn.lock().await.deref_mut()).await?;
164 debug!("updated password for '{user}'");
165 Ok(())
166 }
167
168 pub async fn delete(&self, user: &str) -> Result<(), Error> {
170 let query = query("DELETE FROM main WHERE user = ?").bind(user);
171 query.execute(self.0.conn.lock().await.deref_mut()).await?;
172 info!("deleted user '{user}'");
173 Ok(())
174 }
175}