Merge pull request #30 from EETagent/put_user_data

WIP: Put user's encrypted personal data
This commit is contained in:
Vojtěch Jungmann 2022-11-06 01:16:09 +01:00 committed by GitHub
commit 02ad8f2ab1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 281 additions and 57 deletions

View file

@ -197,7 +197,7 @@ pub fn create_identity() -> (String, String) {
async fn age_encrypt_with_recipients<W: tokio::io::AsyncWrite + Unpin>(
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<R: tokio::io::AsyncRead + Unpin>(
pub async fn encrypt_password_with_recipients(
password_plain_text: &str,
recipients: Vec<&str>,
recipients: &Vec<&str>,
) -> Result<String, age::EncryptError> {
let mut encrypt_buffer = Vec::new();
@ -287,7 +287,7 @@ pub async fn encrypt_file_with_recipients<P: AsRef<Path>>(
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<P: AsRef<Path>>(
@ -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();

View file

@ -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::Model, DbErr> {
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<candidate::Model, sea_orm::DbErr> {
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
}
}

View file

@ -1,4 +1,5 @@
pub struct Mutation;
pub mod session;
pub mod candidate;
pub mod candidate;
pub mod parent;

View file

@ -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<Model, DbErr> {
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<Model, sea_orm::DbErr> {
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
}
}

View file

@ -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<Option<candidate::Model>, DbErr> {
Admin::find_by_id(id).one(db).await
}
pub async fn get_all_admin_public_keys(db: &DbConn) -> Result<Vec<String>, DbErr> {
let admins = Admin::find().all(db).await?;
let public_keys = admins.iter().map(|admin| admin.public_key.clone()).collect();
Ok(public_keys)
}
}

View file

@ -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()

View file

@ -1,4 +1,5 @@
pub struct Query;
pub mod candidate;
pub mod admin;
pub mod session;

View file

@ -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()),
}
}

View file

@ -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<candidate::Model, ServiceError>{
personal_id_number: String,
) -> Result<candidate::Model, ServiceError> {
// 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<entity::candidate::Model, ServiceError> {
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<String, ServiceError> {
SessionService::new_session(db, user_id, password, ip_addr).await
}
pub async fn auth(
db: &DbConn,
session_uuid: Uuid,
) -> Result<candidate::Model, ServiceError> {
pub async fn auth(db: &DbConn, session_uuid: Uuid) -> Result<candidate::Model, ServiceError> {
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);
}
}
#[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());
}
}

View file

@ -17,9 +17,9 @@ pub struct Model {
pub email: Option<String>,
pub sex: Option<String>,
pub study: Option<String>,
pub personal_identification_number: String,
#[sea_orm(column_type = "Text", nullable)]
pub personal_identification_number_hash: Option<String>,
pub personal_identification_number: Option<String>,
pub personal_identification_number_hash: String,
pub public_key: String,
pub private_key: String,
#[sea_orm(default_value = false)]

View file

@ -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))

View file

@ -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),