1use bitwarden_core::{
2 ApiError, MissingFieldError,
3 key_management::{KeyIds, SymmetricKeyId},
4};
5use bitwarden_crypto::{
6 CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, OctetStreamBytes,
7 PrimitiveEncryptable,
8};
9use bitwarden_encoding::B64Url;
10use bitwarden_error::bitwarden_error;
11use bitwarden_state::repository::{Repository, RepositoryError};
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15#[cfg(feature = "wasm")]
16use tsify::Tsify;
17use uuid::Uuid;
18#[cfg(feature = "wasm")]
19use wasm_bindgen::prelude::*;
20
21use crate::{
22 EmptyEmailListError, Send, SendAuthType, SendId, SendView, SendViewType,
23 error::{ItemNotFoundError, SendParseError},
24 send_client::SendClient,
25};
26
27#[allow(missing_docs)]
28#[bitwarden_error(flat)]
29#[derive(Debug, Error)]
30pub enum EditSendError {
31 #[error(transparent)]
32 ItemNotFound(#[from] ItemNotFoundError),
33 #[error(transparent)]
34 Crypto(#[from] CryptoError),
35 #[error(transparent)]
36 Api(#[from] ApiError),
37 #[error(transparent)]
38 EmptyEmailList(#[from] EmptyEmailListError),
39 #[error(transparent)]
40 MissingField(#[from] MissingFieldError),
41 #[error(transparent)]
42 Repository(#[from] RepositoryError),
43 #[error(transparent)]
44 Uuid(#[from] uuid::Error),
45 #[error(transparent)]
46 SendParse(#[from] SendParseError),
47 #[error("Server returned Send with ID {returned:?} but expected {expected}")]
48 IdMismatch {
49 expected: Uuid,
50 returned: Option<Uuid>,
51 },
52}
53
54#[derive(Serialize, Deserialize, Debug)]
56#[serde(rename_all = "camelCase")]
57#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
58pub struct SendEditRequest {
59 pub name: String,
61 pub notes: Option<String>,
63
64 pub view_type: SendViewType,
66
67 pub max_access_count: Option<u32>,
69 pub disabled: bool,
71 pub hide_email: bool,
73
74 pub deletion_date: DateTime<Utc>,
76 pub expiration_date: Option<DateTime<Utc>>,
78
79 pub auth: SendAuthType,
81}
82
83#[derive(Debug)]
86struct SendEditRequestWithKey {
87 request: SendEditRequest,
88 send_key: String,
89}
90
91impl CompositeEncryptable<KeyIds, SymmetricKeyId, bitwarden_api_api::models::SendRequestModel>
92 for SendEditRequestWithKey
93{
94 fn encrypt_composite(
95 &self,
96 ctx: &mut KeyStoreContext<KeyIds>,
97 key: SymmetricKeyId,
98 ) -> Result<bitwarden_api_api::models::SendRequestModel, CryptoError> {
99 let k = B64Url::try_from(self.send_key.as_str())
101 .map_err(|_| CryptoError::InvalidKey)?
102 .as_bytes()
103 .to_vec();
104
105 let send_key = Send::derive_shareable_key(ctx, &k)?;
106
107 let (send_type, file, text) = self
108 .request
109 .view_type
110 .clone()
111 .encrypt_composite(ctx, send_key)?;
112
113 let (password, emails) = self.request.auth.auth_data(&k);
114
115 Ok(bitwarden_api_api::models::SendRequestModel {
116 r#type: Some(send_type),
117 auth_type: Some(self.request.auth.auth_type().into()),
118 file_length: None,
119 name: Some(self.request.name.encrypt(ctx, send_key)?.to_string()),
120 notes: self
121 .request
122 .notes
123 .as_ref()
124 .map(|n| n.encrypt(ctx, send_key))
125 .transpose()?
126 .map(|e| e.to_string()),
127 key: OctetStreamBytes::from(k).encrypt(ctx, key)?.to_string(),
129 max_access_count: self.request.max_access_count.map(|c| c as i32),
130 expiration_date: self.request.expiration_date.map(|d| d.to_rfc3339()),
131 deletion_date: self.request.deletion_date.to_rfc3339(),
132 file,
133 text,
134 password,
135 emails,
136 disabled: self.request.disabled,
137 hide_email: Some(self.request.hide_email),
138 })
139 }
140}
141
142impl IdentifyKey<SymmetricKeyId> for SendEditRequestWithKey {
143 fn key_identifier(&self) -> SymmetricKeyId {
144 SymmetricKeyId::User
145 }
146}
147
148async fn edit_send<R: Repository<Send> + ?Sized>(
149 key_store: &KeyStore<KeyIds>,
150 api_client: &bitwarden_api_api::apis::ApiClient,
151 repository: &R,
152 send_id: SendId,
153 request: SendEditRequest,
154) -> Result<SendView, EditSendError> {
155 request.auth.validate()?;
156
157 let id = send_id.to_string();
158
159 let existing_send = repository.get(send_id).await?.ok_or(ItemNotFoundError)?;
161
162 let existing_send_view: SendView = key_store.decrypt(&existing_send)?;
164 let send_key = existing_send_view.key.ok_or(MissingFieldError("key"))?;
165
166 let request_with_key = SendEditRequestWithKey { request, send_key };
168
169 let send_request = key_store.encrypt(request_with_key)?;
170
171 let resp = api_client
172 .sends_api()
173 .put(&id, Some(send_request))
174 .await
175 .map_err(ApiError::from)?;
176
177 let send: Send = resp.try_into()?;
178
179 if send.id != Some(send_id) {
181 return Err(EditSendError::IdMismatch {
182 expected: send_id.into(),
183 returned: send.id.map(Into::into),
184 });
185 }
186
187 repository.set(send_id, send.clone()).await?;
188
189 Ok(key_store.decrypt(&send)?)
190}
191
192#[cfg_attr(feature = "wasm", wasm_bindgen)]
193impl SendClient {
194 pub async fn edit(
196 &self,
197 send_id: SendId,
198 request: SendEditRequest,
199 ) -> Result<SendView, EditSendError> {
200 let key_store = self.client.internal.get_key_store();
201 let config = self.client.internal.get_api_configurations();
202 let repository = self.get_repository()?;
203
204 edit_send(
205 key_store,
206 &config.api_client,
207 repository.as_ref(),
208 send_id,
209 request,
210 )
211 .await
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use bitwarden_api_api::{apis::ApiClient, models::SendResponseModel};
218 use bitwarden_core::key_management::SymmetricKeyId;
219 use bitwarden_crypto::SymmetricKeyAlgorithm;
220 use bitwarden_test::MemoryRepository;
221 use chrono::{DateTime, Utc};
222 use uuid::uuid;
223
224 use super::*;
225 use crate::{AuthType, SendTextView, SendType, SendViewType};
226
227 #[tokio::test]
228 async fn test_edit_send() {
229 let store: KeyStore<KeyIds> = KeyStore::default();
230 {
231 let mut ctx = store.context_mut();
232 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
233 ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User)
234 .unwrap();
235 }
236
237 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
238
239 let repository = MemoryRepository::<Send>::default();
241 let existing_send_view = SendView {
242 id: None, access_id: None,
244 name: "original".to_string(),
245 notes: Some("original notes".to_string()),
246 key: None, new_password: None,
248 has_password: false,
249 r#type: SendType::Text,
250 file: None,
251 text: Some(SendTextView {
252 text: Some("original text".to_string()),
253 hidden: false,
254 }),
255 max_access_count: None,
256 access_count: 0,
257 disabled: false,
258 hide_email: false,
259 revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
260 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
261 expiration_date: None,
262 emails: Vec::new(),
263 auth_type: AuthType::None,
264 };
265 let mut existing_send = store.encrypt(existing_send_view).unwrap();
266 existing_send.id = Some(crate::send::SendId::new(send_id)); repository
268 .set(SendId::new(send_id), existing_send)
269 .await
270 .unwrap();
271
272 let api_client = ApiClient::new_mocked(move |mock| {
273 mock.sends_api
274 .expect_put()
275 .returning(move |_id, model| {
276 let model = model.unwrap();
277 Ok(SendResponseModel {
278 id: Some(send_id),
279 name: model.name,
280 revision_date: Some("2025-01-02T00:00:00Z".to_string()),
281 object: Some("send".to_string()),
282 access_id: None,
283 r#type: model.r#type,
284 auth_type: model.auth_type,
285 notes: model.notes,
286 file: model.file,
287 text: model.text,
288 key: Some(model.key),
289 max_access_count: model.max_access_count,
290 access_count: Some(0),
291 password: model.password,
292 emails: model.emails,
293 disabled: Some(model.disabled),
294 expiration_date: model.expiration_date,
295 deletion_date: Some(model.deletion_date),
296 hide_email: model.hide_email,
297 })
298 })
299 .once();
300 });
301
302 let result = edit_send(
303 &store,
304 &api_client,
305 &repository,
306 SendId::new(send_id),
307 SendEditRequest {
308 name: "updated".to_string(),
309 notes: Some("updated notes".to_string()),
310 view_type: SendViewType::Text(SendTextView {
311 text: Some("updated text".to_string()),
312 hidden: false,
313 }),
314 max_access_count: None,
315 disabled: false,
316 hide_email: false,
317 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
318 expiration_date: None,
319 auth: SendAuthType::None,
320 },
321 )
322 .await
323 .unwrap();
324
325 assert_eq!(result.id, Some(crate::send::SendId::new(send_id)));
327 assert_eq!(result.name, "updated");
328 assert_eq!(result.notes, Some("updated notes".to_string()));
329 assert!(result.key.is_some(), "Expected a key");
330 assert_eq!(
331 result.revision_date,
332 "2025-01-02T00:00:00Z".parse::<DateTime<Utc>>().unwrap()
333 );
334
335 let stored = repository.get(SendId::new(send_id)).await.unwrap().unwrap();
337 assert_eq!(
338 store
339 .decrypt::<SymmetricKeyId, Send, SendView>(&stored)
340 .unwrap()
341 .name,
342 "updated"
343 );
344 }
345
346 #[tokio::test]
347 async fn test_edit_send_not_found() {
348 let store: KeyStore<KeyIds> = KeyStore::default();
349 {
350 let mut ctx = store.context_mut();
351 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
352 ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User)
353 .unwrap();
354 }
355
356 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
357 let repository = MemoryRepository::<Send>::default();
358 let api_client = ApiClient::new_mocked(move |_mock| {});
359
360 let result = edit_send(
361 &store,
362 &api_client,
363 &repository,
364 SendId::new(send_id),
365 SendEditRequest {
366 name: "test".to_string(),
367 notes: None,
368 view_type: SendViewType::Text(SendTextView {
369 text: Some("test".to_string()),
370 hidden: false,
371 }),
372 max_access_count: None,
373 disabled: false,
374 hide_email: false,
375 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
376 expiration_date: None,
377 auth: SendAuthType::None,
378 },
379 )
380 .await;
381
382 assert!(result.is_err());
383 assert!(matches!(
384 result.unwrap_err(),
385 EditSendError::ItemNotFound(_)
386 ));
387 }
388
389 #[tokio::test]
390 async fn test_edit_send_http_error() {
391 let store: KeyStore<KeyIds> = KeyStore::default();
392 {
393 let mut ctx = store.context_mut();
394 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
395 ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User)
396 .unwrap();
397 }
398
399 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
400
401 let repository = MemoryRepository::<Send>::default();
403 let existing_send_view = SendView {
404 id: None, access_id: None,
406 name: "original".to_string(),
407 notes: Some("original notes".to_string()),
408 key: None, new_password: None,
410 has_password: false,
411 r#type: SendType::Text,
412 file: None,
413 text: Some(SendTextView {
414 text: Some("original text".to_string()),
415 hidden: false,
416 }),
417 max_access_count: None,
418 access_count: 0,
419 disabled: false,
420 hide_email: false,
421 revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
422 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
423 expiration_date: None,
424 emails: Vec::new(),
425 auth_type: AuthType::None,
426 };
427 let mut existing_send = store.encrypt(existing_send_view).unwrap();
428 existing_send.id = Some(crate::send::SendId::new(send_id)); repository
430 .set(SendId::new(send_id), existing_send)
431 .await
432 .unwrap();
433
434 let api_client = ApiClient::new_mocked(move |mock| {
435 mock.sends_api.expect_put().returning(move |_id, _model| {
436 Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other(
437 "Simulated error",
438 )))
439 });
440 });
441
442 let result = edit_send(
443 &store,
444 &api_client,
445 &repository,
446 SendId::new(send_id),
447 SendEditRequest {
448 name: "test".to_string(),
449 notes: None,
450 view_type: SendViewType::Text(SendTextView {
451 text: Some("test".to_string()),
452 hidden: false,
453 }),
454 max_access_count: None,
455 disabled: false,
456 hide_email: false,
457 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
458 expiration_date: None,
459 auth: SendAuthType::None,
460 },
461 )
462 .await;
463
464 assert!(result.is_err());
465 assert!(matches!(result.unwrap_err(), EditSendError::Api(_)));
466 }
467}