diff --git a/src/cache/hashcache.rs b/src/cache/hashcache.rs index a6ea4d7..9508ca6 100644 --- a/src/cache/hashcache.rs +++ b/src/cache/hashcache.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 . + */ + use std::collections::HashMap; use actix::prelude::*; @@ -25,12 +43,21 @@ impl HashCache { } } +/* TODO cache of pow configs need to have lifetimes to prevent replay attacks + * where lifetime = site's cool down period so that people can't generate pow + * configs when the site is cool and use them later with rainbow tables + * when it's under attack. + * + * This comment stays until this feature is implemented. + */ + impl Save for HashCache {} impl Actor for HashCache { type Context = Context; } +/// cache a PoWConfig impl Handler for HashCache { type Result = MessageResult; fn handle(&mut self, msg: Cache, _ctx: &mut Self::Context) -> Self::Result { @@ -38,6 +65,7 @@ impl Handler for HashCache { } } +/// Retrive PoW difficulty_factor for a PoW string impl Handler for HashCache { type Result = MessageResult; fn handle(&mut self, msg: Retrive, _ctx: &mut Self::Context) -> Self::Result { diff --git a/src/errors.rs b/src/errors.rs index 49a0602..fd87308 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -53,6 +53,21 @@ pub enum CaptchaError { /// Difficulty factor should increase with level #[display(fmt = "Actor mailbox error")] MailboxError, + + /// Happens when submitted work doesn't satisfy the required + /// difficulty factor + #[display(fmt = "Insuffiencient Difficulty")] + InsuffiencientDifficulty, + + /// Happens when submitted work is computed over string that + /// 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 e5f0b7e..dbaaafb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -106,6 +106,7 @@ pub mod message { /// message datatypes to interact with [MCaptcha] actor pub mod cache; pub mod pow; +mod utils; pub use crate::cache::hashcache::HashCache; diff --git a/src/pow.rs b/src/pow.rs index c00c3bc..8bf594a 100644 --- a/src/pow.rs +++ b/src/pow.rs @@ -17,10 +17,12 @@ */ use actix::dev::*; use derive_builder::Builder; +use pow_sha256::{Config, PoW}; use serde::Serialize; use crate::cache::messages; use crate::cache::Save; +use crate::errors::*; use crate::master::Master; /// PoW Config that will be sent to clients for generating PoW @@ -31,20 +33,10 @@ pub struct PoWConfig { } impl PoWConfig { pub fn new(m: u32) -> Self { - use std::iter; - - use rand::{distributions::Alphanumeric, rngs::ThreadRng, thread_rng, Rng}; - - let mut rng: ThreadRng = thread_rng(); - - let string = iter::repeat(()) - .map(|()| rng.sample(Alphanumeric)) - .map(char::from) - .take(32) - .collect::(); + use crate::utils::get_random; PoWConfig { - string, + string: get_random(32), difficulty_factor: m, } } @@ -54,12 +46,13 @@ impl PoWConfig { pub struct Actors { master: Addr>, cache: Addr, + pow: Config, } impl Actors where T: Save, - ::Context: ToEnvelope, + ::Context: ToEnvelope + ToEnvelope, { pub async fn get_pow(&self, id: String) -> Option { use crate::cache::messages::Cache; @@ -79,38 +72,130 @@ where .unwrap(); Some(pow_config) } + + pub async fn verify_pow(&self, work: Work) -> CaptchaResult { + use crate::cache::messages::Retrive; + use crate::utils::get_difficulty; + + let string = work.string.clone(); + let msg = Retrive(string.clone()); + let difficulty = self.cache.send(msg).await.unwrap(); + let pow: PoW = work.into(); + match difficulty { + Ok(Some(difficulty)) => { + if self + .pow + .is_sufficient_difficulty(&pow, get_difficulty(difficulty)) + { + Ok(self.pow.is_valid_proof(&pow, &string)) + } else { + Err(CaptchaError::InsuffiencientDifficulty) + } + } + Ok(None) => Err(CaptchaError::StringNotFound), + Err(_) => Err(CaptchaError::Default), + } + } +} + +#[derive(Clone, Serialize, Debug)] +pub struct Work { + pub string: String, + pub result: String, + pub nonce: u64, +} + +impl From for PoW { + fn from(w: Work) -> Self { + use pow_sha256::PoWBuilder; + PoWBuilder::default() + .result(w.result) + .nonce(w.nonce) + .build() + .unwrap() + } } #[cfg(test)] mod tests { + + use pow_sha256::ConfigBuilder; + use super::*; - use crate::cache::HashCache; use crate::master::*; use crate::mcaptcha::tests::*; + use crate::{cache::HashCache, utils::get_difficulty}; - #[actix_rt::test] - async fn get_pow_works() { + const MCAPTCHA_NAME: &str = "batsense.net"; + + async fn boostrap_system() -> Actors { let master = Master::new().start(); let mcaptcha = get_counter().start(); - let mcaptcha_name = "batsense.net"; + let pow = get_config(); let cache = HashCache::default().start(); let msg = AddSiteBuilder::default() - .id(mcaptcha_name.into()) + .id(MCAPTCHA_NAME.into()) .addr(mcaptcha.clone()) .build() .unwrap(); master.send(msg).await.unwrap(); - let actors = ActorsBuilder::default() + ActorsBuilder::default() .master(master) .cache(cache) + .pow(pow) .build() - .unwrap(); + .unwrap() + } - let pow = actors.get_pow(mcaptcha_name.into()).await.unwrap(); + fn get_config() -> Config { + ConfigBuilder::default() + .salt("myrandomsaltisnotlongenoug".into()) + .build() + .unwrap() + } + #[actix_rt::test] + async fn get_pow_works() { + let actors = boostrap_system().await; + let pow = actors.get_pow(MCAPTCHA_NAME.into()).await.unwrap(); assert_eq!(pow.difficulty_factor, LEVEL_1.0); } + + #[actix_rt::test] + async fn verify_pow_works() { + let actors = boostrap_system().await; + let work_req = actors.get_pow(MCAPTCHA_NAME.into()).await.unwrap(); + let config = get_config(); + + let difficulty = get_difficulty(work_req.difficulty_factor); + let work = config.prove_work(&work_req.string, difficulty).unwrap(); + + let difficulty = 1; + let insufficient_work = config.prove_work(&work_req.string, difficulty).unwrap(); + let insufficient_work_payload = Work { + string: work_req.string.clone(), + result: insufficient_work.result, + nonce: insufficient_work.nonce, + }; + + let mut payload = Work { + string: work_req.string, + result: work.result, + nonce: work.nonce, + }; + + let res = actors.verify_pow(payload.clone()).await.unwrap(); + assert!(res); + + payload.string = "wrongstring".into(); + let res = actors.verify_pow(payload.clone()).await; + assert_eq!(res, Err(CaptchaError::StringNotFound)); + + let res = actors.verify_pow(insufficient_work_payload.clone()).await; + + assert_eq!(res, Err(CaptchaError::InsuffiencientDifficulty)); + } } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..21fb2cd --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,17 @@ +pub fn get_random(len: usize) -> String { + use std::iter; + + use rand::{distributions::Alphanumeric, rngs::ThreadRng, thread_rng, Rng}; + + let mut rng: ThreadRng = thread_rng(); + + iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .map(char::from) + .take(len) + .collect::() +} + +pub fn get_difficulty(difficulty_factor: u32) -> u128 { + u128::max_value() - u128::max_value() / difficulty_factor as u128 +}