From b550a3a8f6a0893f45927bfc57d394933cb43b85 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Wed, 11 Feb 2026 19:29:53 -0500 Subject: [PATCH 01/44] Add create/edit/get/list API calls to SendClient --- Cargo.lock | 6 +++ crates/bitwarden-send/Cargo.toml | 18 ++++++- crates/bitwarden-send/src/error.rs | 5 ++ crates/bitwarden-send/src/lib.rs | 3 ++ crates/bitwarden-send/src/send.rs | 29 +++++++++-- crates/bitwarden-send/src/send_client.rs | 65 +++++++++++++++++++++++- 6 files changed, 120 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3051a023ec..a9f1c72939 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -883,13 +883,19 @@ dependencies = [ "bitwarden-core", "bitwarden-crypto", "bitwarden-encoding", + "bitwarden-error", + "bitwarden-state", "chrono", "serde", "serde_repr", "sha2 0.10.9", "thiserror 2.0.12", + "tokio", + "tsify", "uniffi", "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", "zeroize", ] diff --git a/crates/bitwarden-send/Cargo.toml b/crates/bitwarden-send/Cargo.toml index 95ee017ec5..7fb09681a4 100644 --- a/crates/bitwarden-send/Cargo.toml +++ b/crates/bitwarden-send/Cargo.toml @@ -20,20 +20,36 @@ uniffi = [ "bitwarden-crypto/uniffi", "dep:uniffi", ] # Uniffi bindings +wasm = [ + "bitwarden-core/wasm", + "bitwarden-encoding/wasm", + "bitwarden-state/wasm", + "dep:tsify", + "dep:wasm-bindgen", + "dep:wasm-bindgen-futures", +] # WASM support [dependencies] bitwarden-api-api = { workspace = true } -bitwarden-core = { workspace = true } +bitwarden-core = { workspace = true, features = ["internal"] } bitwarden-crypto = { workspace = true } bitwarden-encoding = { workspace = true } +bitwarden-error = { workspace = true } +bitwarden-state = { workspace = true } chrono = { workspace = true } serde = { workspace = true } serde_repr = { workspace = true } sha2 = { version = ">=0.10.6, <0.11" } thiserror = { workspace = true } +tsify = { workspace = true, optional = true } uniffi = { workspace = true, optional = true } uuid = { workspace = true } +wasm-bindgen = { workspace = true, optional = true } +wasm-bindgen-futures = { workspace = true, optional = true } zeroize = { version = ">=1.7.0, <2.0" } +[dev-dependencies] +tokio = { workspace = true, features = ["rt"] } + [lints] workspace = true diff --git a/crates/bitwarden-send/src/error.rs b/crates/bitwarden-send/src/error.rs index d224a82ad6..6d69c13720 100644 --- a/crates/bitwarden-send/src/error.rs +++ b/crates/bitwarden-send/src/error.rs @@ -10,3 +10,8 @@ pub enum SendParseError { #[error(transparent)] MissingField(#[from] bitwarden_core::MissingFieldError), } + +/// Item does not exist error. +#[derive(Debug, thiserror::Error)] +#[error("Item does not exist")] +pub struct ItemNotFoundError; diff --git a/crates/bitwarden-send/src/lib.rs b/crates/bitwarden-send/src/lib.rs index 924df122e9..b42dd09f64 100644 --- a/crates/bitwarden-send/src/lib.rs +++ b/crates/bitwarden-send/src/lib.rs @@ -5,8 +5,11 @@ uniffi::setup_scaffolding!(); #[cfg(feature = "uniffi")] mod uniffi_support; +mod create; +mod edit; mod error; pub use error::SendParseError; +mod get_list; mod send_client; pub use send_client::{ SendClient, SendClientExt, SendDecryptError, SendDecryptFileError, SendEncryptError, diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index a3d2fc2459..51288de5e7 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -18,7 +18,7 @@ use crate::SendParseError; const SEND_ITERATIONS: u32 = 100_000; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] pub struct SendFile { @@ -40,7 +40,7 @@ pub struct SendFileView { pub size_name: Option, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] pub struct SendText { @@ -86,7 +86,7 @@ pub enum AuthType { } #[allow(missing_docs)] -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] pub struct Send { @@ -118,10 +118,13 @@ pub struct Send { pub auth_type: AuthType, } +bitwarden_state::register_repository_item!(Send, "Send"); + #[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct SendView { pub id: Option, pub access_id: Option, @@ -162,6 +165,7 @@ pub struct SendView { #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct SendListView { pub id: Option, pub access_id: Option, @@ -448,6 +452,15 @@ impl From for SendType { } } +impl From for bitwarden_api_api::models::SendType { + fn from(t: SendType) -> Self { + match t { + SendType::Text => bitwarden_api_api::models::SendType::Text, + SendType::File => bitwarden_api_api::models::SendType::File, + } + } +} + impl From for AuthType { fn from(value: bitwarden_api_api::models::AuthType) -> Self { match value { @@ -458,6 +471,16 @@ impl From for AuthType { } } +impl From for bitwarden_api_api::models::AuthType { + fn from(value: AuthType) -> Self { + match value { + AuthType::Email => bitwarden_api_api::models::AuthType::Email, + AuthType::Password => bitwarden_api_api::models::AuthType::Password, + AuthType::None => bitwarden_api_api::models::AuthType::None, + } + } +} + impl TryFrom for SendFile { type Error = SendParseError; diff --git a/crates/bitwarden-send/src/send_client.rs b/crates/bitwarden-send/src/send_client.rs index 546d422eb4..1cf0f60549 100644 --- a/crates/bitwarden-send/src/send_client.rs +++ b/crates/bitwarden-send/src/send_client.rs @@ -1,12 +1,19 @@ -use std::path::Path; +use std::{path::Path, sync::Arc}; use bitwarden_core::Client; use bitwarden_crypto::{ Decryptable, EncString, IdentifyKey, OctetStreamBytes, PrimitiveEncryptable, }; +use bitwarden_state::repository::{Repository, RepositoryError}; use thiserror::Error; +use uuid::Uuid; -use crate::{Send, SendListView, SendView}; +use crate::{ + Send, SendListView, SendView, + create::{CreateSendError, SendAddEditRequest, create_send}, + edit::{EditSendError, edit_send}, + get_list::{GetSendError, get_send, list_folders}, +}; /// Generic error type for send encryption errors. #[allow(missing_docs)] @@ -49,10 +56,12 @@ pub enum SendDecryptFileError { } #[allow(missing_docs)] +#[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct SendClient { client: Client, } +#[cfg_attr(feature = "wasm", wasm_bindgen)] impl SendClient { fn new(client: Client) -> Self { Self { client } @@ -132,6 +141,58 @@ impl SendClient { let encrypted = OctetStreamBytes::from(buffer).encrypt(&mut ctx, key)?; Ok(encrypted.to_buffer()?) } + + /// Create a new [Folder] and save it to the server. + pub async fn create(&self, request: SendAddEditRequest) -> Result { + let key_store = self.client.internal.get_key_store(); + let config = self.client.internal.get_api_configurations().await; + let repository = self.get_repository()?; + + create_send(key_store, &config.api_client, repository.as_ref(), request).await + } + + /// Edit the [Folder] and save it to the server. + pub async fn edit( + &self, + send_id: Uuid, + request: SendAddEditRequest, + ) -> Result { + let key_store = self.client.internal.get_key_store(); + let config = self.client.internal.get_api_configurations().await; + let repository = self.get_repository()?; + + edit_send( + key_store, + &config.api_client, + repository.as_ref(), + send_id, + request, + ) + .await + } + + /// Get all sends from state and decrypt them to a list of [SendView]. + pub async fn list(&self) -> Result, GetSendError> { + let key_store = self.client.internal.get_key_store(); + let repository = self.get_repository()?; + + list_folders(key_store, repository.as_ref()).await + } + + /// Get a specific [Send] by its ID from state and decrypt it to a [SendView]. + pub async fn get(&self, send_id: Uuid) -> Result { + let key_store = self.client.internal.get_key_store(); + let repository = self.get_repository()?; + + get_send(key_store, repository.as_ref(), send_id).await + } +} + +impl SendClient { + /// Helper for getting the repository for sends. + fn get_repository(&self) -> Result>, RepositoryError> { + Ok(self.client.platform().state().get::()?) + } } #[allow(missing_docs)] From 2372323ec28cb455b2e54d90ebf65a87364f34df Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Wed, 11 Feb 2026 19:30:13 -0500 Subject: [PATCH 02/44] Add create/edit/get/list API calls to SendClient --- crates/bitwarden-send/src/create.rs | 142 ++++++++++++++++++++++++++ crates/bitwarden-send/src/edit.rs | 63 ++++++++++++ crates/bitwarden-send/src/get_list.rs | 42 ++++++++ 3 files changed, 247 insertions(+) create mode 100644 crates/bitwarden-send/src/create.rs create mode 100644 crates/bitwarden-send/src/edit.rs create mode 100644 crates/bitwarden-send/src/get_list.rs diff --git a/crates/bitwarden-send/src/create.rs b/crates/bitwarden-send/src/create.rs new file mode 100644 index 0000000000..8ec6d9309f --- /dev/null +++ b/crates/bitwarden-send/src/create.rs @@ -0,0 +1,142 @@ +use bitwarden_core::{ + ApiError, MissingFieldError, + key_management::{KeyIds, SymmetricKeyId}, + require, +}; +use bitwarden_crypto::{ + CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, PrimitiveEncryptable, +}; +use bitwarden_error::bitwarden_error; +use bitwarden_state::repository::{Repository, RepositoryError}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +#[cfg(feature = "wasm")] +use tsify::Tsify; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; + +use crate::{ + AuthType, Send, SendParseError, SendType, SendView, + send::{SendFile, SendText}, +}; + +#[allow(missing_docs)] +#[bitwarden_error(flat)] +#[derive(Debug, Error)] +pub enum CreateSendError { + #[error(transparent)] + Api(#[from] ApiError), + #[error(transparent)] + Crypto(#[from] CryptoError), + #[error(transparent)] + MissingField(#[from] MissingFieldError), + #[error(transparent)] + Repository(#[from] RepositoryError), + #[error(transparent)] + SendParse(#[from] SendParseError), +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub struct SendAddEditRequest { + pub name: String, + pub notes: String, + pub key: String, + pub password: Option, + + pub r#type: SendType, + pub file: Option, + pub text: Option, + + pub max_access_count: Option, + pub disabled: bool, + pub hide_email: bool, + + pub deletion_date: DateTime, + pub expiration_date: Option>, + + /// Email addresses for OTP authentication. + /// **Note**: Mutually exclusive with `new_password`. If both are set, + /// only password authentication will be used. + pub emails: Option, + pub auth_type: AuthType, +} + +impl CompositeEncryptable + for SendAddEditRequest +{ + fn encrypt_composite( + &self, + ctx: &mut KeyStoreContext, + key: SymmetricKeyId, + ) -> Result { + Ok(bitwarden_api_api::models::SendRequestModel { + r#type: Some(self.r#type.into()), + auth_type: Some(self.auth_type.into()), + file_length: None, + name: Some(self.name.encrypt(ctx, key)?.to_string()), + notes: Some(self.notes.encrypt(ctx, key)?.to_string()), + key: self.key.clone(), + max_access_count: self.max_access_count.map(|c| c as i32), + expiration_date: self.expiration_date.map(|d| d.to_rfc3339()), + deletion_date: self.deletion_date.to_rfc3339(), + file: self.file.as_ref().map(|f| { + Box::new(bitwarden_api_api::models::SendFileModel { + id: f.id.clone(), + file_name: Some(f.file_name.to_string()), + size: f.size.as_ref().and_then(|s| s.parse::().ok()), + size_name: f.size_name.clone(), + }) + }), + text: self.text.as_ref().map(|t| { + Box::new(bitwarden_api_api::models::SendTextModel { + text: t.text.as_ref().map(|txt| txt.to_string()), + hidden: Some(t.hidden), + }) + }), + password: self.password.clone(), + emails: self.emails.clone(), + disabled: self.disabled, + hide_email: Some(self.hide_email), + }) + } +} + +impl IdentifyKey for SendAddEditRequest { + fn key_identifier(&self) -> SymmetricKeyId { + SymmetricKeyId::User + } +} + +pub(super) async fn create_send + ?Sized>( + key_store: &KeyStore, + api_client: &bitwarden_api_api::apis::ApiClient, + repository: &R, + request: SendAddEditRequest, +) -> Result { + let send_request = key_store.encrypt(request)?; + + let resp = api_client + .sends_api() + .post(Some(send_request)) + .await + .map_err(ApiError::from)?; + + let send: Send = resp.try_into()?; + + repository + .set(require!(send.id).to_string(), send.clone()) + .await?; + + Ok(key_store.decrypt(&send)?) +} +#[cfg(test)] +mod tests { + #[tokio::test] + async fn test_create_send() {} + + #[tokio::test] + async fn test_create_send_http_error() {} +} diff --git a/crates/bitwarden-send/src/edit.rs b/crates/bitwarden-send/src/edit.rs new file mode 100644 index 0000000000..20dab23aee --- /dev/null +++ b/crates/bitwarden-send/src/edit.rs @@ -0,0 +1,63 @@ +use bitwarden_core::{ApiError, MissingFieldError, key_management::KeyIds}; +use bitwarden_crypto::{CryptoError, KeyStore}; +use bitwarden_error::bitwarden_error; +use bitwarden_state::repository::{Repository, RepositoryError}; +use thiserror::Error; +use uuid::Uuid; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; + +use crate::{ + Send, SendView, + create::SendAddEditRequest, + error::{ItemNotFoundError, SendParseError}, +}; + +#[allow(missing_docs)] +#[bitwarden_error(flat)] +#[derive(Debug, Error)] +pub enum EditSendError { + #[error(transparent)] + ItemNotFound(#[from] ItemNotFoundError), + #[error(transparent)] + Crypto(#[from] CryptoError), + #[error(transparent)] + Api(#[from] ApiError), + #[error(transparent)] + MissingField(#[from] MissingFieldError), + #[error(transparent)] + Repository(#[from] RepositoryError), + #[error(transparent)] + Uuid(#[from] uuid::Error), + #[error(transparent)] + SendParse(#[from] SendParseError), +} + +pub(super) async fn edit_send + ?Sized>( + key_store: &KeyStore, + api_client: &bitwarden_api_api::apis::ApiClient, + repository: &R, + send_id: Uuid, + request: SendAddEditRequest, +) -> Result { + let id = send_id.to_string(); + + // Verify the folder we're updating exists + repository.get(id.clone()).await?.ok_or(ItemNotFoundError)?; + + let send_request = key_store.encrypt(request)?; + + let resp = api_client + .sends_api() + .put(&id, Some(send_request)) + .await + .map_err(ApiError::from)?; + + let send: Send = resp.try_into()?; + + debug_assert!(send.id.unwrap_or_default() == send_id); + + repository.set(id, send.clone()).await?; + + Ok(key_store.decrypt(&send)?) +} diff --git a/crates/bitwarden-send/src/get_list.rs b/crates/bitwarden-send/src/get_list.rs new file mode 100644 index 0000000000..ed391c95b9 --- /dev/null +++ b/crates/bitwarden-send/src/get_list.rs @@ -0,0 +1,42 @@ +use bitwarden_core::key_management::KeyIds; +use bitwarden_crypto::{CryptoError, KeyStore}; +use bitwarden_error::bitwarden_error; +use bitwarden_state::repository::{Repository, RepositoryError}; +use thiserror::Error; +use uuid::Uuid; + +use crate::{Send, SendView, error::ItemNotFoundError}; + +#[allow(missing_docs)] +#[bitwarden_error(flat)] +#[derive(Debug, Error)] +pub enum GetSendError { + #[error(transparent)] + ItemNotFound(#[from] ItemNotFoundError), + #[error(transparent)] + Crypto(#[from] CryptoError), + #[error(transparent)] + Repository(#[from] RepositoryError), +} + +pub(super) async fn get_send( + store: &KeyStore, + repository: &dyn Repository, + id: Uuid, +) -> Result { + let send = repository + .get(id.to_string()) + .await? + .ok_or(ItemNotFoundError)?; + + Ok(store.decrypt(&send)?) +} + +pub(super) async fn list_folders( + store: &KeyStore, + repository: &dyn Repository, +) -> Result, GetSendError> { + let sends = repository.list().await?; + let views = store.decrypt_list(&sends)?; + Ok(views) +} From fc40d32d75d42750b89f02868d4f9918c47250b3 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Wed, 11 Feb 2026 19:44:07 -0500 Subject: [PATCH 03/44] Add wasm client --- crates/bitwarden-wasm-internal/src/client.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/bitwarden-wasm-internal/src/client.rs b/crates/bitwarden-wasm-internal/src/client.rs index 0edffa9bee..9be2575dd7 100644 --- a/crates/bitwarden-wasm-internal/src/client.rs +++ b/crates/bitwarden-wasm-internal/src/client.rs @@ -101,6 +101,11 @@ impl PasswordManagerClient { pub fn exporters(&self) -> ExporterClient { self.0.exporters() } + + /// Exporter related operations. + pub fn sends(&self) -> ExporterClient { + self.0.sends() + } } #[bitwarden_error(basic)] From 929a058d3548f2554f99dbe10e2a114940e64d43 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Wed, 11 Feb 2026 22:19:58 -0500 Subject: [PATCH 04/44] move over tests --- Cargo.lock | 1 + crates/bitwarden-send/Cargo.toml | 2 + crates/bitwarden-send/src/create.rs | 225 ++++++++++++++++--- crates/bitwarden-send/src/edit.rs | 264 +++++++++++++++++++++++ crates/bitwarden-send/src/get_list.rs | 193 ++++++++++++++++- crates/bitwarden-send/src/lib.rs | 2 +- crates/bitwarden-send/src/send.rs | 2 +- crates/bitwarden-send/src/send_client.rs | 4 +- 8 files changed, 664 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a9f1c72939..49fd026589 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -885,6 +885,7 @@ dependencies = [ "bitwarden-encoding", "bitwarden-error", "bitwarden-state", + "bitwarden-test", "chrono", "serde", "serde_repr", diff --git a/crates/bitwarden-send/Cargo.toml b/crates/bitwarden-send/Cargo.toml index 7fb09681a4..d17202171c 100644 --- a/crates/bitwarden-send/Cargo.toml +++ b/crates/bitwarden-send/Cargo.toml @@ -49,6 +49,8 @@ wasm-bindgen-futures = { workspace = true, optional = true } zeroize = { version = ">=1.7.0, <2.0" } [dev-dependencies] +bitwarden-api-api = { workspace = true, features = ["mockall"] } +bitwarden-test = { workspace = true } tokio = { workspace = true, features = ["rt"] } [lints] diff --git a/crates/bitwarden-send/src/create.rs b/crates/bitwarden-send/src/create.rs index 8ec6d9309f..0e4865673c 100644 --- a/crates/bitwarden-send/src/create.rs +++ b/crates/bitwarden-send/src/create.rs @@ -4,8 +4,10 @@ use bitwarden_core::{ require, }; use bitwarden_crypto::{ - CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, PrimitiveEncryptable, + CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, OctetStreamBytes, + PrimitiveEncryptable, generate_random_bytes, }; +use bitwarden_encoding::B64Url; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::{Repository, RepositoryError}; use chrono::{DateTime, Utc}; @@ -17,8 +19,7 @@ use tsify::Tsify; use wasm_bindgen::prelude::*; use crate::{ - AuthType, Send, SendParseError, SendType, SendView, - send::{SendFile, SendText}, + AuthType, Send, SendFileView, SendParseError, SendTextView, SendType, SendView, }; #[allow(missing_docs)] @@ -42,13 +43,13 @@ pub enum CreateSendError { #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct SendAddEditRequest { pub name: String, - pub notes: String, - pub key: String, + pub notes: Option, + pub key: Option, pub password: Option, pub r#type: SendType, - pub file: Option, - pub text: Option, + pub file: Option, + pub text: Option, pub max_access_count: Option, pub disabled: bool, @@ -60,7 +61,7 @@ pub struct SendAddEditRequest { /// Email addresses for OTP authentication. /// **Note**: Mutually exclusive with `new_password`. If both are set, /// only password authentication will be used. - pub emails: Option, + pub emails: Vec, pub auth_type: AuthType, } @@ -72,32 +73,54 @@ impl CompositeEncryptable, key: SymmetricKeyId, ) -> Result { + // Generate or decode the send key + let k = match &self.key { + // Existing send, decode key + Some(k) => B64Url::try_from(k.as_str()) + .map_err(|_| CryptoError::InvalidKey)? + .as_bytes() + .to_vec(), + // New send, generate random key + None => { + let key = generate_random_bytes::<[u8; 16]>(); + key.to_vec() + } + }; + + // Derive the shareable send key for encrypting content + let send_key = Send::derive_shareable_key(ctx, &k)?; + Ok(bitwarden_api_api::models::SendRequestModel { r#type: Some(self.r#type.into()), auth_type: Some(self.auth_type.into()), file_length: None, - name: Some(self.name.encrypt(ctx, key)?.to_string()), - notes: Some(self.notes.encrypt(ctx, key)?.to_string()), - key: self.key.clone(), + name: Some(self.name.encrypt(ctx, send_key)?.to_string()), + notes: self.notes.as_ref().map(|n| n.encrypt(ctx, send_key)).transpose()?.map(|e| e.to_string()), + // Encrypt the send key itself with the user key + key: OctetStreamBytes::from(k).encrypt(ctx, key)?.to_string(), max_access_count: self.max_access_count.map(|c| c as i32), expiration_date: self.expiration_date.map(|d| d.to_rfc3339()), deletion_date: self.deletion_date.to_rfc3339(), - file: self.file.as_ref().map(|f| { - Box::new(bitwarden_api_api::models::SendFileModel { + file: self.file.as_ref().map(|f| -> Result<_, CryptoError> { + Ok(Box::new(bitwarden_api_api::models::SendFileModel { id: f.id.clone(), - file_name: Some(f.file_name.to_string()), + file_name: Some(f.file_name.encrypt(ctx, send_key)?.to_string()), size: f.size.as_ref().and_then(|s| s.parse::().ok()), size_name: f.size_name.clone(), - }) - }), - text: self.text.as_ref().map(|t| { - Box::new(bitwarden_api_api::models::SendTextModel { - text: t.text.as_ref().map(|txt| txt.to_string()), + })) + }).transpose()?, + text: self.text.as_ref().map(|t| -> Result<_, CryptoError> { + Ok(Box::new(bitwarden_api_api::models::SendTextModel { + text: t.text.as_ref().map(|txt| txt.encrypt(ctx, send_key)).transpose()?.map(|e| e.to_string()), hidden: Some(t.hidden), - }) - }), + })) + }).transpose()?, password: self.password.clone(), - emails: self.emails.clone(), + emails: if self.emails.is_empty() { + None + } else { + Some(self.emails.join(",")) + }, disabled: self.disabled, hide_email: Some(self.hide_email), }) @@ -134,9 +157,163 @@ pub(super) async fn create_send + ?Sized>( } #[cfg(test)] mod tests { + use bitwarden_api_api::{apis::ApiClient, models::SendResponseModel}; + use bitwarden_crypto::SymmetricKeyAlgorithm; + use bitwarden_test::MemoryRepository; + use uuid::uuid; + + use super::*; + #[tokio::test] - async fn test_create_send() {} + async fn test_create_send() { + let store: KeyStore = KeyStore::default(); + { + let mut ctx = store.context_mut(); + let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac); + ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User) + .unwrap(); + } + + let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"); + + let api_client = ApiClient::new_mocked(move |mock| { + mock.sends_api + .expect_post() + .returning(move |model| { + let model = model.unwrap(); + Ok(SendResponseModel { + id: Some(send_id), + name: model.name, + revision_date: Some("2025-01-01T00:00:00Z".to_string()), + object: Some("send".to_string()), + access_id: None, + r#type: model.r#type, + auth_type: model.auth_type, + notes: model.notes, + file: model.file, + text: model.text, + key: Some(model.key), + max_access_count: model.max_access_count, + access_count: Some(0), + password: model.password, + emails: model.emails, + disabled: Some(model.disabled), + expiration_date: model.expiration_date, + deletion_date: Some(model.deletion_date), + hide_email: model.hide_email, + }) + }) + .once(); + }); + + let repository = MemoryRepository::::default(); + + let result = create_send( + &store, + &api_client, + &repository, + SendAddEditRequest { + name: "test".to_string(), + notes: Some("notes".to_string()), + key: None, + password: None, + r#type: SendType::Text, + file: None, + text: Some(SendTextView { + text: Some("test".to_string()), + hidden: false, + }), + max_access_count: None, + disabled: false, + hide_email: false, + deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(), + expiration_date: None, + emails: Vec::new(), + auth_type: AuthType::None, + }, + ) + .await + .unwrap(); + + // Verify the result (excluding the generated key which is random) + assert_eq!(result.id, Some(send_id)); + assert_eq!(result.name, "test"); + assert_eq!(result.notes, Some("notes".to_string())); + assert!(result.key.is_some(), "Expected a generated key"); + assert_eq!(result.new_password, None); + assert_eq!(result.has_password, false); + assert_eq!(result.r#type, SendType::Text); + assert_eq!(result.file, None); + assert_eq!(result.text, Some(SendTextView { + text: Some("test".to_string()), + hidden: false, + })); + assert_eq!(result.max_access_count, None); + assert_eq!(result.access_count, 0); + assert_eq!(result.disabled, false); + assert_eq!(result.hide_email, false); + assert_eq!(result.deletion_date, "2025-01-10T00:00:00Z".parse::>().unwrap()); + assert_eq!(result.expiration_date, None); + assert_eq!(result.emails, Vec::::new()); + assert_eq!(result.auth_type, AuthType::None); + assert_eq!(result.revision_date, "2025-01-01T00:00:00Z".parse::>().unwrap()); + + // Confirm the send was stored in the repository + assert_eq!( + store + .decrypt::(&repository.get(send_id.to_string()).await.unwrap().unwrap()) + .unwrap(), + result + ); + } #[tokio::test] - async fn test_create_send_http_error() {} + async fn test_create_send_http_error() { + let store: KeyStore = KeyStore::default(); + { + let mut ctx = store.context_mut(); + let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac); + ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User) + .unwrap(); + } + + let api_client = ApiClient::new_mocked(move |mock| { + mock.sends_api.expect_post().returning(move |_model| { + Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other( + "Simulated error", + ))) + }); + }); + + let repository = MemoryRepository::::default(); + + let result = create_send( + &store, + &api_client, + &repository, + SendAddEditRequest { + name: "test".to_string(), + notes: Some("notes".to_string()), + key: None, + password: None, + r#type: SendType::Text, + file: None, + text: Some(SendTextView { + text: Some("test".to_string()), + hidden: false, + }), + max_access_count: None, + disabled: false, + hide_email: false, + deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(), + expiration_date: None, + emails: Vec::new(), + auth_type: AuthType::None, + }, + ) + .await; + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), CreateSendError::Api(_))); + } } diff --git a/crates/bitwarden-send/src/edit.rs b/crates/bitwarden-send/src/edit.rs index 20dab23aee..b340abb30e 100644 --- a/crates/bitwarden-send/src/edit.rs +++ b/crates/bitwarden-send/src/edit.rs @@ -61,3 +61,267 @@ pub(super) async fn edit_send + ?Sized>( Ok(key_store.decrypt(&send)?) } + +#[cfg(test)] +mod tests { + use bitwarden_api_api::{apis::ApiClient, models::SendResponseModel}; + use bitwarden_core::key_management::SymmetricKeyId; + use bitwarden_crypto::SymmetricKeyAlgorithm; + use bitwarden_test::MemoryRepository; + use chrono::{DateTime, Utc}; + use uuid::uuid; + + use super::*; + use crate::{AuthType, SendTextView, SendType}; + + #[tokio::test] + async fn test_edit_send() { + let store: KeyStore = KeyStore::default(); + { + let mut ctx = store.context_mut(); + let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac); + ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User) + .unwrap(); + } + + let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"); + + // Pre-populate the repository with an existing send by encrypting a SendView + let repository = MemoryRepository::::default(); + let existing_send_view = SendView { + id: None, // No ID initially to allow key generation + access_id: None, + name: "original".to_string(), + notes: Some("original notes".to_string()), + key: None, // Will be generated + new_password: None, + has_password: false, + r#type: SendType::Text, + file: None, + text: Some(SendTextView { + text: Some("original text".to_string()), + hidden: false, + }), + max_access_count: None, + access_count: 0, + disabled: false, + hide_email: false, + revision_date: "2025-01-01T00:00:00Z".parse().unwrap(), + deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(), + expiration_date: None, + emails: Vec::new(), + auth_type: AuthType::None, + }; + let mut existing_send = store.encrypt(existing_send_view).unwrap(); + existing_send.id = Some(send_id); // Set the ID after encryption + repository + .set(send_id.to_string(), existing_send) + .await + .unwrap(); + + let api_client = ApiClient::new_mocked(move |mock| { + mock.sends_api + .expect_put() + .returning(move |_id, model| { + let model = model.unwrap(); + Ok(SendResponseModel { + id: Some(send_id), + name: model.name, + revision_date: Some("2025-01-02T00:00:00Z".to_string()), + object: Some("send".to_string()), + access_id: None, + r#type: model.r#type, + auth_type: model.auth_type, + notes: model.notes, + file: model.file, + text: model.text, + key: Some(model.key), + max_access_count: model.max_access_count, + access_count: Some(0), + password: model.password, + emails: model.emails, + disabled: Some(model.disabled), + expiration_date: model.expiration_date, + deletion_date: Some(model.deletion_date), + hide_email: model.hide_email, + }) + }) + .once(); + }); + + let result = edit_send( + &store, + &api_client, + &repository, + send_id, + SendAddEditRequest { + name: "updated".to_string(), + notes: Some("updated notes".to_string()), + key: None, + password: None, + r#type: SendType::Text, + file: None, + text: Some(SendTextView { + text: Some("updated text".to_string()), + hidden: false, + }), + max_access_count: None, + disabled: false, + hide_email: false, + deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(), + expiration_date: None, + emails: Vec::new(), + auth_type: AuthType::None, + }, + ) + .await + .unwrap(); + + // Verify the result + assert_eq!(result.id, Some(send_id)); + assert_eq!(result.name, "updated"); + assert_eq!(result.notes, Some("updated notes".to_string())); + assert!(result.key.is_some(), "Expected a key"); + assert_eq!(result.revision_date, "2025-01-02T00:00:00Z".parse::>().unwrap()); + + // Confirm the send was updated in the repository + let stored = repository.get(send_id.to_string()).await.unwrap().unwrap(); + assert_eq!( + store + .decrypt::(&stored) + .unwrap() + .name, + "updated" + ); + } + + #[tokio::test] + async fn test_edit_send_not_found() { + let store: KeyStore = KeyStore::default(); + { + let mut ctx = store.context_mut(); + let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac); + ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User) + .unwrap(); + } + + let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"); + let repository = MemoryRepository::::default(); + let api_client = ApiClient::new_mocked(move |_mock| {}); + + let result = edit_send( + &store, + &api_client, + &repository, + send_id, + SendAddEditRequest { + name: "test".to_string(), + notes: None, + key: None, + password: None, + r#type: SendType::Text, + file: None, + text: Some(SendTextView { + text: Some("test".to_string()), + hidden: false, + }), + max_access_count: None, + disabled: false, + hide_email: false, + deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(), + expiration_date: None, + emails: Vec::new(), + auth_type: AuthType::None, + }, + ) + .await; + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), EditSendError::ItemNotFound(_))); + } + + #[tokio::test] + async fn test_edit_send_http_error() { + let store: KeyStore = KeyStore::default(); + { + let mut ctx = store.context_mut(); + let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac); + ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User) + .unwrap(); + } + + let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"); + + // Pre-populate the repository with an existing send by encrypting a SendView + let repository = MemoryRepository::::default(); + let existing_send_view = SendView { + id: None, // No ID initially to allow key generation + access_id: None, + name: "original".to_string(), + notes: Some("original notes".to_string()), + key: None, // Will be generated + new_password: None, + has_password: false, + r#type: SendType::Text, + file: None, + text: Some(SendTextView { + text: Some("original text".to_string()), + hidden: false, + }), + max_access_count: None, + access_count: 0, + disabled: false, + hide_email: false, + revision_date: "2025-01-01T00:00:00Z".parse().unwrap(), + deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(), + expiration_date: None, + emails: Vec::new(), + auth_type: AuthType::None, + }; + let mut existing_send = store.encrypt(existing_send_view).unwrap(); + existing_send.id = Some(send_id); // Set the ID after encryption + repository + .set(send_id.to_string(), existing_send) + .await + .unwrap(); + + let api_client = ApiClient::new_mocked(move |mock| { + mock.sends_api.expect_put().returning(move |_id, _model| { + Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other( + "Simulated error", + ))) + }); + }); + + let result = edit_send( + &store, + &api_client, + &repository, + send_id, + SendAddEditRequest { + name: "test".to_string(), + notes: None, + key: None, + password: None, + r#type: SendType::Text, + file: None, + text: Some(SendTextView { + text: Some("test".to_string()), + hidden: false, + }), + max_access_count: None, + disabled: false, + hide_email: false, + deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(), + expiration_date: None, + emails: Vec::new(), + auth_type: AuthType::None, + }, + ) + .await; + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), EditSendError::Api(_))); + } +} + diff --git a/crates/bitwarden-send/src/get_list.rs b/crates/bitwarden-send/src/get_list.rs index ed391c95b9..dc6377966a 100644 --- a/crates/bitwarden-send/src/get_list.rs +++ b/crates/bitwarden-send/src/get_list.rs @@ -32,7 +32,7 @@ pub(super) async fn get_send( Ok(store.decrypt(&send)?) } -pub(super) async fn list_folders( +pub(super) async fn list_sends( store: &KeyStore, repository: &dyn Repository, ) -> Result, GetSendError> { @@ -40,3 +40,194 @@ pub(super) async fn list_folders( let views = store.decrypt_list(&sends)?; Ok(views) } + +#[cfg(test)] +mod tests { + use bitwarden_core::key_management::SymmetricKeyId; + use bitwarden_crypto::SymmetricKeyAlgorithm; + use bitwarden_test::MemoryRepository; + use uuid::uuid; + + use super::*; + use crate::{AuthType, SendTextView, SendType, SendView}; + + #[tokio::test] + async fn test_get_send() { + let store: KeyStore = KeyStore::default(); + { + let mut ctx = store.context_mut(); + let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac); + ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User) + .unwrap(); + } + + let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"); + + // Create and store a send + let repository = MemoryRepository::::default(); + let send_view = SendView { + id: None, + access_id: None, + name: "Test Send".to_string(), + notes: Some("Test notes".to_string()), + key: None, + new_password: None, + has_password: false, + r#type: SendType::Text, + file: None, + text: Some(SendTextView { + text: Some("Secret text".to_string()), + hidden: false, + }), + max_access_count: None, + access_count: 0, + disabled: false, + hide_email: false, + revision_date: "2025-01-01T00:00:00Z".parse().unwrap(), + deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(), + expiration_date: None, + emails: Vec::new(), + auth_type: AuthType::None, + }; + let mut send = store.encrypt(send_view).unwrap(); + send.id = Some(send_id); + repository.set(send_id.to_string(), send).await.unwrap(); + + // Test getting the send + let result = get_send(&store, &repository, send_id).await.unwrap(); + + assert_eq!(result.id, Some(send_id)); + assert_eq!(result.name, "Test Send"); + assert_eq!(result.notes, Some("Test notes".to_string())); + assert_eq!( + result.text, + Some(SendTextView { + text: Some("Secret text".to_string()), + hidden: false, + }) + ); + } + + #[tokio::test] + async fn test_get_send_not_found() { + let store: KeyStore = KeyStore::default(); + { + let mut ctx = store.context_mut(); + let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac); + ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User) + .unwrap(); + } + + let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"); + let repository = MemoryRepository::::default(); + + // Try to get a send that doesn't exist + let result = get_send(&store, &repository, send_id).await; + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), GetSendError::ItemNotFound(_))); + } + + #[tokio::test] + async fn test_list_sends() { + let store: KeyStore = KeyStore::default(); + { + let mut ctx = store.context_mut(); + let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac); + ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User) + .unwrap(); + } + + let repository = MemoryRepository::::default(); + + // Create and store multiple sends + let send_id_1 = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"); + let send_view_1 = SendView { + id: None, + access_id: None, + name: "Send 1".to_string(), + notes: None, + key: None, + new_password: None, + has_password: false, + r#type: SendType::Text, + file: None, + text: Some(SendTextView { + text: Some("Text 1".to_string()), + hidden: false, + }), + max_access_count: None, + access_count: 0, + disabled: false, + hide_email: false, + revision_date: "2025-01-01T00:00:00Z".parse().unwrap(), + deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(), + expiration_date: None, + emails: Vec::new(), + auth_type: AuthType::None, + }; + let mut send_1 = store.encrypt(send_view_1).unwrap(); + send_1.id = Some(send_id_1); + repository.set(send_id_1.to_string(), send_1).await.unwrap(); + + let send_id_2 = uuid!("36afb22c-9c95-4db5-8bac-c21cb204a3f2"); + let send_view_2 = SendView { + id: None, + access_id: None, + name: "Send 2".to_string(), + notes: None, + key: None, + new_password: None, + has_password: false, + r#type: SendType::Text, + file: None, + text: Some(SendTextView { + text: Some("Text 2".to_string()), + hidden: false, + }), + max_access_count: None, + access_count: 0, + disabled: false, + hide_email: false, + revision_date: "2025-01-02T00:00:00Z".parse().unwrap(), + deletion_date: "2025-01-11T00:00:00Z".parse().unwrap(), + expiration_date: None, + emails: Vec::new(), + auth_type: AuthType::None, + }; + let mut send_2 = store.encrypt(send_view_2).unwrap(); + send_2.id = Some(send_id_2); + repository.set(send_id_2.to_string(), send_2).await.unwrap(); + + // Test listing all sends + let result = list_sends(&store, &repository).await.unwrap(); + + assert_eq!(result.len(), 2); + + // Find sends by name (order may vary) + let send1 = result.iter().find(|s| s.name == "Send 1").unwrap(); + let send2 = result.iter().find(|s| s.name == "Send 2").unwrap(); + + assert_eq!(send1.id, Some(send_id_1)); + assert_eq!(send2.id, Some(send_id_2)); + } + + #[tokio::test] + async fn test_list_sends_empty() { + let store: KeyStore = KeyStore::default(); + { + let mut ctx = store.context_mut(); + let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac); + ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User) + .unwrap(); + } + + let repository = MemoryRepository::::default(); + + // Test listing when repository is empty + let result = list_sends(&store, &repository).await.unwrap(); + + assert_eq!(result.len(), 0); + } +} + diff --git a/crates/bitwarden-send/src/lib.rs b/crates/bitwarden-send/src/lib.rs index b42dd09f64..96be92e499 100644 --- a/crates/bitwarden-send/src/lib.rs +++ b/crates/bitwarden-send/src/lib.rs @@ -16,4 +16,4 @@ pub use send_client::{ SendEncryptFileError, }; mod send; -pub use send::{AuthType, Send, SendListView, SendTextView, SendType, SendView}; +pub use send::{AuthType, Send, SendListView, SendTextView, SendFileView, SendType, SendView}; diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index 51288de5e7..743629ded3 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -193,7 +193,7 @@ impl Send { Self::derive_shareable_key(ctx, &key) } - fn derive_shareable_key( + pub(crate) fn derive_shareable_key( ctx: &mut KeyStoreContext, key: &[u8], ) -> Result { diff --git a/crates/bitwarden-send/src/send_client.rs b/crates/bitwarden-send/src/send_client.rs index 1cf0f60549..e78c092485 100644 --- a/crates/bitwarden-send/src/send_client.rs +++ b/crates/bitwarden-send/src/send_client.rs @@ -12,7 +12,7 @@ use crate::{ Send, SendListView, SendView, create::{CreateSendError, SendAddEditRequest, create_send}, edit::{EditSendError, edit_send}, - get_list::{GetSendError, get_send, list_folders}, + get_list::{GetSendError, get_send, list_sends}, }; /// Generic error type for send encryption errors. @@ -176,7 +176,7 @@ impl SendClient { let key_store = self.client.internal.get_key_store(); let repository = self.get_repository()?; - list_folders(key_store, repository.as_ref()).await + list_sends(key_store, repository.as_ref()).await } /// Get a specific [Send] by its ID from state and decrypt it to a [SendView]. From a68dab8cbfff0e5f6a010910ce29fdc7c98f09ab Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Fri, 13 Feb 2026 09:10:15 -0500 Subject: [PATCH 05/44] Update crates/bitwarden-send/src/edit.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Daniel GarcĂ­a --- crates/bitwarden-send/src/edit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-send/src/edit.rs b/crates/bitwarden-send/src/edit.rs index b340abb30e..6efc7ba734 100644 --- a/crates/bitwarden-send/src/edit.rs +++ b/crates/bitwarden-send/src/edit.rs @@ -42,7 +42,7 @@ pub(super) async fn edit_send + ?Sized>( ) -> Result { let id = send_id.to_string(); - // Verify the folder we're updating exists + // Verify the send we're updating exists repository.get(id.clone()).await?.ok_or(ItemNotFoundError)?; let send_request = key_store.encrypt(request)?; From a6e308aa75afefce9d5904c8016f55e4c54447ca Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Fri, 13 Feb 2026 09:10:23 -0500 Subject: [PATCH 06/44] Update crates/bitwarden-send/src/send_client.rs Co-authored-by: John Harrington <84741727+harr1424@users.noreply.github.com> --- crates/bitwarden-send/src/send_client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-send/src/send_client.rs b/crates/bitwarden-send/src/send_client.rs index e78c092485..849315b13f 100644 --- a/crates/bitwarden-send/src/send_client.rs +++ b/crates/bitwarden-send/src/send_client.rs @@ -142,7 +142,7 @@ impl SendClient { Ok(encrypted.to_buffer()?) } - /// Create a new [Folder] and save it to the server. + /// Create a new [Send] and save it to the server. pub async fn create(&self, request: SendAddEditRequest) -> Result { let key_store = self.client.internal.get_key_store(); let config = self.client.internal.get_api_configurations().await; From 97d59ce9a03b225d4d9ade3c3ecdd6d08ac370e9 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Tue, 17 Feb 2026 12:54:13 -0500 Subject: [PATCH 07/44] fix client --- crates/bitwarden-wasm-internal/src/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-wasm-internal/src/client.rs b/crates/bitwarden-wasm-internal/src/client.rs index 9be2575dd7..a9d7b04cc2 100644 --- a/crates/bitwarden-wasm-internal/src/client.rs +++ b/crates/bitwarden-wasm-internal/src/client.rs @@ -102,8 +102,8 @@ impl PasswordManagerClient { self.0.exporters() } - /// Exporter related operations. - pub fn sends(&self) -> ExporterClient { + /// Send related operations. + pub fn sends(&self) -> SendClient { self.0.sends() } } From d60793d0428fc007a62fa443e2964cfd8fff3eba Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Tue, 17 Feb 2026 12:55:05 -0500 Subject: [PATCH 08/44] Update crates/bitwarden-send/src/send_client.rs Co-authored-by: John Harrington <84741727+harr1424@users.noreply.github.com> --- crates/bitwarden-send/src/send_client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-send/src/send_client.rs b/crates/bitwarden-send/src/send_client.rs index 849315b13f..8242506885 100644 --- a/crates/bitwarden-send/src/send_client.rs +++ b/crates/bitwarden-send/src/send_client.rs @@ -151,7 +151,7 @@ impl SendClient { create_send(key_store, &config.api_client, repository.as_ref(), request).await } - /// Edit the [Folder] and save it to the server. + /// Edit the [Send] and save it to the server. pub async fn edit( &self, send_id: Uuid, From 103bc961d1565f636b87646d73ad8617225a9f17 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Thu, 26 Feb 2026 08:29:58 -0500 Subject: [PATCH 09/44] merge fix --- Cargo.lock | 6 +----- crates/bitwarden-send/src/send.rs | 3 --- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3e7f191a8f..326925db07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -876,13 +876,9 @@ dependencies = [ "serde", "serde_repr", "sha2 0.10.9", -<<<<<<< tools/pm-31067/move-sends-api-calls-to-sdk-2 - "thiserror 2.0.12", + "thiserror 2.0.18", "tokio", "tsify", -======= - "thiserror 2.0.18", ->>>>>>> main "uniffi", "uuid", "wasm-bindgen", diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index 159e7c0f80..8fd4b7327f 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -466,9 +466,6 @@ impl From for bitwarden_api_api::models::SendType { } } -impl From for AuthType { - fn from(value: bitwarden_api_api::models::AuthType) -> Self { - match value { impl TryFrom for AuthType { type Error = bitwarden_core::MissingFieldError; From e2e7ff1b7b2755dfc70b4a6bea63d8226d16ba76 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Thu, 26 Feb 2026 08:54:17 -0500 Subject: [PATCH 10/44] pr fix --- crates/bitwarden-send/src/create.rs | 4 ++-- crates/bitwarden-send/src/edit.rs | 6 +++--- crates/bitwarden-send/src/get_list.rs | 6 +++--- crates/bitwarden-send/src/send.rs | 4 +++- crates/bitwarden-send/src/send_client.rs | 2 ++ 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/crates/bitwarden-send/src/create.rs b/crates/bitwarden-send/src/create.rs index 0e4865673c..afdb7d0a71 100644 --- a/crates/bitwarden-send/src/create.rs +++ b/crates/bitwarden-send/src/create.rs @@ -150,7 +150,7 @@ pub(super) async fn create_send + ?Sized>( let send: Send = resp.try_into()?; repository - .set(require!(send.id).to_string(), send.clone()) + .set(require!(send.id), send.clone()) .await?; Ok(key_store.decrypt(&send)?) @@ -261,7 +261,7 @@ mod tests { // Confirm the send was stored in the repository assert_eq!( store - .decrypt::(&repository.get(send_id.to_string()).await.unwrap().unwrap()) + .decrypt::(&repository.get(send_id).await.unwrap().unwrap()) .unwrap(), result ); diff --git a/crates/bitwarden-send/src/edit.rs b/crates/bitwarden-send/src/edit.rs index 6efc7ba734..5148e9bb7e 100644 --- a/crates/bitwarden-send/src/edit.rs +++ b/crates/bitwarden-send/src/edit.rs @@ -43,7 +43,7 @@ pub(super) async fn edit_send + ?Sized>( let id = send_id.to_string(); // Verify the send we're updating exists - repository.get(id.clone()).await?.ok_or(ItemNotFoundError)?; + repository.get(send_id.clone()).await?.ok_or(ItemNotFoundError)?; let send_request = key_store.encrypt(request)?; @@ -57,7 +57,7 @@ pub(super) async fn edit_send + ?Sized>( debug_assert!(send.id.unwrap_or_default() == send_id); - repository.set(id, send.clone()).await?; + repository.set(send_id, send.clone()).await?; Ok(key_store.decrypt(&send)?) } @@ -115,7 +115,7 @@ mod tests { let mut existing_send = store.encrypt(existing_send_view).unwrap(); existing_send.id = Some(send_id); // Set the ID after encryption repository - .set(send_id.to_string(), existing_send) + .set(send_id, existing_send) .await .unwrap(); diff --git a/crates/bitwarden-send/src/get_list.rs b/crates/bitwarden-send/src/get_list.rs index dc6377966a..265c713e49 100644 --- a/crates/bitwarden-send/src/get_list.rs +++ b/crates/bitwarden-send/src/get_list.rs @@ -25,7 +25,7 @@ pub(super) async fn get_send( id: Uuid, ) -> Result { let send = repository - .get(id.to_string()) + .get(id) .await? .ok_or(ItemNotFoundError)?; @@ -91,7 +91,7 @@ mod tests { }; let mut send = store.encrypt(send_view).unwrap(); send.id = Some(send_id); - repository.set(send_id.to_string(), send).await.unwrap(); + repository.set(send_id, send).await.unwrap(); // Test getting the send let result = get_send(&store, &repository, send_id).await.unwrap(); @@ -197,7 +197,7 @@ mod tests { }; let mut send_2 = store.encrypt(send_view_2).unwrap(); send_2.id = Some(send_id_2); - repository.set(send_id_2.to_string(), send_2).await.unwrap(); + repository.set(send_id_2, send_2).await.unwrap(); // Test listing all sends let result = list_sends(&store, &repository).await.unwrap(); diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index 8fd4b7327f..111fd93e60 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -13,6 +13,8 @@ use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use uuid::Uuid; use zeroize::Zeroizing; +#[cfg(feature = "wasm")] +use {tsify::Tsify, wasm_bindgen::prelude::*}; use crate::SendParseError; @@ -118,7 +120,7 @@ pub struct Send { pub auth_type: AuthType, } -bitwarden_state::register_repository_item!(Send, "Send"); +bitwarden_state::register_repository_item!(Uuid => Send, "Send"); #[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] diff --git a/crates/bitwarden-send/src/send_client.rs b/crates/bitwarden-send/src/send_client.rs index 8242506885..6659043696 100644 --- a/crates/bitwarden-send/src/send_client.rs +++ b/crates/bitwarden-send/src/send_client.rs @@ -7,6 +7,8 @@ use bitwarden_crypto::{ use bitwarden_state::repository::{Repository, RepositoryError}; use thiserror::Error; use uuid::Uuid; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; use crate::{ Send, SendListView, SendView, From 9f8fa82a27f6ef224af65106bc1bef88f0b1f114 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Tue, 3 Mar 2026 08:09:19 -0500 Subject: [PATCH 11/44] PR fixes --- crates/bitwarden-send/src/create.rs | 60 ++++++++++++++++----------- crates/bitwarden-send/src/edit.rs | 18 +++----- crates/bitwarden-send/src/get_list.rs | 2 +- crates/bitwarden-send/src/lib.rs | 2 +- crates/bitwarden-send/src/send.rs | 6 +++ 5 files changed, 49 insertions(+), 39 deletions(-) diff --git a/crates/bitwarden-send/src/create.rs b/crates/bitwarden-send/src/create.rs index afdb7d0a71..a9ab5cb194 100644 --- a/crates/bitwarden-send/src/create.rs +++ b/crates/bitwarden-send/src/create.rs @@ -19,7 +19,7 @@ use tsify::Tsify; use wasm_bindgen::prelude::*; use crate::{ - AuthType, Send, SendFileView, SendParseError, SendTextView, SendType, SendView, + AuthType, Send, SendParseError, SendViewType, SendView, }; #[allow(missing_docs)] @@ -47,9 +47,7 @@ pub struct SendAddEditRequest { pub key: Option, pub password: Option, - pub r#type: SendType, - pub file: Option, - pub text: Option, + pub view_type: SendViewType, pub max_access_count: Option, pub disabled: bool, @@ -90,8 +88,34 @@ impl CompositeEncryptable().ok()), + size_name: f.size_name.clone(), + })) + } else { + None + }; + + let text = if let SendViewType::Text(t) = self.view_type.clone() { + Some(Box::new(bitwarden_api_api::models::SendTextModel { + text: t.text.as_ref().map(|txt| txt.encrypt(ctx, send_key)).transpose()?.map(|e| e.to_string()), + hidden: Some(t.hidden), + })) + } else { + None + }; + + let t = if let SendViewType::File(_) = self.view_type { + bitwarden_api_api::models::SendType::File + } else { + bitwarden_api_api::models::SendType::Text + }; + Ok(bitwarden_api_api::models::SendRequestModel { - r#type: Some(self.r#type.into()), + r#type: Some(t), auth_type: Some(self.auth_type.into()), file_length: None, name: Some(self.name.encrypt(ctx, send_key)?.to_string()), @@ -101,20 +125,8 @@ impl CompositeEncryptable Result<_, CryptoError> { - Ok(Box::new(bitwarden_api_api::models::SendFileModel { - id: f.id.clone(), - file_name: Some(f.file_name.encrypt(ctx, send_key)?.to_string()), - size: f.size.as_ref().and_then(|s| s.parse::().ok()), - size_name: f.size_name.clone(), - })) - }).transpose()?, - text: self.text.as_ref().map(|t| -> Result<_, CryptoError> { - Ok(Box::new(bitwarden_api_api::models::SendTextModel { - text: t.text.as_ref().map(|txt| txt.encrypt(ctx, send_key)).transpose()?.map(|e| e.to_string()), - hidden: Some(t.hidden), - })) - }).transpose()?, + file: file, + text: text, password: self.password.clone(), emails: if self.emails.is_empty() { None @@ -162,6 +174,8 @@ mod tests { use bitwarden_test::MemoryRepository; use uuid::uuid; + use crate::{SendType, SendView, SendTextView}; + use super::*; #[tokio::test] @@ -217,9 +231,7 @@ mod tests { notes: Some("notes".to_string()), key: None, password: None, - r#type: SendType::Text, - file: None, - text: Some(SendTextView { + view_type: SendViewType::Text(SendTextView { text: Some("test".to_string()), hidden: false, }), @@ -296,9 +308,7 @@ mod tests { notes: Some("notes".to_string()), key: None, password: None, - r#type: SendType::Text, - file: None, - text: Some(SendTextView { + view_type: SendViewType::Text(SendTextView { text: Some("test".to_string()), hidden: false, }), diff --git a/crates/bitwarden-send/src/edit.rs b/crates/bitwarden-send/src/edit.rs index 5148e9bb7e..181c5dc8d1 100644 --- a/crates/bitwarden-send/src/edit.rs +++ b/crates/bitwarden-send/src/edit.rs @@ -72,7 +72,7 @@ mod tests { use uuid::uuid; use super::*; - use crate::{AuthType, SendTextView, SendType}; + use crate::{AuthType, SendTextView, SendType, SendViewType}; #[tokio::test] async fn test_edit_send() { @@ -159,9 +159,7 @@ mod tests { notes: Some("updated notes".to_string()), key: None, password: None, - r#type: SendType::Text, - file: None, - text: Some(SendTextView { + view_type: SendViewType::Text(SendTextView { text: Some("updated text".to_string()), hidden: false, }), @@ -185,7 +183,7 @@ mod tests { assert_eq!(result.revision_date, "2025-01-02T00:00:00Z".parse::>().unwrap()); // Confirm the send was updated in the repository - let stored = repository.get(send_id.to_string()).await.unwrap().unwrap(); + let stored = repository.get(send_id).await.unwrap().unwrap(); assert_eq!( store .decrypt::(&stored) @@ -219,9 +217,7 @@ mod tests { notes: None, key: None, password: None, - r#type: SendType::Text, - file: None, - text: Some(SendTextView { + view_type: SendViewType::Text(SendTextView { text: Some("test".to_string()), hidden: false, }), @@ -281,7 +277,7 @@ mod tests { let mut existing_send = store.encrypt(existing_send_view).unwrap(); existing_send.id = Some(send_id); // Set the ID after encryption repository - .set(send_id.to_string(), existing_send) + .set(send_id, existing_send) .await .unwrap(); @@ -303,9 +299,7 @@ mod tests { notes: None, key: None, password: None, - r#type: SendType::Text, - file: None, - text: Some(SendTextView { + view_type: SendViewType::Text(SendTextView { text: Some("test".to_string()), hidden: false, }), diff --git a/crates/bitwarden-send/src/get_list.rs b/crates/bitwarden-send/src/get_list.rs index 265c713e49..694a2e71c2 100644 --- a/crates/bitwarden-send/src/get_list.rs +++ b/crates/bitwarden-send/src/get_list.rs @@ -168,7 +168,7 @@ mod tests { }; let mut send_1 = store.encrypt(send_view_1).unwrap(); send_1.id = Some(send_id_1); - repository.set(send_id_1.to_string(), send_1).await.unwrap(); + repository.set(send_id_1, send_1).await.unwrap(); let send_id_2 = uuid!("36afb22c-9c95-4db5-8bac-c21cb204a3f2"); let send_view_2 = SendView { diff --git a/crates/bitwarden-send/src/lib.rs b/crates/bitwarden-send/src/lib.rs index 96be92e499..106de89d6c 100644 --- a/crates/bitwarden-send/src/lib.rs +++ b/crates/bitwarden-send/src/lib.rs @@ -16,4 +16,4 @@ pub use send_client::{ SendEncryptFileError, }; mod send; -pub use send::{AuthType, Send, SendListView, SendTextView, SendFileView, SendType, SendView}; +pub use send::{AuthType, Send, SendListView, SendTextView, SendFileView, SendType, SendViewType, SendView}; diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index 111fd93e60..b95a927206 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -87,6 +87,12 @@ pub enum AuthType { None = 2, } +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub enum SendViewType { + File(SendFileView), + Text(SendTextView), +} + #[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] From 4c586a045c5848b3c77d46563ddf775eb64e5cae Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Tue, 3 Mar 2026 08:39:42 -0500 Subject: [PATCH 12/44] PR fixes --- crates/bitwarden-send/src/create.rs | 31 ++---- crates/bitwarden-send/src/edit.rs | 128 +++++++++++++++++++++-- crates/bitwarden-send/src/send_client.rs | 8 +- 3 files changed, 131 insertions(+), 36 deletions(-) diff --git a/crates/bitwarden-send/src/create.rs b/crates/bitwarden-send/src/create.rs index a9ab5cb194..91b5684fa8 100644 --- a/crates/bitwarden-send/src/create.rs +++ b/crates/bitwarden-send/src/create.rs @@ -7,7 +7,6 @@ use bitwarden_crypto::{ CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, OctetStreamBytes, PrimitiveEncryptable, generate_random_bytes, }; -use bitwarden_encoding::B64Url; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::{Repository, RepositoryError}; use chrono::{DateTime, Utc}; @@ -41,10 +40,9 @@ pub enum CreateSendError { #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] -pub struct SendAddEditRequest { +pub struct SendAddRequest { pub name: String, pub notes: Option, - pub key: Option, pub password: Option, pub view_type: SendViewType, @@ -64,26 +62,15 @@ pub struct SendAddEditRequest { } impl CompositeEncryptable - for SendAddEditRequest + for SendAddRequest { fn encrypt_composite( &self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, ) -> Result { - // Generate or decode the send key - let k = match &self.key { - // Existing send, decode key - Some(k) => B64Url::try_from(k.as_str()) - .map_err(|_| CryptoError::InvalidKey)? - .as_bytes() - .to_vec(), - // New send, generate random key - None => { - let key = generate_random_bytes::<[u8; 16]>(); - key.to_vec() - } - }; + // Generate the send key + let k = generate_random_bytes::<[u8; 16]>().to_vec(); // Derive the shareable send key for encrypting content let send_key = Send::derive_shareable_key(ctx, &k)?; @@ -139,7 +126,7 @@ impl CompositeEncryptable for SendAddEditRequest { +impl IdentifyKey for SendAddRequest { fn key_identifier(&self) -> SymmetricKeyId { SymmetricKeyId::User } @@ -149,7 +136,7 @@ pub(super) async fn create_send + ?Sized>( key_store: &KeyStore, api_client: &bitwarden_api_api::apis::ApiClient, repository: &R, - request: SendAddEditRequest, + request: SendAddRequest, ) -> Result { let send_request = key_store.encrypt(request)?; @@ -226,10 +213,9 @@ mod tests { &store, &api_client, &repository, - SendAddEditRequest { + SendAddRequest { name: "test".to_string(), notes: Some("notes".to_string()), - key: None, password: None, view_type: SendViewType::Text(SendTextView { text: Some("test".to_string()), @@ -303,10 +289,9 @@ mod tests { &store, &api_client, &repository, - SendAddEditRequest { + SendAddRequest { name: "test".to_string(), notes: Some("notes".to_string()), - key: None, password: None, view_type: SendViewType::Text(SendTextView { text: Some("test".to_string()), diff --git a/crates/bitwarden-send/src/edit.rs b/crates/bitwarden-send/src/edit.rs index 181c5dc8d1..e238543dde 100644 --- a/crates/bitwarden-send/src/edit.rs +++ b/crates/bitwarden-send/src/edit.rs @@ -1,16 +1,19 @@ -use bitwarden_core::{ApiError, MissingFieldError, key_management::KeyIds}; -use bitwarden_crypto::{CryptoError, KeyStore}; +use bitwarden_core::{ApiError, MissingFieldError, key_management::{KeyIds, SymmetricKeyId}}; +use bitwarden_crypto::{CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, OctetStreamBytes, PrimitiveEncryptable, generate_random_bytes}; +use bitwarden_encoding::B64Url; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::{Repository, RepositoryError}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; #[cfg(feature = "wasm")] +use tsify::Tsify; +#[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; use crate::{ - Send, SendView, - create::SendAddEditRequest, - error::{ItemNotFoundError, SendParseError}, + AuthType, Send, SendView, SendViewType, error::{ItemNotFoundError, SendParseError} }; #[allow(missing_docs)] @@ -33,12 +36,119 @@ pub enum EditSendError { SendParse(#[from] SendParseError), } +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub struct SendEditRequest { + pub name: String, + pub notes: Option, + pub key: Option, + pub password: Option, + + pub view_type: SendViewType, + + pub max_access_count: Option, + pub disabled: bool, + pub hide_email: bool, + + pub deletion_date: DateTime, + pub expiration_date: Option>, + + /// Email addresses for OTP authentication. + /// **Note**: Mutually exclusive with `new_password`. If both are set, + /// only password authentication will be used. + pub emails: Vec, + pub auth_type: AuthType, +} + +impl CompositeEncryptable + for SendEditRequest +{ + fn encrypt_composite( + &self, + ctx: &mut KeyStoreContext, + key: SymmetricKeyId, + ) -> Result { + // Generate or decode the send key + let k = match &self.key { + // Existing send, decode key + Some(k) => B64Url::try_from(k.as_str()) + .map_err(|_| CryptoError::InvalidKey)? + .as_bytes() + .to_vec(), + // New send, generate random key + None => { + let key = generate_random_bytes::<[u8; 16]>(); + key.to_vec() + } + }; + + // Derive the shareable send key for encrypting content + let send_key = Send::derive_shareable_key(ctx, &k)?; + + let file = if let SendViewType::File(f) = self.view_type.clone() { + Some(Box::new(bitwarden_api_api::models::SendFileModel { + id: f.id.clone(), + file_name: Some(f.file_name.encrypt(ctx, send_key)?.to_string()), + size: f.size.as_ref().and_then(|s| s.parse::().ok()), + size_name: f.size_name.clone(), + })) + } else { + None + }; + + let text = if let SendViewType::Text(t) = self.view_type.clone() { + Some(Box::new(bitwarden_api_api::models::SendTextModel { + text: t.text.as_ref().map(|txt| txt.encrypt(ctx, send_key)).transpose()?.map(|e| e.to_string()), + hidden: Some(t.hidden), + })) + } else { + None + }; + + let t = if let SendViewType::File(_) = self.view_type { + bitwarden_api_api::models::SendType::File + } else { + bitwarden_api_api::models::SendType::Text + }; + + Ok(bitwarden_api_api::models::SendRequestModel { + r#type: Some(t), + auth_type: Some(self.auth_type.into()), + file_length: None, + name: Some(self.name.encrypt(ctx, send_key)?.to_string()), + notes: self.notes.as_ref().map(|n| n.encrypt(ctx, send_key)).transpose()?.map(|e| e.to_string()), + // Encrypt the send key itself with the user key + key: OctetStreamBytes::from(k).encrypt(ctx, key)?.to_string(), + max_access_count: self.max_access_count.map(|c| c as i32), + expiration_date: self.expiration_date.map(|d| d.to_rfc3339()), + deletion_date: self.deletion_date.to_rfc3339(), + file: file, + text: text, + password: self.password.clone(), + emails: if self.emails.is_empty() { + None + } else { + Some(self.emails.join(",")) + }, + disabled: self.disabled, + hide_email: Some(self.hide_email), + }) + } +} + +impl IdentifyKey for SendEditRequest { + fn key_identifier(&self) -> SymmetricKeyId { + SymmetricKeyId::User + } +} + pub(super) async fn edit_send + ?Sized>( key_store: &KeyStore, api_client: &bitwarden_api_api::apis::ApiClient, repository: &R, send_id: Uuid, - request: SendAddEditRequest, + request: SendEditRequest, ) -> Result { let id = send_id.to_string(); @@ -154,7 +264,7 @@ mod tests { &api_client, &repository, send_id, - SendAddEditRequest { + SendEditRequest { name: "updated".to_string(), notes: Some("updated notes".to_string()), key: None, @@ -212,7 +322,7 @@ mod tests { &api_client, &repository, send_id, - SendAddEditRequest { + SendEditRequest { name: "test".to_string(), notes: None, key: None, @@ -294,7 +404,7 @@ mod tests { &api_client, &repository, send_id, - SendAddEditRequest { + SendEditRequest { name: "test".to_string(), notes: None, key: None, diff --git a/crates/bitwarden-send/src/send_client.rs b/crates/bitwarden-send/src/send_client.rs index 6659043696..4d4762e713 100644 --- a/crates/bitwarden-send/src/send_client.rs +++ b/crates/bitwarden-send/src/send_client.rs @@ -12,8 +12,8 @@ use wasm_bindgen::prelude::*; use crate::{ Send, SendListView, SendView, - create::{CreateSendError, SendAddEditRequest, create_send}, - edit::{EditSendError, edit_send}, + create::{CreateSendError, SendAddRequest, create_send}, + edit::{EditSendError, SendEditRequest, edit_send}, get_list::{GetSendError, get_send, list_sends}, }; @@ -145,7 +145,7 @@ impl SendClient { } /// Create a new [Send] and save it to the server. - pub async fn create(&self, request: SendAddEditRequest) -> Result { + pub async fn create(&self, request: SendAddRequest) -> Result { let key_store = self.client.internal.get_key_store(); let config = self.client.internal.get_api_configurations().await; let repository = self.get_repository()?; @@ -157,7 +157,7 @@ impl SendClient { pub async fn edit( &self, send_id: Uuid, - request: SendAddEditRequest, + request: SendEditRequest, ) -> Result { let key_store = self.client.internal.get_key_store(); let config = self.client.internal.get_api_configurations().await; From fc832cb301d16a708445d4fa11c7749162ee7903 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Tue, 3 Mar 2026 12:56:00 -0500 Subject: [PATCH 13/44] build/cargo fixes --- crates/bitwarden-send/src/create.rs | 4 ++-- crates/bitwarden-send/src/edit.rs | 6 +++--- crates/bitwarden-send/src/send.rs | 9 +++++++++ crates/bitwarden-send/src/send_client.rs | 4 ++-- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/crates/bitwarden-send/src/create.rs b/crates/bitwarden-send/src/create.rs index 91b5684fa8..5554063b91 100644 --- a/crates/bitwarden-send/src/create.rs +++ b/crates/bitwarden-send/src/create.rs @@ -112,8 +112,8 @@ impl CompositeEncryptable + ?Sized>( let id = send_id.to_string(); // Verify the send we're updating exists - repository.get(send_id.clone()).await?.ok_or(ItemNotFoundError)?; + repository.get(send_id).await?.ok_or(ItemNotFoundError)?; let send_request = key_store.encrypt(request)?; diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index b95a927206..18abddffe9 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -20,6 +20,7 @@ use crate::SendParseError; const SEND_ITERATIONS: u32 = 100_000; +/// File-based send content #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] @@ -31,17 +32,22 @@ pub struct SendFile { pub size_name: Option, } +/// View model for decrypted SendFile #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] pub struct SendFileView { + /// The file's ID pub id: Option, + /// The file name pub file_name: String, + /// The file size in bytes as a string pub size: Option, /// Readable size, ex: "4.2 KB" or "1.43 GB" pub size_name: Option, } +/// Text-based send content #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] @@ -87,9 +93,12 @@ pub enum AuthType { None = 2, } +/// View model for decrypted Send type #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] pub enum SendViewType { + /// File-based send File(SendFileView), + /// Text-based send Text(SendTextView), } diff --git a/crates/bitwarden-send/src/send_client.rs b/crates/bitwarden-send/src/send_client.rs index 4d4762e713..21e2963ae7 100644 --- a/crates/bitwarden-send/src/send_client.rs +++ b/crates/bitwarden-send/src/send_client.rs @@ -147,7 +147,7 @@ impl SendClient { /// Create a new [Send] and save it to the server. pub async fn create(&self, request: SendAddRequest) -> Result { let key_store = self.client.internal.get_key_store(); - let config = self.client.internal.get_api_configurations().await; + let config = self.client.internal.get_api_configurations(); let repository = self.get_repository()?; create_send(key_store, &config.api_client, repository.as_ref(), request).await @@ -160,7 +160,7 @@ impl SendClient { request: SendEditRequest, ) -> Result { let key_store = self.client.internal.get_key_store(); - let config = self.client.internal.get_api_configurations().await; + let config = self.client.internal.get_api_configurations(); let repository = self.get_repository()?; edit_send( From 6fc9b3d9c3567e59c3bf9118a0e8c44931cd4860 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Wed, 4 Mar 2026 14:13:45 -0500 Subject: [PATCH 14/44] cargo fmt --- crates/bitwarden-send/src/create.rs | 49 +++++++++++++++++--------- crates/bitwarden-send/src/edit.rs | 50 ++++++++++++++++++--------- crates/bitwarden-send/src/get_list.rs | 6 +--- crates/bitwarden-send/src/lib.rs | 4 ++- 4 files changed, 70 insertions(+), 39 deletions(-) diff --git a/crates/bitwarden-send/src/create.rs b/crates/bitwarden-send/src/create.rs index 5554063b91..e62461efe7 100644 --- a/crates/bitwarden-send/src/create.rs +++ b/crates/bitwarden-send/src/create.rs @@ -17,9 +17,7 @@ use tsify::Tsify; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; -use crate::{ - AuthType, Send, SendParseError, SendViewType, SendView, -}; +use crate::{AuthType, Send, SendParseError, SendView, SendViewType}; #[allow(missing_docs)] #[bitwarden_error(flat)] @@ -88,7 +86,12 @@ impl CompositeEncryptable + ?Sized>( let send: Send = resp.try_into()?; - repository - .set(require!(send.id), send.clone()) - .await?; + repository.set(require!(send.id), send.clone()).await?; Ok(key_store.decrypt(&send)?) } @@ -161,7 +167,7 @@ mod tests { use bitwarden_test::MemoryRepository; use uuid::uuid; - use crate::{SendType, SendView, SendTextView}; + use crate::{SendTextView, SendType, SendView}; use super::*; @@ -242,24 +248,35 @@ mod tests { assert_eq!(result.has_password, false); assert_eq!(result.r#type, SendType::Text); assert_eq!(result.file, None); - assert_eq!(result.text, Some(SendTextView { - text: Some("test".to_string()), - hidden: false, - })); + assert_eq!( + result.text, + Some(SendTextView { + text: Some("test".to_string()), + hidden: false, + }) + ); assert_eq!(result.max_access_count, None); assert_eq!(result.access_count, 0); assert_eq!(result.disabled, false); assert_eq!(result.hide_email, false); - assert_eq!(result.deletion_date, "2025-01-10T00:00:00Z".parse::>().unwrap()); + assert_eq!( + result.deletion_date, + "2025-01-10T00:00:00Z".parse::>().unwrap() + ); assert_eq!(result.expiration_date, None); assert_eq!(result.emails, Vec::::new()); assert_eq!(result.auth_type, AuthType::None); - assert_eq!(result.revision_date, "2025-01-01T00:00:00Z".parse::>().unwrap()); + assert_eq!( + result.revision_date, + "2025-01-01T00:00:00Z".parse::>().unwrap() + ); // Confirm the send was stored in the repository assert_eq!( store - .decrypt::(&repository.get(send_id).await.unwrap().unwrap()) + .decrypt::( + &repository.get(send_id).await.unwrap().unwrap() + ) .unwrap(), result ); diff --git a/crates/bitwarden-send/src/edit.rs b/crates/bitwarden-send/src/edit.rs index 8998255f5c..6b4b133681 100644 --- a/crates/bitwarden-send/src/edit.rs +++ b/crates/bitwarden-send/src/edit.rs @@ -1,19 +1,26 @@ -use bitwarden_core::{ApiError, MissingFieldError, key_management::{KeyIds, SymmetricKeyId}}; -use bitwarden_crypto::{CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, OctetStreamBytes, PrimitiveEncryptable, generate_random_bytes}; +use bitwarden_core::{ + ApiError, MissingFieldError, + key_management::{KeyIds, SymmetricKeyId}, +}; +use bitwarden_crypto::{ + CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, OctetStreamBytes, + PrimitiveEncryptable, generate_random_bytes, +}; use bitwarden_encoding::B64Url; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::{Repository, RepositoryError}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use thiserror::Error; -use uuid::Uuid; #[cfg(feature = "wasm")] use tsify::Tsify; +use uuid::Uuid; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; use crate::{ - AuthType, Send, SendView, SendViewType, error::{ItemNotFoundError, SendParseError} + AuthType, Send, SendView, SendViewType, + error::{ItemNotFoundError, SendParseError}, }; #[allow(missing_docs)] @@ -99,7 +106,12 @@ impl CompositeEncryptable>().unwrap()); + assert_eq!( + result.revision_date, + "2025-01-02T00:00:00Z".parse::>().unwrap() + ); // Confirm the send was updated in the repository let stored = repository.get(send_id).await.unwrap().unwrap(); @@ -343,7 +360,10 @@ mod tests { .await; assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), EditSendError::ItemNotFound(_))); + assert!(matches!( + result.unwrap_err(), + EditSendError::ItemNotFound(_) + )); } #[tokio::test] @@ -386,10 +406,7 @@ mod tests { }; let mut existing_send = store.encrypt(existing_send_view).unwrap(); existing_send.id = Some(send_id); // Set the ID after encryption - repository - .set(send_id, existing_send) - .await - .unwrap(); + repository.set(send_id, existing_send).await.unwrap(); let api_client = ApiClient::new_mocked(move |mock| { mock.sends_api.expect_put().returning(move |_id, _model| { @@ -428,4 +445,3 @@ mod tests { assert!(matches!(result.unwrap_err(), EditSendError::Api(_))); } } - diff --git a/crates/bitwarden-send/src/get_list.rs b/crates/bitwarden-send/src/get_list.rs index 694a2e71c2..246e13c1f8 100644 --- a/crates/bitwarden-send/src/get_list.rs +++ b/crates/bitwarden-send/src/get_list.rs @@ -24,10 +24,7 @@ pub(super) async fn get_send( repository: &dyn Repository, id: Uuid, ) -> Result { - let send = repository - .get(id) - .await? - .ok_or(ItemNotFoundError)?; + let send = repository.get(id).await?.ok_or(ItemNotFoundError)?; Ok(store.decrypt(&send)?) } @@ -230,4 +227,3 @@ mod tests { assert_eq!(result.len(), 0); } } - diff --git a/crates/bitwarden-send/src/lib.rs b/crates/bitwarden-send/src/lib.rs index 106de89d6c..49b65226bd 100644 --- a/crates/bitwarden-send/src/lib.rs +++ b/crates/bitwarden-send/src/lib.rs @@ -16,4 +16,6 @@ pub use send_client::{ SendEncryptFileError, }; mod send; -pub use send::{AuthType, Send, SendListView, SendTextView, SendFileView, SendType, SendViewType, SendView}; +pub use send::{ + AuthType, Send, SendFileView, SendListView, SendTextView, SendType, SendView, SendViewType, +}; From ab48c6ea2f410b0b5691addc7343a8ef728e98e5 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Wed, 4 Mar 2026 14:44:03 -0500 Subject: [PATCH 15/44] cargo fmt --- crates/bitwarden-send/src/create.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/bitwarden-send/src/create.rs b/crates/bitwarden-send/src/create.rs index e62461efe7..34ae7ac97a 100644 --- a/crates/bitwarden-send/src/create.rs +++ b/crates/bitwarden-send/src/create.rs @@ -167,9 +167,8 @@ mod tests { use bitwarden_test::MemoryRepository; use uuid::uuid; - use crate::{SendTextView, SendType, SendView}; - use super::*; + use crate::{SendTextView, SendType, SendView}; #[tokio::test] async fn test_create_send() { From 60918cda79749c02fa19298b8a42e291ce5d217f Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Wed, 4 Mar 2026 15:41:29 -0500 Subject: [PATCH 16/44] Check fix --- crates/bitwarden-send/src/get_list.rs | 4 +++- crates/bitwarden-send/src/send.rs | 4 ++++ crates/bitwarden-send/src/send_client.rs | 4 +++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-send/src/get_list.rs b/crates/bitwarden-send/src/get_list.rs index 246e13c1f8..2442919c35 100644 --- a/crates/bitwarden-send/src/get_list.rs +++ b/crates/bitwarden-send/src/get_list.rs @@ -1,4 +1,4 @@ -use bitwarden_core::key_management::KeyIds; +use bitwarden_core::{MissingFieldError, key_management::KeyIds}; use bitwarden_crypto::{CryptoError, KeyStore}; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::{Repository, RepositoryError}; @@ -16,6 +16,8 @@ pub enum GetSendError { #[error(transparent)] Crypto(#[from] CryptoError), #[error(transparent)] + MissingField(#[from] MissingFieldError), + #[error(transparent)] Repository(#[from] RepositoryError), } diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index 18abddffe9..87d4af22c6 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -24,6 +24,7 @@ const SEND_ITERATIONS: u32 = 100_000; #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct SendFile { pub id: Option, pub file_name: EncString, @@ -51,6 +52,7 @@ pub struct SendFileView { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct SendText { pub text: Option, pub hidden: bool, @@ -95,6 +97,7 @@ pub enum AuthType { /// View model for decrypted Send type #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub enum SendViewType { /// File-based send File(SendFileView), @@ -106,6 +109,7 @@ pub enum SendViewType { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct Send { pub id: Option, pub access_id: Option, diff --git a/crates/bitwarden-send/src/send_client.rs b/crates/bitwarden-send/src/send_client.rs index 21e2963ae7..7f11716785 100644 --- a/crates/bitwarden-send/src/send_client.rs +++ b/crates/bitwarden-send/src/send_client.rs @@ -63,7 +63,6 @@ pub struct SendClient { client: Client, } -#[cfg_attr(feature = "wasm", wasm_bindgen)] impl SendClient { fn new(client: Client) -> Self { Self { client } @@ -143,7 +142,10 @@ impl SendClient { let encrypted = OctetStreamBytes::from(buffer).encrypt(&mut ctx, key)?; Ok(encrypted.to_buffer()?) } +} +#[cfg_attr(feature = "wasm", wasm_bindgen)] +impl SendClient { /// Create a new [Send] and save it to the server. pub async fn create(&self, request: SendAddRequest) -> Result { let key_store = self.client.internal.get_key_store(); From e28eb01ced02a3825e32091e49c81b7452cad612 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Wed, 4 Mar 2026 15:44:40 -0500 Subject: [PATCH 17/44] Check fix --- crates/bitwarden-send/src/send_client.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/bitwarden-send/src/send_client.rs b/crates/bitwarden-send/src/send_client.rs index 7f11716785..0722d9e84a 100644 --- a/crates/bitwarden-send/src/send_client.rs +++ b/crates/bitwarden-send/src/send_client.rs @@ -11,10 +11,7 @@ use uuid::Uuid; use wasm_bindgen::prelude::*; use crate::{ - Send, SendListView, SendView, - create::{CreateSendError, SendAddRequest, create_send}, - edit::{EditSendError, SendEditRequest, edit_send}, - get_list::{GetSendError, get_send, list_sends}, + Send, SendListView, SendView, create::{CreateSendError, SendAddRequest, create_send}, edit::{EditSendError, SendEditRequest, edit_send}, error::ItemNotFoundError, get_list::{GetSendError, get_send, list_sends}, send }; /// Generic error type for send encryption errors. @@ -158,12 +155,13 @@ impl SendClient { /// Edit the [Send] and save it to the server. pub async fn edit( &self, - send_id: Uuid, + send_id: String, request: SendEditRequest, ) -> Result { let key_store = self.client.internal.get_key_store(); let config = self.client.internal.get_api_configurations(); let repository = self.get_repository()?; + let send_id = Uuid::parse_str(&send_id).map_err(|_| EditSendError::ItemNotFound(ItemNotFoundError))?; edit_send( key_store, @@ -184,9 +182,10 @@ impl SendClient { } /// Get a specific [Send] by its ID from state and decrypt it to a [SendView]. - pub async fn get(&self, send_id: Uuid) -> Result { + pub async fn get(&self, send_id: String) -> Result { let key_store = self.client.internal.get_key_store(); let repository = self.get_repository()?; + let send_id = Uuid::parse_str(&send_id).map_err(|_| GetSendError::ItemNotFound(ItemNotFoundError))?; get_send(key_store, repository.as_ref(), send_id).await } From 2b18323b3c6c9843c07c4265ad18f4e734e5e545 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Thu, 5 Mar 2026 09:40:56 -0500 Subject: [PATCH 18/44] cargo fmt --- crates/bitwarden-send/src/send_client.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden-send/src/send_client.rs b/crates/bitwarden-send/src/send_client.rs index 0722d9e84a..c361685bbf 100644 --- a/crates/bitwarden-send/src/send_client.rs +++ b/crates/bitwarden-send/src/send_client.rs @@ -11,7 +11,12 @@ use uuid::Uuid; use wasm_bindgen::prelude::*; use crate::{ - Send, SendListView, SendView, create::{CreateSendError, SendAddRequest, create_send}, edit::{EditSendError, SendEditRequest, edit_send}, error::ItemNotFoundError, get_list::{GetSendError, get_send, list_sends}, send + Send, SendListView, SendView, + create::{CreateSendError, SendAddRequest, create_send}, + edit::{EditSendError, SendEditRequest, edit_send}, + error::ItemNotFoundError, + get_list::{GetSendError, get_send, list_sends}, + send, }; /// Generic error type for send encryption errors. @@ -161,7 +166,8 @@ impl SendClient { let key_store = self.client.internal.get_key_store(); let config = self.client.internal.get_api_configurations(); let repository = self.get_repository()?; - let send_id = Uuid::parse_str(&send_id).map_err(|_| EditSendError::ItemNotFound(ItemNotFoundError))?; + let send_id = Uuid::parse_str(&send_id) + .map_err(|_| EditSendError::ItemNotFound(ItemNotFoundError))?; edit_send( key_store, @@ -185,7 +191,8 @@ impl SendClient { pub async fn get(&self, send_id: String) -> Result { let key_store = self.client.internal.get_key_store(); let repository = self.get_repository()?; - let send_id = Uuid::parse_str(&send_id).map_err(|_| GetSendError::ItemNotFound(ItemNotFoundError))?; + let send_id = + Uuid::parse_str(&send_id).map_err(|_| GetSendError::ItemNotFound(ItemNotFoundError))?; get_send(key_store, repository.as_ref(), send_id).await } From c22c51717ac65c24c2c32feb3c9d40983ba10b2d Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Mon, 9 Mar 2026 09:45:38 -0400 Subject: [PATCH 19/44] PR fixes --- crates/bitwarden-send/src/create.rs | 55 +++++++++++------------- crates/bitwarden-send/src/send_client.rs | 1 - 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/crates/bitwarden-send/src/create.rs b/crates/bitwarden-send/src/create.rs index 34ae7ac97a..4f1d113ad9 100644 --- a/crates/bitwarden-send/src/create.rs +++ b/crates/bitwarden-send/src/create.rs @@ -73,39 +73,34 @@ impl CompositeEncryptable().ok()), - size_name: f.size_name.clone(), - })) - } else { - None - }; - - let text = if let SendViewType::Text(t) = self.view_type.clone() { - Some(Box::new(bitwarden_api_api::models::SendTextModel { - text: t - .text - .as_ref() - .map(|txt| txt.encrypt(ctx, send_key)) - .transpose()? - .map(|e| e.to_string()), - hidden: Some(t.hidden), - })) - } else { - None - }; - - let t = if let SendViewType::File(_) = self.view_type { - bitwarden_api_api::models::SendType::File - } else { - bitwarden_api_api::models::SendType::Text + let (send_type, file, text) = match self.view_type.clone() { + SendViewType::File(f) => ( + bitwarden_api_api::models::SendType::File, + Some(Box::new(bitwarden_api_api::models::SendFileModel { + id: f.id.clone(), + file_name: Some(f.file_name.encrypt(ctx, send_key)?.to_string()), + size: f.size.as_ref().and_then(|s| s.parse::().ok()), + size_name: f.size_name.clone(), + })), + None, + ), + SendViewType::Text(t) => ( + bitwarden_api_api::models::SendType::Text, + None, + Some(Box::new(bitwarden_api_api::models::SendTextModel { + text: t + .text + .as_ref() + .map(|txt| txt.encrypt(ctx, send_key)) + .transpose()? + .map(|e| e.to_string()), + hidden: Some(t.hidden), + })), + ), }; Ok(bitwarden_api_api::models::SendRequestModel { - r#type: Some(t), + r#type: Some(send_type), auth_type: Some(self.auth_type.into()), file_length: None, name: Some(self.name.encrypt(ctx, send_key)?.to_string()), diff --git a/crates/bitwarden-send/src/send_client.rs b/crates/bitwarden-send/src/send_client.rs index c361685bbf..752d5a4200 100644 --- a/crates/bitwarden-send/src/send_client.rs +++ b/crates/bitwarden-send/src/send_client.rs @@ -16,7 +16,6 @@ use crate::{ edit::{EditSendError, SendEditRequest, edit_send}, error::ItemNotFoundError, get_list::{GetSendError, get_send, list_sends}, - send, }; /// Generic error type for send encryption errors. From e3adb586fbe8853832b6091221f1058ee0e4836a Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Mon, 9 Mar 2026 11:00:52 -0400 Subject: [PATCH 20/44] PR fixes --- crates/bitwarden-send/src/create.rs | 8 ++++++++ crates/bitwarden-send/src/send.rs | 1 - 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/bitwarden-send/src/create.rs b/crates/bitwarden-send/src/create.rs index 4f1d113ad9..878f8e327a 100644 --- a/crates/bitwarden-send/src/create.rs +++ b/crates/bitwarden-send/src/create.rs @@ -7,6 +7,7 @@ use bitwarden_crypto::{ CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, OctetStreamBytes, PrimitiveEncryptable, generate_random_bytes, }; +use bitwarden_encoding::B64; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::{Repository, RepositoryError}; use chrono::{DateTime, Utc}; @@ -19,6 +20,8 @@ use wasm_bindgen::prelude::*; use crate::{AuthType, Send, SendParseError, SendView, SendViewType}; +const SEND_ITERATIONS: u32 = 100_000; + #[allow(missing_docs)] #[bitwarden_error(flat)] #[derive(Debug, Error)] @@ -99,6 +102,11 @@ impl CompositeEncryptable Date: Mon, 9 Mar 2026 11:35:27 -0400 Subject: [PATCH 21/44] PR fixes --- crates/bitwarden-send/src/create.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-send/src/create.rs b/crates/bitwarden-send/src/create.rs index 878f8e327a..0559907990 100644 --- a/crates/bitwarden-send/src/create.rs +++ b/crates/bitwarden-send/src/create.rs @@ -125,7 +125,7 @@ impl CompositeEncryptable Date: Tue, 10 Mar 2026 10:16:06 -0400 Subject: [PATCH 22/44] PR fixes --- crates/bitwarden-send/src/create.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden-send/src/create.rs b/crates/bitwarden-send/src/create.rs index 0559907990..30c7a44152 100644 --- a/crates/bitwarden-send/src/create.rs +++ b/crates/bitwarden-send/src/create.rs @@ -247,7 +247,7 @@ mod tests { assert_eq!(result.notes, Some("notes".to_string())); assert!(result.key.is_some(), "Expected a generated key"); assert_eq!(result.new_password, None); - assert_eq!(result.has_password, false); + assert!(!result.has_password); assert_eq!(result.r#type, SendType::Text); assert_eq!(result.file, None); assert_eq!( @@ -259,8 +259,8 @@ mod tests { ); assert_eq!(result.max_access_count, None); assert_eq!(result.access_count, 0); - assert_eq!(result.disabled, false); - assert_eq!(result.hide_email, false); + assert!(!result.disabled); + assert!(!result.hide_email); assert_eq!( result.deletion_date, "2025-01-10T00:00:00Z".parse::>().unwrap() From bc30940c891091539f97cb0de1ceac29c271ce78 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Tue, 10 Mar 2026 14:12:02 -0400 Subject: [PATCH 23/44] PR fixes --- crates/bitwarden-send/src/create.rs | 4 +--- crates/bitwarden-send/src/edit.rs | 10 ++++++++-- crates/bitwarden-send/src/send.rs | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/bitwarden-send/src/create.rs b/crates/bitwarden-send/src/create.rs index 30c7a44152..050ca40ba5 100644 --- a/crates/bitwarden-send/src/create.rs +++ b/crates/bitwarden-send/src/create.rs @@ -18,9 +18,7 @@ use tsify::Tsify; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; -use crate::{AuthType, Send, SendParseError, SendView, SendViewType}; - -const SEND_ITERATIONS: u32 = 100_000; +use crate::{AuthType, Send, SendParseError, SendView, SendViewType, send::SEND_ITERATIONS}; #[allow(missing_docs)] #[bitwarden_error(flat)] diff --git a/crates/bitwarden-send/src/edit.rs b/crates/bitwarden-send/src/edit.rs index 6b4b133681..7bb56b54b5 100644 --- a/crates/bitwarden-send/src/edit.rs +++ b/crates/bitwarden-send/src/edit.rs @@ -6,7 +6,7 @@ use bitwarden_crypto::{ CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, OctetStreamBytes, PrimitiveEncryptable, generate_random_bytes, }; -use bitwarden_encoding::B64Url; +use bitwarden_encoding::{B64, B64Url}; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::{Repository, RepositoryError}; use chrono::{DateTime, Utc}; @@ -21,6 +21,7 @@ use wasm_bindgen::prelude::*; use crate::{ AuthType, Send, SendView, SendViewType, error::{ItemNotFoundError, SendParseError}, + send::SEND_ITERATIONS, }; #[allow(missing_docs)] @@ -124,6 +125,11 @@ impl CompositeEncryptable Date: Wed, 11 Mar 2026 16:13:05 -0400 Subject: [PATCH 24/44] PR fix --- crates/bitwarden-pm/Cargo.toml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/bitwarden-pm/Cargo.toml b/crates/bitwarden-pm/Cargo.toml index 0e66949806..5b48bc95f3 100644 --- a/crates/bitwarden-pm/Cargo.toml +++ b/crates/bitwarden-pm/Cargo.toml @@ -15,9 +15,7 @@ license-file.workspace = true keywords.workspace = true [features] -no-memory-hardening = [ - "bitwarden-core/no-memory-hardening", -] # Disable memory hardening features +no-memory-hardening = ["bitwarden-core/no-memory-hardening"] uniffi = [ "bitwarden-core/uniffi", "bitwarden-exporters/uniffi", @@ -27,19 +25,20 @@ uniffi = [ "bitwarden-state/uniffi", "bitwarden-vault/uniffi", "dep:uniffi", -] # Uniffi bindings +] wasm = [ "bitwarden-auth/wasm", "bitwarden-commercial-vault/wasm", "bitwarden-core/wasm", "bitwarden-exporters/wasm", "bitwarden-generators/wasm", + "bitwarden-send/wasm", "bitwarden-state/wasm", "bitwarden-vault/wasm", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:tsify", -] # WASM support +] bitwarden-license = ["dep:bitwarden-commercial-vault"] [dependencies] From e404f62dbbedac4368fb9a59f482b8bddab024cb Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Wed, 11 Mar 2026 16:47:06 -0400 Subject: [PATCH 25/44] PR fix --- crates/bitwarden-send/src/send.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index 45a712e53d..3099b78840 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -36,6 +36,7 @@ pub struct SendFile { #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct SendFileView { /// The file's ID pub id: Option, @@ -61,6 +62,7 @@ pub struct SendText { #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct SendTextView { /// The text content of the send pub text: Option, @@ -72,6 +74,7 @@ pub struct SendTextView { #[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)] #[repr(u8)] #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub enum SendType { /// Text-based send Text = 0, @@ -83,6 +86,7 @@ pub enum SendType { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr)] #[repr(u8)] #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub enum AuthType { /// Email-based OTP authentication Email = 0, From 76b2d393396b6d4b96032f0299a24c7904642bec Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Thu, 12 Mar 2026 06:58:26 -0400 Subject: [PATCH 26/44] PR fixes --- crates/bitwarden-send/src/create.rs | 77 +++++-------- crates/bitwarden-send/src/edit.rs | 167 +++++++++++++--------------- crates/bitwarden-send/src/lib.rs | 3 +- crates/bitwarden-send/src/send.rs | 82 +++++++++++++- 4 files changed, 189 insertions(+), 140 deletions(-) diff --git a/crates/bitwarden-send/src/create.rs b/crates/bitwarden-send/src/create.rs index 050ca40ba5..b945bd5cb8 100644 --- a/crates/bitwarden-send/src/create.rs +++ b/crates/bitwarden-send/src/create.rs @@ -18,7 +18,9 @@ use tsify::Tsify; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; -use crate::{AuthType, Send, SendParseError, SendView, SendViewType, send::SEND_ITERATIONS}; +use crate::{ + AuthType, Send, SendAuthType, SendParseError, SendView, SendViewType, send::SEND_ITERATIONS, +}; #[allow(missing_docs)] #[bitwarden_error(flat)] @@ -42,7 +44,6 @@ pub enum CreateSendError { pub struct SendAddRequest { pub name: String, pub notes: Option, - pub password: Option, pub view_type: SendViewType, @@ -53,11 +54,12 @@ pub struct SendAddRequest { pub deletion_date: DateTime, pub expiration_date: Option>, - /// Email addresses for OTP authentication. - /// **Note**: Mutually exclusive with `new_password`. If both are set, - /// only password authentication will be used. - pub emails: Vec, - pub auth_type: AuthType, + /// Authentication method for accessing this Send. + /// Use `SendAuthType::None` for no authentication, + /// `SendAuthType::Password` for password protection, or + /// `SendAuthType::Emails` for email OTP authentication. + #[serde(flatten)] + pub auth: SendAuthType, } impl CompositeEncryptable @@ -74,40 +76,27 @@ impl CompositeEncryptable ( - bitwarden_api_api::models::SendType::File, - Some(Box::new(bitwarden_api_api::models::SendFileModel { - id: f.id.clone(), - file_name: Some(f.file_name.encrypt(ctx, send_key)?.to_string()), - size: f.size.as_ref().and_then(|s| s.parse::().ok()), - size_name: f.size_name.clone(), - })), - None, - ), - SendViewType::Text(t) => ( - bitwarden_api_api::models::SendType::Text, - None, - Some(Box::new(bitwarden_api_api::models::SendTextModel { - text: t - .text - .as_ref() - .map(|txt| txt.encrypt(ctx, send_key)) - .transpose()? - .map(|e| e.to_string()), - hidden: Some(t.hidden), - })), - ), - }; + let (send_type, file, text) = self.view_type.clone().into_api_models(ctx, send_key)?; - let password = self.password.as_ref().map(|password| { - let password = bitwarden_crypto::pbkdf2(password.as_bytes(), &k, SEND_ITERATIONS); - B64::from(password.as_slice()).to_string() - }); + let (password, emails) = match &self.auth { + SendAuthType::None => (None, None), + SendAuthType::Password { password } => { + let hashed = bitwarden_crypto::pbkdf2(password.as_bytes(), &k, SEND_ITERATIONS); + (Some(B64::from(hashed.as_slice()).to_string()), None) + } + SendAuthType::Emails { emails } => { + let emails_str = if emails.is_empty() { + None + } else { + Some(emails.join(",")) + }; + (None, emails_str) + } + }; Ok(bitwarden_api_api::models::SendRequestModel { r#type: Some(send_type), - auth_type: Some(self.auth_type.into()), + auth_type: Some(self.auth.auth_type().into()), file_length: None, name: Some(self.name.encrypt(ctx, send_key)?.to_string()), notes: self @@ -124,11 +113,7 @@ impl CompositeEncryptable, + }, } #[derive(Serialize, Deserialize, Debug)] @@ -50,8 +55,6 @@ pub enum EditSendError { pub struct SendEditRequest { pub name: String, pub notes: Option, - pub key: Option, - pub password: Option, pub view_type: SendViewType, @@ -62,80 +65,68 @@ pub struct SendEditRequest { pub deletion_date: DateTime, pub expiration_date: Option>, - /// Email addresses for OTP authentication. - /// **Note**: Mutually exclusive with `new_password`. If both are set, - /// only password authentication will be used. - pub emails: Vec, - pub auth_type: AuthType, + /// Authentication method for accessing this Send. + /// Use `SendAuthType::None` for no authentication, + /// `SendAuthType::Password` for password protection, or + /// `SendAuthType::Emails` for email OTP authentication. + #[serde(flatten)] + pub auth: SendAuthType, +} + +/// Internal helper struct that includes the send key for encryption. +/// The key is retrieved from state during edit operations. +#[derive(Debug)] +struct SendEditRequestWithKey { + request: SendEditRequest, + send_key: String, } impl CompositeEncryptable - for SendEditRequest + for SendEditRequestWithKey { fn encrypt_composite( &self, ctx: &mut KeyStoreContext, key: SymmetricKeyId, ) -> Result { - // Generate or decode the send key - let k = match &self.key { - // Existing send, decode key - Some(k) => B64Url::try_from(k.as_str()) - .map_err(|_| CryptoError::InvalidKey)? - .as_bytes() - .to_vec(), - // New send, generate random key - None => { - let key = generate_random_bytes::<[u8; 16]>(); - key.to_vec() - } - }; + // Decode the send key from the existing send + let k = B64Url::try_from(self.send_key.as_str()) + .map_err(|_| CryptoError::InvalidKey)? + .as_bytes() + .to_vec(); // Derive the shareable send key for encrypting content let send_key = Send::derive_shareable_key(ctx, &k)?; - let file = if let SendViewType::File(f) = self.view_type.clone() { - Some(Box::new(bitwarden_api_api::models::SendFileModel { - id: f.id.clone(), - file_name: Some(f.file_name.encrypt(ctx, send_key)?.to_string()), - size: f.size.as_ref().and_then(|s| s.parse::().ok()), - size_name: f.size_name.clone(), - })) - } else { - None - }; - - let text = if let SendViewType::Text(t) = self.view_type.clone() { - Some(Box::new(bitwarden_api_api::models::SendTextModel { - text: t - .text - .as_ref() - .map(|txt| txt.encrypt(ctx, send_key)) - .transpose()? - .map(|e| e.to_string()), - hidden: Some(t.hidden), - })) - } else { - None - }; - - let t = if let SendViewType::File(_) = self.view_type { - bitwarden_api_api::models::SendType::File - } else { - bitwarden_api_api::models::SendType::Text + let (send_type, file, text) = self + .request + .view_type + .clone() + .into_api_models(ctx, send_key)?; + + let (password, emails) = match &self.request.auth { + SendAuthType::None => (None, None), + SendAuthType::Password { password } => { + let hashed = bitwarden_crypto::pbkdf2(password.as_bytes(), &k, SEND_ITERATIONS); + (Some(B64::from(hashed.as_slice()).to_string()), None) + } + SendAuthType::Emails { emails } => { + let emails_str = if emails.is_empty() { + None + } else { + Some(emails.join(",")) + }; + (None, emails_str) + } }; - let password = self.password.as_ref().map(|password| { - let password = bitwarden_crypto::pbkdf2(password.as_bytes(), &k, SEND_ITERATIONS); - B64::from(password.as_slice()).to_string() - }); - Ok(bitwarden_api_api::models::SendRequestModel { - r#type: Some(t), - auth_type: Some(self.auth_type.into()), + r#type: Some(send_type), + auth_type: Some(self.request.auth.auth_type().into()), file_length: None, - name: Some(self.name.encrypt(ctx, send_key)?.to_string()), + name: Some(self.request.name.encrypt(ctx, send_key)?.to_string()), notes: self + .request .notes .as_ref() .map(|n| n.encrypt(ctx, send_key)) @@ -143,24 +134,20 @@ impl CompositeEncryptable for SendEditRequest { +impl IdentifyKey for SendEditRequestWithKey { fn key_identifier(&self) -> SymmetricKeyId { SymmetricKeyId::User } @@ -175,10 +162,17 @@ pub(super) async fn edit_send + ?Sized>( ) -> Result { let id = send_id.to_string(); - // Verify the send we're updating exists - repository.get(send_id).await?.ok_or(ItemNotFoundError)?; + // Retrieve the existing send to get its key (keys cannot be modified during edit) + let existing_send = repository.get(send_id).await?.ok_or(ItemNotFoundError)?; - let send_request = key_store.encrypt(request)?; + // Decrypt to get the key - we only need the key field + let existing_send_view: SendView = key_store.decrypt(&existing_send)?; + let send_key = existing_send_view.key.ok_or(MissingFieldError("key"))?; + + // Create the wrapper with the key from the existing send + let request_with_key = SendEditRequestWithKey { request, send_key }; + + let send_request = key_store.encrypt(request_with_key)?; let resp = api_client .sends_api() @@ -188,7 +182,13 @@ pub(super) async fn edit_send + ?Sized>( let send: Send = resp.try_into()?; - debug_assert!(send.id.unwrap_or_default() == send_id); + // Verify the server returned the correct send ID + if send.id != Some(send_id) { + return Err(EditSendError::IdMismatch { + expected: send_id, + returned: send.id, + }); + } repository.set(send_id, send.clone()).await?; @@ -226,7 +226,7 @@ mod tests { access_id: None, name: "original".to_string(), notes: Some("original notes".to_string()), - key: None, // Will be generated + key: None, // Generates a new key when first encrypted new_password: None, has_password: false, r#type: SendType::Text, @@ -287,8 +287,6 @@ mod tests { SendEditRequest { name: "updated".to_string(), notes: Some("updated notes".to_string()), - key: None, - password: None, view_type: SendViewType::Text(SendTextView { text: Some("updated text".to_string()), hidden: false, @@ -298,8 +296,7 @@ mod tests { hide_email: false, deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(), expiration_date: None, - emails: Vec::new(), - auth_type: AuthType::None, + auth: SendAuthType::None, }, ) .await @@ -348,8 +345,6 @@ mod tests { SendEditRequest { name: "test".to_string(), notes: None, - key: None, - password: None, view_type: SendViewType::Text(SendTextView { text: Some("test".to_string()), hidden: false, @@ -359,8 +354,7 @@ mod tests { hide_email: false, deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(), expiration_date: None, - emails: Vec::new(), - auth_type: AuthType::None, + auth: SendAuthType::None, }, ) .await; @@ -391,7 +385,7 @@ mod tests { access_id: None, name: "original".to_string(), notes: Some("original notes".to_string()), - key: None, // Will be generated + key: None, // Generates a new key when first encrypted new_password: None, has_password: false, r#type: SendType::Text, @@ -430,8 +424,6 @@ mod tests { SendEditRequest { name: "test".to_string(), notes: None, - key: None, - password: None, view_type: SendViewType::Text(SendTextView { text: Some("test".to_string()), hidden: false, @@ -441,8 +433,7 @@ mod tests { hide_email: false, deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(), expiration_date: None, - emails: Vec::new(), - auth_type: AuthType::None, + auth: SendAuthType::None, }, ) .await; diff --git a/crates/bitwarden-send/src/lib.rs b/crates/bitwarden-send/src/lib.rs index 49b65226bd..3791ed4bb1 100644 --- a/crates/bitwarden-send/src/lib.rs +++ b/crates/bitwarden-send/src/lib.rs @@ -17,5 +17,6 @@ pub use send_client::{ }; mod send; pub use send::{ - AuthType, Send, SendFileView, SendListView, SendTextView, SendType, SendView, SendViewType, + AuthType, Send, SendAuthType, SendFileView, SendListView, SendTextView, SendType, SendView, + SendViewType, }; diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index 3099b78840..dafc61f3f5 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -98,6 +98,38 @@ pub enum AuthType { None = 2, } +/// Type-safe authentication method for a Send, including the authentication data. +/// This ensures that password and email authentication are mutually exclusive. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub enum SendAuthType { + /// No authentication required + None, + /// Password-based authentication + Password { + /// The password required to access the Send + password: String, + }, + /// Email-based OTP authentication + Emails { + /// List of email addresses that will receive OTP codes + emails: Vec, + }, +} + +impl SendAuthType { + /// Returns the AuthType discriminant for this authentication method + pub fn auth_type(&self) -> AuthType { + match self { + SendAuthType::None => AuthType::None, + SendAuthType::Password { .. } => AuthType::Password, + SendAuthType::Emails { .. } => AuthType::Email, + } + } +} + /// View model for decrypted Send type #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] @@ -108,6 +140,49 @@ pub enum SendViewType { Text(SendTextView), } +impl SendViewType { + /// Converts the SendViewType into API models for creating or editing a Send. + /// Returns a tuple of (SendType, optional FileModel, optional TextModel). + pub(crate) fn into_api_models( + self, + ctx: &mut KeyStoreContext, + send_key: SymmetricKeyId, + ) -> Result< + ( + bitwarden_api_api::models::SendType, + Option>, + Option>, + ), + CryptoError, + > { + match self { + SendViewType::File(f) => Ok(( + bitwarden_api_api::models::SendType::File, + Some(Box::new(bitwarden_api_api::models::SendFileModel { + id: f.id.clone(), + file_name: Some(f.file_name.encrypt(ctx, send_key)?.to_string()), + size: f.size.as_ref().and_then(|s| s.parse::().ok()), + size_name: f.size_name.clone(), + })), + None, + )), + SendViewType::Text(t) => Ok(( + bitwarden_api_api::models::SendType::Text, + None, + Some(Box::new(bitwarden_api_api::models::SendTextModel { + text: t + .text + .as_ref() + .map(|txt| txt.encrypt(ctx, send_key)) + .transpose()? + .map(|e| e.to_string()), + hidden: Some(t.hidden), + })), + )), + } + } +} + #[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -135,9 +210,10 @@ pub struct Send { pub deletion_date: DateTime, pub expiration_date: Option>, - /// Email addresses for OTP authentication. - /// **Note**: Mutually exclusive with `new_password`. If both are set, - /// only password authentication will be used. + /// Email addresses for OTP authentication (comma-separated). + /// + /// **Note**: Mutually exclusive with `password`. If both `password` and `emails` are + /// set, password authentication takes precedence and email OTP is ignored. pub emails: Option, pub auth_type: AuthType, } From e6dfe1609a2640b914ab2003d2c54ec13f011dba Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Thu, 12 Mar 2026 10:21:10 -0400 Subject: [PATCH 27/44] fix lint --- crates/bitwarden-send/src/create.rs | 6 ++---- crates/bitwarden-send/src/edit.rs | 4 ++-- crates/bitwarden-send/src/send.rs | 16 ++++++++-------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/crates/bitwarden-send/src/create.rs b/crates/bitwarden-send/src/create.rs index b945bd5cb8..5547a54ffd 100644 --- a/crates/bitwarden-send/src/create.rs +++ b/crates/bitwarden-send/src/create.rs @@ -18,9 +18,7 @@ use tsify::Tsify; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; -use crate::{ - AuthType, Send, SendAuthType, SendParseError, SendView, SendViewType, send::SEND_ITERATIONS, -}; +use crate::{Send, SendAuthType, SendParseError, SendView, SendViewType, send::SEND_ITERATIONS}; #[allow(missing_docs)] #[bitwarden_error(flat)] @@ -154,7 +152,7 @@ mod tests { use uuid::uuid; use super::*; - use crate::{SendTextView, SendType, SendView}; + use crate::{AuthType, SendTextView, SendType, SendView}; #[tokio::test] async fn test_create_send() { diff --git a/crates/bitwarden-send/src/edit.rs b/crates/bitwarden-send/src/edit.rs index 5589d69b98..26516d24d9 100644 --- a/crates/bitwarden-send/src/edit.rs +++ b/crates/bitwarden-send/src/edit.rs @@ -4,7 +4,7 @@ use bitwarden_core::{ }; use bitwarden_crypto::{ CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, OctetStreamBytes, - PrimitiveEncryptable, generate_random_bytes, + PrimitiveEncryptable, }; use bitwarden_encoding::{B64, B64Url}; use bitwarden_error::bitwarden_error; @@ -19,7 +19,7 @@ use uuid::Uuid; use wasm_bindgen::prelude::*; use crate::{ - AuthType, Send, SendAuthType, SendView, SendViewType, + Send, SendAuthType, SendView, SendViewType, error::{ItemNotFoundError, SendParseError}, send::SEND_ITERATIONS, }; diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index dafc61f3f5..ffa309a1c4 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -140,6 +140,13 @@ pub enum SendViewType { Text(SendTextView), } +/// Type alias for the tuple returned by SendViewType::into_api_models +type SendApiModels = ( + bitwarden_api_api::models::SendType, + Option>, + Option>, +); + impl SendViewType { /// Converts the SendViewType into API models for creating or editing a Send. /// Returns a tuple of (SendType, optional FileModel, optional TextModel). @@ -147,14 +154,7 @@ impl SendViewType { self, ctx: &mut KeyStoreContext, send_key: SymmetricKeyId, - ) -> Result< - ( - bitwarden_api_api::models::SendType, - Option>, - Option>, - ), - CryptoError, - > { + ) -> Result { match self { SendViewType::File(f) => Ok(( bitwarden_api_api::models::SendType::File, From 0628752264fee7f53e0be42f34a7dace7c06b222 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Tue, 17 Mar 2026 11:08:51 -0400 Subject: [PATCH 28/44] PR fix --- Cargo.lock | 1 + crates/bitwarden-send/Cargo.toml | 9 ++--- crates/bitwarden-send/src/create.rs | 26 ++++---------- crates/bitwarden-send/src/edit.rs | 29 ++++----------- crates/bitwarden-send/src/get_list.rs | 12 +++---- crates/bitwarden-send/src/lib.rs | 4 +-- crates/bitwarden-send/src/send.rs | 35 ++++++++++++++++--- .../src/key_rotation/data.rs | 2 +- .../src/key_rotation/sync.rs | 3 +- 9 files changed, 59 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 913bf489d6..3c3e8f29da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -890,6 +890,7 @@ dependencies = [ "bitwarden-error", "bitwarden-state", "bitwarden-test", + "bitwarden-uuid", "chrono", "serde", "serde_repr", diff --git a/crates/bitwarden-send/Cargo.toml b/crates/bitwarden-send/Cargo.toml index d17202171c..49e28cb881 100644 --- a/crates/bitwarden-send/Cargo.toml +++ b/crates/bitwarden-send/Cargo.toml @@ -15,11 +15,7 @@ license-file.workspace = true keywords.workspace = true [features] -uniffi = [ - "bitwarden-core/uniffi", - "bitwarden-crypto/uniffi", - "dep:uniffi", -] # Uniffi bindings +uniffi = ["bitwarden-core/uniffi", "bitwarden-crypto/uniffi", "dep:uniffi"] wasm = [ "bitwarden-core/wasm", "bitwarden-encoding/wasm", @@ -27,7 +23,7 @@ wasm = [ "dep:tsify", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", -] # WASM support +] [dependencies] bitwarden-api-api = { workspace = true } @@ -36,6 +32,7 @@ bitwarden-crypto = { workspace = true } bitwarden-encoding = { workspace = true } bitwarden-error = { workspace = true } bitwarden-state = { workspace = true } +bitwarden-uuid = { workspace = true } chrono = { workspace = true } serde = { workspace = true } serde_repr = { workspace = true } diff --git a/crates/bitwarden-send/src/create.rs b/crates/bitwarden-send/src/create.rs index 5547a54ffd..2d770a89d2 100644 --- a/crates/bitwarden-send/src/create.rs +++ b/crates/bitwarden-send/src/create.rs @@ -7,7 +7,6 @@ use bitwarden_crypto::{ CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, OctetStreamBytes, PrimitiveEncryptable, generate_random_bytes, }; -use bitwarden_encoding::B64; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::{Repository, RepositoryError}; use chrono::{DateTime, Utc}; @@ -18,7 +17,7 @@ use tsify::Tsify; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; -use crate::{Send, SendAuthType, SendParseError, SendView, SendViewType, send::SEND_ITERATIONS}; +use crate::{Send, SendAuthType, SendParseError, SendView, SendViewType}; #[allow(missing_docs)] #[bitwarden_error(flat)] @@ -56,7 +55,6 @@ pub struct SendAddRequest { /// Use `SendAuthType::None` for no authentication, /// `SendAuthType::Password` for password protection, or /// `SendAuthType::Emails` for email OTP authentication. - #[serde(flatten)] pub auth: SendAuthType, } @@ -76,21 +74,7 @@ impl CompositeEncryptable (None, None), - SendAuthType::Password { password } => { - let hashed = bitwarden_crypto::pbkdf2(password.as_bytes(), &k, SEND_ITERATIONS); - (Some(B64::from(hashed.as_slice()).to_string()), None) - } - SendAuthType::Emails { emails } => { - let emails_str = if emails.is_empty() { - None - } else { - Some(emails.join(",")) - }; - (None, emails_str) - } - }; + let (password, emails) = self.auth.auth_data(k.clone()); Ok(bitwarden_api_api::models::SendRequestModel { r#type: Some(send_type), @@ -140,7 +124,9 @@ pub(super) async fn create_send + ?Sized>( let send: Send = resp.try_into()?; - repository.set(require!(send.id), send.clone()).await?; + repository + .set(require!(send.id).into(), send.clone()) + .await?; Ok(key_store.decrypt(&send)?) } @@ -221,7 +207,7 @@ mod tests { .unwrap(); // Verify the result (excluding the generated key which is random) - assert_eq!(result.id, Some(send_id)); + assert_eq!(result.id, Some(crate::send::SendId::new(send_id))); assert_eq!(result.name, "test"); assert_eq!(result.notes, Some("notes".to_string())); assert!(result.key.is_some(), "Expected a generated key"); diff --git a/crates/bitwarden-send/src/edit.rs b/crates/bitwarden-send/src/edit.rs index 26516d24d9..ae48c828c1 100644 --- a/crates/bitwarden-send/src/edit.rs +++ b/crates/bitwarden-send/src/edit.rs @@ -6,7 +6,7 @@ use bitwarden_crypto::{ CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, OctetStreamBytes, PrimitiveEncryptable, }; -use bitwarden_encoding::{B64, B64Url}; +use bitwarden_encoding::B64Url; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::{Repository, RepositoryError}; use chrono::{DateTime, Utc}; @@ -21,7 +21,6 @@ use wasm_bindgen::prelude::*; use crate::{ Send, SendAuthType, SendView, SendViewType, error::{ItemNotFoundError, SendParseError}, - send::SEND_ITERATIONS, }; #[allow(missing_docs)] @@ -104,21 +103,7 @@ impl CompositeEncryptable (None, None), - SendAuthType::Password { password } => { - let hashed = bitwarden_crypto::pbkdf2(password.as_bytes(), &k, SEND_ITERATIONS); - (Some(B64::from(hashed.as_slice()).to_string()), None) - } - SendAuthType::Emails { emails } => { - let emails_str = if emails.is_empty() { - None - } else { - Some(emails.join(",")) - }; - (None, emails_str) - } - }; + let (password, emails) = self.request.auth.auth_data(k.clone()); Ok(bitwarden_api_api::models::SendRequestModel { r#type: Some(send_type), @@ -183,10 +168,10 @@ pub(super) async fn edit_send + ?Sized>( let send: Send = resp.try_into()?; // Verify the server returned the correct send ID - if send.id != Some(send_id) { + if send.id != Some(crate::send::SendId::new(send_id)) { return Err(EditSendError::IdMismatch { expected: send_id, - returned: send.id, + returned: send.id.map(Into::into), }); } @@ -246,7 +231,7 @@ mod tests { auth_type: AuthType::None, }; let mut existing_send = store.encrypt(existing_send_view).unwrap(); - existing_send.id = Some(send_id); // Set the ID after encryption + existing_send.id = Some(crate::send::SendId::new(send_id)); // Set the ID after encryption repository.set(send_id, existing_send).await.unwrap(); let api_client = ApiClient::new_mocked(move |mock| { @@ -303,7 +288,7 @@ mod tests { .unwrap(); // Verify the result - assert_eq!(result.id, Some(send_id)); + assert_eq!(result.id, Some(crate::send::SendId::new(send_id))); assert_eq!(result.name, "updated"); assert_eq!(result.notes, Some("updated notes".to_string())); assert!(result.key.is_some(), "Expected a key"); @@ -405,7 +390,7 @@ mod tests { auth_type: AuthType::None, }; let mut existing_send = store.encrypt(existing_send_view).unwrap(); - existing_send.id = Some(send_id); // Set the ID after encryption + existing_send.id = Some(crate::send::SendId::new(send_id)); // Set the ID after encryption repository.set(send_id, existing_send).await.unwrap(); let api_client = ApiClient::new_mocked(move |mock| { diff --git a/crates/bitwarden-send/src/get_list.rs b/crates/bitwarden-send/src/get_list.rs index 2442919c35..75feacb264 100644 --- a/crates/bitwarden-send/src/get_list.rs +++ b/crates/bitwarden-send/src/get_list.rs @@ -89,13 +89,13 @@ mod tests { auth_type: AuthType::None, }; let mut send = store.encrypt(send_view).unwrap(); - send.id = Some(send_id); + send.id = Some(crate::send::SendId::new(send_id)); repository.set(send_id, send).await.unwrap(); // Test getting the send let result = get_send(&store, &repository, send_id).await.unwrap(); - assert_eq!(result.id, Some(send_id)); + assert_eq!(result.id, Some(crate::send::SendId::new(send_id))); assert_eq!(result.name, "Test Send"); assert_eq!(result.notes, Some("Test notes".to_string())); assert_eq!( @@ -166,7 +166,7 @@ mod tests { auth_type: AuthType::None, }; let mut send_1 = store.encrypt(send_view_1).unwrap(); - send_1.id = Some(send_id_1); + send_1.id = Some(crate::send::SendId::new(send_id_1)); repository.set(send_id_1, send_1).await.unwrap(); let send_id_2 = uuid!("36afb22c-9c95-4db5-8bac-c21cb204a3f2"); @@ -195,7 +195,7 @@ mod tests { auth_type: AuthType::None, }; let mut send_2 = store.encrypt(send_view_2).unwrap(); - send_2.id = Some(send_id_2); + send_2.id = Some(crate::send::SendId::new(send_id_2)); repository.set(send_id_2, send_2).await.unwrap(); // Test listing all sends @@ -207,8 +207,8 @@ mod tests { let send1 = result.iter().find(|s| s.name == "Send 1").unwrap(); let send2 = result.iter().find(|s| s.name == "Send 2").unwrap(); - assert_eq!(send1.id, Some(send_id_1)); - assert_eq!(send2.id, Some(send_id_2)); + assert_eq!(send1.id, Some(crate::send::SendId::new(send_id_1))); + assert_eq!(send2.id, Some(crate::send::SendId::new(send_id_2))); } #[tokio::test] diff --git a/crates/bitwarden-send/src/lib.rs b/crates/bitwarden-send/src/lib.rs index 3791ed4bb1..cbe35ae74c 100644 --- a/crates/bitwarden-send/src/lib.rs +++ b/crates/bitwarden-send/src/lib.rs @@ -17,6 +17,6 @@ pub use send_client::{ }; mod send; pub use send::{ - AuthType, Send, SendAuthType, SendFileView, SendListView, SendTextView, SendType, SendView, - SendViewType, + AuthType, Send, SendAuthType, SendFileView, SendId, SendListView, SendTextView, SendType, + SendView, SendViewType, }; diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index ffa309a1c4..6739bea06d 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -8,6 +8,7 @@ use bitwarden_crypto::{ OctetStreamBytes, PrimitiveEncryptable, generate_random_bytes, }; use bitwarden_encoding::{B64, B64Url}; +use bitwarden_uuid::uuid_newtype; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -19,6 +20,8 @@ use {tsify::Tsify, wasm_bindgen::prelude::*}; use crate::SendParseError; pub const SEND_ITERATIONS: u32 = 100_000; +uuid_newtype!(pub SendId); + /// File-based send content #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -128,6 +131,26 @@ impl SendAuthType { SendAuthType::Emails { .. } => AuthType::Email, } } + + /// Returns the password if this is a Password variant, emails if this is an Emails variant, or + /// None otherwise + pub fn auth_data(&self, k: Vec) -> (Option, Option) { + match self { + SendAuthType::Password { password } => { + let hashed = bitwarden_crypto::pbkdf2(password.as_bytes(), &k, SEND_ITERATIONS); + (Some(B64::from(hashed.as_slice()).to_string()), None) + } + SendAuthType::Emails { emails } => { + let emails_str = if emails.is_empty() { + None + } else { + Some(emails.join(",")) + }; + (None, emails_str) + } + SendAuthType::None => (None, None), + } + } } /// View model for decrypted Send type @@ -189,7 +212,7 @@ impl SendViewType { #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct Send { - pub id: Option, + pub id: Option, pub access_id: Option, pub name: EncString, @@ -226,7 +249,7 @@ bitwarden_state::register_repository_item!(Uuid => Send, "Send"); #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct SendView { - pub id: Option, + pub id: Option, pub access_id: Option, pub name: String, @@ -267,7 +290,7 @@ pub struct SendView { #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct SendListView { - pub id: Option, + pub id: Option, pub access_id: Option, pub name: String, @@ -521,7 +544,11 @@ impl TryFrom for Send { } }; Ok(Send { - id: send.id, + id: Some( + send.id + .map(SendId::new) + .unwrap_or_else(|| SendId::new(Uuid::new_v4())), + ), access_id: send.access_id, name: require!(send.name).parse()?, notes: EncString::try_from_optional(send.notes)?, diff --git a/crates/bitwarden-user-crypto-management/src/key_rotation/data.rs b/crates/bitwarden-user-crypto-management/src/key_rotation/data.rs index 86cd245721..bd07805eb0 100644 --- a/crates/bitwarden-user-crypto-management/src/key_rotation/data.rs +++ b/crates/bitwarden-user-crypto-management/src/key_rotation/data.rs @@ -71,7 +71,7 @@ pub(super) fn reencrypt_data( .into_iter() .map(|send| { Ok(SendWithIdRequestModel { - id: send.id.ok_or(DataReencryptionError::DataConversion)?, + id: send.id.ok_or(DataReencryptionError::DataConversion)?.into(), key: send.key.to_string(), // During key-rotation only the "key" (encrypted seed) and id are used, // since we only re-encrypt the "key" diff --git a/crates/bitwarden-user-crypto-management/src/key_rotation/sync.rs b/crates/bitwarden-user-crypto-management/src/key_rotation/sync.rs index d8313ff402..2ef1acbf02 100644 --- a/crates/bitwarden-user-crypto-management/src/key_rotation/sync.rs +++ b/crates/bitwarden-user-crypto-management/src/key_rotation/sync.rs @@ -380,6 +380,7 @@ mod tests { }, }; use bitwarden_encoding::B64; + use bitwarden_send::SendId; use bitwarden_vault::{CipherId, FolderId}; use super::*; @@ -666,7 +667,7 @@ mod tests { // Verify sends assert_eq!(data.sends.len(), 1); - assert_eq!(data.sends[0].id, Some(send_id)); + assert_eq!(data.sends[0].id, Some(SendId::new(send_id))); assert_eq!(data.sends[0].name, TEST_ENC_STRING.parse().unwrap()); assert_eq!(data.sends[0].key, KEY_ENC_STRING.parse().unwrap()); From 609656d06e7f59b9f385f8f932d9004227ec5820 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Tue, 17 Mar 2026 12:11:25 -0400 Subject: [PATCH 29/44] readd comments --- .../bitwarden-commercial-vault/Cargo.toml | 8 ++------ crates/bitwarden-auth/Cargo.toml | 10 +++------- crates/bitwarden-collections/Cargo.toml | 8 ++------ crates/bitwarden-core/Cargo.toml | 10 ++++------ crates/bitwarden-crypto/Cargo.toml | 6 +++--- crates/bitwarden-error/Cargo.toml | 7 +------ crates/bitwarden-exporters/Cargo.toml | 4 ++-- crates/bitwarden-generators/Cargo.toml | 8 ++------ crates/bitwarden-ipc/Cargo.toml | 2 +- crates/bitwarden-policies/Cargo.toml | 4 ++-- .../bitwarden-server-communication-config/Cargo.toml | 4 ++-- crates/bitwarden-ssh/Cargo.toml | 12 +++--------- crates/bitwarden-threading/Cargo.toml | 2 +- crates/bitwarden-user-crypto-management/Cargo.toml | 8 ++------ crates/bitwarden-vault/Cargo.toml | 4 ++-- crates/bitwarden-wasm-internal/Cargo.toml | 2 +- 16 files changed, 33 insertions(+), 66 deletions(-) diff --git a/bitwarden_license/bitwarden-commercial-vault/Cargo.toml b/bitwarden_license/bitwarden-commercial-vault/Cargo.toml index 6367131792..a1e54ab35d 100644 --- a/bitwarden_license/bitwarden-commercial-vault/Cargo.toml +++ b/bitwarden_license/bitwarden-commercial-vault/Cargo.toml @@ -15,12 +15,8 @@ license-file = "../../LICENSE_SDK.txt" keywords.workspace = true [features] -uniffi = ["dep:uniffi"] # Uniffi bindings -wasm = [ - "dep:tsify", - "dep:wasm-bindgen", - "dep:wasm-bindgen-futures", -] # WASM support +uniffi = ["dep:uniffi"] +wasm = ["dep:tsify", "dep:wasm-bindgen", "dep:wasm-bindgen-futures"] # WARNING: This feature is for debugging purposes only and should never be enabled in production. # It disables compile-time debug prevention for cryptographic keys, exposing sensitive key material # in debug output, and adds tracing debug logs to encrypt/decrypt operations. diff --git a/crates/bitwarden-auth/Cargo.toml b/crates/bitwarden-auth/Cargo.toml index 2d8d342e8b..cdfce5dcb1 100644 --- a/crates/bitwarden-auth/Cargo.toml +++ b/crates/bitwarden-auth/Cargo.toml @@ -22,13 +22,9 @@ wasm = [ "dep:tsify", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", -] # WASM support -uniffi = [ - "bitwarden-core/uniffi", - "bitwarden-policies/uniffi", - "dep:uniffi", -] # Uniffi bindings -secrets = ["bitwarden-core/secrets"] # Secrets Manager support +] +uniffi = ["bitwarden-core/uniffi", "bitwarden-policies/uniffi", "dep:uniffi"] +secrets = ["bitwarden-core/secrets"] # Note: dependencies must be alphabetized to pass the cargo sort check in the CI pipeline. [dependencies] diff --git a/crates/bitwarden-collections/Cargo.toml b/crates/bitwarden-collections/Cargo.toml index 6c144ba9da..871a8b0bb3 100644 --- a/crates/bitwarden-collections/Cargo.toml +++ b/crates/bitwarden-collections/Cargo.toml @@ -11,12 +11,8 @@ readme.workspace = true keywords.workspace = true [features] -uniffi = [ - "bitwarden-core/uniffi", - "bitwarden-crypto/uniffi", - "dep:uniffi", -] # Uniffi bindings -wasm = ["bitwarden-core/wasm", "dep:tsify", "dep:wasm-bindgen"] # WASM support +uniffi = ["bitwarden-core/uniffi", "bitwarden-crypto/uniffi", "dep:uniffi"] +wasm = ["bitwarden-core/wasm", "dep:tsify", "dep:wasm-bindgen"] [dependencies] bitwarden-api-api = { workspace = true } diff --git a/crates/bitwarden-core/Cargo.toml b/crates/bitwarden-core/Cargo.toml index f792e25909..d0252898ca 100644 --- a/crates/bitwarden-core/Cargo.toml +++ b/crates/bitwarden-core/Cargo.toml @@ -16,17 +16,15 @@ keywords.workspace = true [features] internal = ["dep:zxcvbn"] -no-memory-hardening = [ - "bitwarden-crypto/no-memory-hardening", -] # Disable memory hardening features -secrets = [] # Secrets manager API +no-memory-hardening = ["bitwarden-crypto/no-memory-hardening"] +secrets = [] uniffi = [ "internal", "bitwarden-crypto/uniffi", "bitwarden-encoding/uniffi", "dep:bitwarden-uniffi-error", "dep:uniffi", -] # Uniffi bindings +] wasm = [ "bitwarden-crypto/wasm", "bitwarden-encoding/wasm", @@ -35,7 +33,7 @@ wasm = [ "dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:tsify", -] # WASM support +] # WARNING: This feature is for debugging purposes only and should never be enabled in production. # It disables compile-time debug prevention for cryptographic keys, exposing sensitive key material # in debug output, and adds tracing debug logs to encrypt/decrypt operations. diff --git a/crates/bitwarden-crypto/Cargo.toml b/crates/bitwarden-crypto/Cargo.toml index d50e59ee46..b7ec86768c 100644 --- a/crates/bitwarden-crypto/Cargo.toml +++ b/crates/bitwarden-crypto/Cargo.toml @@ -16,14 +16,14 @@ keywords.workspace = true [features] default = [] -no-memory-hardening = [] # Disable memory hardening features +no-memory-hardening = [] uniffi = [ "argon2/parallel", "bitwarden-encoding/uniffi", "dep:bitwarden-uniffi-error", "dep:uniffi", -] # Uniffi bindings -wasm = ["dep:tsify", "dep:wasm-bindgen"] # WASM support +] +wasm = ["dep:tsify", "dep:wasm-bindgen"] # WARNING: This feature is for debugging purposes only and should never be enabled in production. # It disables compile-time debug prevention for cryptographic keys, exposing sensitive key material # in debug output, and adds tracing debug logs to encrypt/decrypt operations. diff --git a/crates/bitwarden-error/Cargo.toml b/crates/bitwarden-error/Cargo.toml index ebbfe7a8c9..81f001c4b1 100644 --- a/crates/bitwarden-error/Cargo.toml +++ b/crates/bitwarden-error/Cargo.toml @@ -15,12 +15,7 @@ license-file.workspace = true keywords.workspace = true [features] -wasm = [ - "bitwarden-error-macro/wasm", - "dep:js-sys", - "dep:tsify", - "dep:wasm-bindgen", -] +wasm = ["bitwarden-error-macro/wasm", "dep:js-sys", "dep:tsify", "dep:wasm-bindgen"] [dependencies] bitwarden-error-macro = { workspace = true } diff --git a/crates/bitwarden-exporters/Cargo.toml b/crates/bitwarden-exporters/Cargo.toml index cbb15ade40..3997fe67b4 100644 --- a/crates/bitwarden-exporters/Cargo.toml +++ b/crates/bitwarden-exporters/Cargo.toml @@ -16,13 +16,13 @@ license-file.workspace = true keywords.workspace = true [features] -uniffi = ["dep:uniffi", "bitwarden-core/uniffi"] # Uniffi bindings +uniffi = ["dep:uniffi", "bitwarden-core/uniffi"] wasm = [ "bitwarden-collections/wasm", "bitwarden-vault/wasm", "dep:tsify", "dep:wasm-bindgen", -] # WebAssembly bindings +] [dependencies] bitwarden-collections = { workspace = true } diff --git a/crates/bitwarden-generators/Cargo.toml b/crates/bitwarden-generators/Cargo.toml index e3f4ea0805..f9ef4d6c42 100644 --- a/crates/bitwarden-generators/Cargo.toml +++ b/crates/bitwarden-generators/Cargo.toml @@ -15,12 +15,8 @@ license-file.workspace = true keywords.workspace = true [features] -uniffi = ["dep:uniffi"] # Uniffi bindings -wasm = [ - "bitwarden-core/wasm", - "dep:tsify", - "dep:wasm-bindgen", -] # WebAssembly bindings +uniffi = ["dep:uniffi"] +wasm = ["bitwarden-core/wasm", "dep:tsify", "dep:wasm-bindgen"] [dependencies] bitwarden-core = { workspace = true, features = ["internal"] } diff --git a/crates/bitwarden-ipc/Cargo.toml b/crates/bitwarden-ipc/Cargo.toml index 50b5f876a4..205c4abb91 100644 --- a/crates/bitwarden-ipc/Cargo.toml +++ b/crates/bitwarden-ipc/Cargo.toml @@ -17,7 +17,7 @@ wasm = [ "dep:js-sys", "bitwarden-error/wasm", "bitwarden-threading/wasm", -] # WASM support +] [dependencies] async-trait = { workspace = true } diff --git a/crates/bitwarden-policies/Cargo.toml b/crates/bitwarden-policies/Cargo.toml index 1e60209c5f..d64f2764c5 100644 --- a/crates/bitwarden-policies/Cargo.toml +++ b/crates/bitwarden-policies/Cargo.toml @@ -11,13 +11,13 @@ readme.workspace = true keywords.workspace = true [features] -uniffi = ["bitwarden-core/uniffi", "dep:uniffi"] # Uniffi bindings +uniffi = ["bitwarden-core/uniffi", "dep:uniffi"] wasm = [ "bitwarden-core/wasm", "dep:tsify", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", -] # WASM support +] [dependencies] bitwarden-api-api = { workspace = true } diff --git a/crates/bitwarden-server-communication-config/Cargo.toml b/crates/bitwarden-server-communication-config/Cargo.toml index b1a2137552..7886e07108 100644 --- a/crates/bitwarden-server-communication-config/Cargo.toml +++ b/crates/bitwarden-server-communication-config/Cargo.toml @@ -18,9 +18,9 @@ wasm = [ "dep:serde-wasm-bindgen", "bitwarden-error/wasm", "bitwarden-threading/wasm", -] # WASM support +] -uniffi = ["dep:uniffi"] # UniFFI support for mobile bindings +uniffi = ["dep:uniffi"] [dependencies] async-trait = { workspace = true } diff --git a/crates/bitwarden-ssh/Cargo.toml b/crates/bitwarden-ssh/Cargo.toml index ebff60cfdd..130287c86b 100644 --- a/crates/bitwarden-ssh/Cargo.toml +++ b/crates/bitwarden-ssh/Cargo.toml @@ -16,15 +16,9 @@ license-file.workspace = true keywords.workspace = true [features] -ecdsa-keys = [ -] # Allow ECDSA key generation and import at runtime (disabled by default, since SSH Agent does not support ECDSA keys yet) -wasm = [ - "bitwarden-error/wasm", - "dep:tsify", - "dep:wasm-bindgen", - "getrandom/wasm_js", -] # WASM support -uniffi = ["dep:uniffi"] # Uniffi bindings +ecdsa-keys = [] +wasm = ["bitwarden-error/wasm", "dep:tsify", "dep:wasm-bindgen", "getrandom/wasm_js"] +uniffi = ["dep:uniffi"] [dependencies] bitwarden-error = { workspace = true } diff --git a/crates/bitwarden-threading/Cargo.toml b/crates/bitwarden-threading/Cargo.toml index 90fddd3251..520efa3145 100644 --- a/crates/bitwarden-threading/Cargo.toml +++ b/crates/bitwarden-threading/Cargo.toml @@ -13,7 +13,7 @@ license-file.workspace = true keywords.workspace = true [package.metadata.cargo-udeps.ignore] -development = ["tokio-test"] # only used in doc-tests +development = ["tokio-test"] [features] wasm = [ diff --git a/crates/bitwarden-user-crypto-management/Cargo.toml b/crates/bitwarden-user-crypto-management/Cargo.toml index 7b55e26095..15e2d5e34e 100644 --- a/crates/bitwarden-user-crypto-management/Cargo.toml +++ b/crates/bitwarden-user-crypto-management/Cargo.toml @@ -21,12 +21,8 @@ wasm = [ "dep:tsify", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", -] # WASM support -uniffi = [ - "bitwarden-core/uniffi", - "bitwarden-send/uniffi", - "dep:uniffi", -] # Uniffi bindings +] +uniffi = ["bitwarden-core/uniffi", "bitwarden-send/uniffi", "dep:uniffi"] # Note: dependencies must be alphabetized to pass the cargo sort check in the CI pipeline. [dependencies] diff --git a/crates/bitwarden-vault/Cargo.toml b/crates/bitwarden-vault/Cargo.toml index 1067459899..ed1fbe561c 100644 --- a/crates/bitwarden-vault/Cargo.toml +++ b/crates/bitwarden-vault/Cargo.toml @@ -20,7 +20,7 @@ uniffi = [ "bitwarden-core/uniffi", "bitwarden-crypto/uniffi", "dep:uniffi", -] # Uniffi bindings +] wasm = [ "bitwarden-collections/wasm", "bitwarden-core/wasm", @@ -29,7 +29,7 @@ wasm = [ "dep:tsify", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", -] # WASM support +] [dependencies] async-trait = { workspace = true } diff --git a/crates/bitwarden-wasm-internal/Cargo.toml b/crates/bitwarden-wasm-internal/Cargo.toml index b8a0c5072a..f5f9808ab7 100644 --- a/crates/bitwarden-wasm-internal/Cargo.toml +++ b/crates/bitwarden-wasm-internal/Cargo.toml @@ -28,7 +28,7 @@ performance-tracing = [] [dependencies] async-trait = { workspace = true } bitwarden-commercial-vault = { workspace = true, optional = true, features = [ - "wasm", + "wasm", ] } bitwarden-core = { workspace = true, features = ["wasm", "internal"] } bitwarden-crypto = { workspace = true, features = ["wasm"] } From 55e9eb4ca7c8ee31e10514eafc8450fcb2ba828d Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Tue, 17 Mar 2026 12:15:18 -0400 Subject: [PATCH 30/44] Revert "readd comments" This reverts commit 609656d06e7f59b9f385f8f932d9004227ec5820. --- .../bitwarden-commercial-vault/Cargo.toml | 8 ++++++-- crates/bitwarden-auth/Cargo.toml | 10 +++++++--- crates/bitwarden-collections/Cargo.toml | 8 ++++++-- crates/bitwarden-core/Cargo.toml | 10 ++++++---- crates/bitwarden-crypto/Cargo.toml | 6 +++--- crates/bitwarden-error/Cargo.toml | 7 ++++++- crates/bitwarden-exporters/Cargo.toml | 4 ++-- crates/bitwarden-generators/Cargo.toml | 8 ++++++-- crates/bitwarden-ipc/Cargo.toml | 2 +- crates/bitwarden-policies/Cargo.toml | 4 ++-- .../bitwarden-server-communication-config/Cargo.toml | 4 ++-- crates/bitwarden-ssh/Cargo.toml | 12 +++++++++--- crates/bitwarden-threading/Cargo.toml | 2 +- crates/bitwarden-user-crypto-management/Cargo.toml | 8 ++++++-- crates/bitwarden-vault/Cargo.toml | 4 ++-- crates/bitwarden-wasm-internal/Cargo.toml | 2 +- 16 files changed, 66 insertions(+), 33 deletions(-) diff --git a/bitwarden_license/bitwarden-commercial-vault/Cargo.toml b/bitwarden_license/bitwarden-commercial-vault/Cargo.toml index a1e54ab35d..6367131792 100644 --- a/bitwarden_license/bitwarden-commercial-vault/Cargo.toml +++ b/bitwarden_license/bitwarden-commercial-vault/Cargo.toml @@ -15,8 +15,12 @@ license-file = "../../LICENSE_SDK.txt" keywords.workspace = true [features] -uniffi = ["dep:uniffi"] -wasm = ["dep:tsify", "dep:wasm-bindgen", "dep:wasm-bindgen-futures"] +uniffi = ["dep:uniffi"] # Uniffi bindings +wasm = [ + "dep:tsify", + "dep:wasm-bindgen", + "dep:wasm-bindgen-futures", +] # WASM support # WARNING: This feature is for debugging purposes only and should never be enabled in production. # It disables compile-time debug prevention for cryptographic keys, exposing sensitive key material # in debug output, and adds tracing debug logs to encrypt/decrypt operations. diff --git a/crates/bitwarden-auth/Cargo.toml b/crates/bitwarden-auth/Cargo.toml index cdfce5dcb1..2d8d342e8b 100644 --- a/crates/bitwarden-auth/Cargo.toml +++ b/crates/bitwarden-auth/Cargo.toml @@ -22,9 +22,13 @@ wasm = [ "dep:tsify", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", -] -uniffi = ["bitwarden-core/uniffi", "bitwarden-policies/uniffi", "dep:uniffi"] -secrets = ["bitwarden-core/secrets"] +] # WASM support +uniffi = [ + "bitwarden-core/uniffi", + "bitwarden-policies/uniffi", + "dep:uniffi", +] # Uniffi bindings +secrets = ["bitwarden-core/secrets"] # Secrets Manager support # Note: dependencies must be alphabetized to pass the cargo sort check in the CI pipeline. [dependencies] diff --git a/crates/bitwarden-collections/Cargo.toml b/crates/bitwarden-collections/Cargo.toml index 871a8b0bb3..6c144ba9da 100644 --- a/crates/bitwarden-collections/Cargo.toml +++ b/crates/bitwarden-collections/Cargo.toml @@ -11,8 +11,12 @@ readme.workspace = true keywords.workspace = true [features] -uniffi = ["bitwarden-core/uniffi", "bitwarden-crypto/uniffi", "dep:uniffi"] -wasm = ["bitwarden-core/wasm", "dep:tsify", "dep:wasm-bindgen"] +uniffi = [ + "bitwarden-core/uniffi", + "bitwarden-crypto/uniffi", + "dep:uniffi", +] # Uniffi bindings +wasm = ["bitwarden-core/wasm", "dep:tsify", "dep:wasm-bindgen"] # WASM support [dependencies] bitwarden-api-api = { workspace = true } diff --git a/crates/bitwarden-core/Cargo.toml b/crates/bitwarden-core/Cargo.toml index d0252898ca..f792e25909 100644 --- a/crates/bitwarden-core/Cargo.toml +++ b/crates/bitwarden-core/Cargo.toml @@ -16,15 +16,17 @@ keywords.workspace = true [features] internal = ["dep:zxcvbn"] -no-memory-hardening = ["bitwarden-crypto/no-memory-hardening"] -secrets = [] +no-memory-hardening = [ + "bitwarden-crypto/no-memory-hardening", +] # Disable memory hardening features +secrets = [] # Secrets manager API uniffi = [ "internal", "bitwarden-crypto/uniffi", "bitwarden-encoding/uniffi", "dep:bitwarden-uniffi-error", "dep:uniffi", -] +] # Uniffi bindings wasm = [ "bitwarden-crypto/wasm", "bitwarden-encoding/wasm", @@ -33,7 +35,7 @@ wasm = [ "dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:tsify", -] +] # WASM support # WARNING: This feature is for debugging purposes only and should never be enabled in production. # It disables compile-time debug prevention for cryptographic keys, exposing sensitive key material # in debug output, and adds tracing debug logs to encrypt/decrypt operations. diff --git a/crates/bitwarden-crypto/Cargo.toml b/crates/bitwarden-crypto/Cargo.toml index b7ec86768c..d50e59ee46 100644 --- a/crates/bitwarden-crypto/Cargo.toml +++ b/crates/bitwarden-crypto/Cargo.toml @@ -16,14 +16,14 @@ keywords.workspace = true [features] default = [] -no-memory-hardening = [] +no-memory-hardening = [] # Disable memory hardening features uniffi = [ "argon2/parallel", "bitwarden-encoding/uniffi", "dep:bitwarden-uniffi-error", "dep:uniffi", -] -wasm = ["dep:tsify", "dep:wasm-bindgen"] +] # Uniffi bindings +wasm = ["dep:tsify", "dep:wasm-bindgen"] # WASM support # WARNING: This feature is for debugging purposes only and should never be enabled in production. # It disables compile-time debug prevention for cryptographic keys, exposing sensitive key material # in debug output, and adds tracing debug logs to encrypt/decrypt operations. diff --git a/crates/bitwarden-error/Cargo.toml b/crates/bitwarden-error/Cargo.toml index 81f001c4b1..ebbfe7a8c9 100644 --- a/crates/bitwarden-error/Cargo.toml +++ b/crates/bitwarden-error/Cargo.toml @@ -15,7 +15,12 @@ license-file.workspace = true keywords.workspace = true [features] -wasm = ["bitwarden-error-macro/wasm", "dep:js-sys", "dep:tsify", "dep:wasm-bindgen"] +wasm = [ + "bitwarden-error-macro/wasm", + "dep:js-sys", + "dep:tsify", + "dep:wasm-bindgen", +] [dependencies] bitwarden-error-macro = { workspace = true } diff --git a/crates/bitwarden-exporters/Cargo.toml b/crates/bitwarden-exporters/Cargo.toml index 3997fe67b4..cbb15ade40 100644 --- a/crates/bitwarden-exporters/Cargo.toml +++ b/crates/bitwarden-exporters/Cargo.toml @@ -16,13 +16,13 @@ license-file.workspace = true keywords.workspace = true [features] -uniffi = ["dep:uniffi", "bitwarden-core/uniffi"] +uniffi = ["dep:uniffi", "bitwarden-core/uniffi"] # Uniffi bindings wasm = [ "bitwarden-collections/wasm", "bitwarden-vault/wasm", "dep:tsify", "dep:wasm-bindgen", -] +] # WebAssembly bindings [dependencies] bitwarden-collections = { workspace = true } diff --git a/crates/bitwarden-generators/Cargo.toml b/crates/bitwarden-generators/Cargo.toml index f9ef4d6c42..e3f4ea0805 100644 --- a/crates/bitwarden-generators/Cargo.toml +++ b/crates/bitwarden-generators/Cargo.toml @@ -15,8 +15,12 @@ license-file.workspace = true keywords.workspace = true [features] -uniffi = ["dep:uniffi"] -wasm = ["bitwarden-core/wasm", "dep:tsify", "dep:wasm-bindgen"] +uniffi = ["dep:uniffi"] # Uniffi bindings +wasm = [ + "bitwarden-core/wasm", + "dep:tsify", + "dep:wasm-bindgen", +] # WebAssembly bindings [dependencies] bitwarden-core = { workspace = true, features = ["internal"] } diff --git a/crates/bitwarden-ipc/Cargo.toml b/crates/bitwarden-ipc/Cargo.toml index 205c4abb91..50b5f876a4 100644 --- a/crates/bitwarden-ipc/Cargo.toml +++ b/crates/bitwarden-ipc/Cargo.toml @@ -17,7 +17,7 @@ wasm = [ "dep:js-sys", "bitwarden-error/wasm", "bitwarden-threading/wasm", -] +] # WASM support [dependencies] async-trait = { workspace = true } diff --git a/crates/bitwarden-policies/Cargo.toml b/crates/bitwarden-policies/Cargo.toml index d64f2764c5..1e60209c5f 100644 --- a/crates/bitwarden-policies/Cargo.toml +++ b/crates/bitwarden-policies/Cargo.toml @@ -11,13 +11,13 @@ readme.workspace = true keywords.workspace = true [features] -uniffi = ["bitwarden-core/uniffi", "dep:uniffi"] +uniffi = ["bitwarden-core/uniffi", "dep:uniffi"] # Uniffi bindings wasm = [ "bitwarden-core/wasm", "dep:tsify", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", -] +] # WASM support [dependencies] bitwarden-api-api = { workspace = true } diff --git a/crates/bitwarden-server-communication-config/Cargo.toml b/crates/bitwarden-server-communication-config/Cargo.toml index 7886e07108..b1a2137552 100644 --- a/crates/bitwarden-server-communication-config/Cargo.toml +++ b/crates/bitwarden-server-communication-config/Cargo.toml @@ -18,9 +18,9 @@ wasm = [ "dep:serde-wasm-bindgen", "bitwarden-error/wasm", "bitwarden-threading/wasm", -] +] # WASM support -uniffi = ["dep:uniffi"] +uniffi = ["dep:uniffi"] # UniFFI support for mobile bindings [dependencies] async-trait = { workspace = true } diff --git a/crates/bitwarden-ssh/Cargo.toml b/crates/bitwarden-ssh/Cargo.toml index 130287c86b..ebff60cfdd 100644 --- a/crates/bitwarden-ssh/Cargo.toml +++ b/crates/bitwarden-ssh/Cargo.toml @@ -16,9 +16,15 @@ license-file.workspace = true keywords.workspace = true [features] -ecdsa-keys = [] -wasm = ["bitwarden-error/wasm", "dep:tsify", "dep:wasm-bindgen", "getrandom/wasm_js"] -uniffi = ["dep:uniffi"] +ecdsa-keys = [ +] # Allow ECDSA key generation and import at runtime (disabled by default, since SSH Agent does not support ECDSA keys yet) +wasm = [ + "bitwarden-error/wasm", + "dep:tsify", + "dep:wasm-bindgen", + "getrandom/wasm_js", +] # WASM support +uniffi = ["dep:uniffi"] # Uniffi bindings [dependencies] bitwarden-error = { workspace = true } diff --git a/crates/bitwarden-threading/Cargo.toml b/crates/bitwarden-threading/Cargo.toml index 520efa3145..90fddd3251 100644 --- a/crates/bitwarden-threading/Cargo.toml +++ b/crates/bitwarden-threading/Cargo.toml @@ -13,7 +13,7 @@ license-file.workspace = true keywords.workspace = true [package.metadata.cargo-udeps.ignore] -development = ["tokio-test"] +development = ["tokio-test"] # only used in doc-tests [features] wasm = [ diff --git a/crates/bitwarden-user-crypto-management/Cargo.toml b/crates/bitwarden-user-crypto-management/Cargo.toml index 15e2d5e34e..7b55e26095 100644 --- a/crates/bitwarden-user-crypto-management/Cargo.toml +++ b/crates/bitwarden-user-crypto-management/Cargo.toml @@ -21,8 +21,12 @@ wasm = [ "dep:tsify", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", -] -uniffi = ["bitwarden-core/uniffi", "bitwarden-send/uniffi", "dep:uniffi"] +] # WASM support +uniffi = [ + "bitwarden-core/uniffi", + "bitwarden-send/uniffi", + "dep:uniffi", +] # Uniffi bindings # Note: dependencies must be alphabetized to pass the cargo sort check in the CI pipeline. [dependencies] diff --git a/crates/bitwarden-vault/Cargo.toml b/crates/bitwarden-vault/Cargo.toml index ed1fbe561c..1067459899 100644 --- a/crates/bitwarden-vault/Cargo.toml +++ b/crates/bitwarden-vault/Cargo.toml @@ -20,7 +20,7 @@ uniffi = [ "bitwarden-core/uniffi", "bitwarden-crypto/uniffi", "dep:uniffi", -] +] # Uniffi bindings wasm = [ "bitwarden-collections/wasm", "bitwarden-core/wasm", @@ -29,7 +29,7 @@ wasm = [ "dep:tsify", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", -] +] # WASM support [dependencies] async-trait = { workspace = true } diff --git a/crates/bitwarden-wasm-internal/Cargo.toml b/crates/bitwarden-wasm-internal/Cargo.toml index f5f9808ab7..b8a0c5072a 100644 --- a/crates/bitwarden-wasm-internal/Cargo.toml +++ b/crates/bitwarden-wasm-internal/Cargo.toml @@ -28,7 +28,7 @@ performance-tracing = [] [dependencies] async-trait = { workspace = true } bitwarden-commercial-vault = { workspace = true, optional = true, features = [ - "wasm", + "wasm", ] } bitwarden-core = { workspace = true, features = ["wasm", "internal"] } bitwarden-crypto = { workspace = true, features = ["wasm"] } From 51eef80248ca8858642e6b2b428441006b5546a0 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Thu, 19 Mar 2026 08:38:38 -0400 Subject: [PATCH 31/44] readd comments in cargo --- crates/bitwarden-pm/Cargo.toml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden-pm/Cargo.toml b/crates/bitwarden-pm/Cargo.toml index 5b48bc95f3..4e098f0596 100644 --- a/crates/bitwarden-pm/Cargo.toml +++ b/crates/bitwarden-pm/Cargo.toml @@ -15,7 +15,9 @@ license-file.workspace = true keywords.workspace = true [features] -no-memory-hardening = ["bitwarden-core/no-memory-hardening"] +no-memory-hardening = [ + "bitwarden-core/no-memory-hardening", +] # Disable memory hardening features uniffi = [ "bitwarden-core/uniffi", "bitwarden-exporters/uniffi", @@ -25,7 +27,7 @@ uniffi = [ "bitwarden-state/uniffi", "bitwarden-vault/uniffi", "dep:uniffi", -] +] # Uniffi bindings wasm = [ "bitwarden-auth/wasm", "bitwarden-commercial-vault/wasm", @@ -38,7 +40,7 @@ wasm = [ "dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:tsify", -] +] # WASM support bitwarden-license = ["dep:bitwarden-commercial-vault"] [dependencies] From 40a52c044ff9cc55842609a09d776a96ddde14ea Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Thu, 19 Mar 2026 09:24:31 -0400 Subject: [PATCH 32/44] fix lint --- crates/bitwarden-send/src/send.rs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index ff68602651..f900988024 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -243,7 +243,6 @@ pub struct Send { pub auth_type: AuthType, } - bitwarden_state::register_repository_item!(Uuid => Send, "Send"); impl From for SendWithIdRequestModel { @@ -272,7 +271,8 @@ impl From for SendWithIdRequestModel { hide_email: Some(send.hide_email), id: send .id - .expect("SendWithIdRequestModel conversion requires send id"), + .expect("SendWithIdRequestModel conversion requires send id") + .into(), } } } @@ -642,15 +642,6 @@ impl TryFrom for AuthType { } } -impl From for bitwarden_api_api::models::SendType { - fn from(t: SendType) -> Self { - match t { - SendType::Text => bitwarden_api_api::models::SendType::Text, - SendType::File => bitwarden_api_api::models::SendType::File, - } - } -} - impl From for bitwarden_api_api::models::AuthType { fn from(value: AuthType) -> Self { match value { @@ -976,7 +967,7 @@ mod tests { let text_value = "2.2VPyLzk1tMLug0X3x7RkaQ==|mrMt9vbZsCJhJIj4eebKyg==|aZ7JeyndytEMR1+uEBupEvaZuUE69D/ejhfdJL8oKq0="; let send = Send { - id: Some(send_id), + id: Some(SendId::new(send_id)), access_id: Some("ct2APRQtJk-BLLDwAYqhRA".to_string()), name: name.parse().unwrap(), notes: Some(notes.parse().unwrap()), From 68a0b6740221faf73336d249f3ff567226a58026 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Thu, 19 Mar 2026 12:41:06 -0400 Subject: [PATCH 33/44] npm fix --- crates/bitwarden-send/src/send.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index f900988024..c19e754a08 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -120,7 +120,7 @@ pub enum SendAuthType { /// Email-based OTP authentication Emails { /// List of email addresses that will receive OTP codes - emails: Vec, + emails: String, }, } @@ -146,7 +146,7 @@ impl SendAuthType { let emails_str = if emails.is_empty() { None } else { - Some(emails.join(",")) + Some(emails.clone()) }; (None, emails_str) } From 5fe934dbb131697f4f1971fc9b14a888b1a936c6 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Fri, 20 Mar 2026 13:07:21 -0400 Subject: [PATCH 34/44] fix wasm --- crates/bitwarden-send/src/edit.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bitwarden-send/src/edit.rs b/crates/bitwarden-send/src/edit.rs index ae48c828c1..9296f384ea 100644 --- a/crates/bitwarden-send/src/edit.rs +++ b/crates/bitwarden-send/src/edit.rs @@ -68,7 +68,6 @@ pub struct SendEditRequest { /// Use `SendAuthType::None` for no authentication, /// `SendAuthType::Password` for password protection, or /// `SendAuthType::Emails` for email OTP authentication. - #[serde(flatten)] pub auth: SendAuthType, } From f91ca2e822f5d875cd97b4570a6930e1f2c52563 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Mon, 23 Mar 2026 09:41:23 -0400 Subject: [PATCH 35/44] Update crates/bitwarden-send/src/send.rs Co-authored-by: Oscar Hinton --- crates/bitwarden-send/src/send.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index c19e754a08..3ece1f4fc5 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -578,11 +578,7 @@ impl TryFrom for Send { } }; Ok(Send { - id: Some( - send.id - .map(SendId::new) - .unwrap_or_else(|| SendId::new(Uuid::new_v4())), - ), + id: send.id.map(SendId::new), access_id: send.access_id, name: require!(send.name).parse()?, notes: EncString::try_from_optional(send.notes)?, From 032a198504a7608d45b5b895725cc9e7012b865a Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Mon, 23 Mar 2026 09:41:34 -0400 Subject: [PATCH 36/44] Update crates/bitwarden-send/src/send.rs Co-authored-by: Oscar Hinton --- crates/bitwarden-send/src/send.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index 3ece1f4fc5..48c5ec6d28 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -172,20 +172,18 @@ type SendApiModels = ( Option>, ); -impl SendViewType { - /// Converts the SendViewType into API models for creating or editing a Send. - /// Returns a tuple of (SendType, optional FileModel, optional TextModel). - pub(crate) fn into_api_models( - self, +impl CompositeEncryptable for SendViewType { + fn encrypt_composite( + &self, ctx: &mut KeyStoreContext, - send_key: SymmetricKeyId, + key: SymmetricKeyId, ) -> Result { match self { SendViewType::File(f) => Ok(( bitwarden_api_api::models::SendType::File, Some(Box::new(bitwarden_api_api::models::SendFileModel { id: f.id.clone(), - file_name: Some(f.file_name.encrypt(ctx, send_key)?.to_string()), + file_name: Some(f.file_name.encrypt(ctx, key)?.to_string()), size: f.size.as_ref().and_then(|s| s.parse::().ok()), size_name: f.size_name.clone(), })), @@ -198,7 +196,7 @@ impl SendViewType { text: t .text .as_ref() - .map(|txt| txt.encrypt(ctx, send_key)) + .map(|txt| txt.encrypt(ctx, key)) .transpose()? .map(|e| e.to_string()), hidden: Some(t.hidden), From f5578b57756d862126fa5bc88623fc95ccf2b78f Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Mon, 23 Mar 2026 09:42:15 -0400 Subject: [PATCH 37/44] Update crates/bitwarden-send/src/send.rs Co-authored-by: Oscar Hinton --- crates/bitwarden-send/src/send.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index 48c5ec6d28..eab31c0e49 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -136,7 +136,7 @@ impl SendAuthType { /// Returns the password if this is a Password variant, emails if this is an Emails variant, or /// None otherwise - pub fn auth_data(&self, k: Vec) -> (Option, Option) { + pub(crate) fn auth_data(&self, k: &[u8]) -> (Option, Option) { match self { SendAuthType::Password { password } => { let hashed = bitwarden_crypto::pbkdf2(password.as_bytes(), &k, SEND_ITERATIONS); From 97c33a2977bdabc27404f3db10f6f3e6efa41d18 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Mon, 23 Mar 2026 10:06:49 -0400 Subject: [PATCH 38/44] PR and lint fixes --- crates/bitwarden-send/src/create.rs | 12 +++++------- crates/bitwarden-send/src/edit.rs | 25 +++++++++++++++++-------- crates/bitwarden-send/src/get_list.rs | 19 ++++++++++++++----- crates/bitwarden-send/src/lib.rs | 3 +++ crates/bitwarden-send/src/send.rs | 11 +++++------ 5 files changed, 44 insertions(+), 26 deletions(-) diff --git a/crates/bitwarden-send/src/create.rs b/crates/bitwarden-send/src/create.rs index 2d770a89d2..d5e8402429 100644 --- a/crates/bitwarden-send/src/create.rs +++ b/crates/bitwarden-send/src/create.rs @@ -72,9 +72,9 @@ impl CompositeEncryptable + ?Sized>( let send: Send = resp.try_into()?; - repository - .set(require!(send.id).into(), send.clone()) - .await?; + repository.set(require!(send.id), send.clone()).await?; Ok(key_store.decrypt(&send)?) } @@ -138,7 +136,7 @@ mod tests { use uuid::uuid; use super::*; - use crate::{AuthType, SendTextView, SendType, SendView}; + use crate::{AuthType, SendId, SendTextView, SendType, SendView}; #[tokio::test] async fn test_create_send() { @@ -242,7 +240,7 @@ mod tests { assert_eq!( store .decrypt::( - &repository.get(send_id).await.unwrap().unwrap() + &repository.get(SendId::new(send_id)).await.unwrap().unwrap() ) .unwrap(), result diff --git a/crates/bitwarden-send/src/edit.rs b/crates/bitwarden-send/src/edit.rs index 9296f384ea..5d1f6ac07c 100644 --- a/crates/bitwarden-send/src/edit.rs +++ b/crates/bitwarden-send/src/edit.rs @@ -19,7 +19,7 @@ use uuid::Uuid; use wasm_bindgen::prelude::*; use crate::{ - Send, SendAuthType, SendView, SendViewType, + Send, SendAuthType, SendId, SendView, SendViewType, error::{ItemNotFoundError, SendParseError}, }; @@ -100,9 +100,9 @@ impl CompositeEncryptable + ?Sized>( let id = send_id.to_string(); // Retrieve the existing send to get its key (keys cannot be modified during edit) - let existing_send = repository.get(send_id).await?.ok_or(ItemNotFoundError)?; + let existing_send = repository + .get(SendId::new(send_id)) + .await? + .ok_or(ItemNotFoundError)?; // Decrypt to get the key - we only need the key field let existing_send_view: SendView = key_store.decrypt(&existing_send)?; @@ -174,7 +177,7 @@ pub(super) async fn edit_send + ?Sized>( }); } - repository.set(send_id, send.clone()).await?; + repository.set(SendId::new(send_id), send.clone()).await?; Ok(key_store.decrypt(&send)?) } @@ -231,7 +234,10 @@ mod tests { }; let mut existing_send = store.encrypt(existing_send_view).unwrap(); existing_send.id = Some(crate::send::SendId::new(send_id)); // Set the ID after encryption - repository.set(send_id, existing_send).await.unwrap(); + repository + .set(SendId::new(send_id), existing_send) + .await + .unwrap(); let api_client = ApiClient::new_mocked(move |mock| { mock.sends_api @@ -297,7 +303,7 @@ mod tests { ); // Confirm the send was updated in the repository - let stored = repository.get(send_id).await.unwrap().unwrap(); + let stored = repository.get(SendId::new(send_id)).await.unwrap().unwrap(); assert_eq!( store .decrypt::(&stored) @@ -390,7 +396,10 @@ mod tests { }; let mut existing_send = store.encrypt(existing_send_view).unwrap(); existing_send.id = Some(crate::send::SendId::new(send_id)); // Set the ID after encryption - repository.set(send_id, existing_send).await.unwrap(); + repository + .set(SendId::new(send_id), existing_send) + .await + .unwrap(); let api_client = ApiClient::new_mocked(move |mock| { mock.sends_api.expect_put().returning(move |_id, _model| { diff --git a/crates/bitwarden-send/src/get_list.rs b/crates/bitwarden-send/src/get_list.rs index 75feacb264..186c5c6d85 100644 --- a/crates/bitwarden-send/src/get_list.rs +++ b/crates/bitwarden-send/src/get_list.rs @@ -5,7 +5,7 @@ use bitwarden_state::repository::{Repository, RepositoryError}; use thiserror::Error; use uuid::Uuid; -use crate::{Send, SendView, error::ItemNotFoundError}; +use crate::{Send, SendId, SendView, error::ItemNotFoundError}; #[allow(missing_docs)] #[bitwarden_error(flat)] @@ -26,7 +26,10 @@ pub(super) async fn get_send( repository: &dyn Repository, id: Uuid, ) -> Result { - let send = repository.get(id).await?.ok_or(ItemNotFoundError)?; + let send = repository + .get(SendId::new(id)) + .await? + .ok_or(ItemNotFoundError)?; Ok(store.decrypt(&send)?) } @@ -90,7 +93,7 @@ mod tests { }; let mut send = store.encrypt(send_view).unwrap(); send.id = Some(crate::send::SendId::new(send_id)); - repository.set(send_id, send).await.unwrap(); + repository.set(SendId::new(send_id), send).await.unwrap(); // Test getting the send let result = get_send(&store, &repository, send_id).await.unwrap(); @@ -167,7 +170,10 @@ mod tests { }; let mut send_1 = store.encrypt(send_view_1).unwrap(); send_1.id = Some(crate::send::SendId::new(send_id_1)); - repository.set(send_id_1, send_1).await.unwrap(); + repository + .set(SendId::new(send_id_1), send_1) + .await + .unwrap(); let send_id_2 = uuid!("36afb22c-9c95-4db5-8bac-c21cb204a3f2"); let send_view_2 = SendView { @@ -196,7 +202,10 @@ mod tests { }; let mut send_2 = store.encrypt(send_view_2).unwrap(); send_2.id = Some(crate::send::SendId::new(send_id_2)); - repository.set(send_id_2, send_2).await.unwrap(); + repository + .set(SendId::new(send_id_2), send_2) + .await + .unwrap(); // Test listing all sends let result = list_sends(&store, &repository).await.unwrap(); diff --git a/crates/bitwarden-send/src/lib.rs b/crates/bitwarden-send/src/lib.rs index cbe35ae74c..34a5373157 100644 --- a/crates/bitwarden-send/src/lib.rs +++ b/crates/bitwarden-send/src/lib.rs @@ -6,10 +6,13 @@ uniffi::setup_scaffolding!(); mod uniffi_support; mod create; +pub use create::{CreateSendError, SendAddRequest}; mod edit; +pub use edit::{EditSendError, SendEditRequest}; mod error; pub use error::SendParseError; mod get_list; +pub use get_list::GetSendError; mod send_client; pub use send_client::{ SendClient, SendClientExt, SendDecryptError, SendDecryptFileError, SendEncryptError, diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index eab31c0e49..da56cd99e5 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -14,7 +14,6 @@ use bitwarden_uuid::uuid_newtype; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; -use uuid::Uuid; use zeroize::Zeroizing; #[cfg(feature = "wasm")] use {tsify::Tsify, wasm_bindgen::prelude::*}; @@ -120,7 +119,7 @@ pub enum SendAuthType { /// Email-based OTP authentication Emails { /// List of email addresses that will receive OTP codes - emails: String, + emails: Vec, }, } @@ -139,14 +138,14 @@ impl SendAuthType { pub(crate) fn auth_data(&self, k: &[u8]) -> (Option, Option) { match self { SendAuthType::Password { password } => { - let hashed = bitwarden_crypto::pbkdf2(password.as_bytes(), &k, SEND_ITERATIONS); + let hashed = bitwarden_crypto::pbkdf2(password.as_bytes(), k, SEND_ITERATIONS); (Some(B64::from(hashed.as_slice()).to_string()), None) } SendAuthType::Emails { emails } => { let emails_str = if emails.is_empty() { None } else { - Some(emails.clone()) + Some(emails.join(",")) }; (None, emails_str) } @@ -241,7 +240,7 @@ pub struct Send { pub auth_type: AuthType, } -bitwarden_state::register_repository_item!(Uuid => Send, "Send"); +bitwarden_state::register_repository_item!(SendId => Send, "Send"); impl From for SendWithIdRequestModel { fn from(send: Send) -> Self { @@ -943,7 +942,7 @@ mod tests { #[test] fn test_send_into_send_with_id_request_model() { - let send_id = Uuid::parse_str("3d80dd72-2d14-4f26-812c-b0f0018aa144").unwrap(); + let send_id = "3d80dd72-2d14-4f26-812c-b0f0018aa144".parse().unwrap(); let revision_date = DateTime::parse_from_rfc3339("2024-01-07T23:56:48Z") .unwrap() .with_timezone(&Utc); From 9c4bfe80d9536e9e513ec10158d96fb809b31f7e Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Mon, 23 Mar 2026 10:14:01 -0400 Subject: [PATCH 39/44] add comments --- crates/bitwarden-send/src/create.rs | 9 +++++++++ crates/bitwarden-send/src/edit.rs | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/crates/bitwarden-send/src/create.rs b/crates/bitwarden-send/src/create.rs index d5e8402429..94c15f39f7 100644 --- a/crates/bitwarden-send/src/create.rs +++ b/crates/bitwarden-send/src/create.rs @@ -35,20 +35,29 @@ pub enum CreateSendError { SendParse(#[from] SendParseError), } +/// Request model for creating a new Send. #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct SendAddRequest { + /// The name of the Send. pub name: String, + /// Optional notes visible to the sender. pub notes: Option, + /// The type and content of the Send. pub view_type: SendViewType, + /// Maximum number of times the Send can be accessed. pub max_access_count: Option, + /// Whether the Send is disabled and cannot be accessed. pub disabled: bool, + /// Whether to hide the sender's email from recipients. pub hide_email: bool, + /// Date and time when the Send will be permanently deleted. pub deletion_date: DateTime, + /// Optional date and time when the Send expires and can no longer be accessed. pub expiration_date: Option>, /// Authentication method for accessing this Send. diff --git a/crates/bitwarden-send/src/edit.rs b/crates/bitwarden-send/src/edit.rs index 5d1f6ac07c..2fb3a4dcce 100644 --- a/crates/bitwarden-send/src/edit.rs +++ b/crates/bitwarden-send/src/edit.rs @@ -48,20 +48,29 @@ pub enum EditSendError { }, } +/// Request model for editing an existing Send. #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct SendEditRequest { + /// The name of the Send. pub name: String, + /// Optional notes visible to the sender. pub notes: Option, + /// The type and content of the Send. pub view_type: SendViewType, + /// Maximum number of times the Send can be accessed. pub max_access_count: Option, + /// Whether the Send is disabled and cannot be accessed. pub disabled: bool, + /// Whether to hide the sender's email from recipients. pub hide_email: bool, + /// Date and time when the Send will be permanently deleted. pub deletion_date: DateTime, + /// Optional date and time when the Send expires and can no longer be accessed. pub expiration_date: Option>, /// Authentication method for accessing this Send. From fc1c4b07f1222be5b79afdb223f9b319d74b67c3 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Mon, 23 Mar 2026 10:51:04 -0400 Subject: [PATCH 40/44] PR fix --- crates/bitwarden-send/src/edit.rs | 19 ++++++++----------- crates/bitwarden-send/src/get_list.rs | 14 ++++++-------- crates/bitwarden-send/src/send_client.rs | 12 +++--------- 3 files changed, 17 insertions(+), 28 deletions(-) diff --git a/crates/bitwarden-send/src/edit.rs b/crates/bitwarden-send/src/edit.rs index 2fb3a4dcce..b10680583a 100644 --- a/crates/bitwarden-send/src/edit.rs +++ b/crates/bitwarden-send/src/edit.rs @@ -150,16 +150,13 @@ pub(super) async fn edit_send + ?Sized>( key_store: &KeyStore, api_client: &bitwarden_api_api::apis::ApiClient, repository: &R, - send_id: Uuid, + send_id: SendId, request: SendEditRequest, ) -> Result { let id = send_id.to_string(); // Retrieve the existing send to get its key (keys cannot be modified during edit) - let existing_send = repository - .get(SendId::new(send_id)) - .await? - .ok_or(ItemNotFoundError)?; + let existing_send = repository.get(send_id).await?.ok_or(ItemNotFoundError)?; // Decrypt to get the key - we only need the key field let existing_send_view: SendView = key_store.decrypt(&existing_send)?; @@ -179,14 +176,14 @@ pub(super) async fn edit_send + ?Sized>( let send: Send = resp.try_into()?; // Verify the server returned the correct send ID - if send.id != Some(crate::send::SendId::new(send_id)) { + if send.id != Some(send_id) { return Err(EditSendError::IdMismatch { - expected: send_id, + expected: send_id.into(), returned: send.id.map(Into::into), }); } - repository.set(SendId::new(send_id), send.clone()).await?; + repository.set(send_id, send.clone()).await?; Ok(key_store.decrypt(&send)?) } @@ -282,7 +279,7 @@ mod tests { &store, &api_client, &repository, - send_id, + SendId::new(send_id), SendEditRequest { name: "updated".to_string(), notes: Some("updated notes".to_string()), @@ -340,7 +337,7 @@ mod tests { &store, &api_client, &repository, - send_id, + SendId::new(send_id), SendEditRequest { name: "test".to_string(), notes: None, @@ -422,7 +419,7 @@ mod tests { &store, &api_client, &repository, - send_id, + SendId::new(send_id), SendEditRequest { name: "test".to_string(), notes: None, diff --git a/crates/bitwarden-send/src/get_list.rs b/crates/bitwarden-send/src/get_list.rs index 186c5c6d85..cd2a0f8646 100644 --- a/crates/bitwarden-send/src/get_list.rs +++ b/crates/bitwarden-send/src/get_list.rs @@ -3,7 +3,6 @@ use bitwarden_crypto::{CryptoError, KeyStore}; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::{Repository, RepositoryError}; use thiserror::Error; -use uuid::Uuid; use crate::{Send, SendId, SendView, error::ItemNotFoundError}; @@ -24,12 +23,9 @@ pub enum GetSendError { pub(super) async fn get_send( store: &KeyStore, repository: &dyn Repository, - id: Uuid, + id: SendId, ) -> Result { - let send = repository - .get(SendId::new(id)) - .await? - .ok_or(ItemNotFoundError)?; + let send = repository.get(id).await?.ok_or(ItemNotFoundError)?; Ok(store.decrypt(&send)?) } @@ -96,7 +92,9 @@ mod tests { repository.set(SendId::new(send_id), send).await.unwrap(); // Test getting the send - let result = get_send(&store, &repository, send_id).await.unwrap(); + let result = get_send(&store, &repository, SendId::new(send_id)) + .await + .unwrap(); assert_eq!(result.id, Some(crate::send::SendId::new(send_id))); assert_eq!(result.name, "Test Send"); @@ -124,7 +122,7 @@ mod tests { let repository = MemoryRepository::::default(); // Try to get a send that doesn't exist - let result = get_send(&store, &repository, send_id).await; + let result = get_send(&store, &repository, SendId::new(send_id)).await; assert!(result.is_err()); assert!(matches!(result.unwrap_err(), GetSendError::ItemNotFound(_))); diff --git a/crates/bitwarden-send/src/send_client.rs b/crates/bitwarden-send/src/send_client.rs index 752d5a4200..cd2c2cb60f 100644 --- a/crates/bitwarden-send/src/send_client.rs +++ b/crates/bitwarden-send/src/send_client.rs @@ -6,15 +6,13 @@ use bitwarden_crypto::{ }; use bitwarden_state::repository::{Repository, RepositoryError}; use thiserror::Error; -use uuid::Uuid; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; use crate::{ - Send, SendListView, SendView, + Send, SendId, SendListView, SendView, create::{CreateSendError, SendAddRequest, create_send}, edit::{EditSendError, SendEditRequest, edit_send}, - error::ItemNotFoundError, get_list::{GetSendError, get_send, list_sends}, }; @@ -159,14 +157,12 @@ impl SendClient { /// Edit the [Send] and save it to the server. pub async fn edit( &self, - send_id: String, + send_id: SendId, request: SendEditRequest, ) -> Result { let key_store = self.client.internal.get_key_store(); let config = self.client.internal.get_api_configurations(); let repository = self.get_repository()?; - let send_id = Uuid::parse_str(&send_id) - .map_err(|_| EditSendError::ItemNotFound(ItemNotFoundError))?; edit_send( key_store, @@ -187,11 +183,9 @@ impl SendClient { } /// Get a specific [Send] by its ID from state and decrypt it to a [SendView]. - pub async fn get(&self, send_id: String) -> Result { + pub async fn get(&self, send_id: SendId) -> Result { let key_store = self.client.internal.get_key_store(); let repository = self.get_repository()?; - let send_id = - Uuid::parse_str(&send_id).map_err(|_| GetSendError::ItemNotFound(ItemNotFoundError))?; get_send(key_store, repository.as_ref(), send_id).await } From d06c3623e96f5ea0bbc6ec48c7769c2c96a4ef75 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Mon, 23 Mar 2026 11:38:10 -0400 Subject: [PATCH 41/44] PR fixes --- crates/bitwarden-send/src/create.rs | 4 +--- crates/bitwarden-send/src/edit.rs | 3 --- crates/bitwarden-send/src/send.rs | 3 +++ 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/bitwarden-send/src/create.rs b/crates/bitwarden-send/src/create.rs index 94c15f39f7..a93c630cb3 100644 --- a/crates/bitwarden-send/src/create.rs +++ b/crates/bitwarden-send/src/create.rs @@ -61,9 +61,6 @@ pub struct SendAddRequest { pub expiration_date: Option>, /// Authentication method for accessing this Send. - /// Use `SendAuthType::None` for no authentication, - /// `SendAuthType::Password` for password protection, or - /// `SendAuthType::Emails` for email OTP authentication. pub auth: SendAuthType, } @@ -137,6 +134,7 @@ pub(super) async fn create_send + ?Sized>( Ok(key_store.decrypt(&send)?) } + #[cfg(test)] mod tests { use bitwarden_api_api::{apis::ApiClient, models::SendResponseModel}; diff --git a/crates/bitwarden-send/src/edit.rs b/crates/bitwarden-send/src/edit.rs index b10680583a..18d96b1789 100644 --- a/crates/bitwarden-send/src/edit.rs +++ b/crates/bitwarden-send/src/edit.rs @@ -74,9 +74,6 @@ pub struct SendEditRequest { pub expiration_date: Option>, /// Authentication method for accessing this Send. - /// Use `SendAuthType::None` for no authentication, - /// `SendAuthType::Password` for password protection, or - /// `SendAuthType::Emails` for email OTP authentication. pub auth: SendAuthType, } diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index da56cd99e5..d5cb28c429 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -29,8 +29,11 @@ uuid_newtype!(pub SendId); #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct SendFile { + /// The file's ID pub id: Option, + /// The encrypted file name pub file_name: EncString, + /// The file size in bytes as a string pub size: Option, /// Readable size, ex: "4.2 KB" or "1.43 GB" pub size_name: Option, From 3832bd2f2ca0647c786db9d1beb11d086c868ce5 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Mon, 23 Mar 2026 13:01:40 -0400 Subject: [PATCH 42/44] PR fixes --- crates/bitwarden-send/src/create.rs | 6 +++++- crates/bitwarden-send/src/edit.rs | 6 +++++- crates/bitwarden-send/src/lib.rs | 4 ++-- crates/bitwarden-send/src/send.rs | 17 +++++++++++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-send/src/create.rs b/crates/bitwarden-send/src/create.rs index a93c630cb3..95b6920db9 100644 --- a/crates/bitwarden-send/src/create.rs +++ b/crates/bitwarden-send/src/create.rs @@ -17,7 +17,7 @@ use tsify::Tsify; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; -use crate::{Send, SendAuthType, SendParseError, SendView, SendViewType}; +use crate::{EmptyEmailListError, Send, SendAuthType, SendParseError, SendView, SendViewType}; #[allow(missing_docs)] #[bitwarden_error(flat)] @@ -28,6 +28,8 @@ pub enum CreateSendError { #[error(transparent)] Crypto(#[from] CryptoError), #[error(transparent)] + EmptyEmailList(#[from] EmptyEmailListError), + #[error(transparent)] MissingField(#[from] MissingFieldError), #[error(transparent)] Repository(#[from] RepositoryError), @@ -120,6 +122,8 @@ pub(super) async fn create_send + ?Sized>( repository: &R, request: SendAddRequest, ) -> Result { + request.auth.validate()?; + let send_request = key_store.encrypt(request)?; let resp = api_client diff --git a/crates/bitwarden-send/src/edit.rs b/crates/bitwarden-send/src/edit.rs index 18d96b1789..a1fceb3d84 100644 --- a/crates/bitwarden-send/src/edit.rs +++ b/crates/bitwarden-send/src/edit.rs @@ -19,7 +19,7 @@ use uuid::Uuid; use wasm_bindgen::prelude::*; use crate::{ - Send, SendAuthType, SendId, SendView, SendViewType, + EmptyEmailListError, Send, SendAuthType, SendId, SendView, SendViewType, error::{ItemNotFoundError, SendParseError}, }; @@ -34,6 +34,8 @@ pub enum EditSendError { #[error(transparent)] Api(#[from] ApiError), #[error(transparent)] + EmptyEmailList(#[from] EmptyEmailListError), + #[error(transparent)] MissingField(#[from] MissingFieldError), #[error(transparent)] Repository(#[from] RepositoryError), @@ -150,6 +152,8 @@ pub(super) async fn edit_send + ?Sized>( send_id: SendId, request: SendEditRequest, ) -> Result { + request.auth.validate()?; + let id = send_id.to_string(); // Retrieve the existing send to get its key (keys cannot be modified during edit) diff --git a/crates/bitwarden-send/src/lib.rs b/crates/bitwarden-send/src/lib.rs index 34a5373157..99120a4acf 100644 --- a/crates/bitwarden-send/src/lib.rs +++ b/crates/bitwarden-send/src/lib.rs @@ -20,6 +20,6 @@ pub use send_client::{ }; mod send; pub use send::{ - AuthType, Send, SendAuthType, SendFileView, SendId, SendListView, SendTextView, SendType, - SendView, SendViewType, + AuthType, EmptyEmailListError, Send, SendAuthType, SendFileView, SendId, SendListView, + SendTextView, SendType, SendView, SendViewType, }; diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index d5cb28c429..d6498e484f 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -14,6 +14,7 @@ use bitwarden_uuid::uuid_newtype; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; +use thiserror::Error; use zeroize::Zeroizing; #[cfg(feature = "wasm")] use {tsify::Tsify, wasm_bindgen::prelude::*}; @@ -23,6 +24,11 @@ pub const SEND_ITERATIONS: u32 = 100_000; uuid_newtype!(pub SendId); +/// Error returned when `SendAuthType::Emails` is constructed with an empty email list. +#[derive(Debug, Error)] +#[error("Email authentication requires at least one email address")] +pub struct EmptyEmailListError; + /// File-based send content #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] @@ -136,6 +142,17 @@ impl SendAuthType { } } + /// Validates that the auth configuration is valid. + /// Returns an error if `Emails` is used with an empty list. + pub(crate) fn validate(&self) -> Result<(), EmptyEmailListError> { + if let SendAuthType::Emails { emails } = self + && emails.is_empty() + { + return Err(EmptyEmailListError); + } + Ok(()) + } + /// Returns the password if this is a Password variant, emails if this is an Emails variant, or /// None otherwise pub(crate) fn auth_data(&self, k: &[u8]) -> (Option, Option) { From 28adbd2c30de1038abc634b2262c9b6f2a9308b0 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Tue, 7 Apr 2026 11:02:09 -0400 Subject: [PATCH 43/44] PR fixes --- crates/bitwarden-send/src/create.rs | 19 +++++++- crates/bitwarden-send/src/edit.rs | 26 +++++++++- crates/bitwarden-send/src/get_list.rs | 27 +++++++++-- crates/bitwarden-send/src/lib.rs | 10 ++-- crates/bitwarden-send/src/send.rs | 5 +- crates/bitwarden-send/src/send_client.rs | 60 ++---------------------- 6 files changed, 77 insertions(+), 70 deletions(-) diff --git a/crates/bitwarden-send/src/create.rs b/crates/bitwarden-send/src/create.rs index 95b6920db9..b54b437e5d 100644 --- a/crates/bitwarden-send/src/create.rs +++ b/crates/bitwarden-send/src/create.rs @@ -17,7 +17,10 @@ use tsify::Tsify; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; -use crate::{EmptyEmailListError, Send, SendAuthType, SendParseError, SendView, SendViewType}; +use crate::{ + EmptyEmailListError, Send, SendAuthType, SendParseError, SendView, SendViewType, + send_client::SendClient, +}; #[allow(missing_docs)] #[bitwarden_error(flat)] @@ -116,7 +119,7 @@ impl IdentifyKey for SendAddRequest { } } -pub(super) async fn create_send + ?Sized>( +async fn create_send + ?Sized>( key_store: &KeyStore, api_client: &bitwarden_api_api::apis::ApiClient, repository: &R, @@ -139,6 +142,18 @@ pub(super) async fn create_send + ?Sized>( Ok(key_store.decrypt(&send)?) } +#[cfg_attr(feature = "wasm", wasm_bindgen)] +impl SendClient { + /// Create a new [Send] and save it to the server. + pub async fn create(&self, request: SendAddRequest) -> Result { + let key_store = self.client.internal.get_key_store(); + let config = self.client.internal.get_api_configurations(); + let repository = self.get_repository()?; + + create_send(key_store, &config.api_client, repository.as_ref(), request).await + } +} + #[cfg(test)] mod tests { use bitwarden_api_api::{apis::ApiClient, models::SendResponseModel}; diff --git a/crates/bitwarden-send/src/edit.rs b/crates/bitwarden-send/src/edit.rs index a1fceb3d84..a394ae41de 100644 --- a/crates/bitwarden-send/src/edit.rs +++ b/crates/bitwarden-send/src/edit.rs @@ -21,6 +21,7 @@ use wasm_bindgen::prelude::*; use crate::{ EmptyEmailListError, Send, SendAuthType, SendId, SendView, SendViewType, error::{ItemNotFoundError, SendParseError}, + send_client::SendClient, }; #[allow(missing_docs)] @@ -145,7 +146,7 @@ impl IdentifyKey for SendEditRequestWithKey { } } -pub(super) async fn edit_send + ?Sized>( +async fn edit_send + ?Sized>( key_store: &KeyStore, api_client: &bitwarden_api_api::apis::ApiClient, repository: &R, @@ -189,6 +190,29 @@ pub(super) async fn edit_send + ?Sized>( Ok(key_store.decrypt(&send)?) } +#[cfg_attr(feature = "wasm", wasm_bindgen)] +impl SendClient { + /// Edit the [Send] and save it to the server. + pub async fn edit( + &self, + send_id: SendId, + request: SendEditRequest, + ) -> Result { + let key_store = self.client.internal.get_key_store(); + let config = self.client.internal.get_api_configurations(); + let repository = self.get_repository()?; + + edit_send( + key_store, + &config.api_client, + repository.as_ref(), + send_id, + request, + ) + .await + } +} + #[cfg(test)] mod tests { use bitwarden_api_api::{apis::ApiClient, models::SendResponseModel}; diff --git a/crates/bitwarden-send/src/get_list.rs b/crates/bitwarden-send/src/get_list.rs index cd2a0f8646..d3649e6c9e 100644 --- a/crates/bitwarden-send/src/get_list.rs +++ b/crates/bitwarden-send/src/get_list.rs @@ -3,8 +3,10 @@ use bitwarden_crypto::{CryptoError, KeyStore}; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::{Repository, RepositoryError}; use thiserror::Error; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; -use crate::{Send, SendId, SendView, error::ItemNotFoundError}; +use crate::{Send, SendId, SendView, error::ItemNotFoundError, send_client::SendClient}; #[allow(missing_docs)] #[bitwarden_error(flat)] @@ -20,7 +22,7 @@ pub enum GetSendError { Repository(#[from] RepositoryError), } -pub(super) async fn get_send( +async fn get_send( store: &KeyStore, repository: &dyn Repository, id: SendId, @@ -30,7 +32,7 @@ pub(super) async fn get_send( Ok(store.decrypt(&send)?) } -pub(super) async fn list_sends( +async fn list_sends( store: &KeyStore, repository: &dyn Repository, ) -> Result, GetSendError> { @@ -39,6 +41,25 @@ pub(super) async fn list_sends( Ok(views) } +#[cfg_attr(feature = "wasm", wasm_bindgen)] +impl SendClient { + /// Get all sends from state and decrypt them to a list of [SendView]. + pub async fn list(&self) -> Result, GetSendError> { + let key_store = self.client.internal.get_key_store(); + let repository = self.get_repository()?; + + list_sends(key_store, repository.as_ref()).await + } + + /// Get a specific [Send] by its ID from state and decrypt it to a [SendView]. + pub async fn get(&self, send_id: SendId) -> Result { + let key_store = self.client.internal.get_key_store(); + let repository = self.get_repository()?; + + get_send(key_store, repository.as_ref(), send_id).await + } +} + #[cfg(test)] mod tests { use bitwarden_core::key_management::SymmetricKeyId; diff --git a/crates/bitwarden-send/src/lib.rs b/crates/bitwarden-send/src/lib.rs index 99120a4acf..f69f008fe0 100644 --- a/crates/bitwarden-send/src/lib.rs +++ b/crates/bitwarden-send/src/lib.rs @@ -5,6 +5,11 @@ uniffi::setup_scaffolding!(); #[cfg(feature = "uniffi")] mod uniffi_support; +mod send_client; +pub use send_client::{ + SendClient, SendClientExt, SendDecryptError, SendDecryptFileError, SendEncryptError, + SendEncryptFileError, +}; mod create; pub use create::{CreateSendError, SendAddRequest}; mod edit; @@ -13,11 +18,6 @@ mod error; pub use error::SendParseError; mod get_list; pub use get_list::GetSendError; -mod send_client; -pub use send_client::{ - SendClient, SendClientExt, SendDecryptError, SendDecryptFileError, SendEncryptError, - SendEncryptFileError, -}; mod send; pub use send::{ AuthType, EmptyEmailListError, Send, SendAuthType, SendFileView, SendId, SendListView, diff --git a/crates/bitwarden-send/src/send.rs b/crates/bitwarden-send/src/send.rs index d6498e484f..ded6ed4c01 100644 --- a/crates/bitwarden-send/src/send.rs +++ b/crates/bitwarden-send/src/send.rs @@ -329,8 +329,9 @@ pub struct SendView { pub expiration_date: Option>, /// Email addresses for OTP authentication. - /// **Note**: Mutually exclusive with `new_password`. If both are set, - /// only password authentication will be used. + /// **Note**: Mutually exclusive with `new_password`. If both are set, only password + /// authentication will be used. When creating or editing sends, use [crate::SendAuthType] + /// to ensure mutual exclusivity at the type level. pub emails: Vec, pub auth_type: AuthType, } diff --git a/crates/bitwarden-send/src/send_client.rs b/crates/bitwarden-send/src/send_client.rs index cd2c2cb60f..baa550c38b 100644 --- a/crates/bitwarden-send/src/send_client.rs +++ b/crates/bitwarden-send/src/send_client.rs @@ -9,12 +9,7 @@ use thiserror::Error; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; -use crate::{ - Send, SendId, SendListView, SendView, - create::{CreateSendError, SendAddRequest, create_send}, - edit::{EditSendError, SendEditRequest, edit_send}, - get_list::{GetSendError, get_send, list_sends}, -}; +use crate::{Send, SendListView, SendView}; /// Generic error type for send encryption errors. #[allow(missing_docs)] @@ -59,7 +54,7 @@ pub enum SendDecryptFileError { #[allow(missing_docs)] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct SendClient { - client: Client, + pub(crate) client: Client, } impl SendClient { @@ -143,57 +138,8 @@ impl SendClient { } } -#[cfg_attr(feature = "wasm", wasm_bindgen)] -impl SendClient { - /// Create a new [Send] and save it to the server. - pub async fn create(&self, request: SendAddRequest) -> Result { - let key_store = self.client.internal.get_key_store(); - let config = self.client.internal.get_api_configurations(); - let repository = self.get_repository()?; - - create_send(key_store, &config.api_client, repository.as_ref(), request).await - } - - /// Edit the [Send] and save it to the server. - pub async fn edit( - &self, - send_id: SendId, - request: SendEditRequest, - ) -> Result { - let key_store = self.client.internal.get_key_store(); - let config = self.client.internal.get_api_configurations(); - let repository = self.get_repository()?; - - edit_send( - key_store, - &config.api_client, - repository.as_ref(), - send_id, - request, - ) - .await - } - - /// Get all sends from state and decrypt them to a list of [SendView]. - pub async fn list(&self) -> Result, GetSendError> { - let key_store = self.client.internal.get_key_store(); - let repository = self.get_repository()?; - - list_sends(key_store, repository.as_ref()).await - } - - /// Get a specific [Send] by its ID from state and decrypt it to a [SendView]. - pub async fn get(&self, send_id: SendId) -> Result { - let key_store = self.client.internal.get_key_store(); - let repository = self.get_repository()?; - - get_send(key_store, repository.as_ref(), send_id).await - } -} - impl SendClient { - /// Helper for getting the repository for sends. - fn get_repository(&self) -> Result>, RepositoryError> { + pub(crate) fn get_repository(&self) -> Result>, RepositoryError> { Ok(self.client.platform().state().get::()?) } } From c324c19da6ae63c507ef8822bd2cff5fceb9e74a Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Wed, 8 Apr 2026 08:28:57 -0400 Subject: [PATCH 44/44] PR fix --- crates/bitwarden-send/src/edit.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bitwarden-send/src/edit.rs b/crates/bitwarden-send/src/edit.rs index a394ae41de..eb4132b0eb 100644 --- a/crates/bitwarden-send/src/edit.rs +++ b/crates/bitwarden-send/src/edit.rs @@ -102,7 +102,6 @@ impl CompositeEncryptable