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