Browse Source

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 <noreply@anthropic.com>
pull/7861/head
ICantThinkOfAName-tech 5 days ago
parent
commit
19ce9e9252
  1. 6
      src/api/core/ciphers.rs
  2. 9
      src/api/core/mod.rs
  3. 41
      src/api/core/sends.rs

6
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<Route> {
@ -130,7 +130,7 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option<ClientVer
let mut ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &conn).await;
// Filter out SSH keys if the client version is less than 2024.12.0
let show_ssh_keys = if let Some(client_version) = client_version {
let show_ssh_keys = if let Some(client_version) = &client_version {
let ver_match = semver::VersionReq::parse(">=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<ClientVer
let sends_json: Vec<Value> = 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();

9
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<Value> {
}
#[get("/config")]
fn config() -> Json<Value> {
#[expect(clippy::needless_pass_by_value, reason = "Rocket request guards are passed by value")]
fn config(client_version: Option<ClientVersion>) -> Json<Value> {
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<Value> {
&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

41
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<Send> {
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<semver::VersionReq> =
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<Value> {
async fn get_sends(headers: Headers, client_version: Option<ClientVersion>, conn: DbConn) -> Json<Value> {
let sends = Send::find_by_user(&headers.user.uuid, &conn);
let sends_json: Vec<Value> = sends.await.iter().filter(|s| send_visible(s)).map(Send::to_json).collect();
let sends_json: Vec<Value> = 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<Value> {
}
#[get("/sends/<send_id>")]
async fn get_send(send_id: SendId, headers: Headers, conn: DbConn) -> JsonResult {
async fn get_send(
send_id: SendId,
headers: Headers,
client_version: Option<ClientVersion>,
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 {

Loading…
Cancel
Save