diff --git a/README.md b/README.md index 4479d38..2cc76d0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # PlexPass -A Secured Family-friendly Password Manager +A Secured Family-friendly Password Manager with Multi-factor Authentication and Local Hosting. ## Background With the proliferation of online services and accounts, it has become almost impossible for users to remember unique and strong passwords for each of them. Some users use the same password across multiple accounts, which is risky because if one account is compromised, all other accounts are at risk. With increase of cyber threats such as [2022-Morgan-Stanley](https://techcrunch.com/2022/09/21/morgan-stanley-hard-drives-data-breach/), [2019-Facebook](https://www.wired.com/story/facebook-passwords-plaintext-change-yours/), [2018-MyFitnessPal](https://www.aafp.org/news/practice-professional-issues/20180403myfitnesspal.html), [2019-CapitalOne](https://www.capitalone.com/digital/facts2019/), more services demand stronger and more complex passwords, which are harder to remember. Standards like [FIDO](https://fidoalliance.org/what-is-fido/) (Fast IDentity Online), [WebAuthn](https://webauthn.guide/) (Web Authentication), and [Passkeys](https://fidoalliance.org/passkeys/) aim to address the problems associated with traditional passwords by introducing stronger, simpler, and more phishing-resistant user authentication methods. These standards mitigate Man-in-the-Middle attacks by using decentralized on-device authentication. Yet, their universal adoption remains a work in progress. Until then, a popular alternative for dealing with the password complexity is a Password manager such as [LessPass](https://www.lesspass.com/#/), [1Password](https://1password.com/), and [Bitwarden](https://bitwarden.com/), which offer enhanced security, convenience, and cross-platform access. However, these password managers are also prone to security and privacy risks especially and become a single point of failure when they store user passwords in the cloud. As password managers may also store other sensitive information such as credit card details and secured notes, the Cloud-based password managers with centralized storage become high value target hackers. Many cloud-based password managers implement additional security measures such as end-to-end encryption, zero-knowledge architecture, and multifactor authentication but once hackers get access to the encrypted password vaults, they become vulnerable to sophisticated encryption attacks. For example, In 2022, LastPass, serving 25 million users, experienced [significant security breaches](https://blog.lastpass.com/2023/03/security-incident-update-recommended-actions/). Attackers accessed a range of user data, including billing and email addresses, names, telephone numbers, and IP addresses. More alarmingly, the breach compromised customer vault data, revealing unencrypted website URLs alongside encrypted usernames, passwords, secure notes, and form-filled information. The access to the encrypted vaults allow [“offline attacks” for password cracking](https://krebsonsecurity.com/2023/09/lastpass-horse-gone-barn-bolted-is-strong-password/) attempts that may use powerful computers for trying millions of password guesses per second. In another incident, LastPass users were [locked out of their accounts due to MFA reset](https://www.bleepingcomputer.com/news/security/lastpass-users-furious-after-being-locked-out-due-to-mfa-resets/) after a security upgrade. In order to address these risks with cloud-based password managers, we are building a secured family-friendly password manager named “PlexPass” with an enhanced security and ease of use including multi-device support for family members but without relying on storing data in cloud. diff --git a/assets/javascript/plexpass.js b/assets/javascript/plexpass.js index 85f3c33..1ce4f40 100644 --- a/assets/javascript/plexpass.js +++ b/assets/javascript/plexpass.js @@ -28,7 +28,7 @@ async function viewAccount(id) { } advisories += ``; } - const riskBackgroundColor = account.risk_bg_color; + const riskImage = account.risk_image ? `` : ''; modalBody.innerHTML = ` @@ -54,7 +54,7 @@ async function viewAccount(id) {     - + @@ -80,7 +80,7 @@ async function viewAccount(id) { - +
Notes: ${account.notes || ''}
Account Risk: ${account.risk}Account Risk:${riskImage} ${account.risk}
Advisories:
@@ -136,7 +136,7 @@ function buildOtpSection(otp, generatedOtp) {  
- +
@@ -296,6 +296,12 @@ async function showAccountForm(account) { +
+ + +
@@ -320,12 +326,6 @@ async function showAccountForm(account) {
-
- - -
diff --git a/docs/edit_profile.png b/docs/edit_profile.png index 661947f..e5025a6 100644 Binary files a/docs/edit_profile.png and b/docs/edit_profile.png differ diff --git a/docs/register_mfa.png b/docs/register_mfa.png index 52a31cb..db8b704 100644 Binary files a/docs/register_mfa.png and b/docs/register_mfa.png differ diff --git a/docs/settings.png b/docs/settings.png index 729f2ee..0fd325e 100644 Binary files a/docs/settings.png and b/docs/settings.png differ diff --git a/docs/view_account.png b/docs/view_account.png index 53410c6..ece03b5 100644 Binary files a/docs/view_account.png and b/docs/view_account.png differ diff --git a/resources/en-US/errors.ftl b/resources/en-US/errors.ftl index 540f1f9..f009358 100644 --- a/resources/en-US/errors.ftl +++ b/resources/en-US/errors.ftl @@ -1,3 +1,8 @@ auth-error = We could not validate your credentials, please verify them if you already have an account or Sign up as a new user. weak-master-password = Your master password with {$info}. Please choose a strong password with a minimum of 12 characters, containing uppercase and lowercase letters, numbers, and special symbols such as `{ $sample_password }`. master-confirm-mismatch = Your master-password didn't match confirmed master-password, please confirm again. +email-compromise-error = failed to check email for compromise: {$err}. +short-secret-error = secret length {$len} is too small. +user-id-mismatch-error = user_id in context {$id1} didn't match user_id {$id2} in the request. +username-mismatch-error = username in context didn't match target user entity. +acl-admin-only = only admin can update ACL rules. diff --git a/resources/en-US/main.ftl b/resources/en-US/main.ftl index 1500b5b..7fa10b9 100644 --- a/resources/en-US/main.ftl +++ b/resources/en-US/main.ftl @@ -1,2 +1,3 @@ welcome = Welcome to PlexPass, your personal Password Manaager! hello = Hello { $name } to PlexPass the { $place }. +plexpass = PlexPass/1.0 diff --git a/src/command/startup_command.rs b/src/command/startup_command.rs index e307845..d48a7ac 100644 --- a/src/command/startup_command.rs +++ b/src/command/startup_command.rs @@ -2,12 +2,11 @@ use std::fs::File; use std::io::{BufReader, Read}; use std::path::Path; -use actix_cors::Cors; use actix_files::{Files, NamedFile}; use actix_session::config::PersistentSession; use actix_session::SessionMiddleware; use actix_session::storage::CookieSessionStore; -use actix_web::{App, http, HttpResponse, HttpServer, middleware, web}; +use actix_web::{App, HttpResponse, HttpServer, middleware, web}; use actix_web::cookie::Key; use actix_web_prom::PrometheusMetricsBuilder; use openssl::pkey::{PKey, Private}; @@ -71,21 +70,6 @@ pub async fn execute(config: PassConfig) -> PassResult<()> { .service( Files::new("/assets", "./assets") ) - .wrap( - Cors::default() // allowed_origin return access-control-allow-origin: * by default - .allowed_origin(&format!("http://127.0.0.1:{}", http_port.clone())) - .allowed_origin(&format!("https://127.0.0.1:{}", https_port.clone())) - .allowed_origin(&format!("http://localhost:{}", http_port.clone())) - .allowed_origin(&format!("https://localhost:{}", https_port.clone())) - .send_wildcard() - .allowed_methods(vec!["GET", "POST", "PUT", "DELETE"]) - .allowed_headers(vec![ - http::header::AUTHORIZATION, - http::header::ACCEPT, - http::header::CONTENT_TYPE, - ]) - .max_age(3600), // for cors - ) .wrap(middleware::Logger::default()) .wrap(auth_middleware::Authentication) .wrap(prometheus.clone()) @@ -106,6 +90,8 @@ pub async fn execute(config: PassConfig) -> PassResult<()> { .configure(config_services) }); + println!("----------session {}", config.session_timeout_minutes); + match load_rustls_config(&config) { Ok(server_config) => { log::info!("starting TLS based API server on {}", https_port); diff --git a/src/crypto.rs b/src/crypto.rs index efa9b91..738d403 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -26,6 +26,7 @@ use crate::domain::models::{ CryptoAlgorithm, DecryptRequest, DecryptResponse, EncryptRequest, EncryptResponse, HashAlgorithm, PassResult, PBKDF2_HMAC_SHA256_ITERATIONS, }; +use crate::locales::safe_localized_message; // Cryptographically secure random number generator @@ -117,7 +118,9 @@ pub(crate) fn generate_private_public_keys_from_secret( pub(crate) fn generate_private_key_from_secret(secret: &str) -> PassResult<(SecretKey, [u8; SECRET_LEN])> { let in_bytes = secret.as_bytes(); if in_bytes.len() < SECRET_LEN { - return Err(PassError::validation(format!("secret ({}:{})is too small", secret, secret.len()).as_str(), None)); + return Err(PassError::validation( + &safe_localized_message("short-secret-error", Some(&["len", &secret.len().to_string()])), + None)); } let mut p = [0; SECRET_LEN]; p[..SECRET_LEN].copy_from_slice(&in_bytes[..SECRET_LEN]); diff --git a/src/dao/acl_repository_impl.rs b/src/dao/acl_repository_impl.rs index 8972400..eb7b312 100644 --- a/src/dao/acl_repository_impl.rs +++ b/src/dao/acl_repository_impl.rs @@ -9,6 +9,7 @@ use crate::dao::schema::acls::dsl::*; use crate::dao::{DbConnection, DbPool, ACLRepository, Repository}; use crate::domain::error::PassError; use crate::domain::models::{PaginatedResult, PassResult}; +use crate::locales::safe_localized_message; #[derive(Clone)] pub(crate) struct ACLRepositoryImpl { @@ -49,7 +50,7 @@ impl ACLRepositoryImpl { { Ok(count) => { count > 0 - }, + } Err(_) => false, } } @@ -85,7 +86,9 @@ impl Repository for ACLRepositoryImpl { // create acl. async fn create(&self, ctx: &UserContext, acl_entity: &ACLEntity) -> PassResult { if !ctx.is_admin() { - return Err(PassError::authentication("only admin can create ACL repository")); + return Err(PassError::authentication( + &safe_localized_message("acl-admin-only", None), + )); } let mut conn = self.connection()?; let size = Self::create_conn(acl_entity, &mut conn)?; @@ -95,7 +98,9 @@ impl Repository for ACLRepositoryImpl { // updates existing acl. async fn update(&self, ctx: &UserContext, acl_entity: &ACLEntity) -> PassResult { if !ctx.is_admin() { - return Err(PassError::authentication("only admin can update ACL repository")); + return Err(PassError::authentication( + &safe_localized_message("acl-admin-only", None), + )); } let existing_acl_entity = self.get_entity(ctx, &acl_entity.acl_id).await?; diff --git a/src/dao/models.rs b/src/dao/models.rs index b64f68a..51ee051 100644 --- a/src/dao/models.rs +++ b/src/dao/models.rs @@ -11,6 +11,7 @@ use diesel::prelude::*; use serde::{Deserialize, Serialize}; use std::hash::{Hash, Hasher}; use uuid::Uuid; +use crate::locales::safe_localized_message; pub const CONTEXT_IP_ADDRESS: &str = "ip_address"; @@ -131,7 +132,7 @@ impl UserContext { Ok(()) } else { Err(PassError::authorization( - "username in context didn't match target user entity", + &safe_localized_message("username-mismatch-error", None), )) } } @@ -144,10 +145,9 @@ impl UserContext { Ok(()) } else { eprintln!("backtrace: {}", Backtrace::capture()); - Err(PassError::authorization(&format!( - "user_id in context ({}) didn't match user_id ({}) in the request", - &self.user_id, user_id - ))) + Err(PassError::authorization( + &safe_localized_message("user-id-mismatch-error", Some(&["id1", &self.user_id, "id2", user_id])), + )) } } diff --git a/src/domain/models.rs b/src/domain/models.rs index 4731773..3cc2265 100644 --- a/src/domain/models.rs +++ b/src/domain/models.rs @@ -1386,7 +1386,7 @@ impl AccountPasswordSummary { } pub const DEFAULT_VAULT_NAMES: [&str; 5] = ["Identity", "Personal", "Work", "Financial", "Secure Notes"]; -pub const DEFAULT_CATEGORIES: [&str; 11] = [ +pub const DEFAULT_CATEGORIES: [&str; 10] = [ "Contacts", "Logins", "Finance", @@ -1394,7 +1394,6 @@ pub const DEFAULT_CATEGORIES: [&str; 11] = [ "Shopping", "Travel", "Gaming", - "Chat", "Notes", "Credit Cards", "Miscellaneous", diff --git a/src/hibp.rs b/src/hibp.rs index c6b657e..e79bdf1 100644 --- a/src/hibp.rs +++ b/src/hibp.rs @@ -2,6 +2,7 @@ use crate::crypto::compute_sha1_hex; use crate::domain::error::PassError; use crate::domain::models::PassResult; use log::info; +use crate::locales::safe_localized_message; /// email_compromised checks if an account with given email has been compromised pub(crate) async fn email_compromised(email: &str, api_key: &str) -> PassResult { @@ -11,8 +12,8 @@ pub(crate) async fn email_compromised(email: &str, api_key: &str) -> PassResult< email ); let client = reqwest::Client::builder() - .user_agent("PlexPass/1.0") - .build()?; + .user_agent(safe_localized_message("plexpass", None)) + .build()?; let response = client .get(&url) @@ -28,7 +29,7 @@ pub(crate) async fn email_compromised(email: &str, api_key: &str) -> PassResult< info!("-debug hibp---{}", text); if text.contains("statusCode") || text.contains("hibp-api-key") { return Err(PassError::runtime( - format!("failed to check email for compromise: {}", text).as_str(), + &safe_localized_message("email-compromise-error", Some(&["err", &text])), None, )); }