Skip to main content

bitwarden_send/
create.rs

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/// Request model for creating a new Send.
44#[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    /// The name of the Send.
49    pub name: String,
50    /// Optional notes visible to the sender.
51    pub notes: Option<String>,
52
53    /// The type and content of the Send.
54    pub view_type: SendViewType,
55
56    /// Maximum number of times the Send can be accessed.
57    pub max_access_count: Option<u32>,
58    /// Whether the Send is disabled and cannot be accessed.
59    pub disabled: bool,
60    /// Whether to hide the sender's email from recipients.
61    pub hide_email: bool,
62
63    /// Date and time when the Send will be permanently deleted.
64    pub deletion_date: DateTime<Utc>,
65    /// Optional date and time when the Send expires and can no longer be accessed.
66    pub expiration_date: Option<DateTime<Utc>>,
67
68    /// Authentication method for accessing this Send.
69    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        // Generate the send key
85        let k = generate_random_bytes::<[u8; 16]>().to_vec();
86
87        // Derive the shareable send key for encrypting content
88        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            // Encrypt the send key itself with the user key
106            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    /// Create a new [Send] and save it to the server.
152    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        // Verify the result (excluding the generated key which is random)
238        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        // Confirm the send was stored in the repository
270        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}