From 19ce9e9252bad6222a9e1ef312f16f68eb5d1044 Mon Sep 17 00:00:00 2001 From: ICantThinkOfAName-tech Date: Fri, 16 Oct 2026 21:45:37 +0200 Subject: [PATCH] Hide Item Sends from clients that can't handle them The mobile apps and the CLI don't know the Item Send type, and an unknown Send type makes the whole Android sync fail. Other clients support it since 2026.10.0. Leave Item Sends out of sync, list and get for those clients, and only advertise the item sharing flag to clients that support it. Co-Authored-By: Claude Opus 5.5 --- src/api/core/ciphers.rs | 6 +++--- src/api/core/mod.rs | 9 +++++++-- src/api/core/sends.rs | 41 ++++++++++++++++++++++++++++++++++------- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 75c69a68..7234c2da 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -31,7 +31,7 @@ use crate::{ use super::{ folders::FolderData, - sends::{SIZE_525_MB, send_visible}, + sends::{SIZE_525_MB, send_visible_to}, }; pub fn routes() -> Vec { @@ -130,7 +130,7 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option=2024.12.0").unwrap(); ver_match.matches(&client_version.0) } else { @@ -162,7 +162,7 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option = Send::find_by_user(&headers.user.uuid, &conn) .await .iter() - .filter(|s| send_visible(s)) + .filter(|s| send_visible_to(s, &headers.device, client_version.as_ref())) .map(Send::to_json) .collect(); diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index f04b05fc..b6a737a1 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -21,7 +21,7 @@ use rocket::{Catcher, Route, serde::json::Json, serde::json::Value}; use crate::{ CONFIG, api::{EmptyResult, JsonResult, Notify, UpdateType}, - auth::Headers, + auth::{ClientVersion, Headers}, db::{ DbConn, models::{Membership, MembershipStatus, OrgPolicy, Organization, User}, @@ -208,7 +208,8 @@ fn get_api_webauthn(_headers: Headers) -> Json { } #[get("/config")] -fn config() -> Json { +#[expect(clippy::needless_pass_by_value, reason = "Rocket request guards are passed by value")] +fn config(client_version: Option) -> Json { let domain = CONFIG.domain(); // Official available feature flags can be found here: // Server (v2026.2.1): https://github.com/bitwarden/server/blob/0e42725d0837bd1c0dabd864ff621a579959744b/src/Core/Constants.cs#L135 @@ -220,6 +221,10 @@ fn config() -> Json { &FeatureFlagFilter::ValidOnly, ); feature_states.insert("pm-19148-innovation-archive".to_owned(), true); + // Older clients with only part of the item sharing feature could create Item Sends they can't handle + if !sends::client_version_supports_item_sends(client_version.as_ref()) { + feature_states.remove(sends::ITEM_SENDS_FEATURE_FLAG); + } Json(json!({ // Note: The clients use this version to handle backwards compatibility concerns diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index 76c8273c..8922aca6 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -12,7 +12,7 @@ use serde_json::Value; use crate::{ CONFIG, api::{ApiResult, EmptyResult, JsonResult, Notify, UpdateType}, - auth::{Headers, Host, SendHeaders}, + auth::{ClientVersion, Headers, Host, SendHeaders}, config::PathType, db::{ DbConn, DbPool, @@ -174,7 +174,7 @@ fn create_send(data: SendData, user_id: UserId) -> ApiResult { Ok(send) } -const ITEM_SENDS_FEATURE_FLAG: &str = "pm-34203-temporary-item-sharing"; +pub const ITEM_SENDS_FEATURE_FLAG: &str = "pm-34203-temporary-item-sharing"; // Clients can only decrypt (and rotate the keys of) Item Sends through the SDK const SDK_SENDS_FEATURE_FLAG: &str = "pm-30110-sdk-sends-api"; @@ -188,11 +188,28 @@ pub fn item_sends_enabled() -> bool { flags.contains_key(ITEM_SENDS_FEATURE_FLAG) && flags.contains_key(SDK_SENDS_FEATURE_FLAG) } -/// Whether a Send can be shown to its owner or a recipient, Item Sends are hidden while the feature is disabled. +/// Whether a Send can be accessed by a recipient, Item Sends are hidden while the feature is disabled. pub fn send_visible(send: &Send) -> bool { send.atype != SendType::Item as i32 || item_sends_enabled() } +/// Clients able to handle Item Sends. The mobile apps and the CLI don't know the type (an unknown Send type +/// breaks the whole Android sync), the other clients support it since 2026.10.0. +pub fn client_supports_item_sends(device: &Device, client_version: Option<&ClientVersion>) -> bool { + !device.is_mobile() && !device.is_cli() && client_version_supports_item_sends(client_version) +} + +pub fn client_version_supports_item_sends(client_version: Option<&ClientVersion>) -> bool { + static MIN_VERSION: LazyLock = + LazyLock::new(|| semver::VersionReq::parse(">=2026.10.0").unwrap()); + client_version.is_some_and(|v| MIN_VERSION.matches(&v.0)) +} + +/// Whether a Send can be shown to its owner on the requesting client. +pub fn send_visible_to(send: &Send, device: &Device, client_version: Option<&ClientVersion>) -> bool { + send.atype != SendType::Item as i32 || (item_sends_enabled() && client_supports_item_sends(device, client_version)) +} + /// Data of an Item Send. The vault item is encrypted client side into a single opaque blob, the item id /// is kept in clear so the server can check that the sender is allowed to share it. /// @@ -348,9 +365,14 @@ fn enforce_mail_enabled_for_email_auth(send: &Send) -> EmptyResult { } #[get("/sends")] -async fn get_sends(headers: Headers, conn: DbConn) -> Json { +async fn get_sends(headers: Headers, client_version: Option, conn: DbConn) -> Json { let sends = Send::find_by_user(&headers.user.uuid, &conn); - let sends_json: Vec = sends.await.iter().filter(|s| send_visible(s)).map(Send::to_json).collect(); + let sends_json: Vec = sends + .await + .iter() + .filter(|s| send_visible_to(s, &headers.device, client_version.as_ref())) + .map(Send::to_json) + .collect(); Json(json!({ "data": sends_json, @@ -360,9 +382,14 @@ async fn get_sends(headers: Headers, conn: DbConn) -> Json { } #[get("/sends/")] -async fn get_send(send_id: SendId, headers: Headers, conn: DbConn) -> JsonResult { +async fn get_send( + send_id: SendId, + headers: Headers, + client_version: Option, + conn: DbConn, +) -> JsonResult { if let Some(send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await - && send_visible(&send) + && send_visible_to(&send, &headers.device, client_version.as_ref()) { Ok(Json(send.to_json())) } else {