Skip to main content

bitwarden_send/
create.rs

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/// 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 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        // Generate the send key
81        let k = generate_random_bytes::<[u8; 16]>().to_vec();
82
83        // Derive the shareable send key for encrypting content
84        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            // Encrypt the send key itself with the user key
102            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    /// 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<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        // Verify the result (excluding the generated key which is random)
234        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        // Confirm the send was stored in the repository
266        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}