1use bitwarden_core::{
2 ApiError, MissingFieldError,
3 key_management::{KeyIds, SymmetricKeyId},
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 CompositeEncryptable<KeyIds, SymmetricKeyId, bitwarden_api_api::models::SendRequestModel>
73 for SendAddRequest
74{
75 fn encrypt_composite(
76 &self,
77 ctx: &mut KeyStoreContext<KeyIds>,
78 key: SymmetricKeyId,
79 ) -> Result<bitwarden_api_api::models::SendRequestModel, CryptoError> {
80 let k = generate_random_bytes::<[u8; 16]>().to_vec();
82
83 let send_key = Send::derive_shareable_key(ctx, &k)?;
85
86 let (send_type, file, text) = self.view_type.clone().encrypt_composite(ctx, send_key)?;
87
88 let (password, emails) = self.auth.auth_data(&k);
89
90 Ok(bitwarden_api_api::models::SendRequestModel {
91 r#type: Some(send_type),
92 auth_type: Some(self.auth.auth_type().into()),
93 file_length: None,
94 name: Some(self.name.encrypt(ctx, send_key)?.to_string()),
95 notes: self
96 .notes
97 .as_ref()
98 .map(|n| n.encrypt(ctx, send_key))
99 .transpose()?
100 .map(|e| e.to_string()),
101 key: OctetStreamBytes::from(k).encrypt(ctx, key)?.to_string(),
103 max_access_count: self.max_access_count.map(|c| c as i32),
104 expiration_date: self.expiration_date.map(|d| d.to_rfc3339()),
105 deletion_date: self.deletion_date.to_rfc3339(),
106 file,
107 text,
108 password,
109 emails,
110 disabled: self.disabled,
111 hide_email: Some(self.hide_email),
112 })
113 }
114}
115
116impl IdentifyKey<SymmetricKeyId> for SendAddRequest {
117 fn key_identifier(&self) -> SymmetricKeyId {
118 SymmetricKeyId::User
119 }
120}
121
122async fn create_send<R: Repository<Send> + ?Sized>(
123 key_store: &KeyStore<KeyIds>,
124 api_client: &bitwarden_api_api::apis::ApiClient,
125 repository: &R,
126 request: SendAddRequest,
127) -> Result<SendView, CreateSendError> {
128 request.auth.validate()?;
129
130 let send_request = key_store.encrypt(request)?;
131
132 let resp = api_client
133 .sends_api()
134 .post(Some(send_request))
135 .await
136 .map_err(ApiError::from)?;
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#[cfg_attr(feature = "wasm", wasm_bindgen)]
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<KeyIds> = 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, SymmetricKeyId::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 key: Some(model.key),
196 max_access_count: model.max_access_count,
197 access_count: Some(0),
198 password: model.password,
199 emails: model.emails,
200 disabled: Some(model.disabled),
201 expiration_date: model.expiration_date,
202 deletion_date: Some(model.deletion_date),
203 hide_email: model.hide_email,
204 })
205 })
206 .once();
207 });
208
209 let repository = MemoryRepository::<Send>::default();
210
211 let result = create_send(
212 &store,
213 &api_client,
214 &repository,
215 SendAddRequest {
216 name: "test".to_string(),
217 notes: Some("notes".to_string()),
218 view_type: SendViewType::Text(SendTextView {
219 text: Some("test".to_string()),
220 hidden: false,
221 }),
222 max_access_count: None,
223 disabled: false,
224 hide_email: false,
225 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
226 expiration_date: None,
227 auth: SendAuthType::None,
228 },
229 )
230 .await
231 .unwrap();
232
233 assert_eq!(result.id, Some(crate::send::SendId::new(send_id)));
235 assert_eq!(result.name, "test");
236 assert_eq!(result.notes, Some("notes".to_string()));
237 assert!(result.key.is_some(), "Expected a generated key");
238 assert_eq!(result.new_password, None);
239 assert!(!result.has_password);
240 assert_eq!(result.r#type, SendType::Text);
241 assert_eq!(result.file, None);
242 assert_eq!(
243 result.text,
244 Some(SendTextView {
245 text: Some("test".to_string()),
246 hidden: false,
247 })
248 );
249 assert_eq!(result.max_access_count, None);
250 assert_eq!(result.access_count, 0);
251 assert!(!result.disabled);
252 assert!(!result.hide_email);
253 assert_eq!(
254 result.deletion_date,
255 "2025-01-10T00:00:00Z".parse::<DateTime<Utc>>().unwrap()
256 );
257 assert_eq!(result.expiration_date, None);
258 assert_eq!(result.emails, Vec::<String>::new());
259 assert_eq!(result.auth_type, AuthType::None);
260 assert_eq!(
261 result.revision_date,
262 "2025-01-01T00:00:00Z".parse::<DateTime<Utc>>().unwrap()
263 );
264
265 assert_eq!(
267 store
268 .decrypt::<SymmetricKeyId, Send, SendView>(
269 &repository.get(SendId::new(send_id)).await.unwrap().unwrap()
270 )
271 .unwrap(),
272 result
273 );
274 }
275
276 #[tokio::test]
277 async fn test_create_send_http_error() {
278 let store: KeyStore<KeyIds> = KeyStore::default();
279 {
280 let mut ctx = store.context_mut();
281 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
282 ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User)
283 .unwrap();
284 }
285
286 let api_client = ApiClient::new_mocked(move |mock| {
287 mock.sends_api.expect_post().returning(move |_model| {
288 Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other(
289 "Simulated error",
290 )))
291 });
292 });
293
294 let repository = MemoryRepository::<Send>::default();
295
296 let result = create_send(
297 &store,
298 &api_client,
299 &repository,
300 SendAddRequest {
301 name: "test".to_string(),
302 notes: Some("notes".to_string()),
303 view_type: SendViewType::Text(SendTextView {
304 text: Some("test".to_string()),
305 hidden: false,
306 }),
307 max_access_count: None,
308 disabled: false,
309 hide_email: false,
310 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
311 expiration_date: None,
312 auth: SendAuthType::None,
313 },
314 )
315 .await;
316
317 assert!(result.is_err());
318 assert!(matches!(result.unwrap_err(), CreateSendError::Api(_)));
319 }
320}