1use bitwarden_core::{
2 ApiError, MissingFieldError,
3 key_management::{KeySlotIds, SymmetricKeySlotId},
4 require,
5};
6use bitwarden_crypto::{
7 CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, OctetStreamBytes,
8 PrimitiveEncryptable, generate_random_bytes,
9};
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 wasm_bindgen::prelude::*;
17
18use crate::{
19 EmptyEmailListError, Send, SendAuthType, SendParseError, SendView, SendViewType,
20 send_client::SendClient,
21};
22
23#[allow(missing_docs)]
24#[bitwarden_error(flat)]
25#[derive(Debug, Error)]
26pub enum CreateSendError {
27 #[error(transparent)]
28 Api(#[from] ApiError),
29 #[error(transparent)]
30 Crypto(#[from] CryptoError),
31 #[error(transparent)]
32 EmptyEmailList(#[from] EmptyEmailListError),
33 #[error(transparent)]
34 MissingField(#[from] MissingFieldError),
35 #[error(transparent)]
36 Repository(#[from] RepositoryError),
37 #[error(transparent)]
38 SendParse(#[from] SendParseError),
39}
40
41#[derive(Serialize, Deserialize, Debug)]
43#[serde(rename_all = "camelCase")]
44#[bitwarden_ffi::wasm_record]
45pub struct SendAddRequest {
46 pub name: String,
48 pub notes: Option<String>,
50
51 pub view_type: SendViewType,
53
54 pub max_access_count: Option<u32>,
56 pub disabled: bool,
58 pub hide_email: bool,
60
61 pub deletion_date: DateTime<Utc>,
63 pub expiration_date: Option<DateTime<Utc>>,
65
66 pub auth: SendAuthType,
68}
69
70impl
71 CompositeEncryptable<
72 KeySlotIds,
73 SymmetricKeySlotId,
74 bitwarden_api_api::models::SendRequestModel,
75 > for SendAddRequest
76{
77 fn encrypt_composite(
78 &self,
79 ctx: &mut KeyStoreContext<KeySlotIds>,
80 key: SymmetricKeySlotId,
81 ) -> Result<bitwarden_api_api::models::SendRequestModel, CryptoError> {
82 let k = generate_random_bytes::<[u8; 16]>().to_vec();
84
85 let send_key = Send::derive_shareable_key(ctx, &k)?;
87
88 let (send_type, file, text, data) =
89 self.view_type.clone().encrypt_composite(ctx, send_key)?;
90
91 let (password, emails) = self.auth.auth_data(&k);
92
93 Ok(bitwarden_api_api::models::SendRequestModel {
94 r#type: Some(send_type),
95 auth_type: Some(self.auth.auth_type().into()),
96 file_length: None,
97 name: Some(self.name.encrypt(ctx, send_key)?.to_string()),
98 notes: self
99 .notes
100 .as_ref()
101 .map(|n| n.encrypt(ctx, send_key))
102 .transpose()?
103 .map(|e| e.to_string()),
104 key: OctetStreamBytes::from(k).encrypt(ctx, key)?.to_string(),
106 max_access_count: self.max_access_count.map(|c| c as i32),
107 expiration_date: self.expiration_date.map(|d| d.to_rfc3339()),
108 deletion_date: self.deletion_date.to_rfc3339(),
109 file,
110 text,
111 data,
112 password,
113 emails,
114 disabled: self.disabled,
115 hide_email: Some(self.hide_email),
116 })
117 }
118}
119
120impl IdentifyKey<SymmetricKeySlotId> for SendAddRequest {
121 fn key_identifier(&self) -> SymmetricKeySlotId {
122 SymmetricKeySlotId::User
123 }
124}
125
126async fn create_send<R: Repository<Send> + ?Sized>(
127 key_store: &KeyStore<KeySlotIds>,
128 api_client: &bitwarden_api_api::apis::ApiClient,
129 repository: &R,
130 request: SendAddRequest,
131) -> Result<SendView, CreateSendError> {
132 request.auth.validate()?;
133
134 let send_request = key_store.encrypt(request)?;
135
136 let resp = api_client.sends_api().post(Some(send_request)).await?;
137
138 let send: Send = resp.try_into()?;
139
140 repository.set(require!(send.id), send.clone()).await?;
141
142 Ok(key_store.decrypt(&send)?)
143}
144
145#[bitwarden_ffi::wasm_export]
146impl SendClient {
147 pub async fn create(&self, request: SendAddRequest) -> Result<SendView, CreateSendError> {
149 let key_store = self.client.internal.get_key_store();
150 let config = self.client.internal.get_api_configurations();
151 let repository = self.get_repository()?;
152
153 create_send(key_store, &config.api_client, repository.as_ref(), request).await
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use bitwarden_api_api::{apis::ApiClient, models::SendResponseModel};
160 use bitwarden_crypto::SymmetricKeyAlgorithm;
161 use bitwarden_test::MemoryRepository;
162 use uuid::uuid;
163
164 use super::*;
165 use crate::{AuthType, SendId, SendTextView, SendType, SendView};
166
167 #[tokio::test]
168 async fn test_create_send() {
169 let store: KeyStore<KeySlotIds> = KeyStore::default();
170 {
171 let mut ctx = store.context_mut();
172 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
173 ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
174 .unwrap();
175 }
176
177 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
178
179 let api_client = ApiClient::new_mocked(move |mock| {
180 mock.sends_api
181 .expect_post()
182 .returning(move |model| {
183 let model = model.unwrap();
184 Ok(SendResponseModel {
185 id: Some(send_id),
186 name: model.name,
187 revision_date: Some("2025-01-01T00:00:00Z".to_string()),
188 object: Some("send".to_string()),
189 access_id: None,
190 r#type: model.r#type,
191 auth_type: model.auth_type,
192 notes: model.notes,
193 file: model.file,
194 text: model.text,
195 data: model.data,
196 key: Some(model.key),
197 max_access_count: model.max_access_count,
198 access_count: Some(0),
199 password: model.password,
200 emails: model.emails,
201 disabled: Some(model.disabled),
202 expiration_date: model.expiration_date,
203 deletion_date: Some(model.deletion_date),
204 hide_email: model.hide_email,
205 })
206 })
207 .once();
208 });
209
210 let repository = MemoryRepository::<Send>::default();
211
212 let result = create_send(
213 &store,
214 &api_client,
215 &repository,
216 SendAddRequest {
217 name: "test".to_string(),
218 notes: Some("notes".to_string()),
219 view_type: SendViewType::Text(SendTextView {
220 text: Some("test".to_string()),
221 hidden: false,
222 }),
223 max_access_count: None,
224 disabled: false,
225 hide_email: false,
226 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
227 expiration_date: None,
228 auth: SendAuthType::None,
229 },
230 )
231 .await
232 .unwrap();
233
234 assert_eq!(result.id, Some(crate::send::SendId::new(send_id)));
236 assert_eq!(result.name, "test");
237 assert_eq!(result.notes, Some("notes".to_string()));
238 assert!(result.key.is_some(), "Expected a generated key");
239 assert_eq!(result.new_password, None);
240 assert!(!result.has_password);
241 assert_eq!(result.r#type, SendType::Text);
242 assert_eq!(result.file, None);
243 assert_eq!(
244 result.text,
245 Some(SendTextView {
246 text: Some("test".to_string()),
247 hidden: false,
248 })
249 );
250 assert_eq!(result.max_access_count, None);
251 assert_eq!(result.access_count, 0);
252 assert!(!result.disabled);
253 assert!(!result.hide_email);
254 assert_eq!(
255 result.deletion_date,
256 "2025-01-10T00:00:00Z".parse::<DateTime<Utc>>().unwrap()
257 );
258 assert_eq!(result.expiration_date, None);
259 assert_eq!(result.emails, Vec::<String>::new());
260 assert_eq!(result.auth_type, AuthType::None);
261 assert_eq!(
262 result.revision_date,
263 "2025-01-01T00:00:00Z".parse::<DateTime<Utc>>().unwrap()
264 );
265
266 assert_eq!(
268 store
269 .decrypt::<SymmetricKeySlotId, Send, SendView>(
270 &repository.get(SendId::new(send_id)).await.unwrap().unwrap()
271 )
272 .unwrap(),
273 result
274 );
275 }
276
277 #[tokio::test]
278 async fn test_create_send_http_error() {
279 let store: KeyStore<KeySlotIds> = KeyStore::default();
280 {
281 let mut ctx = store.context_mut();
282 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
283 ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
284 .unwrap();
285 }
286
287 let api_client = ApiClient::new_mocked(move |mock| {
288 mock.sends_api
289 .expect_post()
290 .returning(move |_model| Err(std::io::Error::other("Simulated error").into()));
291 });
292
293 let repository = MemoryRepository::<Send>::default();
294
295 let result = create_send(
296 &store,
297 &api_client,
298 &repository,
299 SendAddRequest {
300 name: "test".to_string(),
301 notes: Some("notes".to_string()),
302 view_type: SendViewType::Text(SendTextView {
303 text: Some("test".to_string()),
304 hidden: false,
305 }),
306 max_access_count: None,
307 disabled: false,
308 hide_email: false,
309 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
310 expiration_date: None,
311 auth: SendAuthType::None,
312 },
313 )
314 .await;
315
316 assert!(result.is_err());
317 assert!(matches!(result.unwrap_err(), CreateSendError::Api(_)));
318 }
319}