cyrtophora

Full-stack users-first, secure web framework.
git clone https://gitlab.com/kwatafana/cyrtophora.git
Log | Files | Refs | README

commit 04bf46132d478f0d17c28a957e9841722c306ae0
parent 3e23f650c4b8768b4919be5065999e57fd44ec07
Author: kaindume <cy6erlion@protonmail.com>
Date:   Thu, 22 Sep 2022 14:10:39 +0000

Merge branch 'database' into 'root'

Database

See merge request kwatafana/cyrtophora!1
Diffstat:
M.gitignore | 1+
MREADME.md | 11++---------
Mphora/Cargo.lock | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mphora/Cargo.toml | 8++++++--
Mphora/src/account.rs | 9+++------
Dphora/src/api.rs | 13-------------
Mphora/src/data.rs | 4++--
Aphora/src/database/error.rs | 27+++++++++++++++++++++++++++
Aphora/src/database/mod.rs | 15+++++++++++++++
Aphora/src/database/sqlite.rs | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mphora/src/lib.rs | 55++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Aspec/.gitignore | 1+
Aspec/book.toml | 6++++++
Dspec/password-hashing.md | 77-----------------------------------------------------------------------------
Aspec/src/SUMMARY.md | 8++++++++
Rspec/accounts.md -> spec/src/accounts.md | 0
Aspec/src/cyrtophora.md | 3+++
Aspec/src/database.md | 6++++++
Aspec/src/password-hashing.md | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aspec/src/sqlite-support.md | 11+++++++++++
Aspec/src/validation.md | 1+
21 files changed, 447 insertions(+), 110 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,3 +1,4 @@ *~ phora/target /Cargo.lock +test-data diff --git a/README.md b/README.md @@ -11,7 +11,6 @@ </a><br> <small><em>Clone it!</em></small> </div> - ___ @@ -20,17 +19,11 @@ ___ ░█░░░░█░░█▀▄░░█░░█░█░█▀▀░█▀█░█░█░█▀▄░█▀█ ░▀▀▀░░▀░░▀░▀░░▀░░▀▀▀░▀░░░▀░▀░▀▀▀░▀░▀░▀░▀ ``` - -Depends on: - -- [scrypt](https://github.com/RustCrypto/password-hashes/tree/master/scrypt): Used for password hashing. -- [ed25519 from the ring crate](https://github.com/briansmith/ring): Digital Signatures -- [XChaCha20-Poly1305](https://github.com/RustCrypto/AEADs/tree/master/chacha20poly1305): Symmetric Encryption - ## Features -- Accounts +- Account Management - Input validation +- Database ## Unlicense diff --git a/phora/Cargo.lock b/phora/Cargo.lock @@ -12,6 +12,17 @@ dependencies = [ ] [[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] name = "anyhow" version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -30,6 +41,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bdca834647821e0b13d9539a8634eb62d3501b6b6c2cec1722786ee6671b851" [[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] name = "block-buffer" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -127,6 +144,7 @@ dependencies = [ "base64", "chacha20poly1305", "ring", + "rusqlite", "scrypt", "serde", ] @@ -143,6 +161,18 @@ dependencies = [ ] [[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] name = "generic-array" version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -164,6 +194,24 @@ dependencies = [ ] [[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452c155cb93fecdfb02a73dd57b5d8e442c2063bd7aac72f1bc5e4263a43086" +dependencies = [ + "hashbrown", +] + +[[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -197,6 +245,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64de3cc433455c14174d42e554d4027ee631c4d046d43e3ecc6efc4636cdc7a7" [[package]] +name = "libsqlite3-sys" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f0455f2c1bc9a7caa792907026e469c1d91761fb0ea37cbb16427c77280cf35" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] name = "log" version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -238,6 +296,12 @@ dependencies = [ ] [[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + +[[package]] name = "poly1305" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -291,6 +355,20 @@ dependencies = [ ] [[package]] +name = "rusqlite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] name = "salsa20" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -344,6 +422,12 @@ dependencies = [ ] [[package]] +name = "smallvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" + +[[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -395,6 +479,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/phora/Cargo.toml b/phora/Cargo.toml @@ -9,4 +9,8 @@ ring = "0.16.20" chacha20poly1305 = "0.9.0" base64 = "0.13.0" scrypt = "0.10.0" -serde = { version = "1.0.144", features = ["derive"] } -\ No newline at end of file +serde = { version = "1.0.144", features = ["derive"] } +rusqlite = { version = "0.28.0", optional = true } + +[features] +sqlite = ["dep:rusqlite"] +\ No newline at end of file diff --git a/phora/src/account.rs b/phora/src/account.rs @@ -1,4 +1,4 @@ -use crate::data::AccountRegistration; +use crate::data::AccountCreationInput; use crate::validate::ValidationError; use serde::{Deserialize, Serialize}; @@ -15,19 +15,16 @@ pub struct Account { impl Account { /// Create new account - pub fn create(payload: AccountRegistration) -> Result<PublicAccount, ValidationError> { + pub fn create(payload: AccountCreationInput) -> Result<Self, ValidationError> { payload.is_valid()?; - // TODO: email verification - // TODO: database registration - let account = Account { username: payload.username, email: payload.email, password: payload.password, }; - Ok(account.into()) + Ok(account) } } diff --git a/phora/src/api.rs b/phora/src/api.rs @@ -1,13 +0,0 @@ -pub struct API { - pub version: String, - pub accounts: String, -} - -impl Default for API { - fn default() -> Self { - API { - version: "/v0".to_string(), - accounts: "/v0/accounts".to_string(), - } - } -} diff --git a/phora/src/data.rs b/phora/src/data.rs @@ -3,14 +3,14 @@ use serde::{Deserialize, Serialize}; /// User registration Data #[derive(Deserialize, Serialize)] -pub struct AccountRegistration { +pub struct AccountCreationInput { pub username: String, pub password: String, pub retyped_password: String, pub email: Option<String>, } -impl AccountRegistration { +impl AccountCreationInput { pub fn is_valid(&self) -> Result<(), ValidationError> { if self.password != self.retyped_password { return Err(ValidationError::Password); diff --git a/phora/src/database/error.rs b/phora/src/database/error.rs @@ -0,0 +1,27 @@ +use std::fmt; + +#[derive(Debug)] +pub enum Error { + Connection, + CreateTable, + AccountRetrieval, +} + +impl std::error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::Connection => write!(f, "Database connection error"), + Error::CreateTable => write!(f, "Could not create Sql table"), + Error::AccountRetrieval => write!(f, "Could not create retrieve account"), + } + } +} + +#[cfg(feature = "sqlite")] +impl From<rusqlite::Error> for Error { + fn from(_: rusqlite::Error) -> Self { + Error::Connection + } +} diff --git a/phora/src/database/mod.rs b/phora/src/database/mod.rs @@ -0,0 +1,15 @@ +use crate::account::{Account, PublicAccount}; +use error::Error; + +pub mod error; +#[cfg(feature = "sqlite")] +pub mod sqlite; + +pub trait DB { + /// Connect to database + fn connect(&mut self) -> Result<(), Error>; + /// Store user account + fn account_store(&self, account: &Account) -> Result<(), Error>; + /// Get account public data + fn account_get_pub(&self, username: &str) -> Result<PublicAccount, Error>; +} diff --git a/phora/src/database/sqlite.rs b/phora/src/database/sqlite.rs @@ -0,0 +1,136 @@ +use super::{error::Error, DB}; +use crate::account::{Account, PublicAccount}; +use rusqlite::{params, Connection, Result}; + +pub mod accounts_sql { + /// Cyrtophora Account Schema + pub const CREATE_TABLE: &str = " + CREATE TABLE IF NOT EXISTS accounts ( + id INTEGER PRIMARY KEY, -- The Identifier of the User, the Rust Type is `i64` + name TEXT, -- Fullname of the account + username TEXT UNIQUE NOT NULL, -- The username of the User + email TEXT UNIQUE, -- Users's email address + password TEXT NOT NULL, -- The user's login password + joined TEXT DEFAULT(date('now')) NOT NULL) -- The date when the user joined, the Rust Type is `chrono::DateTime`"; + + /// Insert a user in the users table + pub const STORE: &str = " + INSERT INTO accounts ( + username, + password, + email + ) + VALUES (?1, ?2, ?3)"; + + /// Get by username + pub const GET_PUBLIC: &str = "SELECT username FROM accounts WHERE username = :username;"; + + /// Drop accounts table + pub const DESTROY_TABLE: &str = "DROP table accounts"; +} + +/// database controller +pub struct SqliteDB { + /// Database file path + path: String, + /// An SQLite connection handle + conn: Option<Connection>, +} +impl SqliteDB { + pub fn new(path: &str) -> Self { + SqliteDB { + path: path.to_string(), + conn: None, + } + } +} +impl DB for SqliteDB { + fn connect(&mut self) -> Result<(), Error> { + // Open database connection + let conn = Connection::open(self.path.clone())?; + self.conn = Some(conn); + + // Create accounts table if it does not already exists + match &self.conn { + Some(conn) => match conn.execute(accounts_sql::CREATE_TABLE, []) { + Ok(_rows) => Ok(()), + Err(_err) => Err(Error::CreateTable), + }, + None => Err(Error::Connection), + } + } + + fn account_store(&self, account: &Account) -> Result<(), Error> { + match &self.conn { + Some(conn) => { + conn.execute( + accounts_sql::STORE, + params![&account.username, account.password, account.email], + )?; + Ok(()) + } + None => Err(Error::Connection), + } + } + + fn account_get_pub(&self, username: &str) -> Result<PublicAccount, Error> { + match &self.conn { + Some(conn) => { + let mut stmt = conn.prepare(accounts_sql::GET_PUBLIC)?; + let mut rows = stmt.query(&[(":username", username)])?; + match rows.next()? { + Some(s) => Ok(PublicAccount { + username: s.get(0)?, + }), + None => Err(Error::AccountRetrieval), + } + } + None => Err(Error::Connection), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + const TEST_DB_PATH: &str = "test-data/ACCOUNTS.db"; + + #[test] + fn connect_db() { + let mut db = SqliteDB::new(TEST_DB_PATH); + + // Connect to database + db.connect().unwrap(); + + match db.conn { + Some(_conn) => assert!(true), + _ => assert!(false), + } + } + + #[test] + fn test_store_get_account() { + remove_test_db(); + let mut db = SqliteDB::new(TEST_DB_PATH); + let account = Account { + username: String::from("testuszee"), + password: String::from("12345678910"), + email: Some(String::from("info@example.com")), + }; + + db.connect().unwrap(); + db.account_store(&account).unwrap(); + + let public_account = db.account_get_pub("testuszee").unwrap(); + + assert_eq!(&public_account.username, "testuszee"); + } + + fn remove_test_db() { + let test_db_path = std::path::Path::new(TEST_DB_PATH); + if std::path::Path::exists(test_db_path) { + std::fs::remove_file(test_db_path).unwrap(); + } + } +} diff --git a/phora/src/lib.rs b/phora/src/lib.rs @@ -1,5 +1,58 @@ +use account::PublicAccount; +use database::DB; + pub mod account; -pub mod api; pub mod crypto; pub mod data; +pub mod database; pub mod validate; + +pub struct Cyrtophora<D> +where + D: DB, +{ + database: Option<D>, +} + +impl<D: DB> Cyrtophora<D> { + #[cfg(feature = "sqlite")] + pub fn new_sqlite( + path: &str, + ) -> Result<Cyrtophora<database::sqlite::SqliteDB>, database::error::Error> { + let mut db = database::sqlite::SqliteDB::new(path); + db.connect()?; + let c = Cyrtophora { database: Some(db) }; + Ok(c) + } + + /// Create a new account + pub fn account_create( + &mut self, + payload: data::AccountCreationInput, + ) -> Result<PublicAccount, Box<dyn std::error::Error>> { + let account = account::Account::create(payload)?; + // TODO: email verification + + // store account in database + if cfg!(sqlite) { + if let Some(db) = &self.database { + db.account_store(&account)?; + } + } + Ok(account.into()) + } + + /// Get account public data, using the username as ID + pub fn account_get(&self, username: &str) -> Result<PublicAccount, database::error::Error> { + if cfg!(sqlite) { + if let Some(db) = &self.database { + let public_account = db.account_get_pub(username)?; + Ok(public_account) + } else { + Err(database::error::Error::Connection) + } + } else { + Err(database::error::Error::Connection) + } + } +} diff --git a/spec/.gitignore b/spec/.gitignore @@ -0,0 +1 @@ +book diff --git a/spec/book.toml b/spec/book.toml @@ -0,0 +1,6 @@ +[book] +authors = ["Jackson G. Kaindume"] +language = "en" +multilingual = false +src = "src" +title = "Cyber Defense" diff --git a/spec/password-hashing.md b/spec/password-hashing.md @@ -1,77 +0,0 @@ ---- -title: Password Hashing -subtitle: 🔐 -author: Jackson G. Kaindume -date: 2022-08-14 -... ---- - -## Why hash? - -It is only a matter of time until your server gets hacked, and -when that happens you don't want the users passwords to be leaked -- -this will allow the attacker to gain access to the users resources. -Some users also use the same password across many services, your -web-server can be the root cause of a chain of breaches. - -A cool way to prevent this type of leak is by __obfuscating__ the -users password with a [__hash function__](https://en.wikipedia.org/wiki/Hash_function). - -There are lots of hash functions that can be used, but most of these -will be a bad idea to use. For example if you use SHA-256 or other -computationally cheap functions (hash function without a __work factor__ -parameter), they are vulnerable to rainbow table attacks. -Bruteforce is also possible if the password length is short/known, -asic miners can generate 100 TeraHashes PER Second. - -The server can increase the passwords entropy by concatenating it with -a random string aka the __salt__. Users can also protect themselves -by using longer passwords. - -The best method to use against plaintext password leaks and rainbow -table attacks is to use a __Password Hash Function__. Which is a hash -function specially designed to be slow/expensive to compute even on -specialized hardware. - -## Scrypt [recommended] - -The [scrypt](https://www.tarsnap.com/scrypt.html) hash function uses large amounts of memory when hashing -making it expensive to scale to the point of reasonable bruteforce -attacks. Secure against hardware brute-force attacks. - -A number of cryptocurrencies use __scrypt__ for proof of work. - -Created by Colin Percival of [Tarsnap](https://en.wikipedia.org/wiki/Tarsnap) - -## Argon2d [recommended] - -The [Argon2d](https://en.wikipedia.org/wiki/Argon2) function is -designed to resist GPU cracking attacks. Secure against hardware -brute-force attacks. - -It is the winner of [Password Hashing Competition](https://www.password-hashing.net/). - -## Bcrypt - -[Bcrypt](https://en.wikipedia.org/wiki/Bcrypt) is based on the -[blowfish](https://en.wikipedia.org/wiki/Blowfish_(cipher)) cipher. - -Vulnerable against hardware brute-force attacks. - -## PBKDF2 - -[PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) is an key derivation -function with a sliding computational cost to reduce bruteforce -search. - -Vulnerable against hardware brute-force attacks. - -## Conclusion - -A cool way to prevent password leaks is by __obfuscating__ them -with a password hash functions which offer additional security -against bruteforce from specialliazed hardware such as asics. If -password hash functions are used and implemented correctly even the -administrators of the server will not be able to read the users -passwords especially if the server is open source and the users can -audit the code for themselves. diff --git a/spec/src/SUMMARY.md b/spec/src/SUMMARY.md @@ -0,0 +1,8 @@ +# Summary + +- [Cyrtophora](./cyrtophora.md) +- [Accounts](./accounts.md) +- [Database](./database.md) + - [SQLite Support](./sqlite-support.md) + - [Password-hashing](./password-hashing.md) +- [Validation](./validation.md) diff --git a/spec/accounts.md b/spec/src/accounts.md diff --git a/spec/src/cyrtophora.md b/spec/src/cyrtophora.md @@ -0,0 +1,3 @@ +# Cyrtophora + +Full-stack users-first, secure web framework. diff --git a/spec/src/database.md b/spec/src/database.md @@ -0,0 +1,6 @@ +# Database + +Cyrtophora stores structured data in a database. The following data is +is stored: + +1. Accounts diff --git a/spec/src/password-hashing.md b/spec/src/password-hashing.md @@ -0,0 +1,75 @@ +# Password Hashing + +## Why hash? + +It is only a matter of time until your server gets hacked, and +when that happens you don't want the users passwords to be leaked -- +this will allow the attacker to gain access to the users resources. +Some users also use the same password across many services, your +web-server can be the root cause of a chain of breaches. + +A cool way to prevent this type of leak is by __obfuscating__ the +users password with a [__hash function__](https://en.wikipedia.org/wiki/Hash_function). + +There are lots of hash functions that can be used, but most of these +will be a bad idea to use. For example if you use SHA-256 or other +computationally cheap functions (hash function without a __work factor__ +parameter), they are vulnerable to rainbow table attacks. +Bruteforce is also possible if the password length is short/known, +asic miners can generate 100 TeraHashes PER Second. + +The server can increase the passwords entropy by concatenating it with +a random string aka the __salt__. Users can also protect themselves +by using longer passwords. + +The best method to use against plaintext password leaks and rainbow +table attacks is to use a __Password Hash Function__. Which is a hash +function specially designed to be slow/expensive to compute even on +specialized hardware. + +## Scrypt [recommended] + +The [scrypt](https://www.tarsnap.com/scrypt.html) hash function uses large amounts of memory when hashing +making it expensive to scale to the point of reasonable bruteforce +attacks. Secure against hardware brute-force attacks. + +A number of cryptocurrencies use __scrypt__ for proof of work. + +Created by Colin Percival of [Tarsnap](https://en.wikipedia.org/wiki/Tarsnap) + +## Argon2d [recommended] + +The [Argon2d](https://en.wikipedia.org/wiki/Argon2) function is +designed to resist GPU cracking attacks. Secure against hardware +brute-force attacks. + +It is the winner of [Password Hashing Competition](https://www.password-hashing.net/). + +## Bcrypt + +[Bcrypt](https://en.wikipedia.org/wiki/Bcrypt) is based on the +[blowfish](https://en.wikipedia.org/wiki/Blowfish_(cipher)) cipher. + +Vulnerable against hardware brute-force attacks. + +## PBKDF2 + +[PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) is an key derivation +function with a sliding computational cost to reduce bruteforce +search. + +Vulnerable against hardware brute-force attacks. + +## Conclusion + +A cool way to prevent password leaks is by __obfuscating__ them +with a password hash functions which offer additional security +against bruteforce from specialliazed hardware such as asics. If +password hash functions are used and implemented correctly even the +administrators of the server will not be able to read the users +passwords especially if the server is open source and the users can +audit the code for themselves. + +<https://www.troyhunt.com/our-password-hashing-has-no-clothes/> +<https://paragonie.com/blog/2016/02/how-safely-store-password-in-2016> +<https://www.troyhunt.com/passwords-evolved-authentication-guidance-for-the-modern-era/> diff --git a/spec/src/sqlite-support.md b/spec/src/sqlite-support.md @@ -0,0 +1,11 @@ +# Sqlite Support + +Sqlite is supported in cyrtophora as an optional feature: + +```toml +cyrtophora = { path = "../../cyrtophora/phora", features = ["sqlite"] } +``` +When the sqlite feature is enabled user account data will be saved in +a sqlite database. + + diff --git a/spec/src/validation.md b/spec/src/validation.md @@ -0,0 +1 @@ +<https://beesbuzz.biz/code/439-Falsehoods-programmers-believe-about-email>