safe_box/
lib.rs

1pub mod err;
2
3use std::{
4    collections::HashMap,
5    ops::DerefMut,
6    path::Path,
7    sync::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
15fn gen_salt() -> [u8; 64] {
16    let mut buf = [0u8; 64];
17    getrandom::fill(&mut buf).unwrap();
18    buf
19}
20
21/// Interface to the password database.
22pub struct SafeBox {
23    conn: AsyncMutex<SqliteConnection>,
24    argon2: argon2::Config<'static>,
25    token: RwLock<HashMap<String, (String, SystemTime)>>,
26}
27
28pub use err::SafeBoxError as Error;
29
30/// Initialize the database.
31const Q_INIT: &str = "CREATE TABLE IF NOT EXISTS main (user TEXT PRIMARY KEY, phc TEXT);";
32
33impl SafeBox {
34    /// Open an SQLite connection with specified database file and create a `SafeBox`.
35    /// # Example
36    /// ```
37    /// use safe_box::SafeBox;
38    ///
39    /// let safe = SafeBox::new("secure.db").await.unwrap();
40    /// ```
41    pub async fn new(p: impl AsRef<Path>) -> Result<Self, Error> {
42        let opt = SqliteConnectOptions::default()
43            .filename(p)
44            .create_if_missing(true);
45        let mut conn = SqliteConnection::connect_with(&opt).await?;
46        query(Q_INIT).execute(&mut conn).await?;
47        Ok(Self {
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.token
60            .write()
61            .unwrap()
62            .insert(token.clone(), (user.to_owned(), SystemTime::now()));
63        return token;
64    }
65
66    /// Invalidate a token.
67    pub fn invalidate_token(&self, token: &str) {
68        self.token.write().unwrap().remove(token);
69    }
70
71    /// Invalidate all tokens related to specified user.
72    pub fn invalidate_user_token(&self, user: &str) {
73        self.token.write().unwrap().retain(|_, (u, _)| u != user);
74    }
75
76    /// Make all tokens older than `duration` expire.
77    pub fn expire_token(&self, duration: Duration) {
78        self.token.write().unwrap().retain(|_, (_, time)| {
79            SystemTime::now()
80                .duration_since(*time)
81                .is_ok_and(|d| d < duration)
82        });
83    }
84
85    /// Count the current user number.
86    pub async fn user_cnt(&self) -> Result<usize, Error> {
87        let cnt: u64 = query("SELECT COUNT(*) FROM main")
88            .fetch_one(self.conn.lock().await.deref_mut())
89            .await?
90            .get(0);
91        Ok(cnt as usize)
92    }
93
94    /// Create new user entry with `user`name and `pass`word.
95    pub async fn create(&self, user: &str, pass: &str) -> Result<(), Error> {
96        let q = query("SELECT NULL FROM main WHERE user = ?").bind(user);
97        let v = q.fetch_all(self.conn.lock().await.deref_mut()).await?;
98        if v.len() > 0 {
99            return Err(Error::UserAlreadyExist(user.to_owned()));
100        }
101        let hashed = argon2::hash_encoded(pass.as_bytes(), &gen_salt(), &self.argon2)?;
102        let query = query("INSERT INTO main (user, phc) VALUES (?, ?)")
103            .bind(user)
104            .bind(hashed);
105        query.execute(self.conn.lock().await.deref_mut()).await?;
106        Ok(())
107    }
108
109    /// Verify the provided `user`name and `pass`word.
110    /// Return a new token if successful.
111    pub async fn verify(&self, user: &str, pass: &str) -> Result<bool, Error> {
112        let query = query("SELECT phc FROM main WHERE user = ?").bind(user);
113        let mut conn = self.conn.lock().await;
114        let v = query.fetch_all(conn.deref_mut()).await?;
115        match v.len() {
116            0 => return Err(Error::UserNotExist(user.to_owned())),
117            2.. => return Err(Error::InvalidData(format!("duplicate user '{user}'"))),
118            _ => (),
119        };
120        let p = v[0].try_get("phc")?;
121        let res = argon2::verify_encoded(p, pass.as_bytes())?;
122        Ok(res)
123    }
124
125    /// Verify the provided `token`.
126    /// Returns the user it belongs to if valid.
127    pub fn verify_token(&self, token: &str) -> Option<String> {
128        let map = self.token.read().unwrap();
129        map.get(token).map(|(user, _)| user.clone())
130    }
131
132    /// Update a user's password to `new`.
133    pub async fn update(&self, user: &str, new_pass: &str) -> Result<(), Error> {
134        self.invalidate_user_token(user);
135        let hashed = argon2::hash_encoded(new_pass.as_bytes(), &gen_salt(), &self.argon2)?;
136        let query = query("UPDATE main SET phc = ? WHERE user = ?")
137            .bind(hashed)
138            .bind(user);
139        query.execute(self.conn.lock().await.deref_mut()).await?;
140        Ok(())
141    }
142
143    /// Delate a user.
144    pub async fn delete(&self, user: &str) -> Result<(), Error> {
145        let query = query("DELETE FROM main WHERE user = ?").bind(user);
146        query.execute(self.conn.lock().await.deref_mut()).await?;
147        Ok(())
148    }
149}