From ba592de54cd3dce52eb2096dabbe748107f5db2b Mon Sep 17 00:00:00 2001 From: gennyble Date: Fri, 25 Sep 2026 03:56:11 -0500 Subject: splorm auth --- knowing/src/db.rs | 200 ++++++++++++++++++++++++++++++++++++++++++++++++ knowing/src/id.rs | 31 ++++++++ knowing/src/lib.rs | 88 +++++++++++++++++++++ knowing/src/password.rs | 35 +++++++++ 4 files changed, 354 insertions(+) create mode 100644 knowing/src/db.rs create mode 100644 knowing/src/id.rs create mode 100644 knowing/src/lib.rs create mode 100644 knowing/src/password.rs (limited to 'knowing/src') diff --git a/knowing/src/db.rs b/knowing/src/db.rs new file mode 100644 index 0000000..593baad --- /dev/null +++ b/knowing/src/db.rs @@ -0,0 +1,200 @@ +#![allow(async_fn_in_trait)] +#![allow(unused)] + +use crate::Error; +use somethings::{ + DatabaseError, DatabaseHandle, + auth::{AnonymousUser, HashedPassword, Session, SessionId, User, UserAuth, UserId}, + params, +}; + +pub trait AuthDatabase { + async fn auth_create_tables(&self) -> Result<(), DatabaseError>; + + async fn insert_user(&self, id: &UserId, name: &str, hash: HashedPassword) + -> Result<(), Error>; + async fn insert_anonymous_user(&self, id: &UserId) -> Result<(), Error>; + + async fn user_exists(&self, id: &UserId) -> Result; + async fn get_user_by_id(&self, id: &UserId) -> Result; + async fn get_anonymous_user_by_id(&self, id: &UserId) -> Result; + + async fn get_userauth_by_name(&self, name: &str) -> Result; + + async fn insert_session(&self, id: &SessionId, uid: &UserId) -> Result<(), Error>; + + async fn get_session_by_id(&self, id: &SessionId) -> Result; + + async fn delete_session_by_id(&self, id: &SessionId) -> Result<(), Error>; +} + +impl AuthDatabase for DatabaseHandle { + async fn auth_create_tables(&self) -> Result<(), DatabaseError> { + let lock = self.lock().await; + lock.create_table("users", CREATE_TABLE_USERS)?; + lock.create_table("sessions", CREATE_TABLE_SESSIONS) + } + + async fn insert_user( + &self, + id: &UserId, + name: &str, + hash: HashedPassword, + ) -> Result<(), Error> { + self.lock().await.insert( + "INSERT INTO users(id, name, hash) VALUES(?1, ?2, ?3)", + params![id, name, hash.to_string()], + )?; + + Ok(()) + } + + async fn insert_anonymous_user(&self, id: &UserId) -> Result<(), Error> { + self.lock() + .await + .insert("INSERT INTO users(id) VALUES(?1)", params![id])?; + + Ok(()) + } + + async fn user_exists(&self, id: &UserId) -> Result { + match get_dbuser_by_id(self, id).await { + Ok(_) => Ok(true), + Err(Error::DatabaseError(DatabaseError::NoRows)) => Ok(false), + Err(e) => Err(e), + } + } + + async fn get_user_by_id(&self, id: &UserId) -> Result { + get_dbuser_by_id(&self, id).await?.try_into() + } + + async fn get_anonymous_user_by_id(&self, id: &UserId) -> Result { + let dbuser = get_dbuser_by_id(&self, id).await?; + match dbuser.name { + Some(_) => Err(Error::UserIsRegistered), + None => Ok(AnonymousUser::new(dbuser.id)), + } + } + + async fn get_userauth_by_name(&self, name: &str) -> Result { + get_dbuser_by_name(&self, name).await?.try_into() + } + + async fn insert_session(&self, id: &SessionId, uid: &UserId) -> Result<(), Error> { + self.lock().await.insert( + "INSERT INTO sessions(id, user_id) VALUES(?1, ?2)", + params![id, uid], + )?; + + Ok(()) + } + + async fn get_session_by_id(&self, id: &SessionId) -> Result { + let sql = "SELECT sessions.id, users.id, users.name \ + FROM sessions \ + INNER JOIN users ON sessions.user_id = users.id \ + WHERE sessions.id = ?1"; + + self.lock() + .await + .query_one(sql, params![id], |row| { + Ok(Session::new( + row.get(0)?, + User::new(row.get(1)?, row.get(2)?), + )) + }) + .map_err(|dbe| Error::DatabaseError(dbe)) + } + + async fn delete_session_by_id(&self, id: &SessionId) -> Result<(), Error> { + self.lock() + .await + .execute("DELETE FROM session WHERE id = ?1", params![id])?; + Ok(()) + } +} + +async fn get_dbuser_by_id(db: &DatabaseHandle, id: &UserId) -> Result { + db.lock() + .await + .query_one( + "SELECT id, name, hash FROM users WHERE id = ?1", + params![id], + |row| { + Ok(DbUser { + id: row.get(0)?, + name: row.get(1)?, + hash: row.get(2)?, + }) + }, + ) + .map_err(|e| Error::DatabaseError(e)) +} + +async fn get_dbuser_by_name(db: &DatabaseHandle, name: &str) -> Result { + db.lock() + .await + .query_one( + "SELECT id, name, hash FROM users WHERE name = ?1", + params![name], + |row| { + Ok(DbUser { + id: row.get(0)?, + name: row.get(1)?, + hash: row.get(2)?, + }) + }, + ) + .map_err(|e| Error::DatabaseError(e)) +} + +pub const CREATE_TABLE_USERS: &'static str = "\ + CREATE TABLE IF NOT EXISTS users( + id TEXT NOT NULL PRIMARY KEY, + name TEXT, + hash TEXT, + created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + );"; + +pub const CREATE_TABLE_SESSIONS: &'static str = "\ + CREATE TABLE IF NOT EXISTS sessions( + id TEXT NOT NULL PRIMARY KEY, + user_id INTEGER NOT NULL, + created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) + REFERENCES users(id) + );"; + +struct DbUser { + id: UserId, + name: Option, + hash: Option, +} + +impl TryInto for DbUser { + type Error = Error; + + fn try_into(self) -> Result { + let DbUser { id, name, hash: _ } = self; + match name { + Some(name) => Ok(User::new(id, name)), + None => Err(Error::UserIsAnonymous), + } + } +} + +impl TryInto for DbUser { + type Error = Error; + + fn try_into(self) -> Result { + let DbUser { id, name, hash } = self; + match (name, hash) { + (Some(name), Some(hash)) => Ok(UserAuth::new( + User::new(id, name), + HashedPassword::new(hash), + )), + _ => Err(Error::UserIsAnonymous), + } + } +} diff --git a/knowing/src/id.rs b/knowing/src/id.rs new file mode 100644 index 0000000..e990c76 --- /dev/null +++ b/knowing/src/id.rs @@ -0,0 +1,31 @@ +use smallrand::{ChaCha12, DefaultEntropy, Rng}; +use somethings::auth::Base58Id; + +const BASE58: &'static [u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + +pub trait RandomId { + fn rand() -> Self; +} + +impl RandomId for Base58Id { + fn rand() -> Self { + unsafe { Self::new_unchecked(random_base58()) } + } +} + +/// Random Base58 string, `count` characters long, using OsRng which is assumed +/// to be secure +/// > assumed that system always provides high-quality cryptographically secure random data +fn random_base58() -> [u8; N] { + let mut buffer = [0u8; N]; + + // Explicitly using ChaCha12 here because StdRng is not garunteed to be + // cryptographically secure + let mut entropy = DefaultEntropy::new(); + let mut rng = ChaCha12::from_entropy(&mut entropy); + buffer + .iter_mut() + .for_each(|b| *b = BASE58[rng.range(0..BASE58.len())]); + + buffer +} diff --git a/knowing/src/lib.rs b/knowing/src/lib.rs new file mode 100644 index 0000000..2a92f25 --- /dev/null +++ b/knowing/src/lib.rs @@ -0,0 +1,88 @@ +mod db; +mod id; +mod password; + +use std::fmt; + +use db::AuthDatabase; +pub use id::RandomId; +use password::{IntoHashedPassword, PasswordVerifier}; +use somethings::{ + DatabaseError, DatabaseHandle, + auth::{AsUserId, Session, SessionId, User, UserId}, +}; + +pub async fn create_table(db: &DatabaseHandle) -> Result<(), Error> { + db.auth_create_tables().await?; + Ok(()) +} + +pub async fn user_register( + db: &DatabaseHandle, + name: &str, + plaintext_password: String, +) -> Result { + let id = UserId::rand(); + let hash = plaintext_password.hash_password(); + db.insert_user(&id, name, hash).await?; + + Ok(User::new(id, name.to_owned())) +} + +pub async fn user_login( + db: &DatabaseHandle, + name: &str, + plaintext_password: String, +) -> Result { + let userauth = db.get_userauth_by_name(name).await?; + userauth.hash().verify_password(plaintext_password)?; + + let session_id = SessionId::rand(); + db.insert_session(&session_id, userauth.user().userid()) + .await?; + + Ok(Session::new(session_id, userauth.into())) +} + +pub async fn get_session(db: &DatabaseHandle, sid_str: &str) -> Result { + let sid = sid_str.parse().map_err(|_| Error::InvalidSessionId)?; + db.get_session_by_id(&sid).await +} + +pub async fn remove_session(db: &DatabaseHandle, sid_str: &str) -> Result<(), Error> { + let sid = sid_str.parse().map_err(|_| Error::InvalidSessionId)?; + db.delete_session_by_id(&sid).await +} + +#[derive(Debug)] +pub enum Error { + AuthenticationFailed, + InvalidSessionId, + UserIsAnonymous, + UserIsRegistered, + + DatabaseError(DatabaseError), +} +impl std::error::Error for Error {} + +impl From for Error { + fn from(dbe: DatabaseError) -> Self { + Self::DatabaseError(dbe) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::AuthenticationFailed => write!(f, "failed to authenticate user"), + Self::InvalidSessionId => write!(f, "SessionId invalid"), + Self::UserIsAnonymous => { + write!(f, "expected a registered user, but UserId is anonymous") + } + Self::UserIsRegistered => { + write!(f, "expected an anonymous user, but UserId is registered") + } + Self::DatabaseError(dbe) => write!(f, "{dbe}"), + } + } +} diff --git a/knowing/src/password.rs b/knowing/src/password.rs new file mode 100644 index 0000000..013a9f7 --- /dev/null +++ b/knowing/src/password.rs @@ -0,0 +1,35 @@ +use argon2::{Argon2, PasswordHasher, PasswordVerifier as ArgonPasswordVerifier}; +use somethings::auth::HashedPassword; + +use crate::Error; + +pub trait IntoHashedPassword { + fn hash_password(self) -> HashedPassword; +} + +impl IntoHashedPassword for String { + fn hash_password(self) -> HashedPassword { + let a2 = Argon2::default(); + let a2_hash = a2.hash_password(self.as_bytes()).unwrap(); + + HashedPassword::new(a2_hash.to_string()) + } +} + +pub trait PasswordVerifier: Sized { + fn get_password_hash(&self) -> &HashedPassword; + fn verify_password(&self, plain: String) -> Result<(), Error> { + let a2_phash = argon2::PasswordHash::new(&self.get_password_hash().to_string()) + .map_err(|_| Error::AuthenticationFailed)?; + + Argon2::default() + .verify_password(plain.as_bytes(), &a2_phash) + .map_err(|_| Error::AuthenticationFailed) + } +} + +impl PasswordVerifier for HashedPassword { + fn get_password_hash(&self) -> &HashedPassword { + self + } +} -- cgit 1.4.1-3-g733a5