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 0e2e730565124bb65036175b6e77f7fa08b59f98
parent 9a87edc1ff68043fe007db990490ac8e5cdba2b2
Author: Zack Newman <zack@philomathiclife.com>
Date:   Wed,  6 Dec 2023 16:19:27 -0700

use checked arithmetic and safe conversions

Diffstat:
MCargo.toml | 4++--
Msrc/api/core/accounts.rs | 217++++++++++++++++++++++++++++++++-----------------------------------------------
Msrc/api/core/ciphers.rs | 38++++++++++++++------------------------
Msrc/api/core/folders.rs | 29++++++-----------------------
Msrc/api/core/mod.rs | 6+++---
Msrc/api/core/organizations.rs | 65+++++++++++++++++++++++++++--------------------------------------
Msrc/api/core/two_factor/authenticator.rs | 110++++++++++++++++++++++++++++++++-----------------------------------------------
Msrc/api/core/two_factor/protected_actions.rs | 18++++++++++--------
Msrc/api/core/two_factor/webauthn.rs | 26++++++++++++++++----------
Msrc/api/identity.rs | 8+++-----
Msrc/api/mod.rs | 6+++---
Msrc/api/notifications.rs | 91+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/api/web.rs | 2+-
Msrc/auth.rs | 34++++++++++++++++++----------------
Msrc/config.rs | 10+++++-----
Msrc/crypto.rs | 2+-
Msrc/db/mod.rs | 18++++++++----------
Msrc/db/models/cipher.rs | 40+++++++++++++++++++++++-----------------
Msrc/db/models/collection.rs | 12++++++------
Msrc/db/models/device.rs | 63+++++++++++++++++++++++++++++++--------------------------------
Msrc/db/models/folder.rs | 4++--
Msrc/db/models/org_policy.rs | 26++++++++++++++++++++------
Msrc/db/models/organization.rs | 141+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Msrc/db/models/two_factor.rs | 16+++++++++++++---
Msrc/db/models/user.rs | 49+++++++++++++++++++++++++++++++------------------
Msrc/error.rs | 6+++---
Msrc/main.rs | 19+++----------------
Msrc/util.rs | 39+++++++++++++++++++--------------------
28 files changed, 551 insertions(+), 548 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -1,7 +1,7 @@ [package] authors = ["Daniel GarcĂ­a <dani-garcia@users.noreply.github.com>", "Zack Newman <zack@philomathiclife.com>"] categories = ["api-bindings", "web-programming::http-server"] -description = "Fork of Vaultwarden with fewer features and pledge(2) and unveil(2) support." +description = "Hardened fork of Vaultwarden with fewer features and pledge(2) and unveil(2) support." documentation = "https://github.com/dani-garcia/vaultwarden/wiki" edition = "2021" keywords = ["password", "vaultwarden"] @@ -9,7 +9,7 @@ license = "AGPL-3.0-only" name = "vw_small" publish = false repository = "https://git.philomathiclife.com/repos/vw_small/" -version = "1.30.0" +version = "2.0.0" [features] priv_sep = ["dep:priv_sep"] diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs @@ -88,7 +88,7 @@ struct KeysData { fn clean_password_hint(password_hint: &Option<String>) -> Option<String> { password_hint.as_ref().and_then(|h| match h.trim() { "" => None, - ht => Some(ht.to_string()), + ht => Some(ht.to_owned()), }) } @@ -124,17 +124,14 @@ async fn put_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbCo #[post("/accounts/profile", data = "<data>")] async fn post_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult { - let data: ProfileData = data.into_inner().data; - + let prof_data: ProfileData = data.into_inner().data; // Check if the length of the username exceeds 50 characters (Same is Upstream Bitwarden) // This also prevents issues with very long usernames causing to large JWT's. See #2419 - if data.Name.len() > 50 { + if prof_data.Name.len() > 50 { err!("The field Name must be a string with a maximum length of 50."); } - let mut user = headers.user; - user.name = data.Name; - + user.name = prof_data.Name; user.save(&conn).await?; Ok(Json(user.to_json(&conn).await)) } @@ -147,12 +144,11 @@ struct AvatarData { #[put("/accounts/avatar", data = "<data>")] async fn put_avatar(data: JsonUpcase<AvatarData>, headers: Headers, conn: DbConn) -> JsonResult { - let data: AvatarData = data.into_inner().data; - + let av_data: AvatarData = data.into_inner().data; // It looks like it only supports the 6 hex color format. // If you try to add the short value it will not show that color. // Check and force 7 chars, including the #. - if let Some(color) = &data.AvatarColor { + if let Some(ref color) = av_data.AvatarColor { if color.len() != 7 { err!( "The field AvatarColor must be a HTML/Hex color code with a length of 7 characters" @@ -161,8 +157,7 @@ async fn put_avatar(data: JsonUpcase<AvatarData>, headers: Headers, conn: DbConn } let mut user = headers.user; - user.avatar_color = data.AvatarColor; - + user.avatar_color = av_data.AvatarColor; user.save(&conn).await?; Ok(Json(user.to_json(&conn).await)) } @@ -181,10 +176,10 @@ async fn get_public_keys(uuid: &str, _headers: Headers, conn: DbConn) -> JsonRes #[post("/accounts/keys", data = "<data>")] async fn post_keys(data: JsonUpcase<KeysData>, headers: Headers, conn: DbConn) -> JsonResult { - let data: KeysData = data.into_inner().data; + let key_data: KeysData = data.into_inner().data; let mut user = headers.user; - user.private_key = Some(data.EncryptedPrivateKey); - user.public_key = Some(data.PublicKey); + user.private_key = Some(key_data.EncryptedPrivateKey); + user.public_key = Some(key_data.PublicKey); user.save(&conn).await?; Ok(Json(json!({ "PrivateKey": user.private_key, @@ -209,17 +204,16 @@ async fn post_password( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - let data: ChangePassData = data.into_inner().data; + let pass_data: ChangePassData = data.into_inner().data; let mut user = headers.user; - - if !user.check_valid_password(&data.MasterPasswordHash) { + if !user.check_valid_password(&pass_data.MasterPasswordHash) { err!("Invalid password") } - user.password_hint = clean_password_hint(&data.MasterPasswordHint); + user.password_hint = clean_password_hint(&pass_data.MasterPasswordHint); enforce_password_hint_setting(&user.password_hint)?; user.set_password( - &data.NewMasterPasswordHash, - Some(data.Key), + &pass_data.NewMasterPasswordHash, + Some(pass_data.Key), true, Some(vec![ String::from("post_rotatekey"), @@ -229,12 +223,10 @@ async fn post_password( ); let save_result = user.save(&conn).await; - // Prevent logging out the client where the user requested this endpoint from. // If you do logout the user it will causes issues at the client side. // Adding the device uuid will prevent this. nt.send_logout(&user, Some(headers.device.uuid)).await; - save_result } @@ -245,7 +237,6 @@ struct ChangeKdfData { KdfIterations: i32, KdfMemory: Option<i32>, KdfParallelism: Option<i32>, - MasterPasswordHash: String, NewMasterPasswordHash: String, Key: String, @@ -258,34 +249,33 @@ async fn post_kdf( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - let data: ChangeKdfData = data.into_inner().data; + let kdf_data: ChangeKdfData = data.into_inner().data; let mut user = headers.user; - - if !user.check_valid_password(&data.MasterPasswordHash) { + if !user.check_valid_password(&kdf_data.MasterPasswordHash) { err!("Invalid password") } - if data.Kdf == UserKdfType::Pbkdf2 as i32 && data.KdfIterations < 100_000 { + if kdf_data.Kdf == i32::from(UserKdfType::Pbkdf2) && kdf_data.KdfIterations < 100_000i32 { err!("PBKDF2 KDF iterations must be at least 100000.") } - if data.Kdf == UserKdfType::Argon2id as i32 { - if data.KdfIterations < 1 { + if kdf_data.Kdf == i32::from(UserKdfType::Argon2id) { + if kdf_data.KdfIterations < 1i32 { err!("Argon2 KDF iterations must be at least 1.") } - if let Some(m) = data.KdfMemory { - if !(15..=1024).contains(&m) { + if let Some(m) = kdf_data.KdfMemory { + if !(15i32..=1024i32).contains(&m) { err!("Argon2 memory must be between 15 MB and 1024 MB.") } - user.client_kdf_memory = data.KdfMemory; + user.client_kdf_memory = kdf_data.KdfMemory; } else { err!("Argon2 memory parameter is required.") } - if let Some(p) = data.KdfParallelism { - if !(1..=16).contains(&p) { + if let Some(p) = kdf_data.KdfParallelism { + if !(1i32..=16i32).contains(&p) { err!("Argon2 parallelism must be between 1 and 16.") } - user.client_kdf_parallelism = data.KdfParallelism; + user.client_kdf_parallelism = kdf_data.KdfParallelism; } else { err!("Argon2 parallelism parameter is required.") } @@ -293,13 +283,16 @@ async fn post_kdf( user.client_kdf_memory = None; user.client_kdf_parallelism = None; } - user.client_kdf_iter = data.KdfIterations; - user.client_kdf_type = data.Kdf; - user.set_password(&data.NewMasterPasswordHash, Some(data.Key), true, None); + user.client_kdf_iter = kdf_data.KdfIterations; + user.client_kdf_type = kdf_data.Kdf; + user.set_password( + &kdf_data.NewMasterPasswordHash, + Some(kdf_data.Key), + true, + None, + ); let save_result = user.save(&conn).await; - nt.send_logout(&user, Some(headers.device.uuid)).await; - save_result } @@ -329,22 +322,21 @@ async fn post_rotatekey( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - let data: KeyData = data.into_inner().data; - - if !headers.user.check_valid_password(&data.MasterPasswordHash) { + let key_data: KeyData = data.into_inner().data; + if !headers + .user + .check_valid_password(&key_data.MasterPasswordHash) + { err!("Invalid password") } - // Validate the import before continuing // Bitwarden does not process the import if there is one item invalid. // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it. // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. - Cipher::validate_notes(&data.Ciphers)?; - + Cipher::validate_notes(&key_data.Ciphers)?; let user_uuid = &headers.user.uuid; - // Update folder data - for folder_data in data.Folders { + for folder_data in key_data.Folders { let Some(mut saved_folder) = Folder::find_by_uuid(&folder_data.Id, &conn).await else { err!("Folder doesn't exist") }; @@ -356,21 +348,17 @@ async fn post_rotatekey( saved_folder.name = folder_data.Name; saved_folder.save(&conn).await?; } - // Update cipher data use super::ciphers::update_cipher_from_data; - - for cipher_data in data.Ciphers { + for cipher_data in key_data.Ciphers { let Some(mut saved_cipher) = Cipher::find_by_uuid(cipher_data.Id.as_ref().unwrap(), &conn).await else { err!("Cipher doesn't exist") }; - if saved_cipher.user_uuid.as_ref().unwrap() != user_uuid { err!("The cipher is not owned by the user") } - // Prevent triggering cipher updates via WebSockets by settings UpdateType::None // The user sessions are invalidated because all the ciphers were re-encrypted and thus triggering an update could cause issues. // We force the users to logout after the user has been saved to try and prevent these issues. @@ -388,18 +376,14 @@ async fn post_rotatekey( // Update user data let mut user = headers.user; - - user.akey = data.Key; - user.private_key = Some(data.PrivateKey); + user.akey = key_data.Key; + user.private_key = Some(key_data.PrivateKey); user.reset_security_stamp(); - let save_result = user.save(&conn).await; - // Prevent logging out the client where the user requested this endpoint from. // If you do logout the user it will causes issues at the client side. // Adding the device uuid will prevent this. nt.send_logout(&user, Some(headers.device.uuid)).await; - save_result } @@ -410,17 +394,13 @@ async fn post_sstamp( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - let data: PasswordOrOtpData = data.into_inner().data; + let otp_data: PasswordOrOtpData = data.into_inner().data; let mut user = headers.user; - - data.validate(&user, true, &conn).await?; - + otp_data.validate(&user, true, &conn).await?; Device::delete_all_by_user(&user.uuid, &conn).await?; user.reset_security_stamp(); let save_result = user.save(&conn).await; - nt.send_logout(&user, None).await; - save_result } @@ -479,13 +459,11 @@ async fn post_verify_email_token( data: JsonUpcase<VerifyEmailTokenData>, conn: DbConn, ) -> EmptyResult { - let data: VerifyEmailTokenData = data.into_inner().data; - - let Some(mut user) = User::find_by_uuid(&data.UserId, &conn).await else { + let token_data: VerifyEmailTokenData = data.into_inner().data; + let Some(mut user) = User::find_by_uuid(&token_data.UserId, &conn).await else { err!("User doesn't exist") }; - - let Ok(claims) = decode_verify_email(&data.Token) else { + let Ok(claims) = decode_verify_email(&token_data.Token) else { err!("Invalid claim") }; if claims.sub != user.uuid { @@ -493,7 +471,7 @@ async fn post_verify_email_token( } user.verified_at = Some(Utc::now().naive_utc()); user.last_verifying_at = None; - user.login_verify_count = 0; + user.login_verify_count = 0i32; user.save(&conn).await } @@ -521,13 +499,11 @@ async fn post_delete_recover_token( data: JsonUpcase<DeleteRecoverTokenData>, conn: DbConn, ) -> EmptyResult { - let data: DeleteRecoverTokenData = data.into_inner().data; - - let Some(user) = User::find_by_uuid(&data.UserId, &conn).await else { + let token_data: DeleteRecoverTokenData = data.into_inner().data; + let Some(user) = User::find_by_uuid(&token_data.UserId, &conn).await else { err!("User doesn't exist") }; - - let Ok(claims) = decode_delete(&data.Token) else { + let Ok(claims) = decode_delete(&token_data.Token) else { err!("Invalid claim") }; if claims.sub != user.uuid { @@ -551,9 +527,9 @@ async fn delete_account( headers: Headers, conn: DbConn, ) -> EmptyResult { - let data: PasswordOrOtpData = data.into_inner().data; + let otp_data: PasswordOrOtpData = data.into_inner().data; let user = headers.user; - data.validate(&user, true, &conn).await?; + otp_data.validate(&user, true, &conn).await?; user.delete(&conn).await } #[allow(clippy::unnecessary_wraps)] @@ -587,23 +563,22 @@ async fn prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> Json<Value> { } pub async fn _prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> Json<Value> { - let data: PreloginData = data.into_inner().data; - - let (kdf_type, kdf_iter, kdf_mem, kdf_para) = match User::find_by_mail(&data.Email, &conn).await - { - Some(user) => ( - user.client_kdf_type, - user.client_kdf_iter, - user.client_kdf_memory, - user.client_kdf_parallelism, - ), - None => ( - User::CLIENT_KDF_TYPE_DEFAULT, - User::CLIENT_KDF_ITER_DEFAULT, - None, - None, - ), - }; + let login_data: PreloginData = data.into_inner().data; + let (kdf_type, kdf_iter, kdf_mem, kdf_para) = + 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, + ), + None => ( + User::client_kdf_type_default(), + User::CLIENT_KDF_ITER_DEFAULT, + None, + None, + ), + }; let result = json!({ "Kdf": kdf_type, @@ -611,7 +586,6 @@ pub async fn _prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> Json<Val "KdfMemory": kdf_mem, "KdfParallelism": kdf_para, }); - Json(result) } @@ -624,13 +598,11 @@ struct SecretVerificationRequest { #[post("/accounts/verify-password", data = "<data>")] fn verify_password(data: JsonUpcase<SecretVerificationRequest>, headers: Headers) -> EmptyResult { - let data: SecretVerificationRequest = data.into_inner().data; + let req: SecretVerificationRequest = data.into_inner().data; let user = headers.user; - - if !user.check_valid_password(&data.MasterPasswordHash) { + if !user.check_valid_password(&req.MasterPasswordHash) { err!("Invalid password") } - Ok(()) } @@ -641,17 +613,13 @@ async fn _api_key( conn: DbConn, ) -> JsonResult { use crate::util::format_date; - - let data: PasswordOrOtpData = data.into_inner().data; + let otp_data: PasswordOrOtpData = data.into_inner().data; let mut user = headers.user; - - data.validate(&user, true, &conn).await?; - + otp_data.validate(&user, true, &conn).await?; if rotate || user.api_key.is_none() { user.api_key = Some(crypto::generate_api_key()); user.save(&conn).await.expect("Error saving API key"); } - Ok(Json(json!({ "ApiKey": user.api_key, "RevisionDate": format_date(&user.updated_at), @@ -703,7 +671,6 @@ struct KnownDevice { #[rocket::async_trait] impl<'r> FromRequest<'r> for KnownDevice { type Error = &'static str; - async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> { let email = if let Some(email_b64) = req.headers().get_one("X-Request-Email") { let Ok(email_bytes) = data_encoding::BASE64URL_NOPAD.decode(email_b64.as_bytes()) @@ -727,7 +694,7 @@ impl<'r> FromRequest<'r> for KnownDevice { }; let uuid = if let Some(uuid) = req.headers().get_one("X-Device-Identifier") { - uuid.to_string() + uuid.to_owned() } else { return Outcome::Error((Status::BadRequest, "X-Device-Identifier value is required")); }; @@ -789,26 +756,21 @@ async fn post_auth_request( conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - let data = data.into_inner(); - let Some(user) = User::find_by_mail(&data.email, &conn).await else { + let inner_data = data.into_inner(); + let Some(user) = User::find_by_mail(&inner_data.email, &conn).await else { err!("AuthRequest doesn't exist") }; let mut auth_request = AuthRequest::new( user.uuid.clone(), - data.deviceIdentifier.clone(), + inner_data.deviceIdentifier.clone(), headers.device_type, headers.ip.ip.to_string(), - data.accessCode, - data.publicKey, + inner_data.accessCode, + inner_data.publicKey, ); auth_request.save(&conn).await?; - nt.send_auth_request( - &user.uuid, - &auth_request.uuid, - &data.deviceIdentifier, - &conn, - ) - .await; + nt.send_auth_request(&user.uuid, &auth_request.uuid, &inner_data.deviceIdentifier) + .await; Ok(Json(json!({ "id": auth_request.uuid, "publicKey": auth_request.public_key, @@ -868,17 +830,17 @@ async fn put_auth_request( ant: AnonymousNotify<'_>, nt: Notify<'_>, ) -> JsonResult { - let data = data.into_inner(); + let inner_data = data.into_inner(); let mut auth_request: AuthRequest = match AuthRequest::find_by_uuid(uuid, &conn).await { Some(auth_request) => auth_request, None => { err!("AuthRequest doesn't exist") } }; - auth_request.approved = Some(data.requestApproved); - auth_request.enc_key = Some(data.key); - auth_request.master_password_hash = data.masterPasswordHash; - auth_request.response_device_id = Some(data.deviceIdentifier.clone()); + auth_request.approved = Some(inner_data.requestApproved); + auth_request.enc_key = Some(inner_data.key); + auth_request.master_password_hash = inner_data.masterPasswordHash; + auth_request.response_device_id = Some(inner_data.deviceIdentifier.clone()); auth_request.save(&conn).await?; if auth_request.approved.unwrap_or(false) { ant.send_auth_response(&auth_request.user_uuid, &auth_request.uuid) @@ -886,8 +848,7 @@ async fn put_auth_request( nt.send_auth_response( &auth_request.user_uuid, &auth_request.uuid, - data.deviceIdentifier, - &conn, + inner_data.deviceIdentifier, ) .await; } diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs @@ -397,7 +397,7 @@ pub async fn update_cipher_from_data( err!("Organization mismatch. Please resync the client before updating the cipher") } - if let Some(note) = &data.Notes { + if let Some(ref note) = data.Notes { if note.len() > 10_000 { err!("The field Notes exceeds the maximum encrypted value length of 10000 characters.") } @@ -457,22 +457,22 @@ pub async fn update_cipher_from_data( } let type_data_opt = match data.Type { - 1 => data.Login, - 2 => data.SecureNote, - 3 => data.Card, - 4 => data.Identity, + 1i32 => data.Login, + 2i32 => data.SecureNote, + 3i32 => data.Card, + 4i32 => data.Identity, _ => err!("Invalid type"), }; let type_data = match type_data_opt { - Some(mut data) => { + Some(mut data_in_type) => { // Remove the 'Response' key from the base object. - data.as_object_mut().unwrap().remove("Response"); + data_in_type.as_object_mut().unwrap().remove("Response"); // Remove the 'Response' key from every Uri. - if data["Uris"].is_array() { - data["Uris"] = _clean_cipher_data(data["Uris"].clone()); + if data_in_type["Uris"].is_array() { + data_in_type["Uris"] = _clean_cipher_data(data_in_type["Uris"].clone()); } - data + data_in_type } None => err!("Data missing"), }; @@ -500,7 +500,6 @@ pub async fn update_cipher_from_data( &cipher.update_users_revision(conn).await, &headers.device.uuid, None, - conn, ) .await; } @@ -800,14 +799,12 @@ async fn post_collections_admin( } } } - nt.send_cipher_update( UpdateType::SyncCipherUpdate, &cipher, &cipher.update_users_revision(&conn).await, &headers.device.uuid, Some(Vec::from_iter(posted_collections)), - &conn, ) .await; Ok(()) @@ -910,9 +907,9 @@ async fn share_cipher_by_uuid( let mut shared_to_collection = false; - if let Some(organization_uuid) = &data.Cipher.OrganizationId { - for uuid in &data.CollectionIds { - match Collection::find_by_uuid_and_org(uuid, organization_uuid, conn).await { + if let Some(ref organization_uuid) = data.Cipher.OrganizationId { + for col_uuid in &data.CollectionIds { + match Collection::find_by_uuid_and_org(col_uuid, organization_uuid, conn).await { None => err!("Invalid collection ID provided"), Some(collection) => { if collection @@ -1299,19 +1296,16 @@ async fn move_cipher_selected( if !cipher.is_accessible_to_user(&user_uuid, &conn).await { err!("Cipher is not accessible by user") } - // Move cipher cipher .move_to_folder(data.FolderId.clone(), &user_uuid, &conn) .await?; - nt.send_cipher_update( UpdateType::SyncCipherUpdate, &cipher, &[user_uuid.clone()], &headers.device.uuid, None, - &conn, ) .await; } @@ -1408,7 +1402,6 @@ async fn _delete_cipher_by_uuid( &cipher.update_users_revision(conn).await, &headers.device.uuid, None, - conn, ) .await; } else { @@ -1419,7 +1412,6 @@ async fn _delete_cipher_by_uuid( &cipher.update_users_revision(conn).await, &headers.device.uuid, None, - conn, ) .await; } @@ -1473,14 +1465,12 @@ async fn _restore_cipher_by_uuid( cipher.deleted_at = None; cipher.save(conn).await?; - nt.send_cipher_update( UpdateType::SyncCipherUpdate, &cipher, &cipher.update_users_revision(conn).await, &headers.device.uuid, None, - conn, ) .await; Ok(Json( @@ -1566,7 +1556,7 @@ impl CipherSyncData { } // Generate a HashMap with the Cipher UUID as key and one or more Collection UUID's let user_cipher_collections = - Cipher::get_collections_with_cipher_by_user(user_uuid.to_string(), conn).await; + Cipher::get_collections_with_cipher_by_user(user_uuid.to_owned(), conn).await; let mut cipher_collections: HashMap<String, Vec<String>> = HashMap::with_capacity(user_cipher_collections.len()); for (cipher, collection) in user_cipher_collections { diff --git a/src/api/core/folders.rs b/src/api/core/folders.rs @@ -61,13 +61,8 @@ async fn post_folders( let mut folder = Folder::new(headers.user.uuid, data.Name); folder.save(&conn).await?; - nt.send_folder_update( - UpdateType::SyncFolderCreate, - &folder, - &headers.device.uuid, - &conn, - ) - .await; + nt.send_folder_update(UpdateType::SyncFolderCreate, &folder, &headers.device.uuid) + .await; Ok(Json(folder.to_json())) } @@ -104,13 +99,8 @@ async fn put_folder( folder.name = data.Name; folder.save(&conn).await?; - nt.send_folder_update( - UpdateType::SyncFolderUpdate, - &folder, - &headers.device.uuid, - &conn, - ) - .await; + nt.send_folder_update(UpdateType::SyncFolderUpdate, &folder, &headers.device.uuid) + .await; Ok(Json(folder.to_json())) } @@ -134,16 +124,9 @@ async fn delete_folder(uuid: &str, headers: Headers, conn: DbConn, nt: Notify<'_ if folder.user_uuid != headers.user.uuid { err!("Folder belongs to another user") } - // Delete the actual folder entry folder.delete(&conn).await?; - - nt.send_folder_update( - UpdateType::SyncFolderDelete, - &folder, - &headers.device.uuid, - &conn, - ) - .await; + nt.send_folder_update(UpdateType::SyncFolderDelete, &folder, &headers.device.uuid) + .await; Ok(()) } diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs @@ -110,8 +110,8 @@ async fn post_eq_domains( let mut user = headers.user; use serde_json::to_string; - user.excluded_globals = to_string(&excluded_globals).unwrap_or_else(|_| "[]".to_string()); - user.equivalent_domains = to_string(&equivalent_domains).unwrap_or_else(|_| "[]".to_string()); + user.excluded_globals = to_string(&excluded_globals).unwrap_or_else(|_| "[]".to_owned()); + user.equivalent_domains = to_string(&equivalent_domains).unwrap_or_else(|_| "[]".to_owned()); user.save(&conn).await?; Ok(Json(json!({}))) @@ -190,7 +190,7 @@ pub fn catchers() -> Vec<Catcher> { fn api_not_found() -> Json<Value> { Json(json!({ "error": { - "code": 404, + "code": 404i32, "reason": "Not Found", "description": "The requested resource could not be found." } diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs @@ -390,7 +390,6 @@ async fn post_organization_collection_update( conn: DbConn, ) -> JsonResult { let data: NewCollectionData = data.into_inner().data; - let Some(org) = Organization::find_by_uuid(org_id, &conn).await else { err!("Can't find organization details") }; @@ -724,12 +723,8 @@ async fn get_org_users( let mut users_json = Vec::new(); for u in UserOrganization::find_by_org(org_id, &conn).await { users_json.push( - u.to_json_user_details( - data.include_collections.unwrap_or(false), - data.include_groups.unwrap_or(false), - &conn, - ) - .await, + u.to_json_user_details(data.include_collections.unwrap_or(false), &conn) + .await, ); } @@ -889,12 +884,8 @@ async fn get_user( // Else these will not be shown in the interface, and could lead to missing collections when saved. let include_groups = data.include_groups.unwrap_or(false); Ok(Json( - user.to_json_user_details( - data.include_collections.unwrap_or(include_groups), - include_groups, - &conn, - ) - .await, + user.to_json_user_details(data.include_collections.unwrap_or(include_groups), &conn) + .await, )) } @@ -959,7 +950,7 @@ async fn edit_user( if user_to_edit.atype == UserOrgType::Owner && new_type != UserOrgType::Owner - && user_to_edit.status == UserOrgStatus::Confirmed as i32 + && user_to_edit.status == i32::from(UserOrgStatus::Confirmed) { // Removing owner permission, check that there is at least one other confirmed owner if UserOrganization::count_confirmed_by_org_and_type(org_id, UserOrgType::Owner, &conn) @@ -983,10 +974,8 @@ async fn edit_user( } } } - user_to_edit.access_all = data.AccessAll; - user_to_edit.atype = new_type as i32; - + user_to_edit.atype = i32::from(new_type); // Delete all the odd collections for c in CollectionUser::find_by_organization_and_user_uuid(org_id, &user_to_edit.user_uuid, &conn) @@ -1089,7 +1078,7 @@ async fn _delete_user( } if user_to_delete.atype == UserOrgType::Owner - && user_to_delete.status == UserOrgStatus::Confirmed as i32 + && user_to_delete.status == i32::from(UserOrgStatus::Confirmed) { // Removing owner, check that there is at least one other confirmed owner if UserOrganization::count_confirmed_by_org_and_type(org_id, UserOrgType::Owner, conn).await @@ -1225,8 +1214,8 @@ async fn post_org_import( for (cipher_index, coll_index) in relations { let cipher_id = &ciphers[cipher_index].uuid; let coll = &collections[coll_index]; - let coll_id = match coll { - Ok(coll) => coll.uuid.as_str(), + let coll_id = match *coll { + Ok(ref coll) => coll.uuid.as_str(), Err(_) => err!("Failed to assign to collection"), }; @@ -1282,7 +1271,7 @@ async fn get_policy( err!("Invalid or unsupported policy type") }; let policy = (OrgPolicy::find_by_org_and_type(org_id, pol_type_enum, &conn).await).map_or_else( - || OrgPolicy::new(String::from(org_id), pol_type_enum, "null".to_string()), + || OrgPolicy::new(String::from(org_id), pol_type_enum, "null".to_owned()), core::convert::identity, ); Ok(Json(policy.to_json())) @@ -1321,7 +1310,7 @@ async fn put_policy( // Invited users still need to accept the invite and will get an error when they try to accept the invite. if user_twofactor_disabled && member.atype < UserOrgType::Admin - && member.status != UserOrgStatus::Invited as i32 + && member.status != i32::from(UserOrgStatus::Invited) { member.delete(&conn).await?; } @@ -1336,7 +1325,7 @@ async fn put_policy( // Those users will not be allowed to accept or be activated because of the policy checks done there. // We check if the count is larger then 1, because it includes this organization also. if member.atype < UserOrgType::Admin - && member.status != UserOrgStatus::Invited as i32 + && member.status != i32::from(UserOrgStatus::Invited) && UserOrganization::count_accepted_and_confirmed_by_user(&member.user_uuid, &conn) .await > 1 @@ -1347,7 +1336,7 @@ async fn put_policy( } let mut policy = (OrgPolicy::find_by_org_and_type(org_id, pol_type_enum, &conn).await) .map_or_else( - || OrgPolicy::new(String::from(org_id), pol_type_enum, "{}".to_string()), + || OrgPolicy::new(String::from(org_id), pol_type_enum, "{}".to_owned()), |p| p, ); policy.enabled = data.enabled; @@ -1373,21 +1362,21 @@ fn get_plans() -> Json<Value> { "Object": "list", "Data": [{ "Object": "plan", - "Type": 0, - "Product": 0, + "Type": 0i32, + "Product": 0i32, "Name": "Free", "NameLocalizationKey": "planNameFree", - "BitwardenProduct": 0, - "MaxUsers": 0, + "BitwardenProduct": 0i32, + "MaxUsers": 0i32, "DescriptionLocalizationKey": "planDescFree" },{ "Object": "plan", - "Type": 0, - "Product": 1, + "Type": 0i32, + "Product": 1i32, "Name": "Free", "NameLocalizationKey": "planNameFree", - "BitwardenProduct": 1, - "MaxUsers": 0, + "BitwardenProduct": 1i32, + "MaxUsers": 0i32, "DescriptionLocalizationKey": "planDescFree" }], "ContinuationToken": null @@ -1475,15 +1464,16 @@ async fn import( .is_none() { if let Some(user) = User::find_by_mail(&user_data.Email, &conn).await { - let user_org_status = UserOrgStatus::Accepted as i32; - + let user_org_status = i32::from(UserOrgStatus::Accepted); let mut new_org_user = UserOrganization::new(user.uuid.clone(), String::from(org_id)); new_org_user.access_all = false; - new_org_user.atype = UserOrgType::User as i32; + new_org_user.atype = i32::from(UserOrgType::User); new_org_user.status = user_org_status; new_org_user.save(&conn).await?; } + } else { + // We don't care. } } // If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true) @@ -1501,7 +1491,6 @@ async fn import( } } } - Ok(()) } @@ -1583,7 +1572,7 @@ async fn _revoke_organization_user( conn: &DbConn, ) -> EmptyResult { match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await { - Some(mut user_org) if user_org.status > UserOrgStatus::Revoked as i32 => { + Some(mut user_org) if user_org.status > i32::from(UserOrgStatus::Revoked) => { if user_org.user_uuid == headers.user.uuid { err!("You cannot revoke yourself") } @@ -1688,7 +1677,7 @@ async fn _restore_organization_user( conn: &DbConn, ) -> EmptyResult { match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await { - Some(mut user_org) if user_org.status < UserOrgStatus::Accepted as i32 => { + Some(mut user_org) if user_org.status < i32::from(UserOrgStatus::Accepted) => { if user_org.user_uuid == headers.user.uuid { err!("You cannot restore yourself") } diff --git a/src/api/core/two_factor/authenticator.rs b/src/api/core/two_factor/authenticator.rs @@ -28,7 +28,7 @@ async fn generate_authenticator( let data: PasswordOrOtpData = data.into_inner().data; let user = headers.user; data.validate(&user, false, &conn).await?; - let type_ = TwoFactorType::Authenticator as i32; + let type_ = i32::from(TwoFactorType::Authenticator); let twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await; let (enabled, key) = match twofactor { @@ -110,7 +110,7 @@ pub async fn validate_totp_code_str( validate_totp_code(user_uuid, totp_code, secret, ip, conn).await } - +#[allow(clippy::integer_division, clippy::redundant_else)] async fn validate_totp_code( user_uuid: &str, totp_code: &str, @@ -123,71 +123,51 @@ async fn validate_totp_code( let Ok(decoded_secret) = BASE32.decode(secret.as_bytes()) else { err!("Invalid TOTP secret") }; - let mut twofactor = - (TwoFactor::find_by_user_and_type(user_uuid, TwoFactorType::Authenticator as i32, conn) - .await) - .map_or_else( - || { - TwoFactor::new( - user_uuid.to_string(), - TwoFactorType::Authenticator, - secret.to_string(), - ) - }, - |tf| tf, - ); - let steps = 0; + let mut twofactor = (TwoFactor::find_by_user_and_type( + user_uuid, + i32::from(TwoFactorType::Authenticator), + conn, + ) + .await) + .map_or_else( + || { + TwoFactor::new( + user_uuid.to_owned(), + TwoFactorType::Authenticator, + secret.to_owned(), + ) + }, + |tf| tf, + ); // Get the current system time in UNIX Epoch (UTC) let current_time = chrono::Utc::now(); let current_timestamp = current_time.timestamp(); - - for step in -steps..=steps { - let time_step = (current_timestamp / 30i64) - .checked_add(step) - .expect("overflow during addition of TOTP"); - // We need to calculate the time offsite and cast it as an u64. - // Since we only have times into the future and the totp generator needs an u64 instead of the default i64. - let time = u64::try_from( - current_timestamp - .checked_add( - step.checked_mul(30i64) - .expect("overflow during multiplication in TOTP"), - ) - .expect("overflow during addition in TOTP"), - ) - .expect("underflow when casting to a u64 in TOTP"); - let generated = totp_custom::<Sha1>(30, 6, &decoded_secret, time); - - // Check the the given code equals the generated and if the time_step is larger then the one last used. - if generated == totp_code && time_step > i64::from(twofactor.last_used) { - // If the step does not equals 0 the time is drifted either server or client side. - if step != 0 { - warn!("TOTP Time drift detected. The step offset is {}", step); - } - - // 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.save(conn).await?; - return Ok(()); - } else if generated == totp_code && time_step <= i64::from(twofactor.last_used) { - warn!( - "This TOTP or a TOTP code within {} steps back or forward has already been used!", - steps - ); - err!(format!( - "Invalid TOTP code! Server time: {} IP: {}", - current_time.format("%F %T UTC"), - ip.ip - )); - } + let time_step = current_timestamp / 30i64; + // We need to calculate the time offsite and cast it as an u64. + // Since we only have times into the future and the totp generator needs an 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); + // Check the the given code equals the generated and if the time_step is larger then the one last used. + if generated == totp_code && time_step > i64::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.save(conn).await?; + Ok(()) + } else if generated == totp_code && time_step <= i64::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: {}", + current_time.format("%F %T UTC"), + ip.ip + )); + } else { + // Else no valid code received, deny access + err!(format!( + "Invalid TOTP code! Server time: {} IP: {}", + current_time.format("%F %T UTC"), + ip.ip + )); } - - // Else no valid code received, deny access - err!(format!( - "Invalid TOTP code! Server time: {} IP: {}", - current_time.format("%F %T UTC"), - ip.ip - )); } diff --git a/src/api/core/two_factor/protected_actions.rs b/src/api/core/two_factor/protected_actions.rs @@ -34,9 +34,11 @@ impl ProtectedActionData { Err(_) => err!("Could not decode ProtectedActionData from string"), } } - fn add_attempt(&mut self) { - self.attempts += 1; + self.attempts = self + .attempts + .checked_add(1) + .expect("add attempts overflowed"); } } @@ -69,7 +71,7 @@ pub async fn validate_protected_action_otp( ) -> EmptyResult { let pa = TwoFactor::find_by_user_and_type( user_uuid, - TwoFactorType::ProtectedActions as i32, + i32::from(TwoFactorType::ProtectedActions), conn, ) .await @@ -85,24 +87,24 @@ pub async fn validate_protected_action_otp( pa.delete(conn).await?; err!("Token has expired") } - // Check if the token has expired (Using the email 2fa expiration time) let date = NaiveDateTime::from_timestamp_opt(pa_data.token_sent, 0) .expect("Protected Action token timestamp invalid."); let max_time = 600; - if date + Duration::seconds(max_time) < Utc::now().naive_utc() { + if date + .checked_add_signed(Duration::seconds(max_time)) + .expect("Duration add overflowed") + < Utc::now().naive_utc() + { pa.delete(conn).await?; err!("Token has expired") } - if !crypto::ct_eq(&pa_data.token, otp) { pa.save(conn).await?; err!("Token is invalid") } - if delete_if_valid { pa.delete(conn).await?; } - Ok(()) } diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs @@ -274,7 +274,7 @@ async fn activate_webauthn( .await?; // Retrieve and delete the saved challenge state - let type_ = TwoFactorType::WebauthnRegisterChallenge as i32; + let type_ = i32::from(TwoFactorType::WebauthnRegisterChallenge); let state = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await { Some(tf) => { let state: RegistrationState = serde_json::from_str(&tf.data)?; @@ -345,17 +345,23 @@ async fn delete_webauthn( { err!("Invalid password"); } - let Some(mut tf) = - TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::Webauthn as i32, &conn) - .await + let Some(mut tf) = TwoFactor::find_by_user_and_type( + &headers.user.uuid, + i32::from(TwoFactorType::Webauthn), + &conn, + ) + .await else { err!("Webauthn data not found!") }; - let data: Vec<WebauthnRegistration> = serde_json::from_str(&tf.data)?; - tf.data = serde_json::to_string(&data)?; + let web_authn_data: Vec<WebauthnRegistration> = serde_json::from_str(&tf.data)?; + tf.data = serde_json::to_string(&web_authn_data)?; tf.save(&conn).await?; drop(tf); - let keys_json: Vec<Value> = data.iter().map(WebauthnRegistration::to_json).collect(); + let keys_json: Vec<Value> = web_authn_data + .iter() + .map(WebauthnRegistration::to_json) + .collect(); Ok(Json(json!({ "Enabled": true, "Keys": keys_json, @@ -367,7 +373,7 @@ async fn get_webauthn_registrations( user_uuid: &str, conn: &DbConn, ) -> Result<(bool, Vec<WebauthnRegistration>), Error> { - let type_ = TwoFactorType::Webauthn as i32; + let type_ = i32::from(TwoFactorType::Webauthn); match TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await { Some(tf) => Ok((tf.enabled, serde_json::from_str(&tf.data)?)), None => Ok((false, Vec::new())), // If no data, return empty list @@ -412,7 +418,7 @@ pub async fn validate_webauthn_login( response: &str, conn: &DbConn, ) -> EmptyResult { - let type_ = TwoFactorType::WebauthnLoginChallenge as i32; + let type_ = i32::from(TwoFactorType::WebauthnLoginChallenge); let state = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await { Some(tf) => { let state: AuthenticationState = serde_json::from_str(&tf.data)?; @@ -432,7 +438,7 @@ pub async fn validate_webauthn_login( reg.credential.counter = auth_data.counter; TwoFactor::new( - user_uuid.to_string(), + user_uuid.to_owned(), TwoFactorType::Webauthn, serde_json::to_string(&registrations)?, ) diff --git a/src/api/identity.rs b/src/api/identity.rs @@ -102,7 +102,7 @@ async fn _refresh_login(data: ConnectData, conn: &DbConn) -> JsonResult { Ok(Json(result)) } - +#[allow(clippy::else_if_without_else)] async fn _password_login( data: ConnectData, user_uuid: &mut Option<String>, @@ -126,7 +126,6 @@ async fn _password_login( // Set the user_uuid here to be passed back used for event logging. *user_uuid = Some(user.uuid.clone()); - // Check password let password = data.password.as_ref().unwrap(); if let Some(auth_request_uuid) = data.auth_request.clone() { @@ -151,7 +150,6 @@ async fn _password_login( format!("IP: {}. Username: {}.", ip.ip, username) ) } - // Change the KDF Iterations if user.password_iterations != config::get_config().password_iterations { user.password_iterations = config::get_config().password_iterations; @@ -314,7 +312,7 @@ async fn _organization_api_key_login( Ok(Json(json!({ "access_token": access_token, - "expires_in": 3600, + "expires_in": 3600i32, "token_type": "Bearer", "scope": "api.organization", "unofficialServer": true, @@ -325,7 +323,7 @@ async fn _organization_api_key_login( async fn get_device(data: &ConnectData, conn: &DbConn, user: &User) -> (Device, bool) { // On iOS, device_type sends "iOS", on others it sends a number // When unknown or unable to parse, return 14, which is 'Unknown Browser' - let device_type = util::try_parse_string(data.device_type.as_ref()).unwrap_or(14); + let device_type = util::try_parse_string(data.device_type.as_ref()).unwrap_or(14i32); let device_id = data .device_identifier .clone() diff --git a/src/api/mod.rs b/src/api/mod.rs @@ -82,9 +82,9 @@ impl NumberOrString { #[allow(clippy::wrong_self_convention)] fn into_i32(&self) -> ApiResult<i32> { use std::num::ParseIntError as PIE; - match self { - Self::Number(n) => Ok(*n), - Self::String(s) => s + match *self { + Self::Number(n) => Ok(n), + Self::String(ref s) => s .parse() .map_err(|e: PIE| crate::Error::new("Can't convert to number", e.to_string())), } diff --git a/src/api/notifications.rs b/src/api/notifications.rs @@ -1,9 +1,6 @@ use crate::{ auth::{ClientIp, WsAccessTokenHeader}, - db::{ - models::{Cipher, Folder, User}, - DbConn, - }, + db::models::{Cipher, Folder, User}, Error, }; use chrono::{NaiveDateTime, Utc}; @@ -78,7 +75,7 @@ impl WSEntryMapGuard { impl Drop for WSEntryMapGuard { fn drop(&mut self) { if let Some(mut entry) = self.users.map.get_mut(&self.user_uuid) { - entry.retain(|(uuid, _)| uuid != &self.entry_uuid); + entry.retain(|tup| tup.0 != self.entry_uuid); } } } @@ -140,28 +137,30 @@ fn websockets_hub<'r>( Ok({ rocket_ws::Stream! { ws => { - let mut ws = ws; + let mut ws_copy = ws; let _guard = guard; let mut interval = tokio::time::interval(Duration::from_secs(15)); loop { tokio::select! { - res = ws.next() => { + res = ws_copy.next() => { match res { Some(Ok(message)) => { match message { // Respond to any pings Message::Ping(ping) => yield Message::Pong(ping), Message::Pong(_) => {/* Ignored */}, - // We should receive an initial message with the protocol and version, and we will reply to it Message::Text(ref message) => { - let msg = message.strip_suffix(RECORD_SEPARATOR as char).unwrap_or(message); + let msg = message.strip_suffix(char::from(RECORD_SEPARATOR)).unwrap_or(message); if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) { yield Message::binary(INITIAL_RESPONSE); continue; } } + // Prevent sending anything back when a `Close` Message is received. + // Just break the loop + Message::Close(_) => break, // Just echo anything else the client sends _ => yield message, } @@ -203,28 +202,29 @@ fn anonymous_websockets_hub<'r>( Ok({ rocket_ws::Stream! { ws => { - let mut ws = ws; + let mut ws_copy = ws; let _guard = guard; let mut interval = tokio::time::interval(Duration::from_secs(15)); loop { tokio::select! { - res = ws.next() => { + res = ws_copy.next() => { match res { Some(Ok(message)) => { match message { // Respond to any pings Message::Ping(ping) => yield Message::Pong(ping), Message::Pong(_) => {/* Ignored */}, - // We should receive an initial message with the protocol and version, and we will reply to it Message::Text(ref message) => { - let msg = message.strip_suffix(RECORD_SEPARATOR as char).unwrap_or(message); - + let msg = message.strip_suffix(char::from(RECORD_SEPARATOR)).unwrap_or(message); if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) { yield Message::binary(INITIAL_RESPONSE); continue; } } + // Prevent sending anything back when a `Close` Message is received. + // Just break the loop + Message::Close(_) => break, // Just echo anything else the client sends _ => yield message, } @@ -250,7 +250,6 @@ fn anonymous_websockets_hub<'r>( // // Websockets server // -#[allow(clippy::cast_possible_truncation)] fn serialize(val: Value) -> Vec<u8> { use rmpv::encode::write_value; let mut buf = Vec::new(); @@ -261,11 +260,11 @@ fn serialize(val: Value) -> Vec<u8> { let mut len_buf: Vec<u8> = Vec::new(); loop { let mut size_part = size & 0x7f; - size >>= 7; + size >>= 7i32; if size > 0 { size_part |= 0x80; } - len_buf.push(size_part as u8); + len_buf.push(u8::try_from(size_part).unwrap()); if size == 0 { break; } @@ -273,11 +272,11 @@ fn serialize(val: Value) -> Vec<u8> { len_buf.append(&mut buf); len_buf } - +#[allow(clippy::big_endian_bytes)] fn serialize_date(date: NaiveDateTime) -> Value { let seconds: i64 = date.timestamp(); let nanos: i64 = date.timestamp_subsec_nanos().into(); - let timestamp = nanos << 34 | seconds; + let timestamp = nanos << 34i32 | seconds; let bs = timestamp.to_be_bytes(); // -1 is Timestamp // https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type @@ -312,8 +311,10 @@ pub struct WebSocketUsers { impl WebSocketUsers { async fn send_update(&self, user_uuid: &str, data: &[u8]) { if let Some(user) = self.map.get(user_uuid).map(|v| v.clone()) { - for (_, sender) in &user { - _ = sender.send(Message::binary(data)).await; + for tup in user { + if let Err(e) = tup.1.send(Message::binary(data)).await { + error!("Error sending WS update {e}"); + } } } } @@ -350,7 +351,6 @@ impl WebSocketUsers { ut: UpdateType, folder: &Folder, acting_device_uuid: &String, - _: &DbConn, ) { let data = create_update( vec![ @@ -372,7 +372,6 @@ impl WebSocketUsers { user_uuids: &[String], acting_device_uuid: &String, collection_uuids: Option<Vec<String>>, - _: &DbConn, ) { let org_uuid = convert_option(cipher.organization_uuid.clone()); // Depending if there are collections provided or not, we need to have different values for the following variables. @@ -385,11 +384,11 @@ impl WebSocketUsers { serialize_date(cipher.updated_at), ) }, - |collection_uuids| { + |col_uuids| { ( Value::Nil, Value::Array( - collection_uuids + col_uuids .into_iter() .map(core::convert::Into::into) .collect::<Vec<rmpv::Value>>(), @@ -420,7 +419,6 @@ impl WebSocketUsers { user_uuid: &str, auth_request_uuid: &str, acting_device_uuid: &str, - _: &DbConn, ) { let data = create_update( vec![ @@ -428,7 +426,7 @@ impl WebSocketUsers { ("UserId".into(), user_uuid.to_owned().into()), ], UpdateType::AuthRequest, - Some(acting_device_uuid.to_string()), + Some(acting_device_uuid.to_owned()), ); self.send_update(user_uuid, &data).await; } @@ -438,7 +436,6 @@ impl WebSocketUsers { user_uuid: &str, auth_response_uuid: &str, approving_device_uuid: String, - _: &DbConn, ) { let data = create_update( vec![ @@ -460,7 +457,9 @@ pub struct AnonymousWebSocketSubscriptions { impl AnonymousWebSocketSubscriptions { async fn send_update(&self, token: &str, data: &[u8]) { if let Some(sender) = self.map.get(token).map(|v| v.clone()) { - _ = sender.send(Message::binary(data)).await; + if let Err(e) = sender.send(Message::binary(data)).await { + error!("Error sending WS update {e}"); + }; } } @@ -499,7 +498,7 @@ fn create_update( ) -> Vec<u8> { use rmpv::Value as V; let value = V::Array(vec![ - 1.into(), + 1i32.into(), V::Map(vec![]), V::Nil, "ReceiveMessage".into(), @@ -508,7 +507,7 @@ fn create_update( "ContextId".into(), acting_device_uuid.map_or(V::Nil, core::convert::Into::into), ), - ("Type".into(), (ut as i32).into()), + ("Type".into(), (i32::from(ut)).into()), ("Payload".into(), payload.into()), ])]), ]); @@ -523,12 +522,12 @@ fn create_anonymous_update( use rmpv::Value as V; let value = V::Array(vec![ - 1.into(), + 1i32.into(), V::Map(vec![]), V::Nil, "AuthRequestResponseRecieved".into(), V::Array(vec![V::Map(vec![ - ("Type".into(), (ut as i32).into()), + ("Type".into(), (i32::from(ut)).into()), ("Payload".into(), payload.into()), ("UserId".into(), user_id.into()), ])]), @@ -538,7 +537,7 @@ fn create_anonymous_update( } fn create_ping() -> Vec<u8> { - serialize(Value::Array(vec![6.into()])) + serialize(Value::Array(vec![6i32.into()])) } #[allow(dead_code)] @@ -563,6 +562,30 @@ pub enum UpdateType { AuthRequestResponse = 16, None = 100, } +impl From<UpdateType> for i32 { + fn from(value: UpdateType) -> Self { + match value { + UpdateType::SyncCipherUpdate => 0i32, + UpdateType::SyncCipherCreate => 1i32, + UpdateType::SyncLoginDelete => 2i32, + UpdateType::SyncFolderDelete => 3i32, + UpdateType::SyncCiphers => 4i32, + UpdateType::SyncVault => 5i32, + UpdateType::SyncOrgKeys => 6i32, + UpdateType::SyncFolderCreate => 7i32, + UpdateType::SyncFolderUpdate => 8i32, + UpdateType::SyncCipherDelete => 9i32, + UpdateType::SyncSettings => 10i32, + UpdateType::LogOut => 11i32, + UpdateType::SyncSendCreate => 12i32, + UpdateType::SyncSendUpdate => 13i32, + UpdateType::SyncSendDelete => 14i32, + UpdateType::AuthRequest => 15i32, + UpdateType::AuthRequestResponse => 16i32, + UpdateType::None => 100i32, + } + } +} pub type Notify<'a> = &'a rocket::State<Arc<WebSocketUsers>>; pub type AnonymousNotify<'a> = &'a rocket::State<Arc<AnonymousWebSocketSubscriptions>>; diff --git a/src/api/web.rs b/src/api/web.rs @@ -63,7 +63,7 @@ fn app_id() -> Cached<(ContentType, Json<Value>)> { Json(json!({ "trustedFacets": [ { - "version": { "major": 1, "minor": 0 }, + "version": { "major": 1i32, "minor": 0i32 }, "ids": [ // Per <https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-appid-and-facets-v2.0-id-20180227.html#determining-the-facetid-of-a-calling-application>: // diff --git a/src/auth.rs b/src/auth.rs @@ -214,27 +214,27 @@ fn decode_jwt<T: DeserializeOwned>(token: &str, issuer: String) -> Result<T, Err } pub fn decode_login(token: &str) -> Result<LoginJwtClaims, Error> { - decode_jwt(token, get_jwt_login_issuer().to_string()) + decode_jwt(token, get_jwt_login_issuer().to_owned()) } pub fn decode_invite(token: &str) -> Result<InviteJwtClaims, Error> { - decode_jwt(token, get_jwt_invite_issuer().to_string()) + decode_jwt(token, get_jwt_invite_issuer().to_owned()) } pub fn decode_delete(token: &str) -> Result<BasicJwtClaims, Error> { - decode_jwt(token, get_jwt_delete_issuer().to_string()) + decode_jwt(token, get_jwt_delete_issuer().to_owned()) } pub fn decode_verify_email(token: &str) -> Result<BasicJwtClaims, Error> { - decode_jwt(token, get_jwt_verifyemail_issuer().to_string()) + decode_jwt(token, get_jwt_verifyemail_issuer().to_owned()) } pub fn decode_api_org(token: &str) -> Result<OrgApiKeyLoginJwtClaims, Error> { - decode_jwt(token, get_jwt_org_api_key_issuer().to_string()) + decode_jwt(token, get_jwt_org_api_key_issuer().to_owned()) } pub fn decode_file_download(token: &str) -> Result<FileDownloadClaims, Error> { - decode_jwt(token, get_jwt_file_download_issuer().to_string()) + decode_jwt(token, get_jwt_file_download_issuer().to_owned()) } #[derive(Debug, Serialize, Deserialize)] @@ -319,8 +319,10 @@ pub fn generate_organization_api_key_login_claims( let time_now = Utc::now().naive_utc(); OrgApiKeyLoginJwtClaims { nbf: time_now.timestamp(), - exp: (time_now + Duration::hours(1)).timestamp(), - iss: get_jwt_org_api_key_issuer().to_string(), + exp: (time_now.checked_add_signed(Duration::hours(1))) + .expect("Duration add overflowed") + .timestamp(), + iss: get_jwt_org_api_key_issuer().to_owned(), sub: uuid, client_id: format!("organization.{org_id}"), client_sub: org_id, @@ -399,7 +401,7 @@ impl<'r> FromRequest<'r> for ClientHeaders { let device_type: i32 = request .headers() .get_one("device-type") - .map_or(14, |d| d.parse().unwrap_or(14)); + .map_or(14i32, |d| d.parse().unwrap_or(14i32)); Outcome::Success(Self { host, @@ -415,7 +417,7 @@ pub struct Headers { pub user: User, pub ip: ClientIp, } - +#[allow(clippy::else_if_without_else)] #[rocket::async_trait] impl<'r> FromRequest<'r> for Headers { type Error = &'static str; @@ -464,7 +466,6 @@ impl<'r> FromRequest<'r> for Headers { let Some(current_route) = request.route().and_then(|r| r.name.as_deref()) else { err_handler!("Error getting current route for stamp exception") }; - // 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. @@ -473,9 +474,11 @@ impl<'r> FromRequest<'r> for Headers { // This prevents checking this stamp exception for new requests. let mut user = user; user.reset_stamp_exception(); - _ = user.save(&conn).await; + if let Err(e) = user.save(&conn).await { + error!("Error updating user: {:#?}", e); + } err_handler!("Stamp exception is expired") - } else if !stamp_exception.routes.contains(&current_route.to_string()) { + } else if !stamp_exception.routes.contains(&current_route.to_owned()) { err_handler!( "Invalid security stamp: Current route and exception route do not match" ) @@ -486,7 +489,6 @@ impl<'r> FromRequest<'r> for Headers { err_handler!("Invalid security stamp") } } - Outcome::Success(Self { host, device, @@ -542,7 +544,7 @@ impl<'r> FromRequest<'r> for OrgHeaders { .await { Some(user) => { - if user.status == UserOrgStatus::Confirmed as i32 { + if user.status == i32::from(UserOrgStatus::Confirmed) { user } else { err_handler!( @@ -809,7 +811,7 @@ pub struct ClientIp { #[rocket::async_trait] impl<'r> FromRequest<'r> for ClientIp { type Error = (); - + #[allow(clippy::map_err_ignore)] async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> { let ip = req.headers().get_one("X-Real-IP").and_then(|ip| { ip.find(',') diff --git a/src/config.rs b/src/config.rs @@ -118,9 +118,9 @@ impl Config { address: config_file.ip, cli_colors: false, limits: Limits::new() - .limit("json", 20.megabytes()) - .limit("data-form", 525.megabytes()) - .limit("file", 525.megabytes()), + .limit("json", 20i32.megabytes()) + .limit("data-form", 525i32.megabytes()) + .limit("file", 525i32.megabytes()), log_level: LogLevel::Off, port: config_file.port, temp_dir: Self::TMP_FOLDER.into(), @@ -128,7 +128,7 @@ impl Config { ..Default::default() }; if let Some(count) = config_file.workers { - rocket.workers = count.get() as usize; + rocket.workers = usize::from(count.get()); } let domain = Url::parse( format!( @@ -157,7 +157,7 @@ impl Config { password_iterations: match config_file.password_iterations { None => 600_000, Some(count) => { - if count < 100_000 { + if count < 100_000i32 { return Err(ConfigErr::InvalidPasswordIterations(count)); } count diff --git a/src/crypto.rs b/src/crypto.rs @@ -48,7 +48,7 @@ fn get_random_string(alphabet: &[u8], num_chars: usize) -> String { (0..num_chars) .map(|_| { let i = rng.gen_range(0..alphabet.len()); - alphabet[i] as char + char::from(alphabet[i]) }) .collect() } diff --git a/src/db/mod.rs b/src/db/mod.rs @@ -78,30 +78,28 @@ impl Drop for DbPool { } impl DbPool { // For the given database URL, guess its type, run migrations, create pool, and return it - #[allow(clippy::cast_lossless)] pub fn from_config() -> Result<Self, Error> { let url = Config::DATABASE_URL; paste::paste! {sqlite_migrations::run_migrations()?; } let manager = ConnectionManager::new(url); let pool = Pool::builder() - .max_size(config::get_config().database_max_conns.get() as u32) - .connection_timeout(Duration::from_secs( - config::get_config().database_timeout as u64, - )) + .max_size(u32::from(config::get_config().database_max_conns.get())) + .connection_timeout(Duration::from_secs(u64::from( + config::get_config().database_timeout, + ))) .connection_customizer(Box::new(DbConnOptions)) .build(manager) .map_res("Failed to create pool")?; Ok(Self { pool: Some(pool), - semaphore: Arc::new(Semaphore::new( - config::get_config().database_max_conns.get() as usize, - )), + semaphore: Arc::new(Semaphore::new(usize::from( + config::get_config().database_max_conns.get(), + ))), }) } // Get a connection from the pool - #[allow(clippy::cast_lossless)] async fn get(&self) -> Result<DbConn, Error> { - let duration = Duration::from_secs(config::get_config().database_timeout as u64); + let duration = Duration::from_secs(u64::from(config::get_config().database_timeout)); let permit = match timeout(duration, Arc::clone(&self.semaphore).acquire_owned()).await { Ok(p) => p.expect("Semaphore should be open"), Err(_) => { diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs @@ -29,10 +29,15 @@ db_object! { } } -#[allow(dead_code)] enum RepromptType { None = 0, - Password = 1, // not currently used in server +} +impl From<RepromptType> for i32 { + fn from(value: RepromptType) -> Self { + match value { + RepromptType::None => 0i32, + } + } } /// Local methods @@ -66,7 +71,7 @@ impl Cipher { pub fn validate_notes(cipher_data: &[CipherData]) -> EmptyResult { let mut validation_errors = serde_json::Map::new(); for (index, cipher) in cipher_data.iter().enumerate() { - if let Some(note) = &cipher.Notes { + if let Some(ref note) = cipher.Notes { if note.len() > 10_000 { validation_errors.insert( format!("Ciphers[{index}].Notes"), @@ -134,7 +139,7 @@ impl Cipher { // NOTE: This was marked as *Backwards Compatibility Code*, but as of January 2021 this is still being used by upstream // Set the first element of the Uris array as Uri, this is needed several (mobile) clients. - if self.atype == 1 { + if self.atype == 1i32 { if type_data_json["Uris"].is_array() { let uri = type_data_json["Uris"][0]["Uri"].clone(); type_data_json["Uri"] = uri; @@ -159,7 +164,7 @@ impl Cipher { .get(&self.uuid) .map_or_else(|| Cow::from(Vec::with_capacity(0)), Cow::from) } else { - Cow::from(self.get_collections(user_uuid.to_string(), conn).await) + Cow::from(self.get_collections(user_uuid.to_owned(), conn).await) }; // There are three types of cipher response models in upstream @@ -176,7 +181,7 @@ impl Cipher { "CreationDate": format_date(&self.created_at), "RevisionDate": format_date(&self.updated_at), "DeletedDate": self.deleted_at.map_or(Value::Null, |d| Value::String(format_date(&d))), - "Reprompt": self.reprompt.unwrap_or(RepromptType::None as i32), + "Reprompt": self.reprompt.unwrap_or_else(|| i32::from(RepromptType::None)), "OrganizationId": self.organization_uuid, "Key": self.key, "Attachments": Value::Null, @@ -227,10 +232,10 @@ impl Cipher { } let key = match self.atype { - 1 => "Login", - 2 => "SecureNote", - 3 => "Card", - 4 => "Identity", + 1i32 => "Login", + 2i32 => "SecureNote", + 3i32 => "Card", + 4i32 => "Identity", _ => panic!("Wrong type"), }; @@ -356,6 +361,7 @@ impl Cipher { } /// Returns whether this cipher is owned by an org in which the user has full access. + #[allow(clippy::else_if_without_else)] async fn is_in_full_access_org( &self, user_uuid: &str, @@ -429,9 +435,9 @@ impl Cipher { // booleans and this behavior isn't portable anyway. let mut read_only = true; let mut hide_passwords = true; - for (ro, hp) in &rows { - read_only &= ro; - hide_passwords &= hp; + for tup in rows { + read_only &= tup.0; + hide_passwords &= tup.1; } Some((read_only, hide_passwords)) @@ -532,7 +538,7 @@ impl Cipher { .left_join(users_organizations::table.on( ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()) .and(users_organizations::user_uuid.eq(user_uuid)) - .and(users_organizations::status.eq(UserOrgStatus::Confirmed as i32)) + .and(users_organizations::status.eq(i32::from(UserOrgStatus::Confirmed))) )) .left_join(users_collections::table.on( ciphers_collections::collection_uuid.eq(users_collections::collection_uuid) @@ -546,7 +552,7 @@ impl Cipher { if !visible_only { query = query.or_filter( - users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin/owner + users_organizations::atype.le(i32::from(UserOrgType::Admin)) // Org admin/owner ); } @@ -641,7 +647,7 @@ impl Cipher { .filter(ciphers_collections::cipher_uuid.eq(&self.uuid)) .filter(users_collections::user_uuid.eq(user_id).or( // User has access to collection users_organizations::access_all.eq(true).or( // User has access all - users_organizations::atype.le(UserOrgType::Admin as i32) // User is admin or owner + users_organizations::atype.le(i32::from(UserOrgType::Admin)) // User is admin or owner ) )) .select(ciphers_collections::collection_uuid) @@ -672,7 +678,7 @@ impl Cipher { )) .or_filter(users_collections::user_uuid.eq(user_id)) // User has access to collection .or_filter(users_organizations::access_all.eq(true)) // User has access all - .or_filter(users_organizations::atype.le(UserOrgType::Admin as i32)) // User is admin or owner + .or_filter(users_organizations::atype.le(i32::from(UserOrgType::Admin))) // User is admin or owner .select(ciphers_collections::all_columns) .distinct() .load::<(String, String)>(conn).unwrap_or_default() diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs @@ -182,7 +182,7 @@ impl Collection { ) )) .filter( - users_organizations::status.eq(UserOrgStatus::Confirmed as i32) + users_organizations::status.eq(i32::from(UserOrgStatus::Confirmed)) ) .filter( users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection @@ -275,7 +275,7 @@ impl Collection { .filter( users_collections::collection_uuid.eq(uuid).or( // Directly accessed collection users_organizations::access_all.eq(true).or( // access_all in Organization - users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner + users_organizations::atype.le(i32::from(UserOrgType::Admin)) // Org admin or owner ))).select(collections::all_columns) .first::<CollectionDb>(conn).ok() .from_db() @@ -283,7 +283,7 @@ impl Collection { } pub async fn is_writable_by_user(&self, user_uuid: &str, conn: &DbConn) -> bool { - let user_uuid = user_uuid.to_string(); + let user_uuid = user_uuid.to_owned(); db_run! { conn: { collections::table .left_join(users_collections::table.on( @@ -300,7 +300,7 @@ impl Collection { .filter( users_collections::collection_uuid.eq(&self.uuid).and(users_collections::read_only.eq(false)).or(// Directly accessed collection users_organizations::access_all.eq(true).or( // access_all in Organization - users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner + users_organizations::atype.le(i32::from(UserOrgType::Admin)) // Org admin or owner )) ) .count() @@ -311,7 +311,7 @@ impl Collection { } async fn hide_passwords_for_user(&self, user_uuid: &str, conn: &DbConn) -> bool { - let user_uuid = user_uuid.to_string(); + let user_uuid = user_uuid.to_owned(); db_run! { conn: { collections::table .left_join(users_collections::table.on( @@ -328,7 +328,7 @@ impl Collection { .filter( users_collections::collection_uuid.eq(&self.uuid).and(users_collections::hide_passwords.eq(true)).or(// Directly accessed collection users_organizations::access_all.eq(true).or( // access_all in Organization - users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner + users_organizations::atype.le(i32::from(UserOrgType::Admin)) // Org admin or owner )) ) .count() diff --git a/src/db/models/device.rs b/src/db/models/device.rs @@ -64,22 +64,22 @@ impl Device { let orgowner: Vec<_> = orgs .iter() - .filter(|o| o.atype == 0) + .filter(|o| o.atype == 0i32) .map(|o| o.org_uuid.clone()) .collect(); let orgadmin: Vec<_> = orgs .iter() - .filter(|o| o.atype == 1) + .filter(|o| o.atype == 1i32) .map(|o| o.org_uuid.clone()) .collect(); let orguser: Vec<_> = orgs .iter() - .filter(|o| o.atype == 2) + .filter(|o| o.atype == 2i32) .map(|o| o.org_uuid.clone()) .collect(); let orgmanager: Vec<_> = orgs .iter() - .filter(|o| o.atype == 3) + .filter(|o| o.atype == 3i32) .map(|o| o.org_uuid.clone()) .collect(); @@ -87,20 +87,19 @@ impl Device { use crate::auth::{self, encode_jwt, LoginJwtClaims}; let claims = LoginJwtClaims { nbf: time_now.timestamp(), - exp: (time_now + *auth::get_default_validity()).timestamp(), - iss: auth::get_jwt_login_issuer().to_string(), + exp: (time_now.checked_add_signed(*auth::get_default_validity())) + .expect("Duration add overflowed") + .timestamp(), + iss: auth::get_jwt_login_issuer().to_owned(), sub: user.uuid.clone(), - premium: true, name: user.name.clone(), email: user.email.clone(), email_verified: true, - orgowner, orgadmin, orguser, orgmanager, - sstamp: user.security_stamp.clone(), device: self.uuid.clone(), scope, @@ -203,7 +202,7 @@ pub enum DeviceType { impl fmt::Display for DeviceType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { + match *self { Self::Android => write!(f, "Android"), Self::Ios => write!(f, "iOS"), Self::ChromeExtension => write!(f, "Chrome Extension"), @@ -234,28 +233,28 @@ impl fmt::Display for DeviceType { impl DeviceType { pub const fn from_i32(value: i32) -> Self { match value { - 0 => Self::Android, - 1 => Self::Ios, - 2 => Self::ChromeExtension, - 3 => Self::FirefoxExtension, - 4 => Self::OperaExtension, - 5 => Self::EdgeExtension, - 6 => Self::WindowsDesktop, - 7 => Self::MacOsDesktop, - 8 => Self::LinuxDesktop, - 9 => Self::ChromeBrowser, - 10 => Self::FirefoxBrowser, - 11 => Self::OperaBrowser, - 12 => Self::EdgeBrowser, - 13 => Self::IEBrowser, - 15 => Self::AndroidAmazon, - 16 => Self::Uwp, - 17 => Self::SafariBrowser, - 18 => Self::VivaldiBrowser, - 19 => Self::VivaldiExtension, - 20 => Self::SafariExtension, - 21 => Self::Sdk, - 22 => Self::Server, + 0i32 => Self::Android, + 1i32 => Self::Ios, + 2i32 => Self::ChromeExtension, + 3i32 => Self::FirefoxExtension, + 4i32 => Self::OperaExtension, + 5i32 => Self::EdgeExtension, + 6i32 => Self::WindowsDesktop, + 7i32 => Self::MacOsDesktop, + 8i32 => Self::LinuxDesktop, + 9i32 => Self::ChromeBrowser, + 10i32 => Self::FirefoxBrowser, + 11i32 => Self::OperaBrowser, + 12i32 => Self::EdgeBrowser, + 13i32 => Self::IEBrowser, + 15i32 => Self::AndroidAmazon, + 16i32 => Self::Uwp, + 17i32 => Self::SafariBrowser, + 18i32 => Self::VivaldiBrowser, + 19i32 => Self::VivaldiExtension, + 20i32 => Self::SafariExtension, + 21i32 => Self::Sdk, + 22i32 => Self::Server, _ => Self::UnknownBrowser, } } diff --git a/src/db/models/folder.rs b/src/db/models/folder.rs @@ -52,8 +52,8 @@ impl Folder { impl FolderCipher { pub fn new(folder_uuid: &str, cipher_uuid: &str) -> Self { Self { - folder_uuid: folder_uuid.to_string(), - cipher_uuid: cipher_uuid.to_string(), + folder_uuid: folder_uuid.to_owned(), + cipher_uuid: cipher_uuid.to_owned(), } } } diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs @@ -31,6 +31,20 @@ pub enum OrgPolicyType { SendOptions = 7, ResetPassword = 8, } +impl From<OrgPolicyType> for i32 { + fn from(value: OrgPolicyType) -> Self { + match value { + OrgPolicyType::TwoFactorAuthentication => 0i32, + OrgPolicyType::MasterPassword => 1i32, + OrgPolicyType::PasswordGenerator => 2i32, + OrgPolicyType::SingleOrg => 3i32, + OrgPolicyType::PersonalOwnership => 5i32, + OrgPolicyType::DisableSend => 6i32, + OrgPolicyType::SendOptions => 7i32, + OrgPolicyType::ResetPassword => 8i32, + } + } +} // https://github.com/bitwarden/server/blob/5cbdee137921a19b1f722920f0fa3cd45af2ef0f/src/Core/Models/Data/Organizations/Policies/ResetPasswordDataModel.cs #[derive(Deserialize)] @@ -51,7 +65,7 @@ impl OrgPolicy { Self { uuid: crate::util::get_uuid(), org_uuid, - atype: atype as i32, + atype: i32::from(atype), enabled: false, data, } @@ -112,7 +126,7 @@ impl OrgPolicy { .and(users_organizations::user_uuid.eq(user_uuid))) ) .filter( - users_organizations::status.eq(UserOrgStatus::Confirmed as i32) + users_organizations::status.eq(i32::from(UserOrgStatus::Confirmed)) ) .select(org_policies::all_columns) .load::<OrgPolicyDb>(conn) @@ -129,7 +143,7 @@ impl OrgPolicy { db_run! { conn: { org_policies::table .filter(org_policies::org_uuid.eq(org_uuid)) - .filter(org_policies::atype.eq(policy_type as i32)) + .filter(org_policies::atype.eq(i32::from(policy_type))) .first::<OrgPolicyDb>(conn) .ok() .from_db() @@ -157,12 +171,12 @@ impl OrgPolicy { .and(users_organizations::user_uuid.eq(user_uuid))) ) .filter( - users_organizations::status.eq(UserOrgStatus::Accepted as i32) + users_organizations::status.eq(i32::from(UserOrgStatus::Accepted)) ) .or_filter( - users_organizations::status.eq(UserOrgStatus::Confirmed as i32) + users_organizations::status.eq(i32::from(UserOrgStatus::Confirmed)) ) - .filter(org_policies::atype.eq(policy_type as i32)) + .filter(org_policies::atype.eq(i32::from(policy_type))) .filter(org_policies::enabled.eq(true)) .select(org_policies::all_columns) .load::<OrgPolicyDb>(conn) diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs @@ -48,6 +48,16 @@ pub enum UserOrgStatus { Accepted = 1, Confirmed = 2, } +impl From<UserOrgStatus> for i32 { + fn from(value: UserOrgStatus) -> Self { + match value { + UserOrgStatus::Revoked => -1i32, + UserOrgStatus::Invited => 0i32, + UserOrgStatus::Accepted => 1i32, + UserOrgStatus::Confirmed => 2i32, + } + } +} #[derive(Copy, Clone, PartialEq, Eq, num_derive::FromPrimitive)] pub enum UserOrgType { @@ -56,6 +66,26 @@ pub enum UserOrgType { User = 2, Manager = 3, } +impl From<UserOrgType> for i32 { + fn from(value: UserOrgType) -> Self { + match value { + UserOrgType::Owner => 0i32, + UserOrgType::Admin => 1i32, + UserOrgType::User => 2i32, + UserOrgType::Manager => 3i32, + } + } +} +impl From<UserOrgType> for usize { + fn from(value: UserOrgType) -> Self { + match value { + UserOrgType::Owner => 0, + UserOrgType::Admin => 1, + UserOrgType::User => 2, + UserOrgType::Manager => 3, + } + } +} impl UserOrgType { pub fn from_str(s: &str) -> Option<Self> { @@ -71,14 +101,26 @@ impl UserOrgType { impl Ord for UserOrgType { fn cmp(&self, other: &Self) -> Ordering { - // For easy comparison, map each variant to an access level (where 0 is lowest). - static ACCESS_LEVEL: [i32; 4] = [ - 3, // Owner - 2, // Admin - 0, // User - 1, // Manager - ]; - ACCESS_LEVEL[*self as usize].cmp(&ACCESS_LEVEL[*other as usize]) + match *self { + Self::Owner => match *other { + Self::Owner => Ordering::Equal, + Self::Admin | Self::User | Self::Manager => Ordering::Greater, + }, + Self::Admin => match *other { + Self::Owner => Ordering::Less, + Self::Admin => Ordering::Equal, + Self::User | Self::Manager => Ordering::Greater, + }, + Self::User => match *other { + Self::User => Ordering::Equal, + Self::Owner | Self::Admin | Self::Manager => Ordering::Less, + }, + Self::Manager => match *other { + Self::Owner | Self::Admin => Ordering::Less, + Self::User => Ordering::Greater, + Self::Manager => Ordering::Equal, + }, + } } } @@ -90,7 +132,7 @@ impl PartialOrd for UserOrgType { impl PartialEq<i32> for UserOrgType { fn eq(&self, other: &i32) -> bool { - *other == *self as i32 + *other == i32::from(*self) } } @@ -116,7 +158,7 @@ impl PartialOrd<i32> for UserOrgType { impl PartialEq<UserOrgType> for i32 { fn eq(&self, other: &UserOrgType) -> bool { - *self == *other as Self + *self == Self::from(*other) } } @@ -148,10 +190,10 @@ impl Organization { "Id": self.uuid, "Identifier": null, // not supported by us "Name": self.name, - "Seats": 10, // The value doesn't matter, we don't check server-side + "Seats": 10i32, // The value doesn't matter, we don't check server-side // "MaxAutoscaleSeats": null, // The value doesn't matter, we don't check server-side - "MaxCollections": 10, // The value doesn't matter, we don't check server-side - "MaxStorageGb": 10, // 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 "Use2fa": true, "UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet) "UseEvents": false, @@ -165,17 +207,15 @@ impl Organization { "UseApi": true, "HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(), "UseResetPassword": false, - "BusinessName": null, "BusinessAddress1": null, "BusinessAddress2": null, "BusinessAddress3": null, "BusinessCountry": null, "BusinessTaxNumber": null, - "BillingEmail": self.billing_email, "Plan": "TeamsAnnually", - "PlanType": 5, // TeamsAnnually plan + "PlanType": 5i32, // TeamsAnnually plan "UsersGetPremium": true, "Object": "organization", }) @@ -185,7 +225,7 @@ impl Organization { // The number 128 should be fine, it is well within the range of an i32 // The same goes for the database where we only use INTEGER (the same as an i32) // It should also provide enough room for 100+ types, which i doubt will ever happen. -static ACTIVATE_REVOKE_DIFF: i32 = 128; +static ACTIVATE_REVOKE_DIFF: i32 = 128i32; impl UserOrganization { pub fn new(user_uuid: String, org_uuid: String) -> Self { @@ -197,27 +237,35 @@ impl UserOrganization { access_all: false, akey: String::new(), - status: UserOrgStatus::Accepted as i32, - atype: UserOrgType::User as i32, + status: i32::from(UserOrgStatus::Accepted), + atype: i32::from(UserOrgType::User), reset_password_key: None, external_id: None, } } pub fn restore(&mut self) -> bool { - if self.status < UserOrgStatus::Accepted as i32 { - self.status += ACTIVATE_REVOKE_DIFF; - return true; + if self.status < i32::from(UserOrgStatus::Invited) { + self.status = self + .status + .checked_add(ACTIVATE_REVOKE_DIFF) + .expect("i32 add overflowed"); + true + } else { + false } - false } pub fn revoke(&mut self) -> bool { - if self.status > UserOrgStatus::Revoked as i32 { - self.status -= ACTIVATE_REVOKE_DIFF; - return true; + if self.status > i32::from(UserOrgStatus::Revoked) { + self.status = self + .status + .checked_sub(ACTIVATE_REVOKE_DIFF) + .expect("i32 sub underflowed"); + true + } else { + false } - false } pub fn set_external_id(&mut self, external_id: Option<String>) -> bool { @@ -320,8 +368,8 @@ impl UserOrganization { "Id": self.org_uuid, "Identifier": null, // Not supported "Name": org.name, - "Seats": 10, // The value doesn't matter, we don't check server-side - "MaxCollections": 10, // The value doesn't matter, we don't check server-side + "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 "UsersGetPremium": true, "Use2fa": true, "UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet) @@ -339,7 +387,7 @@ impl UserOrganization { "UseSso": false, // Not supported "ProviderId": null, "ProviderName": null, - "MaxStorageGb": 10, // The value doesn't matter, we don't check server-side + "MaxStorageGb": 10i32, // The value doesn't matter, we don't check server-side "UserId": self.user_uuid, "Key": self.akey, "Status": self.status, @@ -348,18 +396,13 @@ impl UserOrganization { "Object": "profileOrganization", }) } - pub async fn to_json_user_details( - &self, - include_collections: bool, - _: bool, - conn: &DbConn, - ) -> Value { + pub async fn to_json_user_details(&self, include_collections: bool, conn: &DbConn) -> Value { let user = User::find_by_uuid(&self.user_uuid, conn).await.unwrap(); // Because BitWarden want the status to be -1 for revoked users we need to catch that here. // We subtract/add a number so we can restore/activate the user to it's previous state again. - let status = if self.status < UserOrgStatus::Revoked as i32 { - UserOrgStatus::Revoked as i32 + let status = if self.status < i32::from(UserOrgStatus::Revoked) { + i32::from(UserOrgStatus::Revoked) } else { self.status }; @@ -473,8 +516,8 @@ impl UserOrganization { None } - const fn has_status(&self, status: UserOrgStatus) -> bool { - self.status == status as i32 + fn has_status(&self, status: UserOrgStatus) -> bool { + self.status == i32::from(status) } pub fn has_full_access(&self) -> bool { @@ -505,7 +548,7 @@ impl UserOrganization { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) - .filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32)) + .filter(users_organizations::status.eq(i32::from(UserOrgStatus::Confirmed))) .load::<UserOrganizationDb>(conn) .unwrap_or_default().from_db() }} @@ -515,7 +558,7 @@ impl UserOrganization { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) - .filter(users_organizations::status.eq(UserOrgStatus::Invited as i32)) + .filter(users_organizations::status.eq(i32::from(UserOrgStatus::Invited))) .load::<UserOrganizationDb>(conn) .unwrap_or_default().from_db() }} @@ -534,8 +577,8 @@ impl UserOrganization { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) - .filter(users_organizations::status.eq(UserOrgStatus::Accepted as i32)) - .or_filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32)) + .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) @@ -559,7 +602,7 @@ impl UserOrganization { db_run! { conn: { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) - .filter(users_organizations::atype.eq(atype as i32)) + .filter(users_organizations::atype.eq(i32::from(atype))) .load::<UserOrganizationDb>(conn) .expect("Error loading user organizations").from_db() }} @@ -573,8 +616,8 @@ impl UserOrganization { db_run! { conn: { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) - .filter(users_organizations::atype.eq(atype as i32)) - .filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32)) + .filter(users_organizations::atype.eq(i32::from(atype))) + .filter(users_organizations::status.eq(i32::from(UserOrgStatus::Confirmed))) .count() .first::<i64>(conn) .unwrap_or(0) @@ -615,11 +658,11 @@ impl UserOrganization { org_policies::table.on( org_policies::org_uuid.eq(users_organizations::org_uuid) .and(users_organizations::user_uuid.eq(user_uuid)) - .and(org_policies::atype.eq(policy_type as i32)) + .and(org_policies::atype.eq(i32::from(policy_type))) .and(org_policies::enabled.eq(true))) ) .filter( - users_organizations::status.eq(UserOrgStatus::Confirmed as i32) + users_organizations::status.eq(i32::from(UserOrgStatus::Confirmed)) ) .select(users_organizations::all_columns) .load::<UserOrganizationDb>(conn) diff --git a/src/db/models/two_factor.rs b/src/db/models/two_factor.rs @@ -15,7 +15,6 @@ db_object! { } } -#[allow(dead_code)] #[derive(num_derive::FromPrimitive)] pub enum TwoFactorType { Authenticator = 0, @@ -24,6 +23,17 @@ pub enum TwoFactorType { WebauthnLoginChallenge = 1004, ProtectedActions = 2000, } +impl From<TwoFactorType> for i32 { + fn from(value: TwoFactorType) -> Self { + match value { + TwoFactorType::Authenticator => 0i32, + TwoFactorType::Webauthn => 7i32, + TwoFactorType::WebauthnRegisterChallenge => 1003i32, + TwoFactorType::WebauthnLoginChallenge => 1004i32, + TwoFactorType::ProtectedActions => 2000i32, + } + } +} /// Local methods impl TwoFactor { @@ -31,7 +41,7 @@ impl TwoFactor { Self { uuid: crate::util::get_uuid(), user_uuid, - atype: atype as i32, + atype: i32::from(atype), enabled: true, data, last_used: 0, @@ -83,7 +93,7 @@ impl TwoFactor { db_run! { conn: { twofactor::table .filter(twofactor::user_uuid.eq(user_uuid)) - .filter(twofactor::atype.lt(1000)) // Filter implementation types + .filter(twofactor::atype.lt(1000i32)) // Filter implementation types .load::<TwoFactorDb>(conn) .expect("Error loading twofactor") .from_db() diff --git a/src/db/models/user.rs b/src/db/models/user.rs @@ -49,12 +49,29 @@ pub enum UserKdfType { Pbkdf2 = 0, Argon2id = 1, } +impl From<UserKdfType> for i32 { + fn from(value: UserKdfType) -> Self { + match value { + UserKdfType::Pbkdf2 => 0i32, + UserKdfType::Argon2id => 1i32, + } + } +} enum UserStatus { Enabled = 0, Invited = 1, _Disabled = 2, } +impl From<UserStatus> for i32 { + fn from(value: UserStatus) -> Self { + match value { + UserStatus::Enabled => 0i32, + UserStatus::Invited => 1i32, + UserStatus::_Disabled => 2i32, + } + } +} #[derive(Serialize, Deserialize)] pub struct UserStampException { @@ -65,13 +82,14 @@ pub struct UserStampException { /// Local methods impl User { - pub const CLIENT_KDF_TYPE_DEFAULT: i32 = UserKdfType::Pbkdf2 as i32; - pub const CLIENT_KDF_ITER_DEFAULT: i32 = 600_000; + pub fn client_kdf_type_default() -> i32 { + i32::from(UserKdfType::Pbkdf2) + } + pub const CLIENT_KDF_ITER_DEFAULT: i32 = 600_000i32; pub fn new(email: String) -> Self { let now = Utc::now().naive_utc(); let email = email.to_lowercase(); - Self { uuid: crate::util::get_uuid(), enabled: true, @@ -85,33 +103,24 @@ impl User { akey: String::new(), email_new: None, email_new_token: None, - password_hash: Vec::new(), salt: crypto::get_random_bytes::<64>().to_vec(), - password_iterations: config::get_config().password_iterations as i32, - + password_iterations: config::get_config().password_iterations, security_stamp: crate::util::get_uuid(), stamp_exception: None, - password_hint: None, private_key: None, public_key: None, - _totp_secret: None, totp_recover: None, - - equivalent_domains: "[]".to_string(), - excluded_globals: "[]".to_string(), - - client_kdf_type: Self::CLIENT_KDF_TYPE_DEFAULT, + 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_memory: None, client_kdf_parallelism: None, - api_key: None, - avatar_color: None, - external_id: None, // Todo: Needs to be removed in the future, this is not used anymore. } } @@ -189,7 +198,11 @@ impl User { let stamp_exception = UserStampException { routes: route_exception, security_stamp: self.security_stamp.clone(), - expire: (Utc::now().naive_utc() + Duration::minutes(2)).timestamp(), + expire: (Utc::now() + .naive_utc() + .checked_add_signed(Duration::minutes(2))) + .expect("Duration add overflowed") + .timestamp(), }; self.stamp_exception = Some(serde_json::to_string(&stamp_exception).unwrap_or_default()); } @@ -223,7 +236,7 @@ impl User { }; json!({ - "_Status": status as i32, + "_Status": i32::from(status), "Id": self.uuid, "Name": self.name, "Email": self.email, diff --git a/src/error.rs b/src/error.rs @@ -19,13 +19,13 @@ macro_rules! make_error { })+ impl StdError for Error { fn source(&self) -> Option<&(dyn StdError + 'static)> { - match &self.error {$( ErrorKind::$name(e) => $src_fn(e), )+} + match self.error {$( ErrorKind::$name(ref e) => $src_fn(e), )+} } } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.error {$( - ErrorKind::$name(e) => f.write_str(&$usr_msg_fun(e, &self.message)), + match self.error {$( + ErrorKind::$name(ref e) => f.write_str(&$usr_msg_fun(e, &self.message)), )+} } } diff --git a/src/main.rs b/src/main.rs @@ -15,23 +15,14 @@ )] #![allow( clippy::absolute_paths, - clippy::arithmetic_side_effects, - clippy::as_conversions, - clippy::big_endian_bytes, clippy::blanket_clippy_restriction_lints, - clippy::default_numeric_fallback, clippy::doc_markdown, - clippy::else_if_without_else, clippy::error_impl_error, clippy::expect_used, clippy::if_then_some_else_none, clippy::implicit_return, clippy::indexing_slicing, - clippy::integer_division, clippy::items_after_statements, - clippy::let_underscore_must_use, - clippy::let_underscore_untyped, - clippy::map_err_ignore, clippy::min_ident_chars, clippy::missing_docs_in_private_items, clippy::missing_errors_doc, @@ -46,22 +37,18 @@ clippy::panic, clippy::panic_in_result_fn, clippy::partial_pub_fields, - clippy::pattern_type_mismatch, clippy::pub_use, clippy::question_mark_used, clippy::redundant_type_annotations, clippy::ref_patterns, clippy::shadow_reuse, - clippy::shadow_unrelated, clippy::significant_drop_in_scrutinee, clippy::significant_drop_tightening, clippy::single_call_fn, clippy::single_char_lifetime_names, clippy::std_instead_of_alloc, clippy::std_instead_of_core, - clippy::string_add, clippy::string_slice, - clippy::str_to_string, clippy::too_many_lines, clippy::unreachable, clippy::unseparated_literal_suffix, @@ -179,11 +166,11 @@ fn check_web_vault() { exit(1); } } -#[allow(clippy::as_conversions, clippy::cast_lossless, clippy::exit)] +#[allow(clippy::exit)] async fn create_db_pool() -> db::DbPool { (util::retry_db( db::DbPool::from_config, - config::get_config().db_connection_retries.get() as u32, + u32::from(config::get_config().db_connection_retries.get()), ) .await) .map_or_else( @@ -224,6 +211,6 @@ async fn launch_rocket(pool: db::DbPool) -> Result<(), Error> { .expect("Error setting Ctrl-C handler"); shutdown.notify(); }); - let _ = instance.launch().await?; + instance.launch().await?; Ok(()) } diff --git a/src/util.rs b/src/util.rs @@ -217,7 +217,9 @@ impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for Cache res.set_raw_header("Cache-Control", cache_control_header); let time_now = chrono::Local::now(); - let expiry_time = time_now + chrono::Duration::seconds(self.ttl.try_into().unwrap()); + let expiry_time = time_now + .checked_add_signed(chrono::Duration::seconds(self.ttl.try_into().unwrap())) + .expect("Duration add overflowed"); res.set_raw_header("Expires", format_datetime_http(&expiry_time)); Ok(res) } @@ -254,7 +256,7 @@ impl<'r> FromParam<'r> for SafeString { .chars() .all(|c| matches!(c, 'a'..='z' | 'A'..='Z' |'0'..='9' | '-')) { - Ok(Self(param.to_string())) + Ok(Self(param.to_owned())) } else { Err(()) } @@ -276,7 +278,9 @@ use std::str::FromStr; fn upcase_first(s: &str) -> String { let mut c = s.chars(); c.next().map_or_else(String::new, |f| { - f.to_uppercase().collect::<String>() + c.as_str() + let mut val = f.to_uppercase().collect::<String>(); + val.push_str(c.as_str()); + val }) } @@ -284,7 +288,9 @@ fn upcase_first(s: &str) -> String { fn lcase_first(s: &str) -> String { let mut c = s.chars(); c.next().map_or_else(String::new, |f| { - f.to_lowercase().collect::<String>() + c.as_str() + let mut val = f.to_lowercase().collect::<String>(); + val.push_str(c.as_str()); + val }) } @@ -424,14 +430,12 @@ pub fn retry<F, T, E>(mut func: F, max_tries: u32) -> Result<T, E> where F: FnMut() -> Result<T, E>, { - let mut tries = 0; - + let mut tries = 0u32; loop { match func() { ok @ Ok(_) => return ok, err @ Err(_) => { - tries += 1; - + tries = tries.checked_add(1).expect("u32 add overflowed"); if tries >= max_tries { return err; } @@ -446,14 +450,12 @@ where F: FnMut() -> Result<T, E> + Send, E: std::error::Error + Send, { - let mut tries = 0; - + let mut tries = 0u32; loop { match func() { ok @ Ok(_) => return ok, Err(e) => { - tries += 1; - + tries = tries.checked_add(1).expect("u32 add overflowed"); if tries >= max_tries && max_tries > 0 { return Err(e); } @@ -478,13 +480,12 @@ pub fn convert_json_key_lcase_first(src_json: Value) -> Value { Value::Object(obj) => { let mut json_map = JsonMap::new(); - for (key, value) in &obj { - match (key, value) { + for tup in obj { + match tup { (key, Value::Object(elm)) => { let inner_value = convert_json_key_lcase_first(Value::Object(elm.clone())); - json_map.insert(lcase_first(key), inner_value); + json_map.insert(lcase_first(key.as_str()), inner_value); } - (key, Value::Array(elm)) => { let mut inner_array: Vec<Value> = Vec::with_capacity(elm.len()); @@ -492,18 +493,16 @@ pub fn convert_json_key_lcase_first(src_json: Value) -> Value { inner_array.push(convert_json_key_lcase_first(inner_obj.clone())); } - json_map.insert(lcase_first(key), Value::Array(inner_array)); + json_map.insert(lcase_first(key.as_str()), Value::Array(inner_array)); } - (key, value) => { - json_map.insert(lcase_first(key), value.clone()); + json_map.insert(lcase_first(key.as_str()), value.clone()); } } } Value::Object(json_map) } - value => value, } }