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 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
137 .sends_api()
138 .post(Some(send_request))
139 .await
140 .map_err(ApiError::from)?;
141
142 let send: Send = resp.try_into()?;
143
144 repository.set(require!(send.id), send.clone()).await?;
145
146 Ok(key_store.decrypt(&send)?)
147}
148
149#[cfg_attr(feature = "wasm", wasm_bindgen)]
150impl SendClient {
151 pub async fn create(&self, request: SendAddRequest) -> Result<SendView, CreateSendError> {
153 let key_store = self.client.internal.get_key_store();
154 let config = self.client.internal.get_api_configurations();
155 let repository = self.get_repository()?;
156
157 create_send(key_store, &config.api_client, repository.as_ref(), request).await
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use bitwarden_api_api::{apis::ApiClient, models::SendResponseModel};
164 use bitwarden_crypto::SymmetricKeyAlgorithm;
165 use bitwarden_test::MemoryRepository;
166 use uuid::uuid;
167
168 use super::*;
169 use crate::{AuthType, SendId, SendTextView, SendType, SendView};
170
171 #[tokio::test]
172 async fn test_create_send() {
173 let store: KeyStore<KeySlotIds> = KeyStore::default();
174 {
175 let mut ctx = store.context_mut();
176 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
177 ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
178 .unwrap();
179 }
180
181 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
182
183 let api_client = ApiClient::new_mocked(move |mock| {
184 mock.sends_api
185 .expect_post()
186 .returning(move |model| {
187 let model = model.unwrap();
188 Ok(SendResponseModel {
189 id: Some(send_id),
190 name: model.name,
191 revision_date: Some("2025-01-01T00:00:00Z".to_string()),
192 object: Some("send".to_string()),
193 access_id: None,
194 r#type: model.r#type,
195 auth_type: model.auth_type,
196 notes: model.notes,
197 file: model.file,
198 text: model.text,
199 key: Some(model.key),
200 max_access_count: model.max_access_count,
201 access_count: Some(0),
202 password: model.password,
203 emails: model.emails,
204 disabled: Some(model.disabled),
205 expiration_date: model.expiration_date,
206 deletion_date: Some(model.deletion_date),
207 hide_email: model.hide_email,
208 })
209 })
210 .once();
211 });
212
213 let repository = MemoryRepository::<Send>::default();
214
215 let result = create_send(
216 &store,
217 &api_client,
218 &repository,
219 SendAddRequest {
220 name: "test".to_string(),
221 notes: Some("notes".to_string()),
222 view_type: SendViewType::Text(SendTextView {
223 text: Some("test".to_string()),
224 hidden: false,
225 }),
226 max_access_count: None,
227 disabled: false,
228 hide_email: false,
229 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
230 expiration_date: None,
231 auth: SendAuthType::None,
232 },
233 )
234 .await
235 .unwrap();
236
237 assert_eq!(result.id, Some(crate::send::SendId::new(send_id)));
239 assert_eq!(result.name, "test");
240 assert_eq!(result.notes, Some("notes".to_string()));
241 assert!(result.key.is_some(), "Expected a generated key");
242 assert_eq!(result.new_password, None);
243 assert!(!result.has_password);
244 assert_eq!(result.r#type, SendType::Text);
245 assert_eq!(result.file, None);
246 assert_eq!(
247 result.text,
248 Some(SendTextView {
249 text: Some("test".to_string()),
250 hidden: false,
251 })
252 );
253 assert_eq!(result.max_access_count, None);
254 assert_eq!(result.access_count, 0);
255 assert!(!result.disabled);
256 assert!(!result.hide_email);
257 assert_eq!(
258 result.deletion_date,
259 "2025-01-10T00:00:00Z".parse::<DateTime<Utc>>().unwrap()
260 );
261 assert_eq!(result.expiration_date, None);
262 assert_eq!(result.emails, Vec::<String>::new());
263 assert_eq!(result.auth_type, AuthType::None);
264 assert_eq!(
265 result.revision_date,
266 "2025-01-01T00:00:00Z".parse::<DateTime<Utc>>().unwrap()
267 );
268
269 assert_eq!(
271 store
272 .decrypt::<SymmetricKeySlotId, Send, SendView>(
273 &repository.get(SendId::new(send_id)).await.unwrap().unwrap()
274 )
275 .unwrap(),
276 result
277 );
278 }
279
280 #[tokio::test]
281 async fn test_create_send_http_error() {
282 let store: KeyStore<KeySlotIds> = KeyStore::default();
283 {
284 let mut ctx = store.context_mut();
285 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
286 ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
287 .unwrap();
288 }
289
290 let api_client = ApiClient::new_mocked(move |mock| {
291 mock.sends_api.expect_post().returning(move |_model| {
292 Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other(
293 "Simulated error",
294 )))
295 });
296 });
297
298 let repository = MemoryRepository::<Send>::default();
299
300 let result = create_send(
301 &store,
302 &api_client,
303 &repository,
304 SendAddRequest {
305 name: "test".to_string(),
306 notes: Some("notes".to_string()),
307 view_type: SendViewType::Text(SendTextView {
308 text: Some("test".to_string()),
309 hidden: false,
310 }),
311 max_access_count: None,
312 disabled: false,
313 hide_email: false,
314 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
315 expiration_date: None,
316 auth: SendAuthType::None,
317 },
318 )
319 .await;
320
321 assert!(result.is_err());
322 assert!(matches!(result.unwrap_err(), CreateSendError::Api(_)));
323 }
324}