diff --git a/Cargo.toml b/Cargo.toml index 3e194a7..c715a8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "plexpass" -version = "0.2.0" +version = "0.3.0" authors = ["bhatti"] edition = "2021" diff --git a/README.md b/README.md index 1d52aed..52485cd 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,8 @@ Symmetric keys are employed for the encryption and decryption of data, while asy The following section delineates the domain model crafted for implementing a password manager: +![](https://raw.githubusercontent.com/bhatti/PlexPass/main/docs/class_diagram.png) + ### 4.1 User A user refers to any individual utilizing the password manager, which may include family members or other users. The accounts information corresponding to each user is secured with a unique key, generated by combining the user’s master password with a device-specific pepper key. @@ -248,10 +250,10 @@ Data repositories act as the intermediary layer between the underlying database Each repository typically adheres to a common Repository interface, ensuring consistency and predictability across different data models. Additionally, they may include bespoke methods that cater to specific requirements of the data they handle. Leveraging Rust’s Diesel library, these repositories enable seamless interactions with relational databases like SQLite, facilitating the efficient execution of complex queries and ensuring the integrity and performance of data operations within the system. -### 6.1 Encryption Implementation with Repositories - ## 7.0 Domain Services ------------------- +Following diagram illustrates major components of the PlexPass application: +![](https://raw.githubusercontent.com/bhatti/PlexPass/main/docs/components.png) The heart of the password manager’s functionality is orchestrated by domain services, each tailored to execute a segment of the application’s core business logic by interacting with data repository interfaces. These services encompass a diverse range of operations integral to the password manager such as: @@ -827,9 +829,6 @@ PlexPass supports multi-factor authentication using [One-Time-Password](https:// Once you registered the security key, you will be prompted for multi-factor authentication as follows: ![](https://raw.githubusercontent.com/bhatti/PlexPass/main/docs/signin_mfa.png) -Note: When registering a security key, PlexPass will also display recovery codes to reset multi-factor authentication if you lose your security key and multi-factor authentication will allow you to enter those instead, e.g: -![](https://raw.githubusercontent.com/bhatti/PlexPass/main/docs/signin_recover.png) - When you have activated multi-factor authenticaion, the command-line tools and REST API will require passing an OTP code that can viewed in the Settings section of the Web application. Alternatively, you can capture the secret key from the Web application and then generate OTP code as follows: ```bash @@ -2135,7 +2134,7 @@ You can generate otp for a particular account using based on CLI as follows: or using secret as follows: ```bash -./target/release/plexpass -j true --master-username eddie --master-password *** generate-account-otp --otp-secret "JBSWY3DPEHPK3PXP" +./target/release/plexpass -j true --master-username eddie --master-password *** generate-otp --otp-secret "JBSWY3DPEHPK3PXP" ``` An OTP is also defined automatically for each user and You can generate otp for the user using based on CLI as follows: @@ -2154,6 +2153,15 @@ curl -v -k --header "Content-Type: application/json; charset=UTF-8" https://localhost:8443/api/v1/vaults/$vault_id/accounts/$account_id ``` + +You can generate an otp for a specific account using: + +```bash +curl -v -k --header "Content-Type: application/json; charset=UTF-8" + --header "Authorization: Bearer $AUTH_TOKEN" + https://localhost:8443/api/v1/accounts/{account-id}/otp/generate +``` + or, generate otp using a secret ```bash @@ -2185,7 +2193,74 @@ docker run -e DEVICE_PEPPER_KEY=$DEVICE_PEPPER_KEY -e RUST_BACKTRACE=1 -e DATA_DIR=/data -v $PARENT_DIR/PlexPassData:/data plexpass -j true --master-username eddie --master-password *** generate-user-otp ``` -### 11.33 Security Dashboad and Auditing +### 11.33 Resetting multi-factor authentication + +PlexPass Web application allows registering security keys for multi-factor authentication but you can reset it using recovery codes as follows: + +#### 11.33.1 Command Line + +You can reset multi-factor authentication using based on CLI as follows: + +First retrieve otp-code based on user's otp-secret (that can be viewed in the Web UI or from previous API/CLI): +```bash +otp=`./target/release/plexpass -j true generate-otp --otp-secret ***|jq '.otp_code'` +``` + +Then use the otp and recovery-code to reset the multi-factor-authentication as follows + +```bash +./target/release/plexpass -j true --master-username charlie --master-password *** --otp-code $otp reset-multi-factor-authentication --recovery-code *** +``` + +#### 11.33.2 REST API + +First generate OTP with otp-secret such as: + +```bash +curl -k --header "Content-Type: application/json; charset=UTF-8" https://localhost:8443/api/v1/otp/generate -d '{"otp_secret": "**"}' +``` +which would return otp, e.g., +```json +{"otp_code":361509} +``` + +Then signin with username, password and otp-code: +```bash +curl -v -k https://localhost:8443/api/v1/auth/signin + --header "Content-Type: application/json; charset=UTF-8" + -d '{"username": "bob", "master_password": "**", "otp_code": 123}' +``` + +The signin API will return access token in the response header and you can then use it for resetting multi-factor settings: +```bash +curl -v -k --header "Content-Type: application/json; charset=UTF-8" + --header "Authorization: Bearer $AUTH_TOKEN" + https://localhost:8443/api/v1/auth/reset_mfa -d '{"recovery_code": "***"}' +``` + +#### 11.33.3 Docker CLI + +First retrieve otp-code based on user's otp-secret (that can be viewed in the Web UI or from previous API/CLI): + +```bash +otp=`docker run -e DEVICE_PEPPER_KEY=$DEVICE_PEPPER_KEY -e RUST_BACKTRACE=1 + -e DATA_DIR=/data -v $PARENT_DIR/PlexPassData:/data plexpass -j true generate-otp --otp-secret ***|jq '.otp_code'` +``` + +Then use the otp and recovery-code to reset the multi-factor-authentication as follows + +```bash +docker run -e DEVICE_PEPPER_KEY=$DEVICE_PEPPER_KEY -e RUST_BACKTRACE=1 \ + -e DATA_DIR=/data -v $PARENT_DIR/PlexPassData:/data plexpass -j true --master-username charlie --master-password *** \ + --otp-code $otp reset-multi-factor-authentication --recovery-code *** +``` + +#### 11.33.4 Web UI +When registering a security key, PlexPass will display recovery codes to reset multi-factor authentication if you lose your security key and you can reset in the Web application upon signin, e.g., +![](https://raw.githubusercontent.com/bhatti/PlexPass/main/docs/signin_recover.png) + + +### 11.34 Security Dashboad and Auditing The PlexPass web application includes a security dashboard to monitor health of all passwords and allows users to view audit logs for all changes to their accounts, e.g., @@ -2217,4 +2292,5 @@ The design principles and architectural framework outlined above showcase PlexPa 15. **Control over Data**: Users have complete control over their data, including how it’s stored and backed up. 16. **Potentially Lower Risk of Service Shutdown**: Since the data is stored locally, the user’s access to their passwords is not contingent on the continued operation of a third-party service. 17. **Multi-Factor and Local Authentication**: PlexPass supports Multi-Factor Authentication based on One-Time-Passwords (OTP), FIDO, WebAuthN, and YubiKey for authentication. + In summary, PlexPass, with its extensive features, represents a holistic and advanced approach to password management while adhering to the latest industry standards for secure access. diff --git a/docs/class_diagram.png b/docs/class_diagram.png new file mode 100644 index 0000000..683f5bd Binary files /dev/null and b/docs/class_diagram.png differ diff --git a/docs/components.png b/docs/components.png new file mode 100644 index 0000000..1634f78 Binary files /dev/null and b/docs/components.png differ diff --git a/docs/plexpass.drawio b/docs/plexpass.drawio index bb47791..1e088b6 100644 --- a/docs/plexpass.drawio +++ b/docs/plexpass.drawio @@ -1,29 +1,29 @@ - + - + - + - + - + - + - + - + @@ -32,13 +32,13 @@ - + - + - + @@ -46,116 +46,490 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/functional_tests/01_user_tests.py b/functional_tests/01_user_tests.py index 6eb40ca..d1e62b5 100755 --- a/functional_tests/01_user_tests.py +++ b/functional_tests/01_user_tests.py @@ -89,6 +89,5 @@ def test_07_get_user_without_token(self): resp = requests.get(SERVER + '/api/v1/users/' + USER_ID, headers = headers, verify = False) self.assertEqual(401, resp.status_code) - if __name__ == '__main__': unittest.main() diff --git a/functional_tests/11_reset_mfa_test.py b/functional_tests/11_reset_mfa_test.py new file mode 100755 index 0000000..0f4c85e --- /dev/null +++ b/functional_tests/11_reset_mfa_test.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +import unittest +import requests +import time +import json + +USER_ID = '' +JWT_TOKEN = '' +SERVER='https://localhost:8443' + +class UsersTest(unittest.TestCase): + def test_08_reset_mfa(self): + headers = { + 'Content-Type': 'application/json', + } + data = {'otp_secret': 'O4YLRYMTIYDAJH2JUHGNQKM4RMVH3T63LZ3VYQQ6O6R3TER2MRFA'} + resp = requests.post(SERVER + '/api/v1/otp/generate', json = data, headers = headers, verify = False) + self.assertEqual(200, resp.status_code) + otp = json.loads(resp.text)['otp_code'] + + data = {'username': 'bob@cat.us', 'master_password': 'Goose$bob@cat.us$Goat551', 'otp_code': otp} + resp = requests.post(SERVER + '/api/v1/auth/signin', json = data, headers = headers, verify = False) + self.assertEqual(200, resp.status_code) + JWT_TOKEN = resp.headers.get('access_token') + headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + JWT_TOKEN, + } + data = {'recovery_code': 'MKivPbLKJRqX'} + resp = requests.post(SERVER + '/api/v1/auth/reset_mfa', json = data, headers = headers, verify = False) + self.assertEqual(200, resp.status_code) + +if __name__ == '__main__': + unittest.main() diff --git a/functional_tests/134_reset_mfa_command.sh b/functional_tests/134_reset_mfa_command.sh new file mode 100755 index 0000000..38e616e --- /dev/null +++ b/functional_tests/134_reset_mfa_command.sh @@ -0,0 +1,6 @@ +#!/bin/bash -e +source env.sh + +otp=`../target/release/plexpass -j true generate-otp --otp-secret DA6ZIW3ZIR7H32I2PX5L76A4S5TVCEDEDLUMFS3WKEZEULISS6MA|jq '.otp_code'` +../target/release/plexpass -j true --master-username charlie --master-password Cru5h_rfIt:v_Bk --otp-code $otp reset-multi-factor-authentication --recovery-code DiUpaIEOibSy + diff --git a/functional_tests/334_reset_mfa_docker.sh b/functional_tests/334_reset_mfa_docker.sh new file mode 100755 index 0000000..29e67e0 --- /dev/null +++ b/functional_tests/334_reset_mfa_docker.sh @@ -0,0 +1,6 @@ +#!/bin/bash -e +source env.sh + +otp=`docker run -e DEVICE_PEPPER_KEY=$DEVICE_PEPPER_KEY -e DATA_DIR=/data -v $PARENT_DIR/PlexPassData:/data -v $CWD:/files plexpass -j true generate-otp --otp-secret DA6ZIW3ZIR7H32I2PX5L76A4S5TVCEDEDLUMFS3WKEZEULISS6MA|jq '.otp_code'` + +docker run -e DEVICE_PEPPER_KEY=$DEVICE_PEPPER_KEY -e DATA_DIR=/data -v $PARENT_DIR/PlexPassData:/data -v $CWD:/files plexpass -j true ----master-username charlie --master-password Cru5h_rfIt:v_Bk --otp-code $otp reset-multi-factor-authentication --recovery-code DiUpaIEOibSy diff --git a/src/command.rs b/src/command.rs index f8d0a44..6467eba 100644 --- a/src/command.rs +++ b/src/command.rs @@ -35,6 +35,7 @@ pub mod analyze_all_vaults_passwords_command; pub mod search_users_command; pub mod generate_account_otp_command; pub mod generate_user_otp_command; +pub mod generate_otp_command; pub mod reset_mfa_command; pub mod generate_api_token; pub mod asymmetric_user_decrypt_command; diff --git a/src/command/asymmetric_decrypt_command.rs b/src/command/asymmetric_decrypt_command.rs index 93000a1..b0b70e0 100644 --- a/src/command/asymmetric_decrypt_command.rs +++ b/src/command/asymmetric_decrypt_command.rs @@ -5,12 +5,12 @@ use crate::service::locator::ServiceLocator; /// Asymmetric decryption command. pub async fn execute( - config: PassConfig, + config: &PassConfig, secret_key: &str, in_path: &PathBuf, out_path: &PathBuf, ) -> PassResult<()> { - let service_locator = ServiceLocator::new(&config).await?; + let service_locator = ServiceLocator::new(config).await?; let data = fs::read(in_path)?; let res = service_locator.encryption_service.asymmetric_decrypt( secret_key, diff --git a/src/command/asymmetric_encrypt_command.rs b/src/command/asymmetric_encrypt_command.rs index fec35c0..6001561 100644 --- a/src/command/asymmetric_encrypt_command.rs +++ b/src/command/asymmetric_encrypt_command.rs @@ -5,12 +5,12 @@ use crate::service::locator::ServiceLocator; /// Asymmetric encryption command. pub async fn execute( - config: PassConfig, + config: &PassConfig, public_key: &str, in_path: &PathBuf, out_path: &PathBuf, ) -> PassResult<()> { - let service_locator = ServiceLocator::new(&config).await?; + let service_locator = ServiceLocator::new(config).await?; let data = fs::read(in_path)?; let res = service_locator.encryption_service.asymmetric_encrypt( public_key, diff --git a/src/command/create_user_command.rs b/src/command/create_user_command.rs index 39473e7..456ed33 100644 --- a/src/command/create_user_command.rs +++ b/src/command/create_user_command.rs @@ -5,9 +5,9 @@ use crate::service::locator::ServiceLocator; /// Create and register a new user. pub async fn execute( - config: PassConfig, + config: &PassConfig, user: &User, master_password: &str) -> PassResult { - let service_locator = ServiceLocator::new(&config).await?; + let service_locator = ServiceLocator::new(config).await?; service_locator.user_service.register_user(user, master_password, HashMap::new()).await } diff --git a/src/command/generate_account_otp_command.rs b/src/command/generate_account_otp_command.rs index 1cc1b9b..3973795 100644 --- a/src/command/generate_account_otp_command.rs +++ b/src/command/generate_account_otp_command.rs @@ -5,19 +5,10 @@ use crate::domain::models::{PassResult}; /// Generate an otp for account. pub async fn execute( args_ctx: &ArgsContext, - account_id: &Option, - otp_secret : &Option, + account_id: &str, ) -> PassResult { - if let Some(otp_secret) = otp_secret { - return args_ctx.service_locator.otp_service.generate_otp( - otp_secret).await; - } - - if let Some(account_id) = account_id { - let account = AccountResponse::new( - &args_ctx.service_locator.account_service.get_account( - &args_ctx.user_context, account_id).await?); - return Ok(account.generated_otp.unwrap_or(0)); - } - Ok(0) + let account = AccountResponse::new( + &args_ctx.service_locator.account_service.get_account( + &args_ctx.user_context, account_id).await?); + Ok(account.generated_otp.unwrap_or(0)) } diff --git a/src/command/generate_otp_command.rs b/src/command/generate_otp_command.rs new file mode 100644 index 0000000..b89a610 --- /dev/null +++ b/src/command/generate_otp_command.rs @@ -0,0 +1,12 @@ +use crate::domain::models::{PassConfig, PassResult}; +use crate::service::locator::ServiceLocator; + +/// Generate an otp for secret +pub async fn execute( + config: &PassConfig, + otp_secret: &str, +) -> PassResult { + let service_locator = ServiceLocator::new(config).await?; + service_locator.otp_service.generate_otp( + otp_secret).await +} diff --git a/src/command/generate_password_command.rs b/src/command/generate_password_command.rs index 68b1d94..9876567 100644 --- a/src/command/generate_password_command.rs +++ b/src/command/generate_password_command.rs @@ -3,9 +3,9 @@ use crate::service::locator::ServiceLocator; /// Generate a password. pub async fn execute( - config: PassConfig, + config: &PassConfig, policy: &PasswordPolicy, ) -> PassResult> { - let service_locator = ServiceLocator::new(&config).await?; + let service_locator = ServiceLocator::new(config).await?; Ok(service_locator.password_service.generate_password(policy).await) } diff --git a/src/command/generate_private_public_keys_command.rs b/src/command/generate_private_public_keys_command.rs index dbe3f97..91f0523 100644 --- a/src/command/generate_private_public_keys_command.rs +++ b/src/command/generate_private_public_keys_command.rs @@ -3,10 +3,10 @@ use crate::service::locator::ServiceLocator; /// Generate private and public keys for Asymmetric encryption. pub async fn execute( - config: PassConfig, + config: &PassConfig, password: &Option, ) -> PassResult<(String, String)> { - let service_locator = ServiceLocator::new(&config).await?; + let service_locator = ServiceLocator::new(config).await?; let (sk, pk) = service_locator.encryption_service.generate_private_public_keys(password.clone())?; Ok((sk, pk)) } diff --git a/src/command/generate_user_otp_command.rs b/src/command/generate_user_otp_command.rs index 7e99de1..6259db6 100644 --- a/src/command/generate_user_otp_command.rs +++ b/src/command/generate_user_otp_command.rs @@ -4,12 +4,7 @@ use crate::domain::models::{PassResult}; /// Generate an otp for user. pub async fn execute( args_ctx: &ArgsContext, - otp_secret : &Option, ) -> PassResult { - if let Some(otp_secret) = otp_secret { - return args_ctx.service_locator.otp_service.generate_otp( - otp_secret).await; - } args_ctx.service_locator.user_service.generate_user_otp( &args_ctx.user_context).await } diff --git a/src/command/password_compromised_command.rs b/src/command/password_compromised_command.rs index ee0dcfc..b8f5b3f 100644 --- a/src/command/password_compromised_command.rs +++ b/src/command/password_compromised_command.rs @@ -3,9 +3,9 @@ use crate::service::locator::ServiceLocator; /// Check if a password is compromised. pub async fn execute( - config: PassConfig, + config: &PassConfig, password: &str, ) -> PassResult { - let service_locator = ServiceLocator::new(&config).await?; + let service_locator = ServiceLocator::new(config).await?; service_locator.password_service.password_compromised(password).await } diff --git a/src/command/password_strength_command.rs b/src/command/password_strength_command.rs index 1e256cb..aa196a6 100644 --- a/src/command/password_strength_command.rs +++ b/src/command/password_strength_command.rs @@ -3,9 +3,9 @@ use crate::service::locator::ServiceLocator; /// Checks password strength. pub async fn execute( - config: PassConfig, + config: &PassConfig, password: &str, ) -> PassResult { - let service_locator = ServiceLocator::new(&config).await?; + let service_locator = ServiceLocator::new(config).await?; service_locator.password_service.password_info(password).await } diff --git a/src/command/startup_command.rs b/src/command/startup_command.rs index d2276b7..30d0ffe 100644 --- a/src/command/startup_command.rs +++ b/src/command/startup_command.rs @@ -153,10 +153,11 @@ fn config_services(service_config: &mut web::ServiceConfig) { service_config.service(web::resource("/health").to(health)); // API Controllers - // user controller + // auth and user controller service_config.service(auth_api_controller::signup_user) .service(auth_api_controller::signin_user) .service(auth_api_controller::signout_user) + .service(auth_api_controller::recover_mfa) .service(user_api_controller::get_user) .service(user_api_controller::update_user) .service(user_api_controller::search_usernames) @@ -209,6 +210,8 @@ fn config_services(service_config: &mut web::ServiceConfig) { // otp-controller service_config.service(otp_api_controller::generate_otp); + service_config.service(otp_api_controller::generate_user_otp); + service_config.service(otp_api_controller::generate_account_otp); // audit-logs-controller service_config.service(audit_api_controller::audit_logs); diff --git a/src/command/symmetric_decrypt_command.rs b/src/command/symmetric_decrypt_command.rs index 4199f0a..7d8f7ad 100644 --- a/src/command/symmetric_decrypt_command.rs +++ b/src/command/symmetric_decrypt_command.rs @@ -5,12 +5,12 @@ use crate::service::locator::ServiceLocator; /// Symmetric decryption. pub async fn execute( - config: PassConfig, + config: &PassConfig, symmetric_key: &str, in_path: &PathBuf, out_path: &PathBuf, ) -> PassResult<()> { - let service_locator = ServiceLocator::new(&config).await?; + let service_locator = ServiceLocator::new(config).await?; let data = fs::read(in_path)?; let res = service_locator.encryption_service.symmetric_decrypt( "", diff --git a/src/command/symmetric_encrypt_command.rs b/src/command/symmetric_encrypt_command.rs index 9dbd74e..ef7ebca 100644 --- a/src/command/symmetric_encrypt_command.rs +++ b/src/command/symmetric_encrypt_command.rs @@ -5,12 +5,12 @@ use crate::service::locator::ServiceLocator; /// Symmetric encryption. pub async fn execute( - config: PassConfig, + config: &PassConfig, symmetric_key: &str, in_path: &PathBuf, out_path: &PathBuf, ) -> PassResult<()> { - let service_locator = ServiceLocator::new(&config).await?; + let service_locator = ServiceLocator::new(config).await?; let data = fs::read(in_path)?; let res = service_locator.encryption_service.symmetric_encrypt( "", diff --git a/src/command/update_user_command.rs b/src/command/update_user_command.rs index 2ac8da7..a0d6598 100644 --- a/src/command/update_user_command.rs +++ b/src/command/update_user_command.rs @@ -6,7 +6,7 @@ pub async fn execute( args_ctx: &ArgsContext, user: &mut User) -> PassResult { user.user_id = args_ctx.user.user_id.clone(); - user.version = args_ctx.user.version.clone(); + user.version = args_ctx.user.version; args_ctx.service_locator.user_service.update_user( &args_ctx.user_context, user).await } diff --git a/src/controller/auth_api_controller.rs b/src/controller/auth_api_controller.rs index ad2b46e..fd39851 100644 --- a/src/controller/auth_api_controller.rs +++ b/src/controller/auth_api_controller.rs @@ -1,7 +1,7 @@ -use actix_web::{Error, HttpRequest, HttpResponse, post, web}; +use actix_web::{Error, HttpRequest, HttpResponse, post, Responder, web}; use std::collections::HashMap; use serde::Serialize; -use crate::controller::models::{Authenticated, SigninUserRequest, SigninUserResponse, SignupUserRequest, SignupUserResponse}; +use crate::controller::models::{Authenticated, QueryRecoveryCode, SigninUserRequest, SigninUserResponse, SignupUserRequest, SignupUserResponse}; use crate::dao::models::CONTEXT_IP_ADDRESS; use crate::domain::error::PassError; use crate::domain::models::{SessionStatus, UserToken}; @@ -32,22 +32,14 @@ pub async fn signin_user( payload: web::Json, ) -> Result { let context = build_context_with_ip_address(req); - let (ctx, user, token, session_status) = service_locator + let (ctx, _user, token, session_status) = service_locator .auth_service - .signin_user(&payload.username, &payload.master_password, payload.otp_code.clone(), context) + .signin_user(&payload.username, &payload.master_password, payload.otp_code, context) .await?; if session_status == SessionStatus::RequiresMFA { - if let Some(otp_code) = &payload.otp_code { - if !user.verify_otp(otp_code.clone()) { - return Err( - Error::from( - PassError::authentication("could not verify otp-code, please use the Web application for generated otp-code."))); - } - } else { - return Err( - Error::from( - PassError::authentication("signin requires multi-factor authentication, please add parameter for otp_code based that can be seen form the Web application."))); - } + return Err( + Error::from( + PassError::authentication("signin requires multi-factor authentication, please add parameter for otp_code based that can be seen form the Web application."))); } let res = SigninUserResponse::new(&ctx.user_id); ok_response_with_token(&service_locator, &token, res) @@ -65,6 +57,17 @@ pub async fn signout_user( Ok(HttpResponse::Ok().finish()) } +#[post("/api/v1/auth/reset_mfa")] +pub async fn recover_mfa( + service_locator: web::Data, + params: web::Json, + auth: Authenticated, +) -> Result { + service_locator.auth_service.reset_mfa_keys( + &auth.context, ¶ms.recovery_code, &auth.user_token.login_session).await?; + Ok(HttpResponse::Ok().finish()) +} + fn build_context_with_ip_address(req: HttpRequest) -> HashMap { let mut context = HashMap::new(); if let Some(addr) = req.peer_addr() { diff --git a/src/controller/models.rs b/src/controller/models.rs index 79f4a9a..42dafe2 100644 --- a/src/controller/models.rs +++ b/src/controller/models.rs @@ -446,21 +446,21 @@ impl UserResponse { }); UserResponse { user_id: user.user_id.clone(), - version: user.version.clone(), + version: user.version, username: user.username.clone(), roles: user.roles.clone(), name: user.name.clone(), email: user.email.clone(), locale: user.locale.clone(), - light_mode: user.light_mode.clone(), + light_mode: user.light_mode, icon: user.icon.clone(), - notifications: user.notifications.clone(), + notifications: user.notifications, hardware_keys, otp_secret: user.otp_secret.clone(), generated_otp: TOTP::new(&user.otp_secret).generate(30, Utc::now().timestamp() as u64).to_string(), attributes: user.attributes.clone(), - created_at: user.created_at.clone(), - updated_at: user.updated_at.clone(), + created_at: user.created_at, + updated_at: user.updated_at, } } } diff --git a/src/controller/otp_api_controller.rs b/src/controller/otp_api_controller.rs index df5b54c..0d5dc5f 100644 --- a/src/controller/otp_api_controller.rs +++ b/src/controller/otp_api_controller.rs @@ -1,6 +1,6 @@ use actix_web::{Error, HttpResponse, post, web}; use serde_json::json; -use crate::controller::models::{Authenticated, GenerateOTPRequest}; +use crate::controller::models::{AccountResponse, Authenticated, GenerateOTPRequest}; use crate::service::locator::ServiceLocator; #[post("/api/v1/otp/generate")] @@ -17,6 +17,22 @@ pub async fn generate_otp( Ok(HttpResponse::Ok().json(data)) } +#[post("/api/v1/accounts/{account_id}/otp/generate")] +pub async fn generate_account_otp( + service_locator: web::Data, + path: web::Path, + auth: Authenticated, +) -> Result { + let account_id = path.into_inner(); + let account = AccountResponse::new( + &service_locator.account_service.get_account( + &auth.context, &account_id).await?); + let data = json!({ + "otp_code": account.generated_otp.unwrap_or(0), + }); + Ok(HttpResponse::Ok().json(data)) +} + #[post("/api/v1/users/otp/generate")] pub async fn generate_user_otp( service_locator: web::Data, diff --git a/src/dao/login_session_repository_impl.rs b/src/dao/login_session_repository_impl.rs index f43d44c..4add96d 100644 --- a/src/dao/login_session_repository_impl.rs +++ b/src/dao/login_session_repository_impl.rs @@ -82,7 +82,7 @@ impl LoginSessionRepository for LoginSessionRepositoryImpl { if !session.mfa_required { return Err(PassError::validation("mfa is not required", None)); } - if !session.update_mfa() { + if !session.verified_mfa() { return Err(PassError::validation("mfa cannot be updated for stale session", None)); } diff --git a/src/dao/models.rs b/src/dao/models.rs index 4866488..335a0a6 100644 --- a/src/dao/models.rs +++ b/src/dao/models.rs @@ -349,13 +349,13 @@ impl LoginSessionEntity { login_session_id: login_session.login_session_id.clone(), user_id: login_session.user_id.clone(), username: login_session.username.clone(), - roles: login_session.roles.clone(), + roles: login_session.roles, source: login_session.source.clone(), ip_address: login_session.ip_address.clone(), - mfa_required: login_session.mfa_required.clone(), - mfa_verified_at: login_session.mfa_verified_at.clone(), + mfa_required: login_session.mfa_required, + mfa_verified_at: login_session.mfa_verified_at, created_at: Utc::now().naive_utc(), - signed_out_at: login_session.signed_out_at.clone(), + signed_out_at: login_session.signed_out_at, } } pub fn to_login_session(&self) -> LoginSession { @@ -363,13 +363,13 @@ impl LoginSessionEntity { login_session_id: self.login_session_id.clone(), user_id: self.user_id.clone(), username: self.username.clone(), - roles: self.roles.clone(), + roles: self.roles, source: self.source.clone(), ip_address: self.ip_address.clone(), - mfa_required: self.mfa_required.clone(), - mfa_verified_at: self.mfa_verified_at.clone(), - created_at: self.created_at.clone(), - signed_out_at: self.signed_out_at.clone(), + mfa_required: self.mfa_required, + mfa_verified_at: self.mfa_verified_at, + created_at: self.created_at, + signed_out_at: self.signed_out_at, } } } diff --git a/src/domain/args.rs b/src/domain/args.rs index 4ffa271..b72dab1 100644 --- a/src/domain/args.rs +++ b/src/domain/args.rs @@ -442,18 +442,17 @@ pub enum CommandActions { #[arg(long)] q: String, }, + GenerateOTP { + /// otp_secret + #[arg(long)] + otp_secret: String, + }, GenerateAccountOTP { /// account-id #[arg(long)] - account_id: Option, - /// otp_secret - #[arg(long)] - otp_secret: Option, + account_id: String, }, GenerateUserOTP { - /// otp_secret - #[arg(long)] - otp_secret: Option, }, GenerateAPIToken { /// duration of token @@ -761,9 +760,9 @@ impl ArgsContext { pub async fn auth_new(config: &PassConfig, args: &Args) -> PassResult { let master_username = args.master_username.clone().expect("Please specify username with --master-username."); let master_password = args.master_password.clone().expect("Please specify master password with --master-password."); - let service_locator = ServiceLocator::new(&config).await?; + let service_locator = ServiceLocator::new(config).await?; let (ctx, user, token, session_status) = service_locator.auth_service.signin_user( - &master_username, &master_password, args.otp_code.clone(), HashMap::new()).await?; + &master_username, &master_password, args.otp_code, HashMap::new()).await?; if session_status == SessionStatus::RequiresMFA { return Err(PassError::authentication( diff --git a/src/domain/models.rs b/src/domain/models.rs index 1db7545..1ed7f62 100644 --- a/src/domain/models.rs +++ b/src/domain/models.rs @@ -261,7 +261,7 @@ impl LoginSession { } pub fn check_status(&self) -> SessionStatus { - if self.signed_out_at != None { + if self.signed_out_at.is_some() { return SessionStatus::Invalid; } let now = Utc::now().naive_utc(); @@ -269,7 +269,7 @@ impl LoginSession { if self.created_at < eight_hours_ago { return SessionStatus::Invalid; } - if self.mfa_required && self.mfa_verified_at == None { + if self.mfa_required && self.mfa_verified_at.is_none() { if self.expired_mfa() { return SessionStatus::Invalid; } @@ -281,10 +281,10 @@ impl LoginSession { pub fn expired_mfa(&self) -> bool { let now = Utc::now().naive_utc(); let three_minutes_ago = now - Duration::minutes(3); - return self.created_at < three_minutes_ago; + self.created_at < three_minutes_ago } - pub fn update_mfa(&mut self) -> bool { + pub fn verified_mfa(&mut self) -> bool { if !self.mfa_required { return false; } @@ -536,7 +536,7 @@ impl User { // one. Otherwise it's ignored. That is why it is safe to // iterate this over the full list. let mut keys = self.hardware_keys.clone().unwrap_or_default(); - for (_, key) in &mut keys { + for key in keys.values_mut() { key.key.update_credential(auth_result); } self.hardware_keys = Some(keys); @@ -544,7 +544,7 @@ impl User { pub fn mfa_required(&self) -> bool { match &self.hardware_keys { - Some(keys) => keys.len() > 0, + Some(keys) => !keys.is_empty(), None => false, } } @@ -565,7 +565,7 @@ impl User { pub fn update(&mut self, other: &User) { self.version = other.version; // roles must be explicitly copied - if other.roles != None { + if other.roles.is_some() { self.roles = other.roles.clone(); } self.name = other.name.clone(); diff --git a/src/main.rs b/src/main.rs index 38503c6..04221c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use std::io::Write; use clap::Parser; use env_logger::Builder; -use plexpass::command::{analyze_all_vaults_passwords_command, analyze_vault_passwords_command, asymmetric_decrypt_command, asymmetric_encrypt_command, asymmetric_user_decrypt_command, asymmetric_user_encrypt_command, create_account_command, create_category_command, create_user_command, create_vault_command, delete_account_command, delete_category_command, delete_user_command, delete_vault_command, email_compromised_command, export_accounts_command, generate_account_otp_command, generate_api_token, generate_password_command, generate_private_public_keys_command, generate_user_otp_command, get_account_command, get_accounts_command, get_categories_command, get_user_command, get_vault_command, get_vaults_command, import_accounts_command, password_compromised_command, password_strength_command, query_audit_logs_command, reset_mfa_command, search_users_command, share_account_command, share_vault_command, startup_command, symmetric_decrypt_command, symmetric_encrypt_command, update_account_command, update_user_command, update_vault_command}; +use plexpass::command::{analyze_all_vaults_passwords_command, analyze_vault_passwords_command, asymmetric_decrypt_command, asymmetric_encrypt_command, asymmetric_user_decrypt_command, asymmetric_user_encrypt_command, create_account_command, create_category_command, create_user_command, create_vault_command, delete_account_command, delete_category_command, delete_user_command, delete_vault_command, email_compromised_command, export_accounts_command, generate_account_otp_command, generate_api_token, generate_otp_command, generate_password_command, generate_private_public_keys_command, generate_user_otp_command, get_account_command, get_accounts_command, get_categories_command, get_user_command, get_vault_command, get_vaults_command, import_accounts_command, password_compromised_command, password_strength_command, query_audit_logs_command, reset_mfa_command, search_users_command, share_account_command, share_vault_command, startup_command, symmetric_decrypt_command, symmetric_encrypt_command, update_account_command, update_user_command, update_vault_command}; use plexpass::domain::args::{Args, CommandActions}; use crate::plexpass::domain::models::PassConfig; @@ -82,7 +82,7 @@ async fn main() -> std::io::Result<()> { let master_password = args.master_password.clone().expect("Please specify master password with --master-password"); let user = args.to_user().expect("Failed to initialize user"); let _ = create_user_command::execute( - config, + &config, &user, &master_password).await.expect("failed to create user"); if args.json_output.unwrap_or(false) { @@ -305,7 +305,7 @@ async fn main() -> std::io::Result<()> { } CommandActions::GeneratePrivatePublicKeys { password } => { let (sk, pk) = generate_private_public_keys_command::execute( - config, + &config, password, ).await.expect("failed to generate private and public keys"); if args.json_output.unwrap_or(false) { @@ -340,7 +340,7 @@ async fn main() -> std::io::Result<()> { } CommandActions::AsymmetricEncrypt { public_key, in_path, out_path } => { asymmetric_encrypt_command::execute( - config, + &config, public_key, in_path, out_path, @@ -351,7 +351,7 @@ async fn main() -> std::io::Result<()> { } CommandActions::AsymmetricDecrypt { secret_key, in_path, out_path } => { asymmetric_decrypt_command::execute( - config, + &config, secret_key, in_path, out_path, @@ -362,7 +362,7 @@ async fn main() -> std::io::Result<()> { } CommandActions::SymmetricEncrypt { secret_key, in_path, out_path } => { symmetric_encrypt_command::execute( - config, + &config, secret_key, in_path, out_path, @@ -373,7 +373,7 @@ async fn main() -> std::io::Result<()> { } CommandActions::SymmetricDecrypt { secret_key, in_path, out_path } => { symmetric_decrypt_command::execute( - config, + &config, secret_key, in_path, out_path, @@ -413,7 +413,7 @@ async fn main() -> std::io::Result<()> { } CommandActions::GeneratePassword { .. } => { let policy = args.to_policy().expect("could not build password policy"); - let password = generate_password_command::execute(config, &policy) + let password = generate_password_command::execute(&config, &policy) .await.expect("could not generate password"); if args.json_output.unwrap_or(false) { let res = HashMap::from([("password", password)]); @@ -423,7 +423,7 @@ async fn main() -> std::io::Result<()> { } } CommandActions::PasswordCompromised { password } => { - let compromised = password_compromised_command::execute(config, password) + let compromised = password_compromised_command::execute(&config, password) .await.expect("could not check password"); if args.json_output.unwrap_or(false) { let res = HashMap::from([("compromised", compromised)]); @@ -433,7 +433,7 @@ async fn main() -> std::io::Result<()> { } } CommandActions::PasswordStrength { password } => { - let strength = password_strength_command::execute(config, password) + let strength = password_strength_command::execute(&config, password) .await.expect("could not check password"); if args.json_output.unwrap_or(false) { println!("{}", serde_json::to_string(&strength).unwrap()); @@ -451,12 +451,22 @@ async fn main() -> std::io::Result<()> { log::info!("compromised {:?}", compromised); } } - CommandActions::GenerateAccountOTP { account_id, otp_secret } => { + CommandActions::GenerateOTP { otp_secret } => { + let otp_code = generate_otp_command::execute( + &config, otp_secret) + .await.expect("could not generate otp code"); + if args.json_output.unwrap_or(false) { + let res = HashMap::from([("otp_code", otp_code)]); + println!("{}", serde_json::to_string(&res).unwrap()); + } else { + log::info!("otp code: {:?}", otp_code); + } + } + CommandActions::GenerateAccountOTP { account_id} => { let ctx_args = args.to_args_context(&config).await.expect("failed to create args-context"); let otp_code = generate_account_otp_command::execute( &ctx_args, - account_id, - otp_secret) + account_id) .await.expect("could not generate otp code"); if args.json_output.unwrap_or(false) { let res = HashMap::from([("otp_code", otp_code)]); @@ -465,11 +475,10 @@ async fn main() -> std::io::Result<()> { log::info!("otp code: {:?}", otp_code); } } - CommandActions::GenerateUserOTP { otp_secret } => { + CommandActions::GenerateUserOTP { } => { let ctx_args = args.to_args_context(&config).await.expect("failed to create args-context"); let otp_code = generate_user_otp_command::execute( - &ctx_args, - otp_secret) + &ctx_args) .await.expect("could not generate otp code"); if args.json_output.unwrap_or(false) { let res = HashMap::from([("otp_code", otp_code)]); diff --git a/src/service/authentication_service_impl.rs b/src/service/authentication_service_impl.rs index fccf7c5..dd23f72 100644 --- a/src/service/authentication_service_impl.rs +++ b/src/service/authentication_service_impl.rs @@ -73,7 +73,7 @@ impl AuthenticationServiceImpl { user: &User, mfa_verified_at: Option, ) -> PassResult { - let mut login_session = LoginSession::new(&user); + let mut login_session = LoginSession::new(user); login_session.ip_address = context.get(CONTEXT_IP_ADDRESS).cloned(); login_session.mfa_verified_at = mfa_verified_at; let _ = self.login_session_repository.create(&login_session)?; @@ -138,7 +138,7 @@ impl AuthenticationService for AuthenticationServiceImpl { if user.mfa_required() { session_status = SessionStatus::RequiresMFA; if let Some(otp_code) = otp_code { - if user.verify_otp(otp_code.clone()) { + if user.verify_otp(otp_code) { session_status = SessionStatus::Valid; mfa_verified_at = Some(Utc::now().naive_utc()); } @@ -246,7 +246,7 @@ impl AuthenticationService for AuthenticationServiceImpl { self.hsm_store.set_property(&ctx.username, WEBAUTHN_AUTH_STATE, "")?; let auth_state: PasskeyAuthentication = serde_json::from_str(&auth_state_str)?; - let auth_result = self.webauthn.finish_passkey_authentication(&auth, &auth_state)?; + let auth_result = self.webauthn.finish_passkey_authentication(auth, &auth_state)?; let mut user = self.user_repository.get(ctx, &ctx.user_id).await?; user.update_security_keys(&auth_result); diff --git a/src/service/user_service_impl.rs b/src/service/user_service_impl.rs index 03853e2..8f6312e 100644 --- a/src/service/user_service_impl.rs +++ b/src/service/user_service_impl.rs @@ -193,7 +193,7 @@ impl UserService for UserServiceImpl { async fn generate_user_otp(&self, ctx: &UserContext) -> PassResult { let user = self.user_repository.get(ctx, &ctx.user_id).await?; - Ok(TOTP::new(&user.otp_secret).generate(30, Utc::now().timestamp() as u64)) + Ok(TOTP::new(user.otp_secret).generate(30, Utc::now().timestamp() as u64)) } } diff --git a/templates/authenticated_base.html b/templates/authenticated_base.html index 3a31b83..7dc5e12 100755 --- a/templates/authenticated_base.html +++ b/templates/authenticated_base.html @@ -20,7 +20,7 @@