Skip to content

Commit

Permalink
feat: [torrust#56] transfer user password from v1.0.0 to v2.0.0
Browse files Browse the repository at this point in the history
We transfer the field `torrust_users.password` to
`torrust_user_authentication.password_hash`.

The hash value is using the PHC string format since v.1.0.0.

In v1.0.0 we were using the hash function "pbkdf2-sha256".
In v2.0.0 we are using "argon2id".

The packages we use to verify password allow using different hash
functions. So we only had to use a different algorithm depending on the
hash id in the PHC string.
  • Loading branch information
josecelano committed Nov 30, 2022
1 parent 01921ed commit d9b4e87
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 22 deletions.
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ tokio = {version = "1.13", features = ["macros", "io-util", "net", "time", "rt-m
lettre = { version = "0.10.0-rc.3", features = ["builder", "tokio1", "tokio1-rustls-tls", "smtp-transport"]}
sailfish = "0.4.0"
regex = "1.6.0"
pbkdf2 = "0.11.0"
2 changes: 1 addition & 1 deletion src/models/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub struct User {
#[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)]
pub struct UserAuthentication {
pub user_id: i64,
pub password_hash: String,
pub password_hash: String
}

#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, sqlx::FromRow)]
Expand Down
37 changes: 27 additions & 10 deletions src/routes/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use actix_web::{web, HttpRequest, HttpResponse, Responder};
use argon2::password_hash::SaltString;
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use pbkdf2::Pbkdf2;
use rand_core::OsRng;
use serde::{Deserialize, Serialize};

Expand All @@ -10,6 +11,7 @@ use crate::config::EmailOnSignup;
use crate::errors::{ServiceError, ServiceResult};
use crate::mailer::VerifyClaims;
use crate::models::response::{OkResponse, TokenResponse};
use crate::models::user::UserAuthentication;
use crate::utils::regex::validate_email_address;
use crate::utils::time::current_time;

Expand Down Expand Up @@ -139,16 +141,7 @@ pub async fn login(payload: web::Json<Login>, app_data: WebAppData) -> ServiceRe
.await
.map_err(|_| ServiceError::InternalServerError)?;

// wrap string of the hashed password into a PasswordHash struct for verification
let parsed_hash = PasswordHash::new(&user_authentication.password_hash)?;

// verify if the user supplied and the database supplied passwords match
if Argon2::default()
.verify_password(payload.password.as_bytes(), &parsed_hash)
.is_err()
{
return Err(ServiceError::WrongPasswordOrUsername);
}
verify_password(payload.password.as_bytes(), &user_authentication)?;

let settings = app_data.cfg.settings.read().await;

Expand All @@ -174,6 +167,30 @@ pub async fn login(payload: web::Json<Login>, app_data: WebAppData) -> ServiceRe
}))
}

/// Verify if the user supplied and the database supplied passwords match
pub fn verify_password(password: &[u8], user_authentication: &UserAuthentication) -> Result<(), ServiceError> {
// wrap string of the hashed password into a PasswordHash struct for verification
let parsed_hash = PasswordHash::new(&user_authentication.password_hash)?;

match parsed_hash.algorithm.as_str() {
"argon2id" => {
if Argon2::default().verify_password(password, &parsed_hash).is_err() {
return Err(ServiceError::WrongPasswordOrUsername);
}

Ok(())
}
"pbkdf2-sha256" => {
if Pbkdf2.verify_password(password, &parsed_hash).is_err() {
return Err(ServiceError::WrongPasswordOrUsername);
}

Ok(())
}
_ => Err(ServiceError::WrongPasswordOrUsername),
}
}

