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:
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>