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