diff --git a/core/src/crypto.rs b/core/src/crypto.rs index cf95b2f..8614a87 100644 --- a/core/src/crypto.rs +++ b/core/src/crypto.rs @@ -197,7 +197,7 @@ pub fn create_identity() -> (String, String) { async fn age_encrypt_with_recipients( input_buffer: &[u8], output_buffer: &mut W, - recipients: Vec<&str>, + recipients: &Vec<&str>, ) -> Result<(), age::EncryptError> { let public_keys = recipients .into_iter() @@ -248,7 +248,7 @@ async fn age_decrypt_with_private_key( pub async fn encrypt_password_with_recipients( password_plain_text: &str, - recipients: Vec<&str>, + recipients: &Vec<&str>, ) -> Result { let mut encrypt_buffer = Vec::new(); @@ -287,7 +287,7 @@ pub async fn encrypt_file_with_recipients>( tokio::io::AsyncReadExt::read_to_end(&mut plain_file, &mut plain_file_contents).await?; - age_encrypt_with_recipients(plain_file_contents.as_slice(), &mut cipher_file, recipients).await + age_encrypt_with_recipients(plain_file_contents.as_slice(), &mut cipher_file, &recipients).await } pub async fn decrypt_file_with_private_key>( @@ -344,7 +344,7 @@ mod tests { #[tokio::test] async fn test_verify_password() { - const HASH: &str = "$argon2id$v=19$m=4096,t=3,p=1$c2VjcmV0bHl0ZXN0aW5nZXZlcnl0aGluZw$xEzH8wD/ZjzgZTDTl3YtzMFCfcVa5M5m9y6NfSyB1n4"; + const HASH: &str = "$argon2i$v=19$m=6000,t=3,p=10$WE9xCQmmWdBK82R4SEjoqA$TZSc6PuLd4aWK2x2WAb+Lm9sLySqjK3KLbNyqyQmzPQ"; const PASSWORD: &str = "test"; let result = super::verify_password(PASSWORD.to_string(), HASH.to_string()) @@ -446,7 +446,7 @@ mod tests { const PASSWORD: &str = "test"; const PUBLIC_KEY: &str = "age1t220v5c8ye0pjx99kw8nr57y7a5qlw4ke0wchjuxnr2gcvfzt3hq7fufz0"; - let encrypted = super::encrypt_password_with_recipients(PASSWORD, vec![PUBLIC_KEY]) + let encrypted = super::encrypt_password_with_recipients(PASSWORD, &vec![PUBLIC_KEY]) .await .unwrap(); @@ -460,7 +460,7 @@ mod tests { const PUBLIC_KEY_2: &str = "age1ygswsk38cq9r64um5klqxyvzemfdvx6qe5zed99pdexakwwhpatsgatgpw"; let encrypted = - super::encrypt_password_with_recipients(PASSWORD, vec![PUBLIC_KEY_1, PUBLIC_KEY_2]) + super::encrypt_password_with_recipients(PASSWORD, &vec![PUBLIC_KEY_1, PUBLIC_KEY_2]) .await .unwrap(); diff --git a/core/src/database/mutation/candidate.rs b/core/src/database/mutation/candidate.rs index f6b80f0..4c86b08 100644 --- a/core/src/database/mutation/candidate.rs +++ b/core/src/database/mutation/candidate.rs @@ -1,6 +1,6 @@ -use crate::Mutation; +use crate::{Mutation}; -use ::entity::candidate; +use ::entity::candidate::{self, Model}; use sea_orm::{*}; impl Mutation { @@ -8,13 +8,13 @@ impl Mutation { db: &DbConn, application_id: i32, hashed_password: String, - encrypted_personal_id_number: String, + hashed_personal_id_number: String, pubkey: String, encrypted_priv_key: String ) -> Result { candidate::ActiveModel { application: Set(application_id), - personal_identification_number: Set(encrypted_personal_id_number), + personal_identification_number_hash: Set(hashed_personal_id_number), code: Set(hashed_password), public_key: Set(pubkey), private_key: Set(encrypted_priv_key), @@ -25,4 +25,35 @@ impl Mutation { .insert(db) .await } + + pub async fn add_candidate_details( + db: &DbConn, + user: Model, + name: String, + surname: String, + birthplace: String, + birthdate: String, + address: String, + telephone: String, + citizenship: String, + email: String, + sex: String, + study: String, + ) -> Result { + let mut user: candidate::ActiveModel = user.into(); + user.name = Set(Some(name)); + user.surname = Set(Some(surname)); + user.birthplace = Set(Some(birthplace)); + user.birthdate = Set(None); + user.address = Set(Some(address)); + user.telephone = Set(Some(telephone)); + user.citizenship = Set(Some(citizenship)); + user.email = Set(Some(email)); + user.sex = Set(Some(sex)); + user.study = Set(Some(study)); + + user.updated_at = Set(chrono::offset::Local::now().naive_local()); + + user.update(db).await + } } \ No newline at end of file diff --git a/core/src/database/mutation/mod.rs b/core/src/database/mutation/mod.rs index 633a325..80bf6e9 100644 --- a/core/src/database/mutation/mod.rs +++ b/core/src/database/mutation/mod.rs @@ -1,4 +1,5 @@ pub struct Mutation; pub mod session; -pub mod candidate; \ No newline at end of file +pub mod candidate; +pub mod parent; \ No newline at end of file diff --git a/core/src/database/mutation/parent.rs b/core/src/database/mutation/parent.rs new file mode 100644 index 0000000..80c7a7e --- /dev/null +++ b/core/src/database/mutation/parent.rs @@ -0,0 +1,36 @@ +use crate::Mutation; + +use ::entity::parent::{self, Model}; +use sea_orm::*; + +impl Mutation { + pub async fn create_parent(db: &DbConn, application_id: i32) -> Result { + parent::ActiveModel { + application: Set(application_id), + created_at: Set(chrono::offset::Local::now().naive_local()), + updated_at: Set(chrono::offset::Local::now().naive_local()), + ..Default::default() + } + .insert(db) + .await + } + + pub async fn add_parent_details( + db: &DbConn, + user: Model, + name: String, + surname: String, + telephone: String, + email: String, + ) -> Result { + let mut user: parent::ActiveModel = user.into(); + user.name = Set(Some(name)); + user.surname = Set(Some(surname)); + user.telephone = Set(Some(telephone)); + user.email = Set(Some(email)); + + user.updated_at = Set(chrono::offset::Local::now().naive_local()); + + user.update(db).await + } +} diff --git a/core/src/database/query/admin.rs b/core/src/database/query/admin.rs new file mode 100644 index 0000000..fffdfd0 --- /dev/null +++ b/core/src/database/query/admin.rs @@ -0,0 +1,18 @@ +use crate::Query; + +use ::entity::{candidate, candidate::Entity as Admin}; +use sea_orm::*; + +impl Query { + pub async fn find_admin_by_id(db: &DbConn, id: i32) -> Result, DbErr> { + Admin::find_by_id(id).one(db).await + } + + pub async fn get_all_admin_public_keys(db: &DbConn) -> Result, DbErr> { + let admins = Admin::find().all(db).await?; + + let public_keys = admins.iter().map(|admin| admin.public_key.clone()).collect(); + + Ok(public_keys) + } +} diff --git a/core/src/database/query/candidate.rs b/core/src/database/query/candidate.rs index a8b5552..72ef424 100644 --- a/core/src/database/query/candidate.rs +++ b/core/src/database/query/candidate.rs @@ -41,7 +41,7 @@ mod tests { code: Set("test".to_string()), public_key: Set("test".to_string()), private_key: Set("test".to_string()), - personal_identification_number: Set("test".to_string()), + personal_identification_number_hash: Set("test".to_string()), created_at: Set(chrono::offset::Local::now().naive_local()), updated_at: Set(chrono::offset::Local::now().naive_local()), ..Default::default() diff --git a/core/src/database/query/mod.rs b/core/src/database/query/mod.rs index df4cb4a..73362b8 100644 --- a/core/src/database/query/mod.rs +++ b/core/src/database/query/mod.rs @@ -1,4 +1,5 @@ pub struct Query; pub mod candidate; +pub mod admin; pub mod session; \ No newline at end of file diff --git a/core/src/error.rs b/core/src/error.rs index d145b37..5fb8cfc 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -9,6 +9,8 @@ pub enum ServiceError { DbError, UserNotFoundByJwtId, UserNotFoundBySessionId, + CryptoHashFailed, + CryptoEncryptFailed, } impl ServiceError { @@ -24,6 +26,8 @@ impl ServiceError { ServiceError::DbError => (500, "Database error".to_string()), ServiceError::UserNotFoundByJwtId => (500, "User not found, please contact technical support".to_string()), ServiceError::UserNotFoundBySessionId => (500, "User not found, please contact technical support".to_string()), + ServiceError::CryptoHashFailed => (500, "Crypto hash failed, please contact technical support".to_string()), + ServiceError::CryptoEncryptFailed => (500, "Crypto encryption failed, please contact technical support".to_string()), } } diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index 145c953..1791655 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -1,7 +1,12 @@ +use chrono::NaiveDate; use entity::candidate; -use sea_orm::{DbConn, prelude::Uuid}; +use sea_orm::{prelude::Uuid, DbConn}; -use crate::{Mutation, crypto::{hash_password, self}, error::{ServiceError}, Query}; +use crate::{ + crypto::{self, hash_password}, + error::ServiceError, + Mutation, Query, +}; use super::session_service::SessionService; @@ -19,58 +24,143 @@ impl CandidateService { db: &DbConn, application_id: i32, plain_text_password: &String, - personal_id_number: String - ) -> Result{ + personal_id_number: String, + ) -> Result { // Check if application id starts with 101, 102 or 103 if !CandidateService::is_application_id_valid(application_id) { - return Err(ServiceError::InvalidApplicationId) + return Err(ServiceError::InvalidApplicationId); } // Check if user with that application id already exists - if Query::find_candidate_by_id(db, application_id).await.unwrap().is_some() { - return Err(ServiceError::UserAlreadyExists) + if Query::find_candidate_by_id(db, application_id) + .await + .unwrap() + .is_some() + { + return Err(ServiceError::UserAlreadyExists); } - // TODO: unwrap pro testing.. - let hashed_password = hash_password(plain_text_password.to_string()).await.unwrap(); - let (pubkey, priv_key_plain_text) = crypto::create_identity(); - let encrypted_priv_key = crypto::encrypt_password(priv_key_plain_text, plain_text_password.to_string()).await.unwrap(); + let Ok(hashed_password) = hash_password(plain_text_password.to_string()).await else { + return Err(ServiceError::CryptoHashFailed); + }; - let encrypted_personal_id_number = crypto::encrypt_password_with_recipients( - &personal_id_number, vec![&pubkey] - ).await.unwrap(); + let (pubkey, priv_key_plain_text) = crypto::create_identity(); + + let Ok(encrypted_priv_key) = crypto::encrypt_password(priv_key_plain_text, plain_text_password.to_string()).await else { + return Err(ServiceError::CryptoEncryptFailed); + }; + + let Ok(hashed_personal_id_number) = hash_password(personal_id_number).await else { + return Err(ServiceError::CryptoHashFailed); + }; + /* let encrypted_personal_id_number = crypto::encrypt_password_with_recipients( + &personal_id_number, &vec![&pubkey] + ).await.unwrap(); */ Mutation::create_candidate( db, application_id, hashed_password, - encrypted_personal_id_number, + hashed_personal_id_number, pubkey, - encrypted_priv_key + encrypted_priv_key, ) - .await - .map_err(|_| ServiceError::DbError) + .await + .map_err(|_| ServiceError::DbError) + } + + pub async fn add_user_details( + db: &DbConn, + application_id: i32, + name: String, + surname: String, + birthplace: String, + birthdate: String, + address: String, + telephone: String, + citizenship: String, + email: String, + sex: String, + study: String, + ) -> Result { + let Ok(user) = Query::find_candidate_by_id(db, application_id).await else { + return Err(ServiceError::DbError); + }; + + let Some(user_unwrapped) = user else { + return Err(ServiceError::UserNotFound); + }; + + let Ok(admin_public_keys) = Query::get_all_admin_public_keys(db).await else { + return Err(ServiceError::DbError); + }; + + let mut admin_public_keys_refrence: Vec<&str> = + admin_public_keys.iter().map(|s| &**s).collect(); + + let mut recipients = vec![&*user_unwrapped.public_key]; + + recipients.append(&mut admin_public_keys_refrence); + + let ( + enc_name, + enc_surname, + enc_birthplace, + enc_birthdate, + enc_address, + enc_telephone, + enc_citizenship, + enc_email, + enc_sex, + enc_study, + ) = tokio::join!( + crypto::encrypt_password_with_recipients(&name, &recipients), + crypto::encrypt_password_with_recipients(&surname, &recipients), + crypto::encrypt_password_with_recipients(&birthplace, &recipients), + crypto::encrypt_password_with_recipients(&birthdate, &recipients), + crypto::encrypt_password_with_recipients(&address, &recipients), + crypto::encrypt_password_with_recipients(&telephone, &recipients), + crypto::encrypt_password_with_recipients(&citizenship, &recipients), + crypto::encrypt_password_with_recipients(&email, &recipients), + crypto::encrypt_password_with_recipients(&sex, &recipients), + crypto::encrypt_password_with_recipients(&study, &recipients), + ); + + Mutation::add_candidate_details( + db, + user_unwrapped, + enc_name.unwrap(), + enc_surname.unwrap(), + enc_birthplace.unwrap(), + enc_birthdate.unwrap(), + enc_address.unwrap(), + enc_telephone.unwrap(), + enc_citizenship.unwrap(), + enc_email.unwrap(), + enc_sex.unwrap(), + enc_study.unwrap(), + ) + .await + .map_err(|_| ServiceError::DbError) } pub async fn login( db: &DbConn, user_id: i32, password: String, - ip_addr: String + ip_addr: String, ) -> Result { SessionService::new_session(db, user_id, password, ip_addr).await } - pub async fn auth( - db: &DbConn, - session_uuid: Uuid, - ) -> Result { + pub async fn auth(db: &DbConn, session_uuid: Uuid) -> Result { SessionService::auth_user_session(db, session_uuid).await } fn is_application_id_valid(application_id: i32) -> bool { let s = &application_id.to_string(); - if s.len() <= 3 { // TODO: does the field of study prefix have to be exactly 6 digits? + if s.len() <= 3 { + // TODO: does the field of study prefix have to be exactly 6 digits? return false; } let field_of_study_prefix = &s[0..3]; @@ -78,9 +168,9 @@ impl CandidateService { } } - #[cfg(test)] mod tests { + use chrono::NaiveDate; use sea_orm::{Database, DbConn}; use crate::{crypto, services::candidate_service::CandidateService}; @@ -99,16 +189,17 @@ mod tests { #[cfg(test)] async fn get_memory_sqlite_connection() -> DbConn { use entity::candidate; - use sea_orm::{DbBackend, sea_query::TableCreateStatement, ConnectionTrait}; use sea_orm::Schema; - + use sea_orm::{sea_query::TableCreateStatement, ConnectionTrait, DbBackend}; let base_url = "sqlite::memory:"; let db: DbConn = Database::connect(base_url).await.unwrap(); - + let schema = Schema::new(DbBackend::Sqlite); let stmt: TableCreateStatement = schema.create_table_from_entity(candidate::Entity); - db.execute(db.get_database_backend().build(&stmt)).await.unwrap(); + db.execute(db.get_database_backend().build(&stmt)) + .await + .unwrap(); db } @@ -120,16 +211,56 @@ mod tests { let secret_message = "trnka".to_string(); - - let candidate = CandidateService::create(&db, 103151, &plain_text_password, "".to_string()).await.ok().unwrap(); + let candidate = CandidateService::create(&db, 103151, &plain_text_password, "".to_string()) + .await + .ok() + .unwrap(); - let encrypted_message = crypto::encrypt_password_with_recipients(&secret_message, vec![&candidate.public_key]).await.unwrap(); + let encrypted_message = + crypto::encrypt_password_with_recipients(&secret_message, &vec![&candidate.public_key]) + .await + .unwrap(); - let private_key_plain_text = crypto::decrypt_password(candidate.private_key, plain_text_password).await.unwrap(); + let private_key_plain_text = + crypto::decrypt_password(candidate.private_key, plain_text_password) + .await + .unwrap(); - let decrypted_message = crypto::decrypt_password_with_private_key(&encrypted_message, &private_key_plain_text).await.unwrap(); + let decrypted_message = + crypto::decrypt_password_with_private_key(&encrypted_message, &private_key_plain_text) + .await + .unwrap(); assert_eq!(secret_message, decrypted_message); - } -} \ No newline at end of file + + #[tokio::test] + async fn test_put_user_data() { + let db = get_memory_sqlite_connection().await; + let plain_text_password = "test".to_string(); + let candidate = CandidateService::create(&db, 103151, &plain_text_password, "".to_string()) + .await + .ok() + .unwrap(); + + let candidate = CandidateService::add_user_details( + &db, + candidate.application, + "test".to_string(), + "a".to_string(), + "b".to_string(), + NaiveDate::from_ymd(1999, 1, 1).to_string(), + "test".to_string(), + "test".to_string(), + "test".to_string(), + "test".to_string(), + "test".to_string(), + "test".to_string(), + ) + .await + .ok() + .unwrap(); + + assert!(candidate.name.is_some()); + } +} diff --git a/entity/src/candidate.rs b/entity/src/candidate.rs index 2806e76..81b7f72 100644 --- a/entity/src/candidate.rs +++ b/entity/src/candidate.rs @@ -17,9 +17,9 @@ pub struct Model { pub email: Option, pub sex: Option, pub study: Option, - pub personal_identification_number: String, #[sea_orm(column_type = "Text", nullable)] - pub personal_identification_number_hash: Option, + pub personal_identification_number: Option, + pub personal_identification_number_hash: String, pub public_key: String, pub private_key: String, #[sea_orm(default_value = false)] diff --git a/migration/src/m20221024_121621_create_candidate.rs b/migration/src/m20221024_121621_create_candidate.rs index 8743428..1d0e998 100644 --- a/migration/src/m20221024_121621_create_candidate.rs +++ b/migration/src/m20221024_121621_create_candidate.rs @@ -30,8 +30,8 @@ impl MigrationTrait for Migration { .col(ColumnDef::new(Candidate::Email).string()) .col(ColumnDef::new(Candidate::Sex).string()) .col(ColumnDef::new(Candidate::Study).string()) - .col(ColumnDef::new(Candidate::PersonalIdentificationNumber).string().not_null()) - .col(ColumnDef::new(Candidate::PersonalIdentificationNumberHash).text()) + .col(ColumnDef::new(Candidate::PersonalIdentificationNumber).string()) + .col(ColumnDef::new(Candidate::PersonalIdentificationNumberHash).text().not_null()) .col(ColumnDef::new(Candidate::PublicKey).string().not_null()) .col(ColumnDef::new(Candidate::PrivateKey).string().not_null()) .col(ColumnDef::new(Candidate::IsAdmin).boolean().not_null().default(false)) diff --git a/migration/src/m20221024_134454_insert_sample_admin.rs b/migration/src/m20221024_134454_insert_sample_admin.rs index 56f551c..4bee191 100644 --- a/migration/src/m20221024_134454_insert_sample_admin.rs +++ b/migration/src/m20221024_134454_insert_sample_admin.rs @@ -15,11 +15,13 @@ impl Default for Migration { Self { candidate: candidate::ActiveModel { application: Set(1), - name: Set(Some("Administrátor Pepa".to_owned())), - public_key: Set("lorem ipsum".to_owned()), - private_key: Set("lorem ipsum".to_owned()), - code: Set("$argon2id$v=19$m=4096,t=3,p=1$V2M1eENXcnJvenhqTVF1Yw$xwriCZexpzF7Qtj9lwq0Sw".to_owned()), - personal_identification_number: Set("ADMIN".to_owned()), + name: Set(Some("Admin".to_owned())), + public_key: Set("age1u889gp407hsz309wn09kxx9anl6uns30m27lfwnctfyq9tq4qpus8tzmq5".to_owned()), + // AGE-SECRET-KEY-14QG24502DMUUQDT2SPMX2YXPSES0X8UD6NT0PCTDAT6RH8V5Q3GQGSRXPS + private_key: Set("5KCEGk0ueWVGnu5Xo3rmpLoilcVZ2ZWmwIcdZEJ8rrBNW7jwzZU/XTcTXtk/xyy/zjF8s+YnuVpOklQvX3EC/Sn+ZwyPY3jokM2RNwnZZlnqdehOEV1SMm/Y".to_owned()), + // test + code: Set("$argon2i$v=19$m=6000,t=3,p=10$WE9xCQmmWdBK82R4SEjoqA$TZSc6PuLd4aWK2x2WAb+Lm9sLySqjK3KLbNyqyQmzPQ".to_owned()), + personal_identification_number_hash: Set("ADMIN".to_owned()), created_at: Set(Local::now().naive_local()), updated_at: Set(Local::now().naive_local()), is_admin: Set(true),