aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/bin/main.rs15
-rw-r--r--src/block.rs92
-rw-r--r--src/custom_filters.rs5
-rw-r--r--src/db.rs159
-rw-r--r--src/handlers.rs158
-rw-r--r--src/lib.rs35
-rw-r--r--src/main.rs52
-rw-r--r--src/routes.rs8
-rw-r--r--src/schema.rs377
-rw-r--r--src/student.rs96
10 files changed, 481 insertions, 516 deletions
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 @@
1use gradecoin::routes::consensus_routes;
2use gradecoin::schema::create_database;
3
4#[tokio::main]
5async fn main() {
6 log4rs::init_file("log.conf.yml", Default::default()).unwrap();
7
8 let db = create_database();
9
10 let api = consensus_routes(db);
11
12 // Start the server
13 let point = ([127, 0, 0, 1], 8080);
14 warp::serve(api).run(point).await;
15}
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 @@
1//! # Data Representations
2//!
3//! We need persistence for [`Block`]s and [`User`]s, not so much for [`Transaction`]s
4//!
5//! There are around 30 students, a full fledged database would be an overkill (for next year?)
6//!
7//! Pending transactions are held in memory, these are cleared with every new block
8//! Only the last block is held in memory, every block is written to a file
9//! Users are held in memory and they're also backed up to text files
10use chrono::{NaiveDate, NaiveDateTime};
11use serde::{Deserialize, Serialize};
12use std::{string::String, vec::Vec};
13
14pub type Fingerprint = String;
15pub type Id = String;
16
17/// A block that was proposed with `transaction_list` and `nonce`
18/// that made `hash` valid, 6 zeroes at the left hand side of the hash (24 bytes)
19///
20/// We are mining using blake2s algorithm, which produces 256 bit hashes.
21/// Hash/second is roughly 20x10^3.
22///
23/// <https://serde.rs/container-attrs.html> might be valuable to normalize the
24/// serialize/deserialize conventions as these will be hashed
25///
26#[derive(Serialize, Deserialize, Debug, PartialEq)]
27pub struct Block {
28 #[serde(skip_serializing_if = "Vec::is_empty")]
29 pub transaction_list: Vec<Fingerprint>,
30 pub nonce: u32,
31 pub timestamp: NaiveDateTime,
32 pub hash: String,
33}
34
35impl Default for Block {
36 fn default() -> Self {
37 Block {
38 transaction_list: vec!["gradecoin_bank".to_owned()],
39 nonce: 0,
40 timestamp: NaiveDate::from_ymd(2022, 4, 11).and_hms(20, 45, 00),
41 hash: String::from("not_actually_mined"),
42 }
43 }
44}
45
46/// For prototyping and letting serde handle everything json
47#[derive(Serialize, Deserialize, Debug, PartialEq)]
48pub struct NakedBlock {
49 #[serde(skip_serializing_if = "Vec::is_empty", default)]
50 pub transaction_list: Vec<Fingerprint>,
51 pub nonce: u32,
52 pub timestamp: NaiveDateTime,
53}
54
55/// A transaction between `source` and `target` that moves `amount`
56#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
57pub struct Transaction {
58 pub source: Fingerprint,
59 pub target: Fingerprint,
60 pub amount: u16,
61 pub timestamp: NaiveDateTime,
62}
63
64/// A JWT Payload/Claims representation
65///
66/// <https://tools.ietf.org/html/rfc7519#section-4.1>
67///
68/// - `tha`: Transaction Hash, String (custom field)
69/// - `iat`: Issued At, Unix Time, epoch
70/// - `exp`: Expiration Time, epoch
71#[derive(Debug, Serialize, Deserialize, PartialEq)]
72pub struct Claims {
73 pub tha: String,
74 pub iat: usize,
75 pub exp: usize,
76}
77
78/// The plaintext of the initial user authentication request
79#[derive(Serialize, Deserialize, Debug, PartialEq)]
80pub struct AuthRequest {
81 pub student_id: String,
82 pub passwd: String,
83 pub public_key: String,
84}
85
86/// Ciphertext of the initial authentication request, or what we will receive
87#[derive(Serialize, Deserialize, Debug)]
88pub struct InitialAuthRequest {
89 pub c: String,
90 pub iv: String,
91 pub key: String,
92}
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 @@
1/// Functions that extracts Structs to be used in warp routines 1//! # Functions that extracts Structs to be used in warp routines
2use crate::schema::{Block, Db, InitialAuthRequest, Transaction}; 2use crate::block::{Block, InitialAuthRequest, Transaction};
3use crate::Db;
3use std::convert::Infallible; 4use std::convert::Infallible;
4use warp::{Filter, Rejection}; 5use warp::{Filter, Rejection};
5 6
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 @@
1//! # Global Database representation
2//!
3//! [`Db::blockchain`] is just the last block that was mined.
4//! All the blocks are written to disk as text files whenever they are accepted.
5//!
6//! [`Db::pending_transactions`] is the in memory representation of the waiting transactions.
7//! Every user can have only one outstanding transaction at any given time.
8//!
9//! [`Db::users`] is the in memory representation of the users,
10//! with their public keys, `metu_ids` and gradecoin balances.
11use crate::block::{Block, Fingerprint, Id, Transaction};
12use crate::student::{MetuId, User, UserAtRest};
13use log::debug;
14use parking_lot::RwLock;
15use std::{collections::HashMap, fs, io, path::PathBuf, sync::Arc};
16
17#[derive(Debug, Clone, Default)]
18pub struct Db {
19 pub blockchain: Arc<RwLock<Block>>,
20 pub pending_transactions: Arc<RwLock<HashMap<Id, Transaction>>>,
21 pub users: Arc<RwLock<HashMap<Fingerprint, User>>>,
22 // TODO: metu_ids or approved_users or something, metu_id struct <11-04-22, yigit> //
23}
24
25impl Db {
26 pub fn new() -> Self {
27 fs::create_dir_all("blocks").unwrap();
28 fs::create_dir_all("users").unwrap();
29 let mut db = Db::default();
30 if let Some(block_path) = last_block_content() {
31 db.populate_with_last_block(block_path);
32 }
33
34 if let Ok(users_path) = read_users() {
35 db.populate_with_users(users_path);
36 }
37
38 let users: HashMap<Fingerprint, User> = get_friendly_users();
39
40 Db {
41 blockchain: Arc::new(RwLock::new(Block::default())),
42 pending_transactions: Arc::new(RwLock::new(HashMap::new())),
43 users: Arc::new(RwLock::new(users)),
44 }
45 }
46
47 fn populate_with_last_block(&mut self, path: String) {
48 debug!("Populating db with the latest block {}", path);
49 let file = fs::read(path).unwrap();
50 let json = std::str::from_utf8(&file).unwrap();
51 let block: Block = serde_json::from_str(json).unwrap();
52 *self.blockchain.write() = block;
53 }
54
55 fn populate_with_users(&mut self, files: Vec<PathBuf>) {
56 for fs in files {
57 if let Ok(file_content) = fs::read(fs) {
58 let json =
59 String::from_utf8(file_content).expect("we have written a malformed user file");
60 let user_at_rest: UserAtRest = serde_json::from_str(&json).unwrap();
61
62 debug!("Populating db with user: {:?}", user_at_rest);
63 self.users
64 .write()
65 .insert(user_at_rest.fingerprint, user_at_rest.user);
66 }
67 }
68 }
69}
70
71fn last_block_content() -> Option<String> {
72 let blocks = read_block_name().unwrap();
73
74 if blocks.is_empty() {
75 return None;
76 }
77
78 let last_block = blocks[0].to_str().unwrap();
79 let mut last_block = parse_block(last_block);
80 let mut last_block_index = 0;
81
82 for (index, block) in blocks.iter().enumerate() {
83 let block = block.to_str().unwrap();
84 let block = parse_block(block);
85 if block > last_block {
86 last_block = block;
87 last_block_index = index;
88 }
89 }
90 return Some(blocks[last_block_index].to_str().unwrap().parse().unwrap());
91}
92
93fn read_block_name() -> io::Result<Vec<PathBuf>> {
94 let entries = fs::read_dir("./blocks")?
95 .map(|res| res.map(|e| e.path()))
96 .collect::<Result<Vec<_>, io::Error>>()?;
97
98 Ok(entries)
99}
100
101fn parse_block(path: &str) -> u64 {
102 let end_pos = path.find(".block").unwrap();
103 let block_str = path[9..end_pos].to_string();
104 let block_u64: u64 = block_str.parse().unwrap();
105 block_u64
106}
107
108fn read_users() -> io::Result<Vec<PathBuf>> {
109 let entries = fs::read_dir("./users")?
110 .map(|res| res.map(|e| e.path()))
111 .collect::<Result<Vec<_>, io::Error>>()?;
112
113 Ok(entries)
114}
115
116fn get_friendly_users() -> HashMap<Fingerprint, User> {
117 let mut users: HashMap<Fingerprint, User> = HashMap::new();
118
119 users.insert(
120 "cde48537ca2c28084ff560826d0e6388b7c57a51497a6cb56f397289e52ff41b".to_owned(),
121 User {
122 user_id: MetuId::new("friend_1".to_owned(), "not_used".to_owned()).unwrap(),
123 public_key: "not_used".to_owned(),
124 balance: 70,
125 is_bot: true,
126 },
127 );
128
129 users.insert(
130 "a1a38b5bae5866d7d998a9834229ec2f9db7a4fc8fb6f58b1115a96a446875ff".to_owned(),
131 User {
132 user_id: MetuId::new("friend_2".to_owned(), "not_used".to_owned()).unwrap(),
133 public_key: "not_used".to_owned(),
134 balance: 20,
135 is_bot: true,
136 },
137 );
138
139 users.insert(
140 "4e048fd2a62f1307866086e803e9be43f78a702d5df10831fbf434e7663ae0e7".to_owned(),
141 User {
142 user_id: MetuId::new("friend_4".to_owned(), "not_used".to_owned()).unwrap(),
143 public_key: "not_used".to_owned(),
144 balance: 120,
145 is_bot: true,
146 },
147 );
148
149 users.insert(
150 "60e77101e76950a9b1830fa107fd2f8fc545255b3e0f14b6a7797cf9ee005f07".to_owned(),
151 User {
152 user_id: MetuId::new("friend_4".to_owned(), "not_used".to_owned()).unwrap(),
153 public_key: "not_used".to_owned(),
154 balance: 40,
155 is_bot: true,
156 },
157 );
158 users
159}
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 @@
1/// API handlers, the ends of each filter chain 1/// API handlers, the ends of each filter chain
2use crate::block::{AuthRequest, Block, Claims, InitialAuthRequest, NakedBlock, Transaction};
3use crate::student::{MetuId, User, UserAtRest};
4use crate::Db;
2use aes::Aes128; 5use aes::Aes128;
3use askama::Template; 6use askama::Template;
4use blake2::{Blake2s, Digest}; 7use blake2::{Blake2s, Digest};
5use block_modes::block_padding::Pkcs7; 8use block_modes::{block_padding::Pkcs7, BlockMode, Cbc};
6use block_modes::{BlockMode, Cbc};
7use chrono::Utc; 9use chrono::Utc;
8use jsonwebtoken::errors::ErrorKind; 10use jsonwebtoken::errors::ErrorKind;
9use jsonwebtoken::{decode, Algorithm, DecodingKey, TokenData, Validation}; 11use jsonwebtoken::{decode, Algorithm, DecodingKey, TokenData, Validation};
10use lazy_static::lazy_static; 12use lazy_static::lazy_static;
11use log::{debug, warn}; 13use log::{debug, warn};
12use math;
13use md5::Md5; 14use md5::Md5;
14use parking_lot::RwLockUpgradableReadGuard; 15use parking_lot::RwLockUpgradableReadGuard;
15use rsa::{PaddingScheme, RSAPrivateKey}; 16use rsa::{PaddingScheme, RSAPrivateKey};
16use serde::Serialize; 17use serde::Serialize;
17use sha2::Sha256; 18use sha2::Sha256;
18use std::collections::{HashMap, HashSet}; 19use std::{
19use std::convert::Infallible; 20 collections::{HashMap, HashSet},
20use std::fs; 21 convert::Infallible,
21use std::hash::Hash; 22 fs,
23 hash::Hash,
24};
22use warp::{http::StatusCode, reply}; 25use warp::{http::StatusCode, reply};
23 26
24use crate::PRIVATE_KEY; 27use crate::PRIVATE_KEY;
@@ -52,11 +55,6 @@ enum ResponseType {
52 Error, 55 Error,
53} 56}
54 57
55use crate::schema::{
56 AuthRequest, Block, Claims, Db, InitialAuthRequest, MetuId, NakedBlock, Transaction, User,
57 UserAtRest,
58};
59
60const BEARER: &str = "Bearer "; 58const BEARER: &str = "Bearer ";
61 59
62lazy_static! { 60lazy_static! {
@@ -64,7 +62,7 @@ lazy_static! {
64 .lines() 62 .lines()
65 .filter(|line| !line.starts_with('-')) 63 .filter(|line| !line.starts_with('-'))
66 .fold(String::new(), |mut data, line| { 64 .fold(String::new(), |mut data, line| {
67 data.push_str(&line); 65 data.push_str(line);
68 data 66 data
69 }); 67 });
70 68
@@ -87,9 +85,9 @@ lazy_static! {
87/// - Student picks a short temporary key (`k_temp`) 85/// - Student picks a short temporary key (`k_temp`)
88/// - Creates a JSON object (`auth_plaintext`) with their `metu_id` and `public key` in base64 (PEM) format (`S_PK`): 86/// - Creates a JSON object (`auth_plaintext`) with their `metu_id` and `public key` in base64 (PEM) format (`S_PK`):
89/// { 87/// {
90/// student_id: "e12345", 88/// `student_id`: "e12345",
91/// passwd: "15 char secret" 89/// `passwd`: "15 char secret"
92/// public_key: "---BEGIN PUBLIC KEY..." 90/// `public_key`: "---BEGIN PUBLIC KEY..."
93/// } 91/// }
94/// 92///
95/// - 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` 93/// - 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! {
97/// using sha256 with `gradecoin_public_key`, giving us `key_ciphertext` 95/// using sha256 with `gradecoin_public_key`, giving us `key_ciphertext`
98/// - The payload JSON object (`auth_request`) can be JSON serialized now: 96/// - The payload JSON object (`auth_request`) can be JSON serialized now:
99/// { 97/// {
100/// c: "auth_ciphertext" 98/// c: "`auth_ciphertext`"
101/// key: "key_ciphertext" 99/// key: "`key_ciphertext`"
102/// } 100/// }
103/// 101///
104/// ## Gradecoin Side 102/// ## Gradecoin Side
105/// 103///
106/// - 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) 104/// - 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)
107/// - With `temp_key`, we can AES 128 Cbc Pkcs7 decrypt the `auth_request.c`, giving us 105/// - With `temp_key`, we can AES 128 Cbc Pkcs7 decrypt the `auth_request.c`, giving us `auth_plaintext`
108/// auth_plaintext
109/// - The `auth_plaintext` String can be deserialized to [`AuthRequest`] 106/// - The `auth_plaintext` String can be deserialized to [`AuthRequest`]
110/// - We then verify the payload and calculate the User fingerprint 107/// - We then verify the payload and calculate the User fingerprint
111/// - Finally, create the new [`User`] object, insert to users HashMap `<fingerprint, User>` 108/// - Finally, create the new [`User`] object, insert to users `HashMap` `<fingerprint, User>`
112/// 109///
113pub async fn authenticate_user( 110pub async fn authenticate_user(
114 request: InitialAuthRequest, 111 request: InitialAuthRequest,
@@ -226,7 +223,7 @@ pub async fn authenticate_user(
226 223
227 // c field was properly base64 encoded, now available in auth_packet 224 // c field was properly base64 encoded, now available in auth_packet
228 // decryptor was setup properly, with the correct lenght key 225 // decryptor was setup properly, with the correct lenght key
229 let mut buf = auth_packet.to_vec(); 226 let mut buf = auth_packet;
230 let auth_plaintext = match cipher.decrypt(&mut buf) { 227 let auth_plaintext = match cipher.decrypt(&mut buf) {
231 Ok(p) => p, 228 Ok(p) => p,
232 Err(err) => { 229 Err(err) => {
@@ -284,22 +281,21 @@ pub async fn authenticate_user(
284 281
285 // is the student in AuthRequest privileged? 282 // is the student in AuthRequest privileged?
286 let privileged_student_id = 283 let privileged_student_id =
287 match MetuId::new(request.student_id.clone(), request.passwd.clone()) { 284 if let Some(id) = MetuId::new(request.student_id.clone(), request.passwd.clone()) {
288 Some(id) => id, 285 id
289 None => { 286 } else {
290 debug!( 287 debug!(
291 "Someone tried to auth with invalid credentials: {} {}", 288 "Someone tried to auth with invalid credentials: {} {}",
292 &request.student_id, &request.passwd 289 &request.student_id, &request.passwd
293 ); 290 );
294 let res_json = warp::reply::json(&GradeCoinResponse { 291 let res_json = warp::reply::json(&GradeCoinResponse {
295 res: ResponseType::Error, 292 res: ResponseType::Error,
296 message: 293 message:
297 "The credentials given ('student_id', 'passwd') cannot hold a Gradecoin account" 294 "The credentials given ('student_id', 'passwd') cannot hold a Gradecoin account"
298 .to_owned(), 295 .to_owned(),
299 }); 296 });
300 297
301 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); 298 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST));
302 }
303 }; 299 };
304 300
305 // Students should be able to authenticate once 301 // Students should be able to authenticate once
@@ -329,7 +325,7 @@ pub async fn authenticate_user(
329 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); 325 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST));
330 } 326 }
331 327
332 let fingerprint = format!("{:x}", Sha256::digest(&request.public_key.as_bytes())); 328 let fingerprint = format!("{:x}", Sha256::digest(request.public_key.as_bytes()));
333 329
334 let new_user = User { 330 let new_user = User {
335 user_id: privileged_student_id, 331 user_id: privileged_student_id,
@@ -387,7 +383,7 @@ pub async fn list_transactions(db: Db) -> Result<impl warp::Reply, Infallible> {
387/// Proposes a new block for the next round. 383/// Proposes a new block for the next round.
388/// Can reject the block 384/// Can reject the block
389/// 385///
390/// The proposer has to put their transaction as the first transaction of the [`transaction_list`]. 386/// The proposer has to put their transaction as the first transaction of the transaction_list.
391/// This is the analogue of `coinbase` in Bitcoin works 387/// This is the analogue of `coinbase` in Bitcoin works
392/// 388///
393/// The `coinbase` transaction also gets something for their efforts. 389/// The `coinbase` transaction also gets something for their efforts.
@@ -420,9 +416,10 @@ pub async fn propose_block(
420 let pending_transactions = db.pending_transactions.upgradable_read(); 416 let pending_transactions = db.pending_transactions.upgradable_read();
421 417
422 // we get the proposers fingerprint by finding the transaction (id) then extracting the source 418 // we get the proposers fingerprint by finding the transaction (id) then extracting the source
423 let internal_user_fingerprint = match pending_transactions.get(&new_block.transaction_list[0]) { 419 let internal_user_fingerprint =
424 Some(coinbase) => &coinbase.source, 420 if let Some(coinbase) = pending_transactions.get(&new_block.transaction_list[0]) {
425 None => { 421 &coinbase.source
422 } else {
426 debug!( 423 debug!(
427 "Transaction with id {} is not found in the pending_transactions", 424 "Transaction with id {} is not found in the pending_transactions",
428 new_block.transaction_list[0] 425 new_block.transaction_list[0]
@@ -434,34 +431,31 @@ pub async fn propose_block(
434 }); 431 });
435 432
436 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); 433 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST));
437 } 434 };
438 };
439 435
440 let users_store = db.users.upgradable_read(); 436 let users_store = db.users.upgradable_read();
441 437
442 // this probably cannot fail, if the transaction is valid then it must've been checked already 438 // this probably cannot fail, if the transaction is valid then it must've been checked already
443 let internal_user = match users_store.get(internal_user_fingerprint) { 439 let internal_user = if let Some(existing_user) = users_store.get(internal_user_fingerprint) {
444 Some(existing_user) => existing_user, 440 existing_user
445 None => { 441 } else {
446 debug!( 442 debug!(
447 "User with public key signature {:?} is not found in the database", 443 "User with public key signature {:?} is not found in the database",
448 new_block.transaction_list[0] 444 new_block.transaction_list[0]
449 ); 445 );
450 446
451 let res_json = warp::reply::json(&GradeCoinResponse { 447 let res_json = warp::reply::json(&GradeCoinResponse {
452 res: ResponseType::Error, 448 res: ResponseType::Error,
453 message: "User with that public key signature is not found in the database" 449 message: "User with that public key signature is not found in the database".to_owned(),
454 .to_owned(), 450 });
455 });
456 451
457 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST)); 452 return Ok(warp::reply::with_status(res_json, StatusCode::BAD_REQUEST));
458 }
459 }; 453 };
460 454
461 let proposer_public_key = &internal_user.public_key; 455 let proposer_public_key = &internal_user.public_key;
462 456
463 // JWT Check 457 // JWT Check
464 let token_payload = match authorize_proposer(token, &proposer_public_key) { 458 let token_payload = match authorize_proposer(&token, proposer_public_key) {
465 Ok(data) => data, 459 Ok(data) => data,
466 Err(below) => { 460 Err(below) => {
467 debug!("Something went wrong with the JWT {:?}", below); 461 debug!("Something went wrong with the JWT {:?}", below);
@@ -501,7 +495,7 @@ pub async fn propose_block(
501 } 495 }
502 496
503 // Are transactions in the block valid? 497 // Are transactions in the block valid?
504 for transaction_hash in new_block.transaction_list.iter() { 498 for transaction_hash in &new_block.transaction_list {
505 if !pending_transactions.contains_key(transaction_hash) { 499 if !pending_transactions.contains_key(transaction_hash) {
506 let res_json = warp::reply::json(&GradeCoinResponse { 500 let res_json = warp::reply::json(&GradeCoinResponse {
507 res: ResponseType::Error, 501 res: ResponseType::Error,
@@ -536,7 +530,7 @@ pub async fn propose_block(
536 } 530 }
537 531
538 // Are the 6 leftmost characters (=24 bits) zero? 532 // Are the 6 leftmost characters (=24 bits) zero?
539 let should_zero = hashvalue[0] as i32 + hashvalue[1] as i32 + hashvalue[2] as i32; 533 let should_zero = i32::from(hashvalue[0]) + i32::from(hashvalue[1]) + i32::from(hashvalue[2]);
540 534
541 if should_zero != 0 { 535 if should_zero != 0 {
542 debug!("the hash does not have 6 rightmost zero bits"); 536 debug!("the hash does not have 6 rightmost zero bits");
@@ -566,7 +560,7 @@ pub async fn propose_block(
566 let holding: HashMap<String, Transaction> = HashMap::new(); 560 let holding: HashMap<String, Transaction> = HashMap::new();
567 561
568 // Play out the transactions 562 // Play out the transactions
569 for fingerprint in new_block.transaction_list.iter() { 563 for fingerprint in &new_block.transaction_list {
570 if let Some(transaction) = pending_transactions.remove(fingerprint) { 564 if let Some(transaction) = pending_transactions.remove(fingerprint) {
571 let source = &transaction.source; 565 let source = &transaction.source;
572 let target = &transaction.target; 566 let target = &transaction.target;
@@ -581,7 +575,7 @@ pub async fn propose_block(
581 if is_source_bot { 575 if is_source_bot {
582 // Add staking reward 576 // Add staking reward
583 to.balance += 577 to.balance +=
584 math::round::ceil((transaction.amount as f64) * STAKING_REWARD, 0) 578 math::round::ceil((f64::from(transaction.amount)) * STAKING_REWARD, 0)
585 as u16; 579 as u16;
586 } 580 }
587 } 581 }
@@ -592,8 +586,8 @@ pub async fn propose_block(
592 pending_transactions.insert( 586 pending_transactions.insert(
593 transaction_id, 587 transaction_id,
594 Transaction { 588 Transaction {
595 source: target.to_owned(), 589 source: target.clone(),
596 target: source.to_owned(), 590 target: source.clone(),
597 amount: transaction.amount, 591 amount: transaction.amount,
598 timestamp: Utc::now().naive_local(), 592 timestamp: Utc::now().naive_local(),
599 }, 593 },
@@ -602,8 +596,8 @@ pub async fn propose_block(
602 } 596 }
603 } 597 }
604 598
605 for (fp, tx) in holding.iter() { 599 for (fp, tx) in &holding {
606 pending_transactions.insert(fp.to_owned(), tx.to_owned()); 600 pending_transactions.insert(fp.clone(), tx.clone());
607 } 601 }
608 602
609 // just update everyone's .guy file 603 // just update everyone's .guy file
@@ -665,23 +659,21 @@ pub async fn propose_transaction(
665 let users_store = db.users.read(); 659 let users_store = db.users.read();
666 660
667 // Is this transaction from an authorized source? 661 // Is this transaction from an authorized source?
668 let internal_user = match users_store.get(&new_transaction.source) { 662 let internal_user = if let Some(existing_user) = users_store.get(&new_transaction.source) {
669 Some(existing_user) => existing_user, 663 existing_user
670 None => { 664 } else {
671 debug!( 665 debug!(
672 "User with public key signature {:?} is not found in the database", 666 "User with public key signature {:?} is not found in the database",
673 new_transaction.source 667 new_transaction.source
674 ); 668 );
675 669
676 return Ok(warp::reply::with_status( 670 return Ok(warp::reply::with_status(
677 warp::reply::json(&GradeCoinResponse { 671 warp::reply::json(&GradeCoinResponse {
678 res: ResponseType::Error, 672 res: ResponseType::Error,
679 message: "User with the given public key signature is not authorized" 673 message: "User with the given public key signature is not authorized".to_owned(),
680 .to_owned(), 674 }),
681 }), 675 StatusCode::BAD_REQUEST,
682 StatusCode::BAD_REQUEST, 676 ));
683 ));
684 }
685 }; 677 };
686 678
687 if internal_user.is_bot { 679 if internal_user.is_bot {
@@ -702,7 +694,7 @@ pub async fn propose_transaction(
702 // *this* point 694 // *this* point
703 let proposer_public_key = &internal_user.public_key; 695 let proposer_public_key = &internal_user.public_key;
704 696
705 let token_payload = match authorize_proposer(token, &proposer_public_key) { 697 let token_payload = match authorize_proposer(&token, proposer_public_key) {
706 Ok(data) => data, 698 Ok(data) => data,
707 Err(below) => { 699 Err(below) => {
708 debug!("JWT Error: {:?}", below); 700 debug!("JWT Error: {:?}", below);
@@ -815,7 +807,7 @@ pub async fn propose_transaction(
815 807
816 debug!("Taking the hash of {}", serd_tx); 808 debug!("Taking the hash of {}", serd_tx);
817 809
818 let hashed_transaction = Md5::digest(&serd_tx.as_bytes()); 810 let hashed_transaction = Md5::digest(serd_tx.as_bytes());
819 if token_payload.claims.tha != format!("{:x}", hashed_transaction) { 811 if token_payload.claims.tha != format!("{:x}", hashed_transaction) {
820 return Ok(warp::reply::with_status( 812 return Ok(warp::reply::with_status(
821 warp::reply::json(&GradeCoinResponse { 813 warp::reply::json(&GradeCoinResponse {
@@ -859,7 +851,7 @@ pub async fn list_blocks(db: Db) -> Result<impl warp::Reply, Infallible> {
859/// *[`jwt_token`]: The raw JWT token, "Bearer aaa.bbb.ccc" 851/// *[`jwt_token`]: The raw JWT token, "Bearer aaa.bbb.ccc"
860/// *[`user_pem`]: User Public Key, "BEGIN RSA" 852/// *[`user_pem`]: User Public Key, "BEGIN RSA"
861/// NOT async, might look into it if this becomes a bottleneck 853/// NOT async, might look into it if this becomes a bottleneck
862fn authorize_proposer(jwt_token: String, user_pem: &str) -> Result<TokenData<Claims>, String> { 854fn authorize_proposer(jwt_token: &str, user_pem: &str) -> Result<TokenData<Claims>, String> {
863 // Throw away the "Bearer " part 855 // Throw away the "Bearer " part
864 let raw_jwt = jwt_token.trim_start_matches(BEARER).to_owned(); 856 let raw_jwt = jwt_token.trim_start_matches(BEARER).to_owned();
865 857
@@ -929,7 +921,7 @@ pub async fn user_list_handler(db: Db) -> Result<impl warp::Reply, warp::Rejecti
929 921
930 for (fingerprint, user) in users.iter() { 922 for (fingerprint, user) in users.iter() {
931 sane_users.push(DisplayUsers { 923 sane_users.push(DisplayUsers {
932 fingerprint: fingerprint.to_owned(), 924 fingerprint: fingerprint.clone(),
933 balance: user.balance, 925 balance: user.balance,
934 is_bot: user.is_bot, 926 is_bot: user.is_bot,
935 }); 927 });
diff --git a/src/lib.rs b/src/lib.rs
deleted file mode 100644
index c335ae9..0000000
--- a/src/lib.rs
+++ /dev/null
@@ -1,35 +0,0 @@
1//! # Gradecoin
2//!
3//! ## Services
4//! ### /register
5//! - Student creates their own 2048 bit RSA `keypair`
6//! - Downloads `Gradecoin`'s Public Key from Moodle
7//! - Encrypts their JSON wrapped `Public Key` and `Student ID` using Gradecoin's Public Key
8//! - Their public key is now in our Db under [`schema::User::public_key`] and can be used to sign their JWT's during requests
9//!
10//! ### /transaction
11//! - offer a [`schema::Transaction`] - POST request
12//! - The request should have `Authorization`
13//! - The request header should be signed by the Public Key of the `by` field in the transaction
14//! - fetch the list of `Transaction`s - GET request
15//!
16//! ### /block
17//! - offer a [`schema::Block`] - POST request
18//! - The request should have `Authorization`
19//! - The [`schema::Block::transaction_list`] of the block should be a subset of [`schema::Db::pending_transactions`]
20//! - fetch the last accepted [`schema::Block`] - GET request
21//!
22//! `Authorization`: The request header should have Bearer JWT.Token signed with Student Public Key
23
24pub mod custom_filters;
25pub mod handlers;
26pub mod routes;
27pub mod schema;
28
29use lazy_static::lazy_static;
30use std::fs;
31
32lazy_static! {
33 static ref PRIVATE_KEY: String =
34 fs::read_to_string("secrets/gradecoin.pem").expect("error reading 'secrets/gradecoin.pem'");
35}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..f0027f2
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,52 @@
1//! # Gradecoin
2//!
3//! ## Services
4//! ### /register
5//! - Student creates their own 2048 bit RSA `keypair`
6//! - Downloads `Gradecoin`'s Public Key from Moodle
7//! - Encrypts their JSON wrapped `Public Key` and `Student ID` using Gradecoin's Public Key
8//! - Their public key is now in our Db under [`block::User::public_key`] and can be used to sign their JWT's during requests
9//!
10//! ### /transaction
11//! - offer a [`block::Transaction`] - POST request
12//! - The request should have `Authorization`
13//! - The request header should be signed by the Public Key of the `by` field in the transaction
14//! - fetch the list of `Transaction`s - GET request
15//!
16//! ### /block
17//! - offer a [`block::Block`] - POST request
18//! - The request should have `Authorization`
19//! - The [`block::Block::transaction_list`] of the block should be a subset of [`block::Db::pending_transactions`]
20//! - fetch the last accepted [`block::Block`] - GET request
21//!
22//! `Authorization`: The request header should have Bearer JWT.Token signed with Student Public Key
23#![warn(clippy::all, clippy::pedantic)]
24#![allow(clippy::unused_async)]
25
26mod custom_filters;
27mod db;
28mod handlers;
29mod routes;
30mod block;
31mod student;
32
33pub use block::{Fingerprint, Id};
34use db::Db;
35use lazy_static::lazy_static;
36use std::fs;
37
38#[tokio::main]
39async fn main() {
40 log4rs::init_file("log.conf.yml", log4rs::config::Deserializers::default()).unwrap();
41
42 let api = routes::application(Db::new());
43
44 // Start the server
45 let point = ([127, 0, 0, 1], 8080);
46 warp::serve(api).run(point).await;
47}
48
49lazy_static! {
50 static ref PRIVATE_KEY: String =
51 fs::read_to_string("secrets/gradecoin.pem").expect("error reading 'secrets/gradecoin.pem'");
52}
diff --git a/src/routes.rs b/src/routes.rs
index f39e98a..e3322ad 100644
--- a/src/routes.rs
+++ b/src/routes.rs
@@ -1,12 +1,12 @@
1/// Endpoints and their construction 1//! # Endpoints and their construction
2//
2use warp::{Filter, Rejection, Reply}; 3use warp::{Filter, Rejection, Reply};
3
4use crate::custom_filters; 4use crate::custom_filters;
5use crate::handlers; 5use crate::handlers;
6use crate::schema::Db; 6use crate::Db;
7 7
8/// Every route combined 8/// Every route combined
9pub fn consensus_routes(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { 9pub fn application(db: Db) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
10 // Remember when we wanted to implement templating 10 // Remember when we wanted to implement templating
11 // Why would we? Just put a staic webpage under /public (next to Cargo.toml) and place it and 11 // Why would we? Just put a staic webpage under /public (next to Cargo.toml) and place it and
12 // the end of the filter chain 12 // 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 @@
1//! # Data Representations
2//!
3//! We need persistence for [`Block`]s and [`User`]s, not so much for [`Transaction`]s
4//!
5//! There are around 30 students, a full fledged database would be an overkill (for next year?)
6//!
7//! Pending transactions are held in memory, these are cleared with every new block
8//! Only the last block is held in memory, every block is written to a file
9//! Users are held in memory and they're also backed up to text files
10use chrono::{NaiveDate, NaiveDateTime};
11use lazy_static::lazy_static;
12use log::debug;
13use parking_lot::RwLock;
14use serde::{Deserialize, Serialize};
15use std::collections::{HashMap, HashSet};
16use std::fmt;
17use std::fs;
18use std::io;
19use std::path::PathBuf;
20use std::string::String;
21use std::sync::Arc;
22use std::vec::Vec;
23
24pub type Fingerprint = String;
25pub type Id = String;
26
27fn block_parser(path: String) -> u64 {
28 let end_pos = path.find(".block").unwrap();
29 let block_str = path[9..end_pos].to_string();
30 let block_u64: u64 = block_str.parse().unwrap();
31 block_u64
32}
33
34fn last_block_content() -> Option<String> {
35 let blocks = read_block_name().unwrap();
36
37 if blocks.is_empty() {
38 return None;
39 }
40
41 let last_block = blocks[0].to_str().unwrap();
42 let mut last_block = block_parser(last_block.to_string());
43 let mut last_block_index = 0;
44
45 for (index, block) in blocks.iter().enumerate() {
46 let block = block.to_str().unwrap();
47 let block = block_parser(block.to_string());
48 if block > last_block {
49 last_block = block;
50 last_block_index = index;
51 }
52 }
53 return Some(blocks[last_block_index].to_str().unwrap().parse().unwrap());
54}
55
56fn read_block_name() -> io::Result<Vec<PathBuf>> {
57 let entries = fs::read_dir("./blocks")?
58 .map(|res| res.map(|e| e.path()))
59 .collect::<Result<Vec<_>, io::Error>>()?;
60
61 Ok(entries)
62}
63
64fn read_users() -> io::Result<Vec<PathBuf>> {
65 let entries = fs::read_dir("./users")?
66 .map(|res| res.map(|e| e.path()))
67 .collect::<Result<Vec<_>, io::Error>>()?;
68
69 Ok(entries)
70}
71
72fn populate_db_with_last_block(db: &mut Db, path: String) -> &mut Db {
73 debug!("Populating db with last block {}", path);
74 let file = fs::read(path).unwrap();
75 let json = std::str::from_utf8(&file).unwrap();
76 let block: Block = serde_json::from_str(json).unwrap();
77 *db.blockchain.write() = block;
78
79 db
80}
81
82#[derive(Debug, Serialize, Deserialize, PartialEq)]
83pub struct UserAtRest {
84 pub fingerprint: Fingerprint,
85 pub user: User,
86}
87
88fn populate_db_with_users(db: &mut Db, files: Vec<PathBuf>) -> &mut Db {
89 for fs in files {
90 if let Ok(file_content) = fs::read(fs) {
91 let json =
92 String::from_utf8(file_content).expect("we have written a malformed user file");
93 let user_at_rest: UserAtRest = serde_json::from_str(&json).unwrap();
94
95 debug!("Populating db with user: {:?}", user_at_rest);
96 db.users
97 .write()
98 .insert(user_at_rest.fingerprint, user_at_rest.user);
99 }
100 }
101
102 db
103}
104
105/// Creates a new database, uses the previous last block if one exists and attempts the populate
106/// the users
107pub fn create_database() -> Db {
108 fs::create_dir_all("blocks").unwrap();
109 fs::create_dir_all("users").unwrap();
110 let mut db = Db::new();
111 if let Some(block_path) = last_block_content() {
112 populate_db_with_last_block(&mut db, block_path);
113 }
114
115 if let Ok(users_path) = read_users() {
116 populate_db_with_users(&mut db, users_path);
117 }
118
119 db
120}
121
122/// A JWT Payload/Claims representation
123///
124/// https://tools.ietf.org/html/rfc7519#section-4.1
125///
126/// - `tha`: Transaction Hash, String (custom field)
127/// - `iat`: Issued At, Unix Time, epoch
128/// - `exp`: Expiration Time, epoch
129#[derive(Debug, Serialize, Deserialize, PartialEq)]
130pub struct Claims {
131 pub tha: String,
132 pub iat: usize,
133 pub exp: usize,
134}
135
136/// Global Database representation
137///
138/// [`Db::blockchain`] is just the last block that was mined. All the blocks are written to disk as text
139/// files whenever they are accepted.
140///
141/// [`Db::pending_transactions`] is the in memory representation of the waiting transactions. Every
142/// user can have only one outstanding transaction at any given time.
143///
144/// [`Db::users`] is the in memory representation of the users, with their public keys, metu_ids and
145/// gradecoin balances.
146#[derive(Debug, Clone)]
147pub struct Db {
148 pub blockchain: Arc<RwLock<Block>>,
149 pub pending_transactions: Arc<RwLock<HashMap<Id, Transaction>>>,
150 pub users: Arc<RwLock<HashMap<Fingerprint, User>>>,
151}
152
153impl Db {
154 pub fn new() -> Self {
155 let mut users: HashMap<Fingerprint, User> = HashMap::new();
156
157 let friendly_1 = MetuId::new("friend_1".to_owned(), "not_used".to_owned()).unwrap();
158
159 users.insert(
160 "cde48537ca2c28084ff560826d0e6388b7c57a51497a6cb56f397289e52ff41b".to_owned(),
161 User {
162 user_id: friendly_1,
163 public_key: "not_used".to_owned(),
164 balance: 70,
165 is_bot: true,
166 },
167 );
168
169 let friendly_2 = MetuId::new("friend_2".to_owned(), "not_used".to_owned()).unwrap();
170
171 users.insert(
172 "a1a38b5bae5866d7d998a9834229ec2f9db7a4fc8fb6f58b1115a96a446875ff".to_owned(),
173 User {
174 user_id: friendly_2,
175 public_key: "not_used".to_owned(),
176 balance: 20,
177 is_bot: true,
178 },
179 );
180
181 let friendly_3 = MetuId::new("friend_4".to_owned(), "not_used".to_owned()).unwrap();
182
183 users.insert(
184 "4e048fd2a62f1307866086e803e9be43f78a702d5df10831fbf434e7663ae0e7".to_owned(),
185 User {
186 user_id: friendly_3,
187 public_key: "not_used".to_owned(),
188 balance: 120,
189 is_bot: true,
190 },
191 );
192
193 let friendly_4 = MetuId::new("friend_4".to_owned(), "not_used".to_owned()).unwrap();
194
195 users.insert(
196 "60e77101e76950a9b1830fa107fd2f8fc545255b3e0f14b6a7797cf9ee005f07".to_owned(),
197 User {
198 user_id: friendly_4,
199 public_key: "not_used".to_owned(),
200 balance: 40,
201 is_bot: true,
202 },
203 );
204
205 Db {
206 blockchain: Arc::new(RwLock::new(Block::new())),
207 pending_transactions: Arc::new(RwLock::new(HashMap::new())),
208 users: Arc::new(RwLock::new(users)),
209 }
210 }
211}
212
213impl Default for Db {
214 fn default() -> Self {
215 Self::new()
216 }
217}
218
219/// A transaction between `source` and `target` that moves `amount`
220#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
221pub struct Transaction {
222 pub source: Fingerprint,
223 pub target: Fingerprint,
224 pub amount: u16,
225 pub timestamp: NaiveDateTime,
226}
227
228/// A block that was proposed with `transaction_list` and `nonce` that made `hash` valid, 6 zeroes
229/// at the left hand side of the hash (24 bytes)
230///
231/// We are mining using blake2s algorithm, which produces 256 bit hashes. Hash/second is roughly
232/// 20x10^3.
233///
234/// https://serde.rs/container-attrs.html might be valuable to normalize the serialize/deserialize
235/// conventions as these will be hashed
236///
237#[derive(Serialize, Deserialize, Debug, PartialEq)]
238pub struct Block {
239 #[serde(skip_serializing_if = "Vec::is_empty")]
240 pub transaction_list: Vec<Fingerprint>,
241 pub nonce: u32,
242 pub timestamp: NaiveDateTime,
243 pub hash: String,
244}
245
246/// For prototyping and letting serde handle everything json
247#[derive(Serialize, Deserialize, Debug, PartialEq)]
248pub struct NakedBlock {
249 #[serde(skip_serializing_if = "Vec::is_empty", default)]
250 pub transaction_list: Vec<Fingerprint>,
251 pub nonce: u32,
252 pub timestamp: NaiveDateTime,
253}
254
255impl Block {
256 /// Genesis block
257 pub fn new() -> Block {
258 Block {
259 transaction_list: vec!["gradecoin_bank".to_owned()],
260 nonce: 0,
261 timestamp: NaiveDate::from_ymd(2021, 4, 11).and_hms(20, 45, 00),
262 hash: String::from("not_actually_mined"),
263 }
264 }
265}
266
267impl Default for Block {
268 fn default() -> Self {
269 Self::new()
270 }
271}
272
273/// A Student
274///
275/// * [`user_id`]: Can only be one of the repopulated
276/// * [`public_key`]: A PEM format public key "---- BEGIN" and all
277/// * [`balance`]: User's current Gradecoin amount
278///
279/// This should ideally include the fingerprint as well?
280#[derive(Serialize, Deserialize, Debug, PartialEq)]
281pub struct User {
282 pub user_id: MetuId,
283 pub public_key: String,
284 pub balance: u16,
285 #[serde(skip, default = "bool::default")]
286 pub is_bot: bool,
287}
288
289/// The values are hard coded in [`OUR_STUDENTS`] so MetuId::new() can accept/reject values based on that
290#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
291pub struct MetuId {
292 id: String,
293 passwd: String,
294}
295
296impl MetuId {
297 pub fn quick_equal(&self, other: &str) -> bool {
298 self.id == other
299 }
300}
301
302/// The plaintext of the initial user authentication request
303#[derive(Serialize, Deserialize, Debug, PartialEq)]
304pub struct AuthRequest {
305 pub student_id: String,
306 pub passwd: String,
307 pub public_key: String,
308}
309
310/// Ciphertext of the initial authentication request, or what we will receive
311#[derive(Serialize, Deserialize, Debug)]
312pub struct InitialAuthRequest {
313 pub c: String,
314 pub iv: String,
315 pub key: String,
316}
317
318// Students who are authorized to have Gradecoin accounts
319lazy_static! {
320 static ref OUR_STUDENTS: HashSet<(&'static str, &'static str)> = {
321 [
322 ("e254275", "DtNX1qk4YF4saRH"),
323 ("e223687", "cvFEs4XLjuGBD1v"),
324 ("e211024", "voQAcxiKJmEXYRT"),
325 ("e209888", "O75dli6AQtz2tUi"),
326 ("e223725", "xXuTD3Y4tyrv2Jz"),
327 ("e209362", "N7wGm5XU5zVWOWu"),
328 ("e209898", "aKBFfB8fZMq8pVn"),
329 ("e230995", "TgcHGlqeFhQGx42"),
330 ("e223743", "YVWVSWuIHplJk9C"),
331 ("e223747", "8LAeHrsjnwXh59Q"),
332 ("e223749", "HMFeJqVOzwCPHbc"),
333 ("e223751", "NjMsxmtmy2VOwMW"),
334 ("e188126", "QibuPdV2gXfsVJW"),
335 ("e209913", "kMxJvl2vHSWCy4A"),
336 ("e203608", "mfkkR0MWurk6Rp1"),
337 ("e233013", "GCqHxdOaDj2pWXx"),
338 ("e216982", "2Z0xmgCStnj5qg5"),
339 ("e217185", "BcaZNlzlhPph7A3"),
340 ("e223780", "2KvVxKUQaA9H4sn"),
341 ("e194931", "hsC0Wb8PQ5vzwdQ"),
342 ("e223783", "ETUJA3kt1QYvJai"),
343 ("e254550", "rPRjX0A4NefvKWi"),
344 ("e217203", "lN3IWhGyCrGfkk5"),
345 ("e217477", "O9xlMaa7LanC82w"),
346 ("e223786", "UxI6czykJfp9T9N"),
347 ("e231060", "VJgziofQQPCoisH"),
348 ("e223795", "pmcTCKox99NFsqp"),
349 ("e223715", "1H5QuOYI1b2r9ET"),
350 ("e181932", "THANKYOUHAVEFUN"),
351 ("bank", "P7oxDm30g1jeIId"),
352 ("friend_1", "not_used"),
353 ("friend_2", "not_used"),
354 ("friend_3", "not_used"),
355 ("friend_4", "not_used"),
356 ]
357 .iter()
358 .cloned()
359 .collect()
360 };
361}
362
363impl fmt::Display for MetuId {
364 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
365 write!(f, "{}", self.id)
366 }
367}
368
369impl MetuId {
370 pub fn new(id: String, pwd: String) -> Option<Self> {
371 if OUR_STUDENTS.contains(&(&*id, &*pwd)) {
372 Some(MetuId { id, passwd: pwd })
373 } else {
374 None
375 }
376 }
377}
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 @@
1use crate::Fingerprint;
2use lazy_static::lazy_static;
3use serde::{Deserialize, Serialize};
4use std::{collections::HashSet, fmt};
5
6#[derive(Debug, Serialize, Deserialize, PartialEq)]
7pub struct UserAtRest {
8 pub fingerprint: Fingerprint,
9 pub user: User,
10}
11
12/// A Student
13///
14/// - [`user_id`]: Can only be one of the preapproved students (who are enlisted in the course)
15/// - [`public_key`]: A PEM format public key "---- BEGIN" and all
16/// - [`balance`]: User's current Gradecoin amount
17///
18/// This should ideally include the fingerprint as well?
19#[derive(Serialize, Deserialize, Debug, PartialEq)]
20pub struct User {
21 pub user_id: MetuId,
22 pub public_key: String,
23 pub balance: u16,
24 #[serde(skip, default = "bool::default")]
25 pub is_bot: bool,
26}
27
28/// The values are hard coded in [`static@OUR_STUDENTS`] so `MetuId::new`() can accept/reject values based on that
29#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
30pub struct MetuId {
31 id: String,
32 passwd: String,
33}
34
35impl fmt::Display for MetuId {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 write!(f, "{}", self.id)
38 }
39}
40
41impl MetuId {
42 pub fn new(id: String, pwd: String) -> Option<Self> {
43 if OUR_STUDENTS.contains(&(&*id, &*pwd)) {
44 Some(MetuId { id, passwd: pwd })
45 } else {
46 None
47 }
48 }
49}
50
51// TODO: remove this, read from a yaml or something, then MetuId::new gets a self <11-04-22, yigit> //
52
53// Students who are authorized to have Gradecoin accounts
54lazy_static! {
55 static ref OUR_STUDENTS: HashSet<(&'static str, &'static str)> = {
56 [
57 ("e254275", "DtNX1qk4YF4saRH"),
58 ("e223687", "cvFEs4XLjuGBD1v"),
59 ("e211024", "voQAcxiKJmEXYRT"),
60 ("e209888", "O75dli6AQtz2tUi"),
61 ("e223725", "xXuTD3Y4tyrv2Jz"),
62 ("e209362", "N7wGm5XU5zVWOWu"),
63 ("e209898", "aKBFfB8fZMq8pVn"),
64 ("e230995", "TgcHGlqeFhQGx42"),
65 ("e223743", "YVWVSWuIHplJk9C"),
66 ("e223747", "8LAeHrsjnwXh59Q"),
67 ("e223749", "HMFeJqVOzwCPHbc"),
68 ("e223751", "NjMsxmtmy2VOwMW"),
69 ("e188126", "QibuPdV2gXfsVJW"),
70 ("e209913", "kMxJvl2vHSWCy4A"),
71 ("e203608", "mfkkR0MWurk6Rp1"),
72 ("e233013", "GCqHxdOaDj2pWXx"),
73 ("e216982", "2Z0xmgCStnj5qg5"),
74 ("e217185", "BcaZNlzlhPph7A3"),
75 ("e223780", "2KvVxKUQaA9H4sn"),
76 ("e194931", "hsC0Wb8PQ5vzwdQ"),
77 ("e223783", "ETUJA3kt1QYvJai"),
78 ("e254550", "rPRjX0A4NefvKWi"),
79 ("e217203", "lN3IWhGyCrGfkk5"),
80 ("e217477", "O9xlMaa7LanC82w"),
81 ("e223786", "UxI6czykJfp9T9N"),
82 ("e231060", "VJgziofQQPCoisH"),
83 ("e223795", "pmcTCKox99NFsqp"),
84 ("e223715", "1H5QuOYI1b2r9ET"),
85 ("e181932", "THANKYOUHAVEFUN"),
86 ("bank", "P7oxDm30g1jeIId"),
87 ("friend_1", "not_used"),
88 ("friend_2", "not_used"),
89 ("friend_3", "not_used"),
90 ("friend_4", "not_used"),
91 ]
92 .iter()
93 .copied()
94 .collect()
95 };
96}