pub async fn verify_token(payload: web::Json<Token>, app_data: WebAppData) -> ServiceResult<impl Responder> {
// verify if token is valid
let _claims = app_data.auth.verify_jwt(&payload.token).await?;
Expand Down
20 changes: 20 additions & 0 deletions src/upgrades/from_v1_0_0_to_v2_0_0/databases/sqlite_v2_0_0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ impl SqliteDatabaseV2_0_0 {
Self { pool: db }
}

pub async fn migrate(&self) {
sqlx::migrate!("migrations/sqlite3")
.run(&self.pool)
.await
.expect("Could not run database migrations.")
}

pub async fn reset_categories_sequence(&self) -> Result<SqliteQueryResult, DatabaseError> {
query("DELETE FROM `sqlite_sequence` WHERE `name` = 'torrust_categories'")
.execute(&self.pool)
Expand Down Expand Up @@ -95,6 +102,19 @@ impl SqliteDatabaseV2_0_0 {
.map(|v| v.last_insert_rowid())
}

pub async fn insert_user_password_hash(
&self,
user_id: i64,
password_hash: &str,
) -> Result<i64, sqlx::Error> {
query("INSERT INTO torrust_user_authentication (user_id, password_hash) VALUES (?, ?)")
.bind(user_id)
.bind(password_hash)
.execute(&self.pool)
.await
.map(|v| v.last_insert_rowid())
}

pub async fn delete_all_database_rows(&self) -> Result<(), DatabaseError> {
query("DELETE FROM torrust_categories;")
.execute(&self.pool)
Expand Down
53 changes: 42 additions & 11 deletions src/upgrades/from_v1_0_0_to_v2_0_0/upgrader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
//! - In v2, the table `torrust_user_profiles` contains two new fields: `bio` and `avatar`.
//! Empty string is used as default value.
use crate::upgrades::from_v1_0_0_to_v2_0_0::databases::sqlite_v1_0_0::SqliteDatabaseV1_0_0;
use crate::upgrades::from_v1_0_0_to_v2_0_0::databases::sqlite_v2_0_0::SqliteDatabaseV2_0_0;
use crate::{
upgrades::from_v1_0_0_to_v2_0_0::databases::sqlite_v1_0_0::SqliteDatabaseV1_0_0,
};
use chrono::prelude::{DateTime, Utc};
use std::{sync::Arc, time::SystemTime};

Expand Down Expand Up @@ -38,6 +40,11 @@ async fn new_db(db_filename: String) -> Arc<SqliteDatabaseV2_0_0> {
Arc::new(SqliteDatabaseV2_0_0::new(&dest_database_connect_url).await)
}

async fn migrate_destiny_database(dest_database: Arc<SqliteDatabaseV2_0_0>) {
println!("Running migrations ...");
dest_database.migrate().await;
}

async fn reset_destiny_database(dest_database: Arc<SqliteDatabaseV2_0_0>) {
println!("Truncating all tables in destiny database ...");
dest_database
Expand Down Expand Up @@ -80,16 +87,10 @@ async fn transfer_categories(
println!("[v2] categories: {:?}", &dest_categories);
}

pub async fn upgrade() {
// Get connections to source adn destiny databases
let source_database = current_db().await;
let dest_database = new_db("data_v2.db".to_string()).await;

println!("Upgrading data from version v1.0.0 to v2.0.0 ...");

reset_destiny_database(dest_database.clone()).await;
transfer_categories(source_database.clone(), dest_database.clone()).await;

async fn transfer_user_data(
source_database: Arc<SqliteDatabaseV1_0_0>,
dest_database: Arc<SqliteDatabaseV2_0_0>,
) {
// Transfer `torrust_users`

let users = source_database.get_users().await.unwrap();
Expand Down Expand Up @@ -147,7 +148,37 @@ pub async fn upgrade() {
"[v2][torrust_user_profiles] user: {:?} {:?} added.",
&user.user_id, &user.username
);

// [v2] table torrust_user_authentication

println!(
"[v2][torrust_user_authentication] adding password hash ({:?}) for user ({:?}) ...",
&user.password, &user.user_id
);

dest_database
.insert_user_password_hash(user.user_id, &user.password)
.await
.unwrap();

println!(
"[v2][torrust_user_authentication] password hash ({:?}) added for user ({:?}).",
&user.password, &user.user_id
);
}
}

pub async fn upgrade() {
// Get connections to source adn destiny databases
let source_database = current_db().await;
let dest_database = new_db("data_v2.db".to_string()).await;

println!("Upgrading data from version v1.0.0 to v2.0.0 ...");

migrate_destiny_database(dest_database.clone()).await;
reset_destiny_database(dest_database.clone()).await;
transfer_categories(source_database.clone(), dest_database.clone()).await;
transfer_user_data(source_database.clone(), dest_database.clone()).await;

// TODO: WIP. We have to transfer data from the 5 tables in V1 and the torrent files in folder `uploads`.
}

0 comments on commit d9b4e87

Please sign in to comment.