about summary refs log tree commit diff
path: root/knowing/src
diff options
context:
space:
mode:
Diffstat (limited to 'knowing/src')
-rw-r--r--knowing/src/db.rs200
-rw-r--r--knowing/src/id.rs31
-rw-r--r--knowing/src/lib.rs88
-rw-r--r--knowing/src/password.rs35
4 files changed, 354 insertions, 0 deletions
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<bool, Error>;
+	async fn get_user_by_id(&self, id: &UserId) -> Result<User, Error>;
+	async fn get_anonymous_user_by_id(&self, id: &UserId) -> Result<AnonymousUser, Error>;
+
+	async fn get_userauth_by_name(&self, name: &str) -> Result<UserAuth, Error>;
+
+	async fn insert_session(&self, id: &SessionId, uid: &UserId) -> Result<(), Error>;
+
+	async fn get_session_by_id(&self, id: &SessionId) -> Result<Session, Error>;
+
+	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<bool, Error> {
+		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<User, Error> {
+		get_dbuser_by_id(&self, id).await?.try_into()
+	}
+
+	async fn get_anonymous_user_by_id(&self, id: &UserId) -> Result<AnonymousUser, Error> {
+		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<UserAuth, Error> {
+		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<Session, Error> {
+		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<DbUser, Error> {
+	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<DbUser, Error> {
+	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<String>,
+	hash: Option<String>,
+}
+
+impl TryInto<User> for DbUser {
+	type Error = Error;
+
+	fn try_into(self) -> Result<User, Self::Error> {
+		let DbUser { id, name, hash: _ } = self;
+		match name {
+			Some(name) => Ok(User::new(id, name)),
+			None => Err(Error::UserIsAnonymous),
+		}
+	}
+}
+
+impl TryInto<UserAuth> for DbUser {
+	type Error = Error;
+
+	fn try_into(self) -> Result<UserAuth, Self::Error> {
+		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<const N: usize> RandomId for Base58Id<N> {
+	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<const N: usize>() -> [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<User, Error> {
+	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<Session, Error> {
+	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<Session, Error> {
+	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<DatabaseError> 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
+	}
+}