diff --git a/.env.template b/.env.template index 3c7e7d1ead..b2ccf9df0e 100644 --- a/.env.template +++ b/.env.template @@ -72,6 +72,12 @@ # WEBSOCKET_ADDRESS=0.0.0.0 # WEBSOCKET_PORT=3012 +## Enables push notifications, get key and id from https://bitwarden.com/host +# PUSH_ENABLED=true +# PUSH_RELAY_URI=https://push.bitwarden.com +# PUSH_INSTALLATION_ID=CHANGEME +# PUSH_INSTALLATION_KEY=CHANGEME + ## Controls whether users are allowed to create Bitwarden Sends. ## This setting applies globally to all users. ## To control this on a per-org basis instead, use the "Disable Send" org policy. diff --git a/migrations/mysql/2023-02-18-125735_rename-uuid-to-device-identifier/down.sql b/migrations/mysql/2023-02-18-125735_rename-uuid-to-device-identifier/down.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/migrations/mysql/2023-02-18-125735_rename-uuid-to-device-identifier/up.sql b/migrations/mysql/2023-02-18-125735_rename-uuid-to-device-identifier/up.sql new file mode 100644 index 0000000000..ca30e44c11 --- /dev/null +++ b/migrations/mysql/2023-02-18-125735_rename-uuid-to-device-identifier/up.sql @@ -0,0 +1 @@ +ALTER TABLE devices RENAME COLUMN uuid TO identifier; \ No newline at end of file diff --git a/migrations/mysql/2023-02-18-133907_create-new-uuid-table/down.sql b/migrations/mysql/2023-02-18-133907_create-new-uuid-table/down.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/migrations/mysql/2023-02-18-133907_create-new-uuid-table/up.sql b/migrations/mysql/2023-02-18-133907_create-new-uuid-table/up.sql new file mode 100644 index 0000000000..bcd33b9003 --- /dev/null +++ b/migrations/mysql/2023-02-18-133907_create-new-uuid-table/up.sql @@ -0,0 +1 @@ +ALTER TABLE devices ADD uuid TEXT NOT NULL DEFAULT "00000000-0000-0000-0000-000000000000"; \ No newline at end of file diff --git a/migrations/postgresql/2023-02-18-125735_rename-uuid-to-device-identifier/down.sql b/migrations/postgresql/2023-02-18-125735_rename-uuid-to-device-identifier/down.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/migrations/postgresql/2023-02-18-125735_rename-uuid-to-device-identifier/up.sql b/migrations/postgresql/2023-02-18-125735_rename-uuid-to-device-identifier/up.sql new file mode 100644 index 0000000000..ca30e44c11 --- /dev/null +++ b/migrations/postgresql/2023-02-18-125735_rename-uuid-to-device-identifier/up.sql @@ -0,0 +1 @@ +ALTER TABLE devices RENAME COLUMN uuid TO identifier; \ No newline at end of file diff --git a/migrations/postgresql/2023-02-18-133907_create-new-uuid-table/down.sql b/migrations/postgresql/2023-02-18-133907_create-new-uuid-table/down.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/migrations/postgresql/2023-02-18-133907_create-new-uuid-table/up.sql b/migrations/postgresql/2023-02-18-133907_create-new-uuid-table/up.sql new file mode 100644 index 0000000000..bcd33b9003 --- /dev/null +++ b/migrations/postgresql/2023-02-18-133907_create-new-uuid-table/up.sql @@ -0,0 +1 @@ +ALTER TABLE devices ADD uuid TEXT NOT NULL DEFAULT "00000000-0000-0000-0000-000000000000"; \ No newline at end of file diff --git a/migrations/sqlite/2023-02-18-125735_rename-uuid-to-device-identifier/down.sql b/migrations/sqlite/2023-02-18-125735_rename-uuid-to-device-identifier/down.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/migrations/sqlite/2023-02-18-125735_rename-uuid-to-device-identifier/up.sql b/migrations/sqlite/2023-02-18-125735_rename-uuid-to-device-identifier/up.sql new file mode 100644 index 0000000000..d10bcd461e --- /dev/null +++ b/migrations/sqlite/2023-02-18-125735_rename-uuid-to-device-identifier/up.sql @@ -0,0 +1 @@ +ALTER TABLE devices RENAME COLUMN uuid TO identifier; \ No newline at end of file diff --git a/migrations/sqlite/2023-02-18-133907_create-new-uuid-table/down.sql b/migrations/sqlite/2023-02-18-133907_create-new-uuid-table/down.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/migrations/sqlite/2023-02-18-133907_create-new-uuid-table/up.sql b/migrations/sqlite/2023-02-18-133907_create-new-uuid-table/up.sql new file mode 100644 index 0000000000..bcd33b9003 --- /dev/null +++ b/migrations/sqlite/2023-02-18-133907_create-new-uuid-table/up.sql @@ -0,0 +1 @@ +ALTER TABLE devices ADD uuid TEXT NOT NULL DEFAULT "00000000-0000-0000-0000-000000000000"; \ No newline at end of file diff --git a/src/api/admin.rs b/src/api/admin.rs index a25809edf4..38d485c411 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -19,6 +19,7 @@ use crate::{ db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType}, error::{Error, MapResult}, mail, + push::{push_logout, unregister_push_device}, util::{ docker_base_image, format_naive_datetime_local, get_display_size, get_reqwest_client, is_running_in_docker, }, @@ -382,13 +383,22 @@ async fn delete_user(uuid: String, _token: AdminToken, mut conn: DbConn, ip: Cli #[post("/users//deauth")] async fn deauth_user(uuid: String, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { let mut user = get_user_or_404(&uuid, &mut conn).await?; + nt.send_logout(&user, None).await; + push_logout(&user, None, &mut conn).await; // Send logout notifications before deleting devices + + match Device::find_push_device_by_user(&user.uuid, &mut conn).await { + Some(d) => { + match unregister_push_device(d.uuid).await { + Ok(r) => r, + Err(e) => error!("Unable to unregister devices from Bitwarden server: {}", e), + }; + } + None => debug!("No device to unregister for {}", &user.email), + }; Device::delete_all_by_user(&user.uuid, &mut conn).await?; user.reset_security_stamp(); - let save_result = user.save(&mut conn).await; - nt.send_logout(&user, None).await; - save_result } @@ -402,6 +412,7 @@ async fn disable_user(uuid: String, _token: AdminToken, mut conn: DbConn, nt: No let save_result = user.save(&mut conn).await; nt.send_logout(&user, None).await; + push_logout(&user, None, &mut conn).await; save_result } diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 67633f0b8b..9fddcf20ee 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -9,7 +9,9 @@ use crate::{ auth::{decode_delete, decode_invite, decode_verify_email, ClientIp, Headers}, crypto, db::{models::*, DbConn}, - mail, CONFIG, + mail, + push::{self, push_logout}, + CONFIG, }; pub fn routes() -> Vec { @@ -40,6 +42,9 @@ pub fn routes() -> Vec { rotate_api_key, get_known_device, put_avatar, + put_device_token, + clear_device_token, + clear_device_token_post, ] } @@ -332,7 +337,8 @@ async fn post_password( // Prevent loging 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; + nt.send_logout(&user, Some(headers.device.identifier.clone())).await; + push_logout(&user, Some(headers.device.identifier.clone()), &mut conn).await; save_result } @@ -389,7 +395,8 @@ async fn post_kdf(data: JsonUpcase, headers: Headers, mut conn: D user.set_password(&data.NewMasterPasswordHash, Some(data.Key), true, None); let save_result = user.save(&mut conn).await; - nt.send_logout(&user, Some(headers.device.uuid)).await; + nt.send_logout(&user, Some(headers.device.identifier.clone())).await; + push_logout(&user, Some(headers.device.identifier.clone()), &mut conn).await; save_result } @@ -482,7 +489,8 @@ async fn post_rotatekey( // Prevent loging 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; + nt.send_logout(&user, Some(headers.device.identifier.clone())).await; + push_logout(&user, Some(headers.device.identifier.clone()), &mut conn).await; save_result } @@ -506,6 +514,7 @@ async fn post_sstamp( let save_result = user.save(&mut conn).await; nt.send_logout(&user, None).await; + push_logout(&user, None, &mut conn).await; save_result } @@ -609,6 +618,7 @@ async fn post_email( let save_result = user.save(&mut conn).await; nt.send_logout(&user, None).await; + push_logout(&user, None, &mut conn).await; save_result } @@ -872,12 +882,77 @@ async fn rotate_api_key(data: JsonUpcase, headers: He _api_key(data, true, headers, conn).await } -#[get("/devices/knowndevice//")] -async fn get_known_device(email: String, uuid: String, mut conn: DbConn) -> JsonResult { +#[get("/devices/knowndevice//")] +async fn get_known_device(email: String, identifier: String, mut conn: DbConn) -> JsonResult { // This endpoint doesn't have auth header let mut result = false; if let Some(user) = User::find_by_mail(&email, &mut conn).await { - result = Device::find_by_uuid_and_user(&uuid, &user.uuid, &mut conn).await.is_some(); + result = Device::find_by_identifier_and_user(&identifier, &user.uuid, &mut conn).await.is_some(); } Ok(Json(json!(result))) } + +#[derive(Deserialize, Debug)] +#[allow(non_snake_case)] +struct PushTokenData { + pushToken: String, +} + +#[put("/devices/identifier//token", data = "")] +async fn put_device_token( + identifier: String, + data: Json, + headers: Headers, + mut conn: DbConn, +) -> EmptyResult { + let data: PushTokenData = data.into_inner(); + let token = data.pushToken.clone(); + let mut device = + match Device::find_by_identifier_and_user(&headers.device.identifier, &headers.user.uuid, &mut conn).await { + Some(device) => device, + None => Device::new(identifier, headers.user.uuid.clone(), headers.device.name, headers.device.atype), + }; + device.push_token = Some(token); + if device.uuid == *"00000000-0000-0000-0000-000000000000".to_string() { + device.uuid = uuid::Uuid::new_v4().to_string(); + } + if let Err(e) = device.save(&mut conn).await { + error!("An error occured while trying to save the device push token: {}", e); + return Err(e); + } + if CONFIG.push_enabled() { + if let Err(e) = push::register_push_device(headers.user.uuid, device).await { + error!("An error occured while proceeding registration of a device: {}", e); + }; + } + + Ok(()) +} + +#[put("/devices/identifier//clear-token")] +async fn clear_device_token(identifier: String, mut conn: DbConn) -> &'static str { + // This only clears push token + // https://github.com/bitwarden/core/blob/master/src/Api/Controllers/DevicesController.cs#L109 + // https://github.com/bitwarden/core/blob/master/src/Core/Services/Implementations/DeviceService.cs#L37 + // This is somehow not implemented in any app, added it in case it is required + match Device::delete_token_by_identifier(&identifier, &mut conn).await { + Err(e) => error!("{}", e), + Ok(_r) => (), + }; + let device = match Device::find_by_identifier(&identifier, &mut conn).await { + Some(device) => device, + None => return "", + }; + match push::unregister_push_device(device.uuid).await { + Err(e) => error!("{}", e), + Ok(_r) => (), + }; + "" +} + +// On upstream server, both PUT and POST are declared. Sadly Rocket doesn't allows to put multiple methods on the same function, so we call the function manually +#[post("/devices/identifier//clear-token")] +async fn clear_device_token_post(identifier: String, conn: DbConn) -> &'static str { + clear_device_token(identifier, conn).await; + "" +} diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 14d4459789..f663c807ee 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -9,11 +9,13 @@ use rocket::{ }; use serde_json::Value; +use crate::push::push_user_update; use crate::{ api::{self, core::log_event, EmptyResult, JsonResult, JsonUpcase, Notify, PasswordData, UpdateType}, auth::{ClientIp, Headers}, crypto, db::{models::*, DbConn, DbPool}, + push::push_cipher_update, CONFIG, }; @@ -522,10 +524,9 @@ pub async fn update_cipher_from_data( ) .await; } - - nt.send_cipher_update(ut, cipher, &cipher.update_users_revision(conn).await, &headers.device.uuid).await; + nt.send_cipher_update(ut, cipher, &cipher.update_users_revision(conn).await, &headers.device.identifier).await; + push_cipher_update(ut, cipher, &headers.device.identifier, conn).await; } - Ok(()) } @@ -593,6 +594,7 @@ async fn post_ciphers_import( let mut user = headers.user; user.update_revision(&mut conn).await?; nt.send_user_update(UpdateType::SyncVault, &user).await; + push_user_update(UpdateType::SyncVault, &user).await; Ok(()) } @@ -1133,9 +1135,10 @@ async fn save_attachment( UpdateType::SyncCipherUpdate, &cipher, &cipher.update_users_revision(&mut conn).await, - &headers.device.uuid, + &headers.device.identifier, ) .await; + push_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &headers.device.identifier, &mut conn).await; if let Some(org_uuid) = &cipher.organization_uuid { log_event( @@ -1471,7 +1474,9 @@ async fn move_cipher_selected( // Move cipher cipher.move_to_folder(data.FolderId.clone(), &user_uuid, &mut conn).await?; - nt.send_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &[user_uuid.clone()], &headers.device.uuid).await; + nt.send_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &[user_uuid.clone()], &headers.device.identifier) + .await; + push_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &headers.device.identifier, &mut conn).await; } Ok(()) @@ -1520,6 +1525,7 @@ async fn delete_all( if user_org.atype == UserOrgType::Owner { Cipher::delete_all_by_organization(&org_data.org_id, &mut conn).await?; nt.send_user_update(UpdateType::SyncVault, &user).await; + push_user_update(UpdateType::SyncVault, &user).await; log_event( EventType::OrganizationPurgedVault as i32, @@ -1553,6 +1559,7 @@ async fn delete_all( user.update_revision(&mut conn).await?; nt.send_user_update(UpdateType::SyncVault, &user).await; + push_user_update(UpdateType::SyncVault, &user).await; Ok(()) } } @@ -1582,18 +1589,20 @@ async fn _delete_cipher_by_uuid( UpdateType::SyncCipherUpdate, &cipher, &cipher.update_users_revision(conn).await, - &headers.device.uuid, + &headers.device.identifier, ) .await; + push_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &headers.device.identifier, conn).await; } else { cipher.delete(conn).await?; nt.send_cipher_update( UpdateType::SyncCipherDelete, &cipher, &cipher.update_users_revision(conn).await, - &headers.device.uuid, + &headers.device.identifier, ) .await; + push_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &headers.device.identifier, conn).await; } if let Some(org_uuid) = cipher.organization_uuid { @@ -1659,9 +1668,11 @@ async fn _restore_cipher_by_uuid( UpdateType::SyncCipherUpdate, &cipher, &cipher.update_users_revision(conn).await, - &headers.device.uuid, + &headers.device.identifier, ) .await; + push_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &headers.device.identifier, conn).await; + if let Some(org_uuid) = &cipher.organization_uuid { log_event( EventType::CipherRestored as i32, @@ -1742,9 +1753,11 @@ async fn _delete_cipher_attachment_by_id( UpdateType::SyncCipherUpdate, &cipher, &cipher.update_users_revision(conn).await, - &headers.device.uuid, + &headers.device.identifier, ) .await; + push_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &headers.device.identifier, conn).await; + if let Some(org_uuid) = cipher.organization_uuid { log_event( EventType::CipherAttachmentDeleted as i32, diff --git a/src/api/core/folders.rs b/src/api/core/folders.rs index c27d5455b9..9908acf014 100644 --- a/src/api/core/folders.rs +++ b/src/api/core/folders.rs @@ -5,6 +5,7 @@ use crate::{ api::{EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType}, auth::Headers, db::{models::*, DbConn}, + push::push_folder_update, }; pub fn routes() -> Vec { @@ -50,7 +51,8 @@ async fn post_folders(data: JsonUpcase, headers: Headers, mut conn: let mut folder = Folder::new(headers.user.uuid, data.Name); folder.save(&mut conn).await?; - nt.send_folder_update(UpdateType::SyncFolderCreate, &folder, &headers.device.uuid).await; + nt.send_folder_update(UpdateType::SyncFolderCreate, &folder, &headers.device.identifier).await; + push_folder_update(UpdateType::SyncFolderCreate, &folder, &headers.device.identifier, &mut conn).await; Ok(Json(folder.to_json())) } @@ -88,7 +90,8 @@ async fn put_folder( folder.name = data.Name; folder.save(&mut conn).await?; - nt.send_folder_update(UpdateType::SyncFolderUpdate, &folder, &headers.device.uuid).await; + nt.send_folder_update(UpdateType::SyncFolderUpdate, &folder, &headers.device.identifier).await; + push_folder_update(UpdateType::SyncFolderUpdate, &folder, &headers.device.identifier, &mut conn).await; Ok(Json(folder.to_json())) } @@ -112,6 +115,7 @@ async fn delete_folder(uuid: String, headers: Headers, mut conn: DbConn, nt: Not // Delete the actual folder entry folder.delete(&mut conn).await?; - nt.send_folder_update(UpdateType::SyncFolderDelete, &folder, &headers.device.uuid).await; + nt.send_folder_update(UpdateType::SyncFolderDelete, &folder, &headers.device.identifier).await; + push_folder_update(UpdateType::SyncFolderDelete, &folder, &headers.device.identifier, &mut conn).await; Ok(()) } diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 6a483842a0..880d49c4e9 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -14,7 +14,6 @@ pub use sends::purge_sends; pub use two_factor::send_incomplete_2fa_notifications; pub fn routes() -> Vec { - let mut device_token_routes = routes![clear_device_token, put_device_token]; let mut eq_domains_routes = routes![get_eq_domains, post_eq_domains, put_eq_domains]; let mut hibp_routes = routes![hibp_breach]; let mut meta_routes = routes![alive, now, version, config]; @@ -28,7 +27,6 @@ pub fn routes() -> Vec { routes.append(&mut organizations::routes()); routes.append(&mut two_factor::routes()); routes.append(&mut sends::routes()); - routes.append(&mut device_token_routes); routes.append(&mut eq_domains_routes); routes.append(&mut hibp_routes); routes.append(&mut meta_routes); @@ -54,40 +52,10 @@ use crate::{ auth::Headers, db::DbConn, error::Error, + push::push_user_update, util::get_reqwest_client, }; -#[put("/devices/identifier//clear-token")] -fn clear_device_token(uuid: String) -> &'static str { - // This endpoint doesn't have auth header - - let _ = uuid; - // uuid is not related to deviceId - - // This only clears push token - // https://github.com/bitwarden/core/blob/master/src/Api/Controllers/DevicesController.cs#L109 - // https://github.com/bitwarden/core/blob/master/src/Core/Services/Implementations/DeviceService.cs#L37 - "" -} - -#[put("/devices/identifier//token", data = "")] -fn put_device_token(uuid: String, data: JsonUpcase, headers: Headers) -> Json { - let _data: Value = data.into_inner().data; - // Data has a single string value "PushToken" - let _ = uuid; - // uuid is not related to deviceId - - // TODO: This should save the push token, but we don't have push functionality - - Json(json!({ - "Id": headers.device.uuid, - "Name": headers.device.name, - "Type": headers.device.atype, - "Identifier": headers.device.uuid, - "CreationDate": crate::util::format_date(&headers.device.created_at), - })) -} - #[derive(Serialize, Deserialize, Debug)] #[allow(non_snake_case)] struct GlobalDomain { @@ -155,6 +123,7 @@ async fn post_eq_domains( user.save(&mut conn).await?; nt.send_user_update(UpdateType::SyncSettings, &user).await; + push_user_update(UpdateType::SyncSettings, &user).await; Ok(Json(json!({}))) } diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 1353e61b86..da9ec37cbf 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -13,6 +13,7 @@ use crate::{ db::{models::*, DbConn}, error::Error, mail, + push::{push_logout, push_user_update}, util::convert_json_key_lcase_first, CONFIG, }; @@ -1224,6 +1225,7 @@ async fn _confirm_invite( if let Some(user) = User::find_by_uuid(&user_to_confirm.user_uuid, conn).await { nt.send_user_update(UpdateType::SyncOrgKeys, &user).await; + push_user_update(UpdateType::SyncOrgKeys, &user).await; } save_result @@ -1451,6 +1453,7 @@ async fn _delete_user( if let Some(user) = User::find_by_uuid(&user_to_delete.user_uuid, conn).await { nt.send_user_update(UpdateType::SyncOrgKeys, &user).await; + push_user_update(UpdateType::SyncOrgKeys, &user).await; } user_to_delete.delete(conn).await @@ -2701,6 +2704,7 @@ async fn put_reset_password( user.save(&mut conn).await?; nt.send_logout(&user, None).await; + push_logout(&user, None, &mut conn).await; log_event( EventType::OrganizationUserAdminResetPassword as i32, diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index b086663ad2..910e4fd38c 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -7,6 +7,7 @@ use rocket::fs::TempFile; use rocket::serde::json::Json; use serde_json::Value; +use crate::push::push_send_update; use crate::{ api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, UpdateType}, auth::{ClientIp, Headers, Host}, @@ -181,6 +182,7 @@ async fn post_send(data: JsonUpcase, headers: Headers, mut conn: DbCon let mut send = create_send(data, headers.user.uuid)?; send.save(&mut conn).await?; nt.send_send_update(UpdateType::SyncSendCreate, &send, &send.update_users_revision(&mut conn).await).await; + push_send_update(UpdateType::SyncSendCreate, &send).await; Ok(Json(send.to_json())) } @@ -253,6 +255,7 @@ async fn post_send_file(data: Form>, headers: Headers, mut conn: // Save the changes in the database send.save(&mut conn).await?; nt.send_send_update(UpdateType::SyncSendCreate, &send, &send.update_users_revision(&mut conn).await).await; + push_send_update(UpdateType::SyncSendCreate, &send).await; Ok(Json(send.to_json())) } @@ -336,6 +339,7 @@ async fn post_send_file_v2_data( } nt.send_send_update(UpdateType::SyncSendCreate, &send, &send.update_users_revision(&mut conn).await).await; + push_send_update(UpdateType::SyncSendCreate, &send).await; } else { err!("Send not found. Unable to save the file."); } @@ -398,6 +402,7 @@ async fn post_access( send.save(&mut conn).await?; nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await).await; + push_send_update(UpdateType::SyncSendUpdate, &send).await; Ok(Json(send.to_json_access(&mut conn).await)) } @@ -449,6 +454,7 @@ async fn post_access_file( send.save(&mut conn).await?; nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await).await; + push_send_update(UpdateType::SyncSendUpdate, &send).await; let token_claims = crate::auth::generate_send_claims(&send_id, &file_id); let token = crate::auth::encode_jwt(&token_claims); @@ -531,6 +537,7 @@ async fn put_send( send.save(&mut conn).await?; nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await).await; + push_send_update(UpdateType::SyncSendUpdate, &send).await; Ok(Json(send.to_json())) } @@ -548,6 +555,7 @@ async fn delete_send(id: String, headers: Headers, mut conn: DbConn, nt: Notify< send.delete(&mut conn).await?; nt.send_send_update(UpdateType::SyncSendDelete, &send, &send.update_users_revision(&mut conn).await).await; + push_send_update(UpdateType::SyncSendDelete, &send).await; Ok(()) } @@ -568,6 +576,7 @@ async fn put_remove_password(id: String, headers: Headers, mut conn: DbConn, nt: send.set_password(None); send.save(&mut conn).await?; nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await).await; + push_send_update(UpdateType::SyncSendUpdate, &send).await; Ok(Json(send.to_json())) } diff --git a/src/api/identity.rs b/src/api/identity.rs index bb575cca21..ff85f824c0 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -382,16 +382,16 @@ async fn get_device(data: &ConnectData, conn: &mut DbConn, user: &User) -> (Devi // 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_id = data.device_identifier.clone().expect("No device id provided"); + let device_identifier = data.device_identifier.clone().expect("No device identifier provided"); let device_name = data.device_name.clone().expect("No device name provided"); let mut new_device = false; // Find device or create new - let device = match Device::find_by_uuid_and_user(&device_id, &user.uuid, conn).await { + let device = match Device::find_by_identifier_and_user(&device_identifier, &user.uuid, conn).await { Some(device) => device, None => { new_device = true; - Device::new(device_id, user.uuid.clone(), device_name, device_type) + Device::new(device_identifier, user.uuid.clone(), device_name, device_type) } }; @@ -412,7 +412,7 @@ async fn twofactor_auth( return Ok(None); } - TwoFactorIncomplete::mark_incomplete(user_uuid, &device.uuid, &device.name, ip, conn).await?; + TwoFactorIncomplete::mark_incomplete(user_uuid, &device.identifier, &device.name, ip, conn).await?; let twofactor_ids: Vec<_> = twofactors.iter().map(|tf| tf.atype).collect(); let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, asume the first one @@ -466,7 +466,7 @@ async fn twofactor_auth( ), } - TwoFactorIncomplete::mark_complete(user_uuid, &device.uuid, conn).await?; + TwoFactorIncomplete::mark_complete(user_uuid, &device.identifier, conn).await?; if !CONFIG.disable_2fa_remember() && remember == 1 { Ok(Some(device.refresh_twofactor_remember())) diff --git a/src/api/notifications.rs b/src/api/notifications.rs index b4dc55e961..2aa9387681 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -170,11 +170,11 @@ impl WebSocketUsers { self.send_update(&user.uuid, &data).await; } - pub async fn send_logout(&self, user: &User, acting_device_uuid: Option) { + pub async fn send_logout(&self, user: &User, acting_device_identifier: Option) { let data = create_update( vec![("UserId".into(), user.uuid.clone().into()), ("Date".into(), serialize_date(user.updated_at))], UpdateType::LogOut, - acting_device_uuid, + acting_device_identifier, ); self.send_update(&user.uuid, &data).await; @@ -278,7 +278,7 @@ fn create_ping() -> Vec { } #[allow(dead_code)] -#[derive(Eq, PartialEq)] +#[derive(Clone, Copy, Eq, PartialEq)] pub enum UpdateType { SyncCipherUpdate = 0, SyncCipherCreate = 1, diff --git a/src/auth.rs b/src/auth.rs index 380c6a735a..21b4292504 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -367,7 +367,7 @@ impl<'r> FromRequest<'r> for Headers { Err(_) => err_handler!("Invalid claim"), }; - let device_uuid = claims.device; + let device_identifier = claims.device; let user_uuid = claims.sub; let mut conn = match DbConn::from_request(request).await { @@ -375,9 +375,9 @@ impl<'r> FromRequest<'r> for Headers { _ => err_handler!("Error getting DB"), }; - let device = match Device::find_by_uuid_and_user(&device_uuid, &user_uuid, &mut conn).await { + let device = match Device::find_by_identifier_and_user(&device_identifier, &user_uuid, &mut conn).await { Some(device) => device, - None => err_handler!("Invalid device id"), + None => err_handler!("Invalid device identifier"), }; let user = match User::find_by_uuid(&user_uuid, &mut conn).await { diff --git a/src/config.rs b/src/config.rs index f3736a1fc5..6cd1d635e2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -377,6 +377,16 @@ make_config! { /// Websocket port websocket_port: u16, false, def, 3012; }, + push { + /// Enable push notifications + push_enabled: bool, false, def, false; + /// Push relay base uri + push_relay_uri: String, false, def, "https://push.bitwarden.com".to_string(); + /// Installation id |> The installation id from https://bitwarden.com/host + push_installation_id: Pass, false, def, String::new(); + /// Installation key |> The installation key from https://bitwarden.com/host + push_installation_key: Pass, false, def, String::new(); + }, jobs { /// Job scheduler poll interval |> How often the job scheduler thread checks for jobs to run. /// Set to 0 to globally disable scheduled jobs. diff --git a/src/db/models/device.rs b/src/db/models/device.rs index e47ccadcb8..b19e634aa3 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -8,14 +8,15 @@ db_object! { #[diesel(treat_none_as_null = true)] #[diesel(primary_key(uuid, user_uuid))] pub struct Device { - pub uuid: String, + pub uuid: String, // Local identifier for the object in the database pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, pub user_uuid: String, pub name: String, - pub atype: i32, // https://github.com/bitwarden/server/blob/master/src/Core/Enums/DeviceType.cs + pub atype: i32, // https://github.com/bitwarden/server/blob/master/src/Core/Enums/DeviceType.cs + pub identifier: String, // Real identifier of the device pub push_token: Option, pub refresh_token: String, @@ -26,11 +27,11 @@ db_object! { /// Local methods impl Device { - pub fn new(uuid: String, user_uuid: String, name: String, atype: i32) -> Self { + pub fn new(identifier: String, user_uuid: String, name: String, atype: i32) -> Self { let now = Utc::now().naive_utc(); Self { - uuid, + uuid: crate::util::get_uuid(), created_at: now, updated_at: now, @@ -38,6 +39,7 @@ impl Device { name, atype, + identifier, push_token: None, refresh_token: String::new(), twofactor_remember: None, @@ -100,7 +102,7 @@ impl Device { orgmanager, sstamp: user.security_stamp.clone(), - device: self.uuid.clone(), + device: self.identifier.clone(), scope, amr: vec!["Application".into()], }; @@ -129,7 +131,7 @@ impl Device { postgresql { let value = DeviceDb::to_db(self); crate::util::retry( - || diesel::insert_into(devices::table).values(&value).on_conflict((devices::uuid, devices::user_uuid)).do_update().set(&value).execute(conn), + || diesel::insert_into(devices::table).values(&value).on_conflict((devices::identifier, devices::user_uuid)).do_update().set(&value).execute(conn), 10, ).map_res("Error saving device") } @@ -144,10 +146,10 @@ impl Device { }} } - pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_identifier_and_user(identifier: &str, user_uuid: &str, conn: &mut DbConn) -> Option { db_run! { conn: { devices::table - .filter(devices::uuid.eq(uuid)) + .filter(devices::identifier.eq(identifier)) .filter(devices::user_uuid.eq(user_uuid)) .first::(conn) .ok() @@ -155,6 +157,25 @@ impl Device { }} } + pub async fn find_by_identifier(identifier: &str, conn: &mut DbConn) -> Option { + db_run! { conn: { + devices::table + .filter(devices::identifier.eq(identifier)) + .first::(conn) + .ok() + .from_db() + }} + } + + pub async fn delete_token_by_identifier(identifier: &str, conn: &mut DbConn) -> EmptyResult { + db_run! { conn: { + diesel::update(devices::table) + .filter(devices::identifier.eq(identifier)) + .set(devices::push_token.eq::>(None)) + .execute(conn) + .map_res("Error removing push token") + }} + } pub async fn find_by_refresh_token(refresh_token: &str, conn: &mut DbConn) -> Option { db_run! { conn: { devices::table @@ -175,4 +196,15 @@ impl Device { .from_db() }} } + pub async fn find_push_device_by_user(user_uuid: &str, conn: &mut DbConn) -> Option { + db_run! { conn: { + devices::table + .filter(devices::user_uuid.eq(user_uuid)) + .filter(devices::push_token.is_not_null()) + .order(devices::updated_at.desc()) + .first::(conn) + .ok() + .from_db() + }} + } } diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 83a595246c..fcaf4dec16 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -9,6 +9,7 @@ db_object! { #[diesel(table_name = users)] #[diesel(treat_none_as_null = true)] #[diesel(primary_key(uuid))] + #[derive(Clone)] pub struct User { pub uuid: String, pub enabled: bool, diff --git a/src/db/schemas/mysql/schema.rs b/src/db/schemas/mysql/schema.rs index da51799a29..67a2cfb001 100644 --- a/src/db/schemas/mysql/schema.rs +++ b/src/db/schemas/mysql/schema.rs @@ -49,6 +49,7 @@ table! { user_uuid -> Text, name -> Text, atype -> Integer, + identifier -> Text, push_token -> Nullable, refresh_token -> Text, twofactor_remember -> Nullable, diff --git a/src/db/schemas/postgresql/schema.rs b/src/db/schemas/postgresql/schema.rs index aef644921c..18087a639c 100644 --- a/src/db/schemas/postgresql/schema.rs +++ b/src/db/schemas/postgresql/schema.rs @@ -49,6 +49,7 @@ table! { user_uuid -> Text, name -> Text, atype -> Integer, + identifier -> Text, push_token -> Nullable, refresh_token -> Text, twofactor_remember -> Nullable, diff --git a/src/db/schemas/sqlite/schema.rs b/src/db/schemas/sqlite/schema.rs index a30b74338f..0d6499ccd8 100644 --- a/src/db/schemas/sqlite/schema.rs +++ b/src/db/schemas/sqlite/schema.rs @@ -49,6 +49,7 @@ table! { user_uuid -> Text, name -> Text, atype -> Integer, + identifier -> Text, push_token -> Nullable, refresh_token -> Text, twofactor_remember -> Nullable, diff --git a/src/main.rs b/src/main.rs index cd17a2f5d3..035da01baa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -79,6 +79,7 @@ mod crypto; #[macro_use] mod db; mod mail; +mod push; mod ratelimit; mod util; @@ -104,6 +105,7 @@ async fn main() -> Result<(), Error> { exit(1); }); check_web_vault(); + check_push_config(); create_dir(&CONFIG.icon_cache_folder(), "icon cache"); create_dir(&CONFIG.tmp_folder(), "tmp folder"); @@ -336,6 +338,22 @@ async fn check_data_folder() { } } +fn check_push_config() { + if CONFIG.push_enabled() + && (CONFIG.push_installation_id() == String::new() || CONFIG.push_installation_key() == String::new()) + { + error!( + "Misconfigured Push Notification service\n\ + ########################################################################################\n\ + # It looks like you enabled Push Notification feature, but you didn't configured the #\n\ + # value. Make sure the installation id and key from https://bitwarden.com/host are #\n\ + # added to your configuration. #\n\ + ########################################################################################\n" + ); + exit(1); + } +} + /// Detect when using Docker or Podman the DATA_FOLDER is either a bind-mount or a volume created manually. /// If not created manually, then the data will not be persistent. /// A none persistent volume in either Docker or Podman is represented by a 64 alphanumerical string. diff --git a/src/push.rs b/src/push.rs new file mode 100644 index 0000000000..69368563a4 --- /dev/null +++ b/src/push.rs @@ -0,0 +1,297 @@ +use handlebars::JsonValue; +use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE}; +use serde_json::Value; +use std::collections::HashMap; + +use crate::{ + api::{EmptyResult, UpdateType}, + db::models::{Cipher, Device, Folder, Send, User}, + util::get_reqwest_client, + CONFIG, +}; + +use once_cell::sync::Lazy; // 1.3.1 +use std::sync::Mutex; +use std::time::{Duration, Instant}; + +#[derive(Deserialize, Debug)] +#[allow(non_snake_case)] +struct AuthPushToken { + access_token: String, + expires_in: i32, +} + +#[derive(Debug)] +struct LocalAuthPushToken { + access_token: String, + valid_until: Instant, +} + +static PUSH_TOKEN: Lazy> = Lazy::new(|| { + Mutex::new(LocalAuthPushToken { + access_token: String::new(), + valid_until: Instant::now(), + }) +}); + +pub async fn get_auth_push_token() -> EmptyResult { + if PUSH_TOKEN.lock().unwrap().valid_until.saturating_duration_since(Instant::now()).as_secs() > 0 { + debug!("Auth Push token still valid, no need for a new one"); + return Ok(()); + } + + let installationid = CONFIG.push_installation_id(); + let mut client_id = "installation.".to_string(); + let client_secret = CONFIG.push_installation_key(); + + client_id.push_str(&installationid); + + let mut params = HashMap::new(); + params.insert("grant_type", "client_credentials"); + params.insert("scope", "api.push"); + params.insert("client_id", &client_id); + params.insert("client_secret", &client_secret); + + let res = match get_reqwest_client().post("https://identity.bitwarden.com/connect/token").form(¶ms).send().await + { + Ok(r) => r, + Err(e) => err!(format!("Error getting push token from bitwarden server: {e}")), + }; + + let json_pushtoken = match res.json::().await { + Ok(r) => r, + Err(e) => err!(format!("Unexpected push token received from bitwarden server: {e}")), + }; + + PUSH_TOKEN.lock().unwrap().valid_until = Instant::now() + .checked_add(Duration::new((json_pushtoken.expires_in / 2) as u64, 0)) // Token valid for half the specified time + .unwrap(); + + PUSH_TOKEN.lock().unwrap().access_token = json_pushtoken.access_token; + + debug!( + "Token still valid for {}", + PUSH_TOKEN.lock().unwrap().valid_until.saturating_duration_since(Instant::now()).as_secs() + ); + Ok(()) +} + +pub async fn register_push_device(user_uuid: String, device: Device) -> EmptyResult { + get_auth_push_token().await?; + + //Needed to register a device for push to bitwarden : + let data = json!({ + "userId": user_uuid, + "deviceId": device.uuid, + "identifier": device.identifier, + "type": device.atype, + "pushToken": device.push_token + }); + + let mut auth_header = "Bearer ".to_string(); + auth_header.push_str(&PUSH_TOKEN.lock().unwrap().access_token); + + get_reqwest_client() + .post(CONFIG.push_relay_uri() + "/push/register") + .header(CONTENT_TYPE, "application/json") + .header(ACCEPT, "application/json") + .header(AUTHORIZATION, auth_header) + .json(&data) + .send() + .await? + .error_for_status()?; + Ok(()) +} + +pub async fn unregister_push_device(uuid: String) -> EmptyResult { + get_auth_push_token().await?; + + let mut auth_header = "Bearer ".to_string(); + auth_header.push_str(&PUSH_TOKEN.lock().unwrap().access_token); + match get_reqwest_client() + .delete(CONFIG.push_relay_uri() + "/push/" + &uuid) + .header(AUTHORIZATION, auth_header) + .send() + .await + { + Ok(r) => r, + Err(e) => err!(format!("An error occured during device unregistration: {e}")), + }; + Ok(()) +} + +pub async fn push_cipher_update( + ut: UpdateType, + cipher: &Cipher, + acting_device_identifier: &String, + conn: &mut crate::db::DbConn, +) { + if !CONFIG.push_enabled() { + return; + } + // We shouldn't send a push notification on cipher update if the cipher belongs to an organization, this isn't implemented in the upstream server too. + if cipher.organization_uuid.is_some() { + return; + }; + let user_uuid = match cipher.user_uuid.clone() { + Some(c) => c, + None => { + debug!("Cipher has no uuid"); + return; + } + }; + + let device = match Device::find_by_identifier_and_user(acting_device_identifier, &user_uuid, conn).await { + Some(d) => d, + None => { + debug!("Device doesn't exist"); + return; + } + }; + + let data = json!({ + "userId": user_uuid, + "organizationId": (), + "deviceId": device.uuid, + "identifier": acting_device_identifier, + "type": ut as i32, + "payload": { + "Id": cipher.uuid, + "UserId": cipher.user_uuid, + "OrganizationId": (), + "RevisionDate": cipher.updated_at + } + }); + + send_to_push_relay(data).await; +} +pub async fn push_logout(user: &User, acting_device_identifier: Option, conn: &mut crate::db::DbConn) { + let data: JsonValue; + if let Some(d) = acting_device_identifier { + if let Some(device) = Device::find_by_identifier_and_user(d.as_str(), &user.uuid, conn).await { + data = json!({ + "userId": user.uuid, + "organizationId": (), + "deviceId": device.uuid, + "identifier": d, + "type": UpdateType::LogOut as i32, + "payload": { + "UserId": user.uuid, + "Date": user.updated_at + } + }); + send_to_push_relay(data).await; + } else { + debug!("Device doesn't exist"); + } + } else { + data = json!({ + "userId": user.uuid, + "organizationId": (), + "deviceId": (), + "identifier": (), + "type": UpdateType::LogOut as i32, + "payload": { + "UserId": user.uuid, + "Date": user.updated_at + } + }); + send_to_push_relay(data).await; + } +} + +pub async fn push_user_update(ut: UpdateType, user: &User) { + let data = json!({ + "userId": user.uuid, + "organizationId": (), + "deviceId": (), + "identifier": (), + "type": ut as i32, + "payload": { + "UserId": user.uuid, + "Date": user.updated_at + } + }); + + send_to_push_relay(data).await; +} + +pub async fn push_folder_update( + ut: UpdateType, + folder: &Folder, + acting_device_identifier: &String, + conn: &mut crate::db::DbConn, +) { + let device = match Device::find_by_identifier_and_user(acting_device_identifier, &folder.user_uuid, conn).await { + Some(d) => d, + None => { + debug!("Device doesn't exist"); + return; + } + }; + + let data = json!({ + "userId": folder.user_uuid, + "organizationId": (), + "deviceId": device.uuid, + "identifier": acting_device_identifier, + "type": ut as i32, + "payload": { + "Id": folder.uuid, + "UserId": folder.user_uuid, + "RevisionDate": folder.updated_at + } + }); + + send_to_push_relay(data).await; +} + +pub async fn push_send_update(ut: UpdateType, send: &Send) { + if send.user_uuid.is_none() { + return; + } + + let data = json!({ + "userId": send.user_uuid, + "organizationId": (), + "deviceId": (), + "identifier": (), + "type": ut as i32, + "payload": { + "Id": send.uuid, + "UserId": send.user_uuid, + "RevisionDate": send.revision_date + } + }); + + send_to_push_relay(data).await; +} + +async fn send_to_push_relay(data: Value) { + if !CONFIG.push_enabled() { + return; + } + + match get_auth_push_token().await { + Ok(_) => (), + Err(e) => { + debug!("Cannot get the auth push token: {}", e); + return; + } + }; + + let mut auth_header = "Bearer ".to_string(); + auth_header.push_str(&PUSH_TOKEN.lock().unwrap().access_token); + + if let Err(e) = get_reqwest_client() + .post(CONFIG.push_relay_uri() + "/push/send") + .header(ACCEPT, "application/json") + .header(CONTENT_TYPE, "application/json") + .header(AUTHORIZATION, auth_header) + .json(&data) + .send() + .await + { + error!("An error occured while sending a send update to the push relay: {}", e); + }; +}