vw_small

Hardened fork of Vaultwarden (https://github.com/dani-garcia/vaultwarden) with fewer features.
git clone https://git.philomathiclife.com/repos/vw_small
Log | Files | Refs | README

commit 91ae28f2ffdf190f485172e31e7ed46938791aef
parent 2612a25bb8c575f6916a599cd073aaad1fb8cc05
Author: Zack Newman <zack@philomathiclife.com>
Date:   Fri, 15 Dec 2023 14:45:49 -0700

use better types. improve database types

Diffstat:
Msrc/api/core/accounts.rs | 32++++++++++++++++----------------
Msrc/api/core/two_factor/authenticator.rs | 16+++++++---------
Msrc/api/identity.rs | 24++++++++++++------------
Msrc/auth.rs | 4+++-
Msrc/config.rs | 8++++----
Msrc/db/models/cipher.rs | 42++++++++++++++++++++++++------------------
Msrc/db/models/collection.rs | 21++++++++++++---------
Msrc/db/models/organization.rs | 60+++++++++++++++++++++++++++++++++---------------------------
Msrc/db/models/two_factor.rs | 10+++++++++-
Msrc/db/models/user.rs | 64++++++++++++++++++++++++++++++++++++++++++++++++++--------------
10 files changed, 170 insertions(+), 111 deletions(-)

diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs @@ -233,9 +233,9 @@ async fn post_password( #[allow(non_snake_case)] struct ChangeKdfData { Kdf: i32, - KdfIterations: i32, - KdfMemory: Option<i32>, - KdfParallelism: Option<i32>, + KdfIterations: u32, + KdfMemory: Option<u32>, + KdfParallelism: Option<u32>, MasterPasswordHash: String, NewMasterPasswordHash: String, Key: String, @@ -253,34 +253,34 @@ async fn post_kdf( if !user.check_valid_password(&kdf_data.MasterPasswordHash) { err!("Invalid password") } - if kdf_data.Kdf == i32::from(UserKdfType::Pbkdf2) && kdf_data.KdfIterations < 100_000i32 { + if kdf_data.Kdf == i32::from(UserKdfType::Pbkdf2) && kdf_data.KdfIterations < 100_000u32 { err!("PBKDF2 KDF iterations must be at least 100000.") } if kdf_data.Kdf == i32::from(UserKdfType::Argon2id) { - if kdf_data.KdfIterations < 1i32 { + if kdf_data.KdfIterations < 1u32 { err!("Argon2 KDF iterations must be at least 1.") } if let Some(m) = kdf_data.KdfMemory { - if !(15i32..=1024i32).contains(&m) { + if !(15u32..=1024u32).contains(&m) { err!("Argon2 memory must be between 15 MB and 1024 MB.") } - user.client_kdf_memory = kdf_data.KdfMemory; + user.set_client_kdf_memory(kdf_data.KdfMemory); } else { err!("Argon2 memory parameter is required.") } if let Some(p) = kdf_data.KdfParallelism { - if !(1i32..=16i32).contains(&p) { + if !(1u32..=16u32).contains(&p) { err!("Argon2 parallelism must be between 1 and 16.") } - user.client_kdf_parallelism = kdf_data.KdfParallelism; + user.set_client_kdf_parallelism(kdf_data.KdfParallelism); } else { err!("Argon2 parallelism parameter is required.") } } else { - user.client_kdf_memory = None; - user.client_kdf_parallelism = None; + user.set_client_kdf_memory(None); + user.set_client_kdf_parallelism(None); } - user.client_kdf_iter = kdf_data.KdfIterations; + user.set_client_kdf_iter(kdf_data.KdfIterations); user.client_kdf_type = kdf_data.Kdf; user.set_password( &kdf_data.NewMasterPasswordHash, @@ -467,7 +467,7 @@ async fn post_verify_email_token( } user.verified_at = Some(Utc::now().naive_utc()); user.last_verifying_at = None; - user.login_verify_count = 0i32; + user.set_login_verify_count(0); user.save(&conn).await } @@ -564,9 +564,9 @@ pub async fn _prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> Json<Val match User::find_by_mail(&login_data.Email, &conn).await { Some(user) => ( user.client_kdf_type, - user.client_kdf_iter, - user.client_kdf_memory, - user.client_kdf_parallelism, + user.client_kdf_iter(), + user.client_kdf_memory(), + user.client_kdf_parallelism(), ), None => ( User::client_kdf_type_default(), diff --git a/src/api/core/two_factor/authenticator.rs b/src/api/core/two_factor/authenticator.rs @@ -134,21 +134,19 @@ async fn validate_totp_code( ); // Get the current system time in UNIX Epoch (UTC) let current_time = chrono::Utc::now(); - let current_timestamp = current_time.timestamp(); - let time_step = current_timestamp / 30i64; - // We need to calculate the time offsite and cast it as a u64. + let current_timestamp = u64::try_from(current_time.timestamp()).expect("underflow"); + let time_step = current_timestamp / 30u64; + // We need to calculate the time offset and cast it as a u64. // Since we only have times into the future and the totp generator needs, a u64 instead of the default i64. - let time = u64::try_from(current_timestamp).expect("underflow when casting to a u64 in TOTP"); - let generated = totp_custom::<Sha1>(30, 6, &decoded_secret, time); + let generated = totp_custom::<Sha1>(30, 6, &decoded_secret, current_timestamp); // Check the given code equals the generated one and if the time_step is larger than the one last used. - if generated == totp_code && time_step > i64::from(twofactor.last_used) { + if generated == totp_code && time_step > u64::from(twofactor.last_used()) { // Save the last used time step so only totp time steps higher then this one are allowed. // This will also save a newly created twofactor if the code is correct. - twofactor.last_used = - i32::try_from(time_step).expect("overflow or underflow when casting to an i32 in TOTP"); + twofactor.set_last_used(u32::try_from(time_step).expect("overflow")); twofactor.save(conn).await?; Ok(()) - } else if generated == totp_code && time_step <= i64::from(twofactor.last_used) { + } else if generated == totp_code && time_step <= u64::from(twofactor.last_used()) { warn!("This TOTP or a TOTP code within 0 steps back or forward has already been used!"); err!(format!( "Invalid TOTP code! Server time: {} IP: {}", diff --git a/src/api/identity.rs b/src/api/identity.rs @@ -84,9 +84,9 @@ async fn _refresh_login(data: ConnectData, conn: &DbConn) -> JsonResult { "Key": user.akey, "PrivateKey": user.private_key, "Kdf": user.client_kdf_type, - "KdfIterations": user.client_kdf_iter, - "KdfMemory": user.client_kdf_memory, - "KdfParallelism": user.client_kdf_parallelism, + "KdfIterations": user.client_kdf_iter(), + "KdfMemory": user.client_kdf_memory(), + "KdfParallelism": user.client_kdf_parallelism(), "ResetMasterPassword": false, // TODO: according to official server seems something like: user.password_hash.is_empty(), but would need testing "scope": scope, "unofficialServer": true, @@ -141,8 +141,8 @@ async fn _password_login( ) } // Change the KDF Iterations - if user.password_iterations != config::get_config().password_iterations { - user.password_iterations = config::get_config().password_iterations; + if user.password_iterations() != config::get_config().password_iterations { + user.set_password_iterations(config::get_config().password_iterations); user.set_password(password, None, false, None); if let Err(e) = user.save(conn).await { panic!("Error updating user: {e:#?}"); @@ -174,9 +174,9 @@ async fn _password_login( "Key": user.akey, "PrivateKey": user.private_key, "Kdf": user.client_kdf_type, - "KdfIterations": user.client_kdf_iter, - "KdfMemory": user.client_kdf_memory, - "KdfParallelism": user.client_kdf_parallelism, + "KdfIterations": user.client_kdf_iter(), + "KdfMemory": user.client_kdf_memory(), + "KdfParallelism": user.client_kdf_parallelism(), "ResetMasterPassword": false,// TODO: Same as above "scope": scope, "unofficialServer": true, @@ -260,9 +260,9 @@ async fn _user_api_key_login( "Key": user.akey, "PrivateKey": user.private_key, "Kdf": user.client_kdf_type, - "KdfIterations": user.client_kdf_iter, - "KdfMemory": user.client_kdf_memory, - "KdfParallelism": user.client_kdf_parallelism, + "KdfIterations": user.client_kdf_iter(), + "KdfMemory": user.client_kdf_memory(), + "KdfParallelism": user.client_kdf_parallelism(), "ResetMasterPassword": false, // TODO: Same as above "scope": "api", "unofficialServer": true, @@ -457,7 +457,7 @@ struct ConnectData { #[field(name = uncased("two_factor_remember"))] #[field(name = uncased("twofactorremember"))] #[allow(dead_code)] - two_factor_remember: Option<i32>, + two_factor_remember: Option<u32>, #[field(name = uncased("authrequest"))] auth_request: Option<String>, } diff --git a/src/auth.rs b/src/auth.rs @@ -478,7 +478,9 @@ impl<'r> FromRequest<'r> for Headers { // Check if the stamp exception has expired first. // Then, check if the current route matches any of the allowed routes. // After that check the stamp in exception matches the one in the claims. - if Utc::now().naive_utc().timestamp() > stamp_exception.expire { + if u64::try_from(Utc::now().naive_utc().timestamp()).expect("underflow") + > stamp_exception.expire + { // If the stamp exception has been expired remove it from the database. // This prevents checking this stamp exception for new requests. let mut user = user; diff --git a/src/config.rs b/src/config.rs @@ -26,7 +26,7 @@ pub enum ConfigErr { De(de::Error), Url(ParseError), BadDomain, - InvalidPasswordIterations(i32), + InvalidPasswordIterations(u32), } impl Display for ConfigErr { #[inline] @@ -78,7 +78,7 @@ struct ConfigFile { db_connection_retries: Option<NonZeroU8>, domain: String, ip: IpAddr, - password_iterations: Option<i32>, + password_iterations: Option<u32>, port: u16, tls: Tls, web_vault_enabled: Option<bool>, @@ -90,7 +90,7 @@ pub struct Config { pub database_timeout: u16, pub db_connection_retries: NonZeroU8, pub domain: Url, - pub password_iterations: i32, + pub password_iterations: u32, pub rocket: rocket::Config, pub web_vault_enabled: bool, } @@ -157,7 +157,7 @@ impl Config { password_iterations: match config_file.password_iterations { None => 600_000, Some(count) => { - if count < 100_000i32 { + if count < 100_000u32 { return Err(ConfigErr::InvalidPasswordIterations(count)); } count diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs @@ -548,15 +548,18 @@ impl Cipher { }} } - pub async fn count_owned_by_user(user_uuid: &str, conn: &DbConn) -> i64 { - db_run! {conn: { - ciphers::table - .filter(ciphers::user_uuid.eq(user_uuid)) - .count() - .first::<i64>(conn) - .ok() - .unwrap_or(0) - }} + pub async fn count_owned_by_user(user_uuid: &str, conn: &DbConn) -> u64 { + u64::try_from({ + db_run! {conn: { + ciphers::table + .filter(ciphers::user_uuid.eq(user_uuid)) + .count() + .first::<i64>(conn) + .ok() + .unwrap_or(0) + }} + }) + .expect("underflow") } pub async fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> { @@ -567,15 +570,18 @@ impl Cipher { }} } - pub async fn count_by_org(org_uuid: &str, conn: &DbConn) -> i64 { - db_run! {conn: { - ciphers::table - .filter(ciphers::organization_uuid.eq(org_uuid)) - .count() - .first::<i64>(conn) - .ok() - .unwrap_or(0) - }} + pub async fn count_by_org(org_uuid: &str, conn: &DbConn) -> u64 { + u64::try_from({ + db_run! {conn: { + ciphers::table + .filter(ciphers::organization_uuid.eq(org_uuid)) + .count() + .first::<i64>(conn) + .ok() + .unwrap_or(0) + }} + }) + .expect("underflow") } pub async fn find_by_folder(folder_uuid: &str, conn: &DbConn) -> Vec<Self> { diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs @@ -227,15 +227,18 @@ impl Collection { }} } - pub async fn count_by_org(org_uuid: &str, conn: &DbConn) -> i64 { - db_run! { conn: { - collections::table - .filter(collections::org_uuid.eq(org_uuid)) - .count() - .first::<i64>(conn) - .ok() - .unwrap_or(0) - }} + pub async fn count_by_org(org_uuid: &str, conn: &DbConn) -> u64 { + u64::try_from({ + db_run! { conn: { + collections::table + .filter(collections::org_uuid.eq(org_uuid)) + .count() + .first::<i64>(conn) + .ok() + .unwrap_or(0) + }} + }) + .expect("underflow") } pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> { diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs @@ -190,9 +190,9 @@ impl Organization { "Id": self.uuid, "Identifier": null, // not supported by us "Name": self.name, - "Seats": 10i32, // The value doesn't matter, we don't check server-side - "MaxCollections": 10i32, // The value doesn't matter, we don't check server-side - "MaxStorageGb": 10i32, // The value doesn't matter, we don't check server-side + "Seats": 10u32, // The value doesn't matter, we don't check server-side + "MaxCollections": 10u32, // The value doesn't matter, we don't check server-side + "MaxStorageGb": 10u32, // The value doesn't matter, we don't check server-side "Use2fa": true, "UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet) "UseEvents": false, @@ -212,7 +212,7 @@ impl Organization { "BusinessTaxNumber": null, "BillingEmail": self.billing_email, "Plan": "TeamsAnnually", - "PlanType": 5i32, // TeamsAnnually plan + "PlanType": 5u32, // TeamsAnnually plan "UsersGetPremium": true, "Object": "organization", }) @@ -356,8 +356,8 @@ impl UserOrganization { "Id": self.org_uuid, "Identifier": null, // Not supported "Name": org.name, - "Seats": 10i32, // The value doesn't matter, we don't check server-side - "MaxCollections": 10i32, // The value doesn't matter, we don't check server-side + "Seats": 10u32, // The value doesn't matter, we don't check server-side + "MaxCollections": 10u32, // The value doesn't matter, we don't check server-side "UsersGetPremium": true, "Use2fa": true, "UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet) @@ -374,7 +374,7 @@ impl UserOrganization { "UseSso": false, // Not supported "ProviderId": null, "ProviderName": null, - "MaxStorageGb": 10i32, // The value doesn't matter, we don't check server-side + "MaxStorageGb": 10u32, // The value doesn't matter, we don't check server-side "UserId": self.user_uuid, "Key": self.akey, "Status": self.status, @@ -554,16 +554,19 @@ impl UserOrganization { }} } - pub async fn count_accepted_and_confirmed_by_user(user_uuid: &str, conn: &DbConn) -> i64 { - db_run! { conn: { - users_organizations::table - .filter(users_organizations::user_uuid.eq(user_uuid)) - .filter(users_organizations::status.eq(i32::from(UserOrgStatus::Accepted))) - .or_filter(users_organizations::status.eq(i32::from(UserOrgStatus::Confirmed))) - .count() - .first::<i64>(conn) - .unwrap_or(0) - }} + pub async fn count_accepted_and_confirmed_by_user(user_uuid: &str, conn: &DbConn) -> u64 { + u64::try_from({ + db_run! { conn: { + users_organizations::table + .filter(users_organizations::user_uuid.eq(user_uuid)) + .filter(users_organizations::status.eq(i32::from(UserOrgStatus::Accepted))) + .or_filter(users_organizations::status.eq(i32::from(UserOrgStatus::Confirmed))) + .count() + .first::<i64>(conn) + .unwrap_or(0) + }} + }) + .expect("underflow") } pub async fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> { @@ -593,16 +596,19 @@ impl UserOrganization { org_uuid: &str, atype: UserOrgType, conn: &DbConn, - ) -> i64 { - db_run! { conn: { - users_organizations::table - .filter(users_organizations::org_uuid.eq(org_uuid)) - .filter(users_organizations::atype.eq(i32::from(atype))) - .filter(users_organizations::status.eq(i32::from(UserOrgStatus::Confirmed))) - .count() - .first::<i64>(conn) - .unwrap_or(0) - }} + ) -> u64 { + u64::try_from({ + db_run! { conn: { + users_organizations::table + .filter(users_organizations::org_uuid.eq(org_uuid)) + .filter(users_organizations::atype.eq(i32::from(atype))) + .filter(users_organizations::status.eq(i32::from(UserOrgStatus::Confirmed))) + .count() + .first::<i64>(conn) + .unwrap_or(0) + }} + }) + .expect("underflow") } pub async fn find_by_user_and_org( diff --git a/src/db/models/two_factor.rs b/src/db/models/two_factor.rs @@ -11,7 +11,15 @@ db_object! { pub atype: i32, pub enabled: bool, pub data: String, - pub last_used: i32, + last_used: i32, + } +} +impl TwoFactor { + pub fn last_used(&self) -> u32 { + u32::try_from(self.last_used).expect("underflow") + } + pub fn set_last_used(&mut self, last: u32) { + self.last_used = i32::try_from(last).expect("overflow"); } } diff --git a/src/db/models/user.rs b/src/db/models/user.rs @@ -15,14 +15,14 @@ db_object! { pub updated_at: NaiveDateTime, pub verified_at: Option<NaiveDateTime>, pub last_verifying_at: Option<NaiveDateTime>, - pub login_verify_count: i32, + login_verify_count: i32, pub email: String, pub email_new: Option<String>, pub email_new_token: Option<String>, pub name: String, pub password_hash: Vec<u8>, pub salt: Vec<u8>, - pub password_iterations: i32, + password_iterations: i32, pub password_hint: Option<String>, pub akey: String, pub private_key: Option<String>, @@ -35,9 +35,9 @@ db_object! { pub equivalent_domains: String, pub excluded_globals: String, pub client_kdf_type: i32, - pub client_kdf_iter: i32, - pub client_kdf_memory: Option<i32>, - pub client_kdf_parallelism: Option<i32>, + client_kdf_iter: i32, + client_kdf_memory: Option<i32>, + client_kdf_parallelism: Option<i32>, pub api_key: Option<String>, pub avatar_color: Option<String>, pub external_id: Option<String>, // Todo: Needs to be removed in the future, this is not used anymore. @@ -76,7 +76,7 @@ impl From<UserStatus> for i32 { pub struct UserStampException { pub routes: Vec<String>, pub security_stamp: String, - pub expire: i64, + pub expire: u64, } /// Local methods @@ -84,7 +84,7 @@ impl User { pub fn client_kdf_type_default() -> i32 { i32::from(UserKdfType::Pbkdf2) } - pub const CLIENT_KDF_ITER_DEFAULT: i32 = 600_000i32; + pub const CLIENT_KDF_ITER_DEFAULT: u32 = 600_000u32; pub fn new(email: &str) -> Self { let now = Utc::now().naive_utc(); @@ -104,7 +104,8 @@ impl User { email_new_token: None, password_hash: Vec::new(), salt: crypto::get_random_bytes::<64>().to_vec(), - password_iterations: config::get_config().password_iterations, + password_iterations: i32::try_from(config::get_config().password_iterations) + .expect("overflow"), security_stamp: crate::util::get_uuid(), stamp_exception: None, password_hint: None, @@ -115,7 +116,7 @@ impl User { equivalent_domains: "[]".to_owned(), excluded_globals: "[]".to_owned(), client_kdf_type: Self::client_kdf_type_default(), - client_kdf_iter: Self::CLIENT_KDF_ITER_DEFAULT, + client_kdf_iter: i32::try_from(Self::CLIENT_KDF_ITER_DEFAULT).expect("overflow"), client_kdf_memory: None, client_kdf_parallelism: None, api_key: None, @@ -123,6 +124,38 @@ impl User { external_id: None, // Todo: Needs to be removed in the future, this is not used anymore. } } + pub fn login_verify_count(&self) -> u32 { + u32::try_from(self.login_verify_count).expect("underflow") + } + pub fn set_login_verify_count(&mut self, count: u32) { + self.login_verify_count = i32::try_from(count).expect("overflow"); + } + pub fn password_iterations(&self) -> u32 { + u32::try_from(self.password_iterations).expect("underflow") + } + pub fn set_password_iterations(&mut self, iter: u32) { + self.password_iterations = i32::try_from(iter).expect("overflow"); + } + pub fn client_kdf_iter(&self) -> u32 { + u32::try_from(self.client_kdf_iter).expect("underflow") + } + pub fn set_client_kdf_iter(&mut self, iter: u32) { + self.password_iterations = i32::try_from(iter).expect("overflow"); + } + pub fn client_kdf_memory(&self) -> Option<u32> { + self.client_kdf_memory + .map(|mem| u32::try_from(mem).expect("underflow")) + } + pub fn set_client_kdf_memory(&mut self, mem: Option<u32>) { + self.client_kdf_memory = mem.map(|kdf| i32::try_from(kdf).expect("overflow")); + } + pub fn client_kdf_parallelism(&self) -> Option<u32> { + self.client_kdf_parallelism + .map(|mem| u32::try_from(mem).expect("underflow")) + } + pub fn set_client_kdf_parallelism(&mut self, par: Option<u32>) { + self.client_kdf_parallelism = par.map(|pll| i32::try_from(pll).expect("overflow")); + } pub fn check_valid_password(&self, password: &str) -> bool { crypto::verify_password_hash( @@ -194,11 +227,14 @@ impl User { let stamp_exception = UserStampException { routes: route_exception, security_stamp: self.security_stamp.clone(), - expire: (Utc::now() - .naive_utc() - .checked_add_signed(Duration::minutes(2))) - .expect("Duration add overflowed") - .timestamp(), + expire: u64::try_from( + (Utc::now() + .naive_utc() + .checked_add_signed(Duration::minutes(2))) + .expect("Duration add overflowed") + .timestamp(), + ) + .expect("underflow"), }; self.stamp_exception = Some(serde_json::to_string(&stamp_exception).unwrap_or_default()); }