diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index d1bb89a..e2becd9 100644 --- a/README.md +++ b/README.md @@ -7,18 +7,25 @@ [![Documentation](https://img.shields.io/badge/docs-master-blue)](https://mcaptcha.github.io/mCaptcha/m_captcha/index.html) ![CI (Linux)]() [![dependency status](https://deps.rs/repo/github/mCaptcha/mCaptcha/status.svg)](https://deps.rs/repo/github/mCaptcha/mCaptcha) +[![AGPL License](https://img.shields.io/badge/license-AGPL-blue.svg)](http://www.gnu.org/licenses/agpl-3.0)
[![codecov](https://codecov.io/gh/mCaptcha/mCaptcha/branch/master/graph/badge.svg)](https://codecov.io/gh/mCaptcha/mCaptcha) +[![Documentation](https://img.shields.io/badge/matrix-community-purple)](https://matrix.to/#/+mcaptcha:matrix.batsense.net) -### STATUS: ACTIVE DEVELOPMENT (fancy word for unusable) + mCaptcha uses SHA256 based proof-of-work(PoW) to rate limit users. -**If someone wants to hammer your site, they will have to do more work to +If someone wants to hammer your site, they will have to do more work to send requests than your server will have to do to respond to their -request.** +request. + +>**NOTE:** `0.1` is out, expect breaking changes as ergonomics and +performance is improved. Checkout [changelog](./CHANGELOG.md) for +changes and migration pointers. + ## Why use mCaptcha? - Free software, privacy focused diff --git a/examples/simple.rs b/examples/simple.rs new file mode 100644 index 0000000..a386a73 --- /dev/null +++ b/examples/simple.rs @@ -0,0 +1,115 @@ +use m_captcha::{ + cache::HashCache, + master::{AddSiteBuilder, Master}, + pow::{ConfigBuilder, Work}, + system::SystemBuilder, + DefenseBuilder, LevelBuilder, MCaptchaBuilder, +}; +// traits from actix needs to be in scope for starting actor +use actix::prelude::*; + +#[actix_rt::main] +async fn main() -> std::io::Result<()> { + // start cahce actor + // cache is used to store PoW requirements that are sent to clients + // This way, it can be verified that the client computed work over a config + // that _we_ sent. Offers protection against rainbow tables powered dictionary attacks + let cache = HashCache::default().start(); + + // create PoW config with unique salt. Salt has to be safely guarded. + // salts protect us from replay attacks + let pow = ConfigBuilder::default() + .salt("myrandomsaltisnotlongenoug".into()) + .build() + .unwrap(); + + // start master actor. Master actor is responsible for managing MCaptcha actors + // each mCaptcha system should have only one master + let master = Master::new().start(); + + // Create system. System encapsulates master and cache and provides useful abstraction + // each mCaptcha system should have only one system + let system = SystemBuilder::default() + .master(master) + .cache(cache) + .pow(pow.clone()) + .build() + .unwrap(); + + // configure defense. This is a per site configuration. A site can have several levels + // of defenses configured + let defense = DefenseBuilder::default() + // add as many defense as you see fit + .add_level( + LevelBuilder::default() + // visitor_threshold is the threshold/limit at which + // mCaptcha will adjust difficulty defense + // it is advisable to set small values for the first + // defense visitor_threshold and difficulty_factor + // as this will be the work that clients will be + // computing when there's no load + .visitor_threshold(50) + .difficulty_factor(500) + .unwrap() + .build() + .unwrap(), + ) + .unwrap() + .add_level( + LevelBuilder::default() + .visitor_threshold(5000) + .difficulty_factor(50000) + .unwrap() + .build() + .unwrap(), + ) + .unwrap() + .build() + .unwrap(); + + // create and start MCaptcha actor that uses the above defense configuration + // This is what manages the difficulty factor of sites that an mCaptcha protects + let mcaptcha = MCaptchaBuilder::default() + .defense(defense) + // leaky bucket algorithm's emission interval + .duration(30) + // .cache(cache) + .build() + .unwrap() + .start(); + + // unique value identifying an MCaptcha actor + let mcaptcha_name = "batsense.net"; + + // add MCaptcha to Master + let msg = AddSiteBuilder::default() + .id(mcaptcha_name.into()) + .addr(mcaptcha.clone()) + .build() + .unwrap(); + system.master.send(msg).await.unwrap(); + + // Get PoW config. Should be called everytime there's a visitor for a + // managed site(here mcaptcha_name) + let work_req = system.get_pow(mcaptcha_name.into()).await.unwrap(); + + // the following computation should be done on the client but for the purpose + // of this illustration, we are going to do it on the server it self + let work = pow + .prove_work(&work_req.string, work_req.difficulty_factor) + .unwrap(); + + // the payload that the client sends to the server + let payload = Work { + string: work_req.string, + result: work.result, + nonce: work.nonce, + }; + + // Server evaluates client's work. Returns true if everything + // checksout and Err() if something fishy is happening + let res = system.verify_pow(payload.clone()).await.unwrap(); + assert!(res); + + Ok(()) +} diff --git a/src/errors.rs b/src/errors.rs index fd87308..5f2bb6b 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -63,11 +63,6 @@ pub enum CaptchaError { /// isn't in cache #[display(fmt = "String now found")] StringNotFound, - - /// Catcha all default error - /// used for development, must remove before production - #[display(fmt = "TODO remove before prod")] - Default, } /// [Result] datatype for m_captcha diff --git a/src/lib.rs b/src/lib.rs index 8816998..0a60aba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,20 +31,59 @@ //! - Difficulty(Factor): Minimum ammount of work that a client must do to make a valid //! request. //! - [Defense]: A datatype that various visitor-difficulty mappigns -//! - [Visitor][crate::message::Visitor]: Smallest unit of traffic, usually a single request. The more you have, the busier +//! - [Visitor][crate::mcaptcha::Visitor]: Smallest unit of traffic, usually a single request. The more you have, the busier //! your service is. Determines mCaptcha defense defense //! - Visitor threshold: The threshold at which [MCaptcha] will adjust defense defense +//! - [Cache][crate::cache] : A datatype that implements [Save][crate::cache::Save]. Used to store +//! PoW requirements to defend against replay attacks and dictionary attacks. +//! - [Master][crate::master::Master]: A datatype that manages [MCaptcha][crate::mcaptcha::MCaptcha] actors. Works like a DNS for [Visitor][crate::mcaptcha::Visitor] messages. +//! - [System][crate::system::System]: mCaptcha system that manages cache, master and provides +//! useful abstractions. An mCaptcha system/instance can have only a single +//! [System][crate::system::System] //! //! ## Example: //! //! ```rust -//! use m_captcha::{LevelBuilder, cache::HashCache, DefenseBuilder, message::Visitor, MCaptchaBuilder}; +//! use m_captcha::{ +//! cache::HashCache, +//! master::{AddSiteBuilder, Master}, +//! pow::{ConfigBuilder, Work}, +//! system::SystemBuilder, +//! DefenseBuilder, LevelBuilder, MCaptchaBuilder, +//! }; //! // traits from actix needs to be in scope for starting actor //! use actix::prelude::*; //! //! #[actix_rt::main] //! async fn main() -> std::io::Result<()> { -//! // configure defense +//! // start cahce actor +//! // cache is used to store PoW requirements that are sent to clients +//! // This way, it can be verified that the client computed work over a config +//! // that _we_ sent. Offers protection against rainbow tables powered dictionary attacks +//! let cache = HashCache::default().start(); +//! +//! // create PoW config with unique salt. Salt has to be safely guarded. +//! // salts protect us from replay attacks +//! let pow = ConfigBuilder::default() +//! .salt("myrandomsaltisnotlongenoug".into()) +//! .build() +//! .unwrap(); +//! +//! // start master actor. Master actor is responsible for managing MCaptcha actors +//! // each mCaptcha system should have only one master +//! let master = Master::new().start(); +//! +//! // Create system. System encapsulates master and cache and provides useful abstraction +//! // each mCaptcha system should have only one system +//! let system = SystemBuilder::default() +//! .master(master) +//! .cache(cache) +//! .pow(pow.clone()) +//! .build() +//! .unwrap(); +//! +//! // configure defense. This is a per site configuration. A site can have several levels +//! // of defenses configured //! let defense = DefenseBuilder::default() //! // add as many defense as you see fit //! .add_level( @@ -74,20 +113,49 @@ //! .build() //! .unwrap(); //! -//! //let cache = HashCache::default().start(); -//! -//! // create and start MCaptcha actor +//! // create and start MCaptcha actor that uses the above defense configuration +//! // This is what manages the difficulty factor of sites that an mCaptcha protects //! let mcaptcha = MCaptchaBuilder::default() //! .defense(defense) //! // leaky bucket algorithm's emission interval //! .duration(30) -//! // .cache(cache) +//! // .cache(cache) //! .build() //! .unwrap() //! .start(); //! -//! // increment count when user visits protected routes -//! mcaptcha.send(Visitor).await.unwrap(); +//! // unique value identifying an MCaptcha actor +//! let mcaptcha_name = "batsense.net"; +//! +//! // add MCaptcha to Master +//! let msg = AddSiteBuilder::default() +//! .id(mcaptcha_name.into()) +//! .addr(mcaptcha.clone()) +//! .build() +//! .unwrap(); +//! system.master.send(msg).await.unwrap(); +//! +//! // Get PoW config. Should be called everytime there's a visitor for a +//! // managed site(here mcaptcha_name) +//! let work_req = system.get_pow(mcaptcha_name.into()).await.unwrap(); +//! +//! // the following computation should be done on the client but for the purpose +//! // of this illustration, we are going to do it on the server it self +//! let work = pow +//! .prove_work(&work_req.string, work_req.difficulty_factor) +//! .unwrap(); +//! +//! // the payload that the client sends to the server +//! let payload = Work { +//! string: work_req.string, +//! result: work.result, +//! nonce: work.nonce, +//! }; +//! +//! // Server evaluates client's work. Returns true if everything +//! // checksout and Err() if something fishy is happening +//! let res = system.verify_pow(payload.clone()).await.unwrap(); +//! assert!(res); //! //! Ok(()) //! } @@ -98,11 +166,6 @@ pub mod errors; pub mod master; pub mod mcaptcha; -/// message datatypes to interact with [MCaptcha] actor -pub mod message { - pub use crate::mcaptcha::Visitor; -} - /// message datatypes to interact with [MCaptcha] actor pub mod cache; pub mod pow; diff --git a/src/mcaptcha.rs b/src/mcaptcha.rs index b7aca52..b996b82 100644 --- a/src/mcaptcha.rs +++ b/src/mcaptcha.rs @@ -19,7 +19,7 @@ //! //! ## Usage: //! ```rust -//! use m_captcha::{message::Visitor, MCaptchaBuilder, cache::HashCache, LevelBuilder, DefenseBuilder}; +//! use m_captcha::{mcaptcha::Visitor, MCaptchaBuilder, cache::HashCache, LevelBuilder, DefenseBuilder}; //! // traits from actix needs to be in scope for starting actor //! use actix::prelude::*; //! diff --git a/src/pow.rs b/src/pow.rs index ed55c19..a3e3b83 100644 --- a/src/pow.rs +++ b/src/pow.rs @@ -20,6 +20,8 @@ use pow_sha256::PoW; use serde::Serialize; +pub use pow_sha256::ConfigBuilder; + /// PoW requirement datatype that is be sent to clients for generating PoW #[derive(Clone, Serialize, Debug)] pub struct PoWConfig { diff --git a/src/system.rs b/src/system.rs index 2c0d8c4..b5b9121 100644 --- a/src/system.rs +++ b/src/system.rs @@ -24,7 +24,6 @@ use crate::cache::messages; use crate::cache::Save; use crate::errors::*; use crate::master::Master; -//use crate::models::*; use crate::pow::*; /// struct describing various bits of data required for an mCaptcha system @@ -33,7 +32,6 @@ pub struct System { pub master: Addr, cache: Addr, pow: Config, - // db: PgPool, } impl System @@ -65,62 +63,20 @@ where let string = work.string.clone(); let msg = Retrive(string.clone()); - let difficulty = self.cache.send(msg).await.unwrap(); let pow: PoW = work.into(); + + let difficulty = self.cache.send(msg).await.unwrap()?; match difficulty { - Ok(Some(difficulty)) => { + Some(difficulty) => { if self.pow.is_sufficient_difficulty(&pow, difficulty) { Ok(self.pow.is_valid_proof(&pow, &string)) } else { Err(CaptchaError::InsuffiencientDifficulty) } } - Ok(None) => Err(CaptchaError::StringNotFound), - Err(_) => Err(CaptchaError::Default), + None => Err(CaptchaError::StringNotFound), } } - - // pub async fn register(&self, u: &Users) { - // sqlx::query!("INSERT INTO mcaptcha_users (name) VALUES ($1)", u.name) - // .execute(&self.db) - // .await - // .unwrap(); - // } - // - // pub async fn levels(&self, l: &Levels) { - // sqlx::query!( - // "INSERT INTO mcaptcha_levels (id, difficulty_factor, visitor_threshold) VALUES ($1, $2, $3)", - // l.id, - // l.difficulty_factor, - // l.visitor_threshold - // ) - // .execute(&self.db) - // .await - // .unwrap(); - // } - // - // pub async fn add_mcaptcha(&self, m: &MCaptchaSystem) { - // sqlx::query!( - // "INSERT INTO mcaptcha_config (id, name, duration) VALUES ($1, $2, $3)", - // m.id, - // m.name, - // m.duration - // ) - // .execute(&self.db) - // .await - // .unwrap(); - // } - // - // async fn init_mcaptcha(&self, m: &MCaptchaSystem) { - // let id = sqlx::query_as!( - // Duration, - // "SELECT duration FROM mcaptcha_config WHERE id = ($1)", - // m.id, - // ) - // .fetch_one(&self.db) - // .await - // .unwrap(); - // } } #[cfg(test)] diff --git a/src/utils.rs b/src/utils.rs index 69dc751..c2c2c2e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,3 +1,21 @@ +/* + * mCaptcha - A proof of work based DoS protection system + * Copyright © 2021 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + // utility function to get a randomly generated string // of size len pub fn get_random(len: usize) -> String {