From 426e83ec6fba028692bed334803ae9d3a645cb18 Mon Sep 17 00:00:00 2001 From: Yigit Sever Date: Mon, 11 Apr 2022 19:01:42 +0300 Subject: [WIP] Spring cleaning --- src/bin/main.rs | 15 -- src/block.rs | 92 ++++++++++++ src/custom_filters.rs | 5 +- src/db.rs | 159 +++++++++++++++++++++ src/handlers.rs | 158 ++++++++++----------- src/lib.rs | 35 ----- src/main.rs | 52 +++++++ src/routes.rs | 8 +- src/schema.rs | 377 -------------------------------------------------- src/student.rs | 96 +++++++++++++ 10 files changed, 481 insertions(+), 516 deletions(-) delete mode 100644 src/bin/main.rs create mode 100644 src/block.rs create mode 100644 src/db.rs delete mode 100644 src/lib.rs create mode 100644 src/main.rs delete mode 100644 src/schema.rs create mode 100644 src/student.rs diff --git a/src/bin/main.rs b/src/bin/main.rs deleted file mode 100644 index f1fa908..0000000 --- a/src/bin/main.rs +++ /dev/null @@ -1,15 +0,0 @@ -use gradecoin::routes::consensus_routes; -use gradecoin::schema::create_database; - -#[tokio::main] -async fn main() { - log4rs::init_file("log.conf.yml", Default::default()).unwrap(); - - let db = create_database(); - - let api = consensus_routes(db); - - // Start the server - let point = ([127, 0, 0, 1], 8080); - warp::serve(api).run(point).await; -} diff --git a/src/block.rs b/src/block.rs new file mode 100644 index 0000000..e707779 --- /dev/null +++ b/src/block.rs @@ -0,0 +1,92 @@ +//! # Data Representations +//! +//! We need persistence for [`Block`]s and [`User`]s, not so much for [`Transaction`]s +//! +//! There are around 30 students, a full fledged database would be an overkill (for next year?) +//! +//! Pending transactions are held in memory, these are cleared with every new block +//! Only the last block is held in memory, every block is written to a file +//! Users are held in memory and they're also backed up to text files +use chrono::{NaiveDate, NaiveDateTime}; +use serde::{Deserialize, Serialize}; +use std::{string::String, vec::Vec}; + +pub type Fingerprint = String; +pub type Id = String; + +/// A block that was proposed with `transaction_list` and `nonce` +/// that made `hash` valid, 6 zeroes at the left hand side of the hash (24 bytes) +/// +/// We are mining using blake2s algorithm, which produces 256 bit hashes. +/// Hash/second is roughly 20x10^3. +/// +/// might be valuable to normalize the +/// serialize/deserialize conventions as these will be hashed +/// +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct Block { + #[serde(skip_serializing_if = "Vec::is_empty")] + pub transaction_list: Vec, + pub nonce: u32, + pub timestamp: NaiveDateTime, + pub hash: String, +} + +impl Default for Block { + fn default() -> Self { + Block { + transaction_list: vec!["gradecoin_bank".to_owned()], + nonce: 0, + timestamp: NaiveDate::from_ymd(2022, 4, 11).and_hms(20, 45, 00), + hash: String::from("not_actually_mined"), + } + } +} + +/// For prototyping and letting serde handle everything json +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct NakedBlock { + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub transaction_list: Vec, + pub nonce: u32, + pub timestamp: NaiveDateTime, +} + +/// A transaction between `source` and `target` that moves `amount` +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct Transaction { + pub source: Fingerprint, + pub target: Fingerprint, + pub amount: u16, + pub timestamp: NaiveDateTime, +} + +/// A JWT Payload/Claims representation +/// +/// +/// +/// - `tha`: Transaction Hash, String (custom field) +/// - `iat`: Issued At, Unix Time, epoch +/// - `exp`: Expiration Time, epoch +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct Claims { + pub tha: String, + pub iat: usize, + pub exp: usize, +} + +/// The plaintext of the initial user authentication request +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct AuthRequest { + pub student_id: String, + pub passwd: String, + pub public_key: String, +} + +/// Ciphertext of the initial authentication request, or what we will receive +#[derive(Serialize, Deserialize, Debug)] +pub struct InitialAuthRequest { + pub c: String, + pub iv: String, + pub key: String, +} diff --git a/src/custom_filters.rs b/src/custom_filters.rs index 7e0cae5..5bf284b 100644 --- a/src/custom_filters.rs +++ b/src/custom_filters.rs @@ -1,5 +1,6 @@ -/// Functions that extracts Structs to be used in warp routines -use crate::schema::{Block, Db, InitialAuthRequest, Transaction}; +//! # Functions that extracts Structs to be used in warp routines +use crate::block::{Block, InitialAuthRequest, Transaction}; +use crate::Db; use std::convert::Infallible; use warp::{Filter, Rejection}; diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..bf094ab --- /dev/null +++ b/src/db.rs @@ -0,0 +1,159 @@ +//! # Global Database representation +//! +//! [`Db::blockchain`] is just the last block that was mined. +//! All the blocks are written to disk as text files whenever they are accepted. +//! +//! [`Db::pending_transactions`] is the in memory representation of the waiting transactions. +//! Every user can have only one outstanding transaction at any given time. +//! +//! [`Db::users`] is the in memory representation of the users, +//! with their public keys, `metu_ids` and gradecoin balances. +use crate::block::{Block, Fingerprint, Id, Transaction}; +use crate::student::{MetuId, User, UserAtRest}; +use log::debug; +use parking_lot::RwLock; +use std::{collections::HashMap, fs, io, path::PathBuf, sync::Arc}; + +#[derive(Debug, Clone, Default)] +pub struct Db { + pub blockchain: Arc>, + pub pending_transactions: Arc>>, + pub users: Arc>>, + // TODO: metu_ids or approved_users or something, metu_id struct <11-04-22, yigit> // +} + +impl Db { + pub fn new() -> Self { + fs::create_dir_all("blocks").unwrap(); + fs::create_dir_all("users").unwrap(); + let mut db = Db::default(); + if let Some(block_path) = last_block_content() { + db.populate_with_last_block(block_path); + } + + if let Ok(users_path) = read_users() { + db.populate_with_users(users_path); + } + + let users: HashMap = get_friendly_users(); + + Db { + blockchain: Arc::new(RwLock::new(Block::default())), + pending_transactions: Arc::new(RwLock::new(HashMap::new())), + users: Arc::new(RwLock::new(users)), + } + } + + fn populate_with_last_block(&mut self, path: String) { + debug!("Populating db with the latest block {}", path); + let file = fs::read(path).unwrap(); + let json = std::str::from_utf8(&file).unwrap(); + let block: Block = serde_json::from_str(json).unwrap(); + *self.blockchain.write() = block; + } + + fn populate_with_users(&mut self, files: Vec) { + for fs in files { + if let Ok(file_content) = fs::read(fs) { + let json = + String::from_utf8(file_content).expect("we have written a malformed user file"); + let user_at_rest: UserAtRest = serde_json::from_str(&json).unwrap(); + + debug!("Populating db with user: {:?}", user_at_rest); + self.users + .write() + .insert(user_at_rest.fingerprint, user_at_rest.user); + } + } + } +} + +fn last_block_content() -> Option { + let blocks = read_block_name().unwrap(); + + if blocks.is_empty() { + return None; + } + + let last_block = blocks[0].to_str().unwrap(); + let mut last_block = parse_block(last_block); + let mut last_block_index = 0; + + for (index, block) in blocks.iter().enumerate() { + let block = block.to_str().unwrap(); + let block = parse_block(block); + if block > last_block { + last_block = block; + last_block_index = index; + } + } + return Some(blocks[last_block_index].to_str().unwrap().parse().unwrap()); +} + +fn read_block_name() -> io::Result> { + let entries = fs::read_dir("./blocks")? + .map(|res| res.map(|e| e.path())) + .collect::, io::Error>>()?; + + Ok(entries) +} + +fn parse_block(path: &str) -> u64 { + let end_pos = path.find(".block").unwrap(); + let block_str = path[9..end_pos].to_string(); + let block_u64: u64 = block_str.parse().unwrap(); + block_u64 +} + +fn read_users() -> io::Result> { + let entries = fs::read_dir("./users")? + .map(|res| res.map(|e| e.path())) + .collect::, io::Error>>()?; + + Ok(entries) +} + +fn get_friendly_users() -> HashMap { + let mut users: HashMap = HashMap::new(); + + users.insert( + "cde48537ca2c28084ff560826d0e6388b7c57a51497a6cb56f397289e52ff41b".to_owned(), + User { + user_id: MetuId::new("friend_1".to_owned(), "not_used".to_owned()).unwrap(), + public_key: "not_used".to_owned(), + balance: 70, + is_bot: true, + }, + ); + + users.insert( + "a1a38b5bae5866d7d998a9834229ec2f9db7a4fc8fb6f58b1115a96a446875ff".to_owned(), + User { + user_id: MetuId::new("friend_2".to_owned(), "not_used".to_owned()).unwrap(), + public_key: "not_used".to_owned(), + balance: 20, + is_bot: true, + }, + ); + + users.insert( + "4e048fd2a62f1307866086e803e9be43f78a702d5df10831fbf434e7663ae0e7".to_owned(), + User { + user_id: MetuId::new("friend_4".to_owned(), "not_used".to_owned()).unwrap(), + public_key: "not_used".to_owned(), + balance: 120, + is_bot: true, + }, + ); + + users.insert( + "60e77101e76950a9b1830fa107fd2f8fc545255b3e0f14b6a7797cf9ee005f07".to_owned(), + User { + user_id: MetuId::new("friend_4".to_owned(), "not_used".to_owned()).unwrap(), + public_key: "not_used".to_owned(), + balance: 40, + is_bot: true, + }, + ); + users +} diff --git a/src/handlers.rs b/src/handlers.rs index 2e9964c..8d8b62f 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -1,24 +1,27 @@ /// API handlers, the ends of each filter chain +use crate::block::{AuthRequest, Block, Claims, InitialAuthRequest, NakedBlock, Transaction}; +use crate::student::{MetuId, User, UserAtRest}; +use crate::Db; use aes::Aes128; use askama::Template; use blake2::{Blake2s, Digest}; -use block_modes::block_padding::Pkcs7; -use block_modes::{BlockMode, Cbc}; +use block_modes::{block_padding::Pkcs7, BlockMode, Cbc}; use chrono::Utc; use jsonwebtoken::errors::ErrorKind; use jsonwebtoken::{decode, Algorithm, DecodingKey, TokenData, Validation}; use lazy_static::lazy_static; use log::{debug, warn}; -use math; use md5::Md5; use parking_lot::RwLockUpgradableReadGuard; use rsa::{PaddingScheme, RSAPrivateKey}; use serde::Serialize; use sha2::Sha256; -use std::collections::{HashMap, HashSet}; -use std::convert::Infallible; -use std::fs; -use std::hash::Hash; +use std::{ + collections::{HashMap, HashSet}, + convert::Infallible, + fs, + hash::Hash, +}; use warp::{http::StatusCode, reply}; use crate::PRIVATE_KEY; @@ -52,11 +55,6 @@ enum ResponseType { Error, } -use crate::schema::{ - AuthRequest, Block, Claims, Db, InitialAuthRequest, MetuId, NakedBlock, Transaction, User, - UserAtRest, -}; - const BEARER: &str = "Bearer "; lazy_static! { @@ -64,7 +62,7 @@ lazy_static! { .lines() .filter(|line| !line.starts_with('-')) .fold(String::new(), |mut data, line| { - data.push_str(&line); + data.push_str(line); data }); @@ -87,9 +85,9 @@ lazy_static! { /// - Student picks a short temporary key (`k_temp`) /// - Creates a JSON object (`auth_plaintext`) with their `metu_id` and `public key` in base64 (PEM) format (`S_PK`): /// { -/// student_id: "e12345", -/// passwd: "15 char secret" -/// public_key: "---BEGIN PUBLIC KEY..." +/// `student_id`: "e12345", +/// `passwd`: "15 char secret" +/// `public_key`: "---BEGIN PUBLIC KEY..." /// } /// /// - Encrypts the serialized string of `auth_plaintext` with 128 bit block AES in CBC mode with Pkcs7 padding using the temporary key (`k_temp`), the result is `auth_ciphertext` @@ -97,18 +95,17 @@ lazy_static! { /// using sha256 with `gradecoin_public_key`, giving us `key_ciphertext` /// - The payload JSON object (`auth_request`) can be JSON serialized now: /// { -/// c: "auth_ciphertext" -/// key: "key_ciphertext" +/// c: "`auth_ciphertext`" +/// key: "`key_ciphertext`" /// } /// /// ## Gradecoin Side /// -/// - Upon receiving, we first RSA decrypt with OAEP padding scheme using SHA256 with `gradecoin_private_key` as the key and auth_request.key `key` as the ciphertext, receiving `temp_key` (this is the temporary key chosen by student) -/// - With `temp_key`, we can AES 128 Cbc Pkcs7 decrypt the `auth_request.c`, giving us -/// auth_plaintext +/// - Upon receiving, we first RSA decrypt with OAEP padding scheme using SHA256 with `gradecoin_private_key` as the key and `auth_request.key` `key` as the ciphertext, receiving `temp_key` (this is the temporary key chosen by student) +/// - With `temp_key`, we can AES 128 Cbc Pkcs7 decrypt the `auth_request.c`, giving us `auth_plaintext` /// - The `auth_plaintext` String can be deserialized to [`AuthRequest`] /// - We then verify the payload and calculate the User fingerprint -/// - Finally, create the new [`User`] object, insert to users HashMap `` +/// - Finally, create the new [`User`] object, insert to users `HashMap` `` /// pub async fn authenticate_user( request: InitialAuthRequest, @@ -226,7 +223,7 @@ pub async fn authenticate_user( // c field was properly base64 encoded, now available in auth_packet // decryptor was setup properly, with the correct lenght key - let mut buf = auth_packet.to_vec(); + let mut buf = auth_packet; let auth_plaintext = match cipher.decrypt(&mut buf) { Ok(p) => p, Err(err) => { @@ -284,22 +281,21 @@ pub async fn authenticate_user( // is the student in AuthRequest privileged? let privileged_student_id = - match MetuId::new(request.student_id.clone(), request.passwd.clone()) { - Some(id) => id, - None => { - debug!( - "Someone tried to auth with invalid credentials: {} {}", - &request.student_id, &request.passwd - ); - let res_json = warp::reply::json(&GradeCoinResponse { + if let Some(id) = MetuId::new(request.student_id.clone(), request.passwd.clone()) { + id + } else { + debug!( + "Someone tried to auth with invalid credentials: {} {}", + &request.student_id, &request.passwd + ); + let res_json = warp::reply::json(&GradeCoinResponse { res: ResponseType::Error, message: "The credentials given ('student_id', 'passwd') cannot hold a Gradecoin account" .to_owned(), }); - return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); - } + return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); }; // Students should be able to authenticate once @@ -329,7 +325,7 @@ pub async fn authenticate_user( return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); } - let fingerprint = format!("{:x}", Sha256::digest(&request.public_key.as_bytes())); + let fingerprint = format!("{:x}", Sha256::digest(request.public_key.as_bytes())); let new_user = User { user_id: privileged_student_id, @@ -387,7 +383,7 @@ pub async fn list_transactions(db: Db) -> Result { /// Proposes a new block for the next round. /// Can reject the block /// -/// The proposer has to put their transaction as the first transaction of the [`transaction_list`]. +/// The proposer has to put their transaction as the first transaction of the transaction_list. /// This is the analogue of `coinbase` in Bitcoin works /// /// The `coinbase` transaction also gets something for their efforts. @@ -420,9 +416,10 @@ pub async fn propose_block( let pending_transactions = db.pending_transactions.upgradable_read(); // we get the proposers fingerprint by finding the transaction (id) then extracting the source - let internal_user_fingerprint = match pending_transactions.get(&new_block.transaction_list[0]) { - Some(coinbase) => &coinbase.source, - None => { + let internal_user_fingerprint = + if let Some(coinbase) = pending_transactions.get(&new_block.transaction_list[0]) { + &coinbase.source + } else { debug!( "Transaction with id {} is not found in the pending_transactions", new_block.transaction_list[0] @@ -434,34 +431,31 @@ pub async fn propose_block( }); return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); - } - }; + }; let users_store = db.users.upgradable_read(); // this probably cannot fail, if the transaction is valid then it must've been checked already - let internal_user = match users_store.get(internal_user_fingerprint) { - Some(existing_user) => existing_user, - None => { - debug!( - "User with public key signature {:?} is not found in the database", - new_block.transaction_list[0] - ); + let internal_user = if let Some(existing_user) = users_store.get(internal_user_fingerprint) { + existing_user + } else { + debug!( + "User with public key signature {:?} is not found in the database", + new_block.transaction_list[0] + ); - let res_json = warp::reply::json(&GradeCoinResponse { - res: ResponseType::Error, - message: "User with that public key signature is not found in the database" - .to_owned(), - }); + let res_json = warp::reply::json(&GradeCoinResponse { + res: ResponseType::Error, + message: "User with that public key signature is not found in the database".to_owned(), + }); - return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); - } + return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); }; let proposer_public_key = &internal_user.public_key; // JWT Check - let token_payload = match authorize_proposer(token, &proposer_public_key) { + let token_payload = match authorize_proposer(&token, proposer_public_key) { Ok(data) => data, Err(below) => { debug!("Something went wrong with the JWT {:?}", below); @@ -501,7 +495,7 @@ pub async fn propose_block( } // Are transactions in the block valid? - for transaction_hash in new_block.transaction_list.iter() { + for transaction_hash in &new_block.transaction_list { if !pending_transactions.contains_key(transaction_hash) { let res_json = warp::reply::json(&GradeCoinResponse { res: ResponseType::Error, @@ -536,7 +530,7 @@ pub async fn propose_block( } // Are the 6 leftmost characters (=24 bits) zero? - let should_zero = hashvalue[0] as i32 + hashvalue[1] as i32 + hashvalue[2] as i32; + let should_zero = i32::from(hashvalue[0]) + i32::from(hashvalue[1]) + i32::from(hashvalue[2]); if should_zero != 0 { debug!("the hash does not have 6 rightmost zero bits"); @@ -566,7 +560,7 @@ pub async fn propose_block( let holding: HashMap = HashMap::new(); // Play out the transactions - for fingerprint in new_block.transaction_list.iter() { + for fingerprint in &new_block.transaction_list { if let Some(transaction) = pending_transactions.remove(fingerprint) { let source = &transaction.source; let target = &transaction.target; @@ -581,7 +575,7 @@ pub async fn propose_block( if is_source_bot { // Add staking reward to.balance += - math::round::ceil((transaction.amount as f64) * STAKING_REWARD, 0) + math::round::ceil((f64::from(transaction.amount)) * STAKING_REWARD, 0) as u16; } } @@ -592,8 +586,8 @@ pub async fn propose_block( pending_transactions.insert( transaction_id, Transaction { - source: target.to_owned(), - target: source.to_owned(), + source: target.clone(), + target: source.clone(), amount: transaction.amount, timestamp: Utc::now().naive_local(), }, @@ -602,8 +596,8 @@ pub async fn propose_block( } } - for (fp, tx) in holding.iter() { - pending_transactions.insert(fp.to_owned(), tx.to_owned()); + for (fp, tx) in &holding { + pending_transactions.insert(fp.clone(), tx.clone()); } // just update everyone's .guy file @@ -665,23 +659,21 @@ pub async fn propose_transaction( let users_store = db.users.read(); // Is this transaction from an authorized source? - let internal_user = match users_store.get(&new_transaction.source) { - Some(existing_user) => existing_user, - None => { - debug!( - "User with public key signature {:?} is not found in the database", - new_transaction.source - ); + let internal_user = if let Some(existing_user) = users_store.get(&new_transaction.source) { + existing_user + } else { + debug!( + "User with public key signature {:?} is not found in the database", + new_transaction.source + ); - return Ok(warp::reply::with_status( - warp::reply::json(&GradeCoinResponse { - res: ResponseType::Error, - message: "User with the given public key signature is not authorized" - .to_owned(), - }), - StatusCode::BAD_REQUEST, - )); - } + return Ok(warp::reply::with_status( + warp::reply::json(&GradeCoinResponse { + res: ResponseType::Error, + message: "User with the given public key signature is not authorized".to_owned(), + }), + StatusCode::BAD_REQUEST, + )); }; if internal_user.is_bot { @@ -702,7 +694,7 @@ pub async fn propose_transaction( // *this* point let proposer_public_key = &internal_user.public_key; - let token_payload = match authorize_proposer(token, &proposer_public_key) { + let token_payload = match authorize_proposer(&token, proposer_public_key) { Ok(data) => data, Err(below) => { debug!("JWT Error: {:?}", below); @@ -815,7 +807,7 @@ pub async fn propose_transaction( debug!("Taking the hash of {}", serd_tx); - let hashed_transaction = Md5::digest(&serd_tx.as_bytes()); + let hashed_transaction = Md5::digest(serd_tx.as_bytes()); if token_payload.claims.tha != format!("{:x}", hashed_transaction) { return Ok(warp::reply::with_status( warp::reply::json(&GradeCoinResponse { @@ -859,7 +851,7 @@ pub async fn list_blocks(db: Db) -> Result { /// *[`jwt_token`]: The raw JWT token, "Bearer aaa.bbb.ccc" /// *[`user_pem`]: User Public Key, "BEGIN RSA" /// NOT async, might look into it if this becomes a bottleneck -fn authorize_proposer(jwt_token: String, user_pem: &str) -> Result, String> { +fn authorize_proposer(jwt_token: &str, user_pem: &str) -> Result, String> { // Throw away the "Bearer " part let raw_jwt = jwt_token.trim_start_matches(BEARER).to_owned(); @@ -929,7 +921,7 @@ pub async fn user_list_handler(db: Db) -> Result impl Filter + Clone { +pub fn application(db: Db) -> impl Filter + Clone { // Remember when we wanted to implement templating // Why would we? Just put a staic webpage under /public (next to Cargo.toml) and place it and // the end of the filter chain diff --git a/src/schema.rs b/src/schema.rs deleted file mode 100644 index bbd4628..0000000 --- a/src/schema.rs +++ /dev/null @@ -1,377 +0,0 @@ -//! # Data Representations -//! -//! We need persistence for [`Block`]s and [`User`]s, not so much for [`Transaction`]s -//! -//! There are around 30 students, a full fledged database would be an overkill (for next year?) -//! -//! Pending transactions are held in memory, these are cleared with every new block -//! Only the last block is held in memory, every block is written to a file -//! Users are held in memory and they're also backed up to text files -use chrono::{NaiveDate, NaiveDateTime}; -use lazy_static::lazy_static; -use log::debug; -use parking_lot::RwLock; -use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; -use std::fmt; -use std::fs; -use std::io; -use std::path::PathBuf; -use std::string::String; -use std::sync::Arc; -use std::vec::Vec; - -pub type Fingerprint = String; -pub type Id = String; - -fn block_parser(path: String) -> u64 { - let end_pos = path.find(".block").unwrap(); - let block_str = path[9..end_pos].to_string(); - let block_u64: u64 = block_str.parse().unwrap(); - block_u64 -} - -fn last_block_content() -> Option { - let blocks = read_block_name().unwrap(); - - if blocks.is_empty() { - return None; - } - - let last_block = blocks[0].to_str().unwrap(); - let mut last_block = block_parser(last_block.to_string()); - let mut last_block_index = 0; - - for (index, block) in blocks.iter().enumerate() { - let block = block.to_str().unwrap(); - let block = block_parser(block.to_string()); - if block > last_block { - last_block = block; - last_block_index = index; - } - } - return Some(blocks[last_block_index].to_str().unwrap().parse().unwrap()); -} - -fn read_block_name() -> io::Result> { - let entries = fs::read_dir("./blocks")? - .map(|res| res.map(|e| e.path())) - .collect::, io::Error>>()?; - - Ok(entries) -} - -fn read_users() -> io::Result> { - let entries = fs::read_dir("./users")? - .map(|res| res.map(|e| e.path())) - .collect::, io::Error>>()?; - - Ok(entries) -} - -fn populate_db_with_last_block(db: &mut Db, path: String) -> &mut Db { - debug!("Populating db with last block {}", path); - let file = fs::read(path).unwrap(); - let json = std::str::from_utf8(&file).unwrap(); - let block: Block = serde_json::from_str(json).unwrap(); - *db.blockchain.write() = block; - - db -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct UserAtRest { - pub fingerprint: Fingerprint, - pub user: User, -} - -fn populate_db_with_users(db: &mut Db, files: Vec) -> &mut Db { - for fs in files { - if let Ok(file_content) = fs::read(fs) { - let json = - String::from_utf8(file_content).expect("we have written a malformed user file"); - let user_at_rest: UserAtRest = serde_json::from_str(&json).unwrap(); - - debug!("Populating db with user: {:?}", user_at_rest); - db.users - .write() - .insert(user_at_rest.fingerprint, user_at_rest.user); - } - } - - db -} - -/// Creates a new database, uses the previous last block if one exists and attempts the populate -/// the users -pub fn create_database() -> Db { - fs::create_dir_all("blocks").unwrap(); - fs::create_dir_all("users").unwrap(); - let mut db = Db::new(); - if let Some(block_path) = last_block_content() { - populate_db_with_last_block(&mut db, block_path); - } - - if let Ok(users_path) = read_users() { - populate_db_with_users(&mut db, users_path); - } - - db -} - -/// A JWT Payload/Claims representation -/// -/// https://tools.ietf.org/html/rfc7519#section-4.1 -/// -/// - `tha`: Transaction Hash, String (custom field) -/// - `iat`: Issued At, Unix Time, epoch -/// - `exp`: Expiration Time, epoch -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct Claims { - pub tha: String, - pub iat: usize, - pub exp: usize, -} - -/// Global Database representation -/// -/// [`Db::blockchain`] is just the last block that was mined. All the blocks are written to disk as text -/// files whenever they are accepted. -/// -/// [`Db::pending_transactions`] is the in memory representation of the waiting transactions. Every -/// user can have only one outstanding transaction at any given time. -/// -/// [`Db::users`] is the in memory representation of the users, with their public keys, metu_ids and -/// gradecoin balances. -#[derive(Debug, Clone)] -pub struct Db { - pub blockchain: Arc>, - pub pending_transactions: Arc>>, - pub users: Arc>>, -} - -impl Db { - pub fn new() -> Self { - let mut users: HashMap = HashMap::new(); - - let friendly_1 = MetuId::new("friend_1".to_owned(), "not_used".to_owned()).unwrap(); - - users.insert( - "cde48537ca2c28084ff560826d0e6388b7c57a51497a6cb56f397289e52ff41b".to_owned(), - User { - user_id: friendly_1, - public_key: "not_used".to_owned(), - balance: 70, - is_bot: true, - }, - ); - - let friendly_2 = MetuId::new("friend_2".to_owned(), "not_used".to_owned()).unwrap(); - - users.insert( - "a1a38b5bae5866d7d998a9834229ec2f9db7a4fc8fb6f58b1115a96a446875ff".to_owned(), - User { - user_id: friendly_2, - public_key: "not_used".to_owned(), - balance: 20, - is_bot: true, - }, - ); - - let friendly_3 = MetuId::new("friend_4".to_owned(), "not_used".to_owned()).unwrap(); - - users.insert( - "4e048fd2a62f1307866086e803e9be43f78a702d5df10831fbf434e7663ae0e7".to_owned(), - User { - user_id: friendly_3, - public_key: "not_used".to_owned(), - balance: 120, - is_bot: true, - }, - ); - - let friendly_4 = MetuId::new("friend_4".to_owned(), "not_used".to_owned()).unwrap(); - - users.insert( - "60e77101e76950a9b1830fa107fd2f8fc545255b3e0f14b6a7797cf9ee005f07".to_owned(), - User { - user_id: friendly_4, - public_key: "not_used".to_owned(), - balance: 40, - is_bot: true, - }, - ); - - Db { - blockchain: Arc::new(RwLock::new(Block::new())), - pending_transactions: Arc::new(RwLock::new(HashMap::new())), - users: Arc::new(RwLock::new(users)), - } - } -} - -impl Default for Db { - fn default() -> Self { - Self::new() - } -} - -/// A transaction between `source` and `target` that moves `amount` -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct Transaction { - pub source: Fingerprint, - pub target: Fingerprint, - pub amount: u16, - pub timestamp: NaiveDateTime, -} - -/// A block that was proposed with `transaction_list` and `nonce` that made `hash` valid, 6 zeroes -/// at the left hand side of the hash (24 bytes) -/// -/// We are mining using blake2s algorithm, which produces 256 bit hashes. Hash/second is roughly -/// 20x10^3. -/// -/// https://serde.rs/container-attrs.html might be valuable to normalize the serialize/deserialize -/// conventions as these will be hashed -/// -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct Block { - #[serde(skip_serializing_if = "Vec::is_empty")] - pub transaction_list: Vec, - pub nonce: u32, - pub timestamp: NaiveDateTime, - pub hash: String, -} - -/// For prototyping and letting serde handle everything json -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct NakedBlock { - #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub transaction_list: Vec, - pub nonce: u32, - pub timestamp: NaiveDateTime, -} - -impl Block { - /// Genesis block - pub fn new() -> Block { - Block { - transaction_list: vec!["gradecoin_bank".to_owned()], - nonce: 0, - timestamp: NaiveDate::from_ymd(2021, 4, 11).and_hms(20, 45, 00), - hash: String::from("not_actually_mined"), - } - } -} - -impl Default for Block { - fn default() -> Self { - Self::new() - } -} - -/// A Student -/// -/// * [`user_id`]: Can only be one of the repopulated -/// * [`public_key`]: A PEM format public key "---- BEGIN" and all -/// * [`balance`]: User's current Gradecoin amount -/// -/// This should ideally include the fingerprint as well? -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct User { - pub user_id: MetuId, - pub public_key: String, - pub balance: u16, - #[serde(skip, default = "bool::default")] - pub is_bot: bool, -} - -/// The values are hard coded in [`OUR_STUDENTS`] so MetuId::new() can accept/reject values based on that -#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] -pub struct MetuId { - id: String, - passwd: String, -} - -impl MetuId { - pub fn quick_equal(&self, other: &str) -> bool { - self.id == other - } -} - -/// The plaintext of the initial user authentication request -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct AuthRequest { - pub student_id: String, - pub passwd: String, - pub public_key: String, -} - -/// Ciphertext of the initial authentication request, or what we will receive -#[derive(Serialize, Deserialize, Debug)] -pub struct InitialAuthRequest { - pub c: String, - pub iv: String, - pub key: String, -} - -// Students who are authorized to have Gradecoin accounts -lazy_static! { - static ref OUR_STUDENTS: HashSet<(&'static str, &'static str)> = { - [ - ("e254275", "DtNX1qk4YF4saRH"), - ("e223687", "cvFEs4XLjuGBD1v"), - ("e211024", "voQAcxiKJmEXYRT"), - ("e209888", "O75dli6AQtz2tUi"), - ("e223725", "xXuTD3Y4tyrv2Jz"), - ("e209362", "N7wGm5XU5zVWOWu"), - ("e209898", "aKBFfB8fZMq8pVn"), - ("e230995", "TgcHGlqeFhQGx42"), - ("e223743", "YVWVSWuIHplJk9C"), - ("e223747", "8LAeHrsjnwXh59Q"), - ("e223749", "HMFeJqVOzwCPHbc"), - ("e223751", "NjMsxmtmy2VOwMW"), - ("e188126", "QibuPdV2gXfsVJW"), - ("e209913", "kMxJvl2vHSWCy4A"), - ("e203608", "mfkkR0MWurk6Rp1"), - ("e233013", "GCqHxdOaDj2pWXx"), - ("e216982", "2Z0xmgCStnj5qg5"), - ("e217185", "BcaZNlzlhPph7A3"), - ("e223780", "2KvVxKUQaA9H4sn"), - ("e194931", "hsC0Wb8PQ5vzwdQ"), - ("e223783", "ETUJA3kt1QYvJai"), - ("e254550", "rPRjX0A4NefvKWi"), - ("e217203", "lN3IWhGyCrGfkk5"), - ("e217477", "O9xlMaa7LanC82w"), - ("e223786", "UxI6czykJfp9T9N"), - ("e231060", "VJgziofQQPCoisH"), - ("e223795", "pmcTCKox99NFsqp"), - ("e223715", "1H5QuOYI1b2r9ET"), - ("e181932", "THANKYOUHAVEFUN"), - ("bank", "P7oxDm30g1jeIId"), - ("friend_1", "not_used"), - ("friend_2", "not_used"), - ("friend_3", "not_used"), - ("friend_4", "not_used"), - ] - .iter() - .cloned() - .collect() - }; -} - -impl fmt::Display for MetuId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.id) - } -} - -impl MetuId { - pub fn new(id: String, pwd: String) -> Option { - if OUR_STUDENTS.contains(&(&*id, &*pwd)) { - Some(MetuId { id, passwd: pwd }) - } else { - None - } - } -} diff --git a/src/student.rs b/src/student.rs new file mode 100644 index 0000000..4b7acf1 --- /dev/null +++ b/src/student.rs @@ -0,0 +1,96 @@ +use crate::Fingerprint; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use std::{collections::HashSet, fmt}; + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct UserAtRest { + pub fingerprint: Fingerprint, + pub user: User, +} + +/// A Student +/// +/// - [`user_id`]: Can only be one of the preapproved students (who are enlisted in the course) +/// - [`public_key`]: A PEM format public key "---- BEGIN" and all +/// - [`balance`]: User's current Gradecoin amount +/// +/// This should ideally include the fingerprint as well? +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct User { + pub user_id: MetuId, + pub public_key: String, + pub balance: u16, + #[serde(skip, default = "bool::default")] + pub is_bot: bool, +} + +/// The values are hard coded in [`static@OUR_STUDENTS`] so `MetuId::new`() can accept/reject values based on that +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct MetuId { + id: String, + passwd: String, +} + +impl fmt::Display for MetuId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.id) + } +} + +impl MetuId { + pub fn new(id: String, pwd: String) -> Option { + if OUR_STUDENTS.contains(&(&*id, &*pwd)) { + Some(MetuId { id, passwd: pwd }) + } else { + None + } + } +} + +// TODO: remove this, read from a yaml or something, then MetuId::new gets a self <11-04-22, yigit> // + +// Students who are authorized to have Gradecoin accounts +lazy_static! { + static ref OUR_STUDENTS: HashSet<(&'static str, &'static str)> = { + [ + ("e254275", "DtNX1qk4YF4saRH"), + ("e223687", "cvFEs4XLjuGBD1v"), + ("e211024", "voQAcxiKJmEXYRT"), + ("e209888", "O75dli6AQtz2tUi"), + ("e223725", "xXuTD3Y4tyrv2Jz"), + ("e209362", "N7wGm5XU5zVWOWu"), + ("e209898", "aKBFfB8fZMq8pVn"), + ("e230995", "TgcHGlqeFhQGx42"), + ("e223743", "YVWVSWuIHplJk9C"), + ("e223747", "8LAeHrsjnwXh59Q"), + ("e223749", "HMFeJqVOzwCPHbc"), + ("e223751", "NjMsxmtmy2VOwMW"), + ("e188126", "QibuPdV2gXfsVJW"), + ("e209913", "kMxJvl2vHSWCy4A"), + ("e203608", "mfkkR0MWurk6Rp1"), + ("e233013", "GCqHxdOaDj2pWXx"), + ("e216982", "2Z0xmgCStnj5qg5"), + ("e217185", "BcaZNlzlhPph7A3"), + ("e223780", "2KvVxKUQaA9H4sn"), + ("e194931", "hsC0Wb8PQ5vzwdQ"), + ("e223783", "ETUJA3kt1QYvJai"), + ("e254550", "rPRjX0A4NefvKWi"), + ("e217203", "lN3IWhGyCrGfkk5"), + ("e217477", "O9xlMaa7LanC82w"), + ("e223786", "UxI6czykJfp9T9N"), + ("e231060", "VJgziofQQPCoisH"), + ("e223795", "pmcTCKox99NFsqp"), + ("e223715", "1H5QuOYI1b2r9ET"), + ("e181932", "THANKYOUHAVEFUN"), + ("bank", "P7oxDm30g1jeIId"), + ("friend_1", "not_used"), + ("friend_2", "not_used"), + ("friend_3", "not_used"), + ("friend_4", "not_used"), + ] + .iter() + .copied() + .collect() + }; +} -- cgit v1.2.3-70-g09d2