simple_safe/
lib.rs

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/// Interface to the password database.
31#[derive(Clone)]
32pub struct Safe(Arc<SafeInst>);
33
34/// Initialize the database.
35const Q_INIT: &str = "CREATE TABLE IF NOT EXISTS main (user TEXT PRIMARY KEY, phc TEXT);";
36
37impl Safe {
38    /// Open an SQLite connection with specified database file and create a `SafeBox`.
39    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    /// Issue a token to the speficied user.
55    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    /// Invalidate a token.
69    /// # Example
70    /// ```
71    /// use simple_safe::Safe;
72    ///
73    /// let safe = Safe::new("password.db").await.unwrap();
74    ///
75    /// let token = safe.issue_token("alice");
76    /// assert!(safe.verify_token(&token).unwrap() == "alice");
77    ///
78    /// safe.invalidate_token(&token);
79    /// assert!(safe.verify_token(&token).is_none())
80    /// ```
81    pub fn invalidate_token(&self, token: &str) {
82        self.0.token.write().unwrap().remove(token);
83        trace!("invalidated token '{}**'", token);
84    }
85
86    /// Invalidate all tokens related to specified user.
87    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    /// Make all tokens older than `duration` expire.
93    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    /// Count the current user number.
106    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    /// Create new user entry with `user`name and `pass`word.
115    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    /// Verify the provided `user`name and `pass`word.
131    /// Return a new token if successful.
132    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    /// Verify the provided `token`.
150    /// Returns the user it belongs to if valid.
151    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    /// Update a user's password to `new`.
157    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    /// Delate a user.
169    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}