1use bitwarden_api_api::models::{CipherCreateRequestModel, CipherRequestModel};
2use bitwarden_collections::collection::CollectionId;
3use bitwarden_core::{
4 ApiError, MissingFieldError, NotAuthenticatedError, OrganizationId, UserId,
5 key_management::KeySlotIds, require,
6};
7use bitwarden_crypto::{CryptoError, IdentifyKey, KeyStore};
8use bitwarden_error::bitwarden_error;
9use bitwarden_state::repository::{Repository, RepositoryError};
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13#[cfg(feature = "wasm")]
14use tsify::Tsify;
15#[cfg(feature = "wasm")]
16use wasm_bindgen::prelude::*;
17
18use super::CiphersClient;
19use crate::{
20 Cipher, CipherRepromptType, CipherView, FieldView, FolderId, VaultParseError,
21 cipher::cipher::PartialCipher, cipher_view_type::CipherViewType,
22};
23
24#[allow(missing_docs)]
25#[bitwarden_error(flat)]
26#[derive(Debug, Error)]
27pub enum CreateCipherError {
28 #[error(transparent)]
29 Crypto(#[from] CryptoError),
30 #[error(transparent)]
31 Api(#[from] ApiError),
32 #[error(transparent)]
33 VaultParse(#[from] VaultParseError),
34 #[error(transparent)]
35 MissingField(#[from] MissingFieldError),
36 #[error(transparent)]
37 NotAuthenticated(#[from] NotAuthenticatedError),
38 #[error(transparent)]
39 Repository(#[from] RepositoryError),
40}
41
42impl<T> From<bitwarden_api_api::apis::Error<T>> for CreateCipherError {
43 fn from(val: bitwarden_api_api::apis::Error<T>) -> Self {
44 Self::Api(val.into())
45 }
46}
47
48#[derive(Serialize, Deserialize, Clone, Debug)]
50#[serde(rename_all = "camelCase")]
51#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
52#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
53pub struct CipherCreateRequest {
54 pub organization_id: Option<OrganizationId>,
55 pub collection_ids: Vec<CollectionId>,
56 pub folder_id: Option<FolderId>,
57 pub name: String,
58 pub notes: Option<String>,
59 pub favorite: bool,
60 pub reprompt: CipherRepromptType,
61 pub r#type: CipherViewType,
62 pub fields: Vec<FieldView>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub archived_date: Option<DateTime<Utc>>,
65}
66
67pub(crate) fn convert_request_to_cipher_view(r: CipherCreateRequest) -> CipherView {
74 let now = chrono::Utc::now();
77 CipherView {
78 id: None,
79 organization_id: r.organization_id,
80 folder_id: r.folder_id,
81 collection_ids: r.collection_ids,
82 key: None,
83 name: r.name,
84 notes: r.notes,
85 r#type: r.r#type.get_cipher_type(),
86 login: r.r#type.as_login_view().cloned(),
87 identity: r.r#type.as_identity_view().cloned(),
88 card: r.r#type.as_card_view().cloned(),
89 secure_note: r.r#type.as_secure_note_view().cloned(),
90 ssh_key: r.r#type.as_ssh_key_view().cloned(),
91 bank_account: r.r#type.as_bank_account_view().cloned(),
92 drivers_license: r.r#type.as_drivers_license_view().cloned(),
93 passport: r.r#type.as_passport_view().cloned(),
94 favorite: r.favorite,
95 reprompt: r.reprompt,
96 organization_use_totp: false,
97 edit: true,
98 permissions: None,
99 view_password: true,
100 local_data: None,
101 attachments: None,
102 attachment_decryption_failures: None,
103 fields: Some(r.fields),
104 password_history: None,
105 creation_date: now,
106 deleted_date: None,
107 revision_date: now,
108 archived_date: r.archived_date,
109 }
110}
111
112async fn create_cipher<R: Repository<Cipher> + ?Sized>(
113 key_store: &KeyStore<KeySlotIds>,
114 api_client: &bitwarden_api_api::apis::ApiClient,
115 repository: &R,
116 encrypted_for: UserId,
117 view: CipherView,
118) -> Result<CipherView, CreateCipherError> {
119 let collection_ids = view.collection_ids.clone();
120
121 let cipher: Cipher = key_store.encrypt(view)?;
122 let mut cipher_request: CipherRequestModel = cipher.try_into()?;
123 cipher_request.encrypted_for = Some(encrypted_for.into());
124
125 let mut cipher: Cipher;
126 if !collection_ids.is_empty() {
127 cipher = api_client
128 .ciphers_api()
129 .post_create(Some(CipherCreateRequestModel {
130 collection_ids: Some(collection_ids.iter().cloned().map(Into::into).collect()),
131 cipher: Box::new(cipher_request),
132 }))
133 .await
134 .map_err(ApiError::from)?
135 .merge_with_cipher(None)?;
136 cipher.collection_ids = collection_ids;
137 repository.set(require!(cipher.id), cipher.clone()).await?;
138 } else {
139 cipher = api_client
140 .ciphers_api()
141 .post(Some(cipher_request))
142 .await
143 .map_err(ApiError::from)?
144 .merge_with_cipher(None)?;
145 repository.set(require!(cipher.id), cipher.clone()).await?;
146 }
147
148 Ok(key_store.decrypt(&cipher)?)
149}
150
151#[allow(deprecated)]
152#[cfg_attr(feature = "wasm", wasm_bindgen)]
153impl CiphersClient {
154 pub async fn create(
156 &self,
157 request: CipherCreateRequest,
158 ) -> Result<CipherView, CreateCipherError> {
159 let key_store = self.client.internal.get_key_store();
160 let config = self.client.internal.get_api_configurations();
161 let repository = self.get_repository()?;
162
163 let user_id = self
164 .client
165 .internal
166 .get_user_id()
167 .ok_or(NotAuthenticatedError)?;
168
169 let mut view: CipherView = convert_request_to_cipher_view(request);
170
171 if self.client.flags().get().await.enable_cipher_key_encryption {
174 let key = view.key_identifier();
175 view.generate_cipher_key(&mut key_store.context(), key)?;
176 }
177
178 create_cipher(
179 key_store,
180 &config.api_client,
181 repository.as_ref(),
182 user_id,
183 view,
184 )
185 .await
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use bitwarden_api_api::{apis::ApiClient, models::CipherResponseModel};
192 use bitwarden_core::key_management::SymmetricKeySlotId;
193 use bitwarden_crypto::SymmetricKeyAlgorithm;
194 use bitwarden_test::MemoryRepository;
195 use chrono::Utc;
196
197 use super::*;
198 use crate::{CipherId, LoginView};
199
200 const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
201 const TEST_COLLECTION_ID: &str = "73546b86-8802-4449-ad2a-69ea981b4ffd";
202 const TEST_USER_ID: &str = "550e8400-e29b-41d4-a716-446655440000";
203 const TEST_ORG_ID: &str = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8";
204
205 fn generate_test_cipher_create_request() -> CipherCreateRequest {
206 CipherCreateRequest {
207 name: "Test Login".to_string(),
208 notes: Some("Test notes".to_string()),
209 r#type: CipherViewType::Login(LoginView {
210 username: Some("[email protected]".to_string()),
211 password: Some("password123".to_string()),
212 password_revision_date: None,
213 uris: None,
214 totp: None,
215 autofill_on_page_load: None,
216 fido2_credentials: None,
217 }),
218 organization_id: Default::default(),
219 folder_id: Default::default(),
220 favorite: Default::default(),
221 reprompt: Default::default(),
222 fields: Default::default(),
223 collection_ids: vec![],
224 archived_date: None,
225 }
226 }
227
228 #[tokio::test]
229 async fn test_create_cipher() {
230 let store: KeyStore<KeySlotIds> = KeyStore::default();
231 {
232 let mut ctx = store.context_mut();
233 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
234 ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
235 .unwrap();
236 }
237
238 let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
239
240 let api_client = ApiClient::new_mocked(move |mock| {
241 mock.ciphers_api
242 .expect_post()
243 .returning(move |body| {
244 let body = body.unwrap();
245 Ok(CipherResponseModel {
246 object: Some("cipher".to_string()),
247 id: Some(cipher_id.into()),
248 name: Some(body.name.clone()),
249 r#type: body.r#type,
250 organization_id: body
251 .organization_id
252 .as_ref()
253 .and_then(|id| uuid::Uuid::parse_str(id).ok()),
254 folder_id: body
255 .folder_id
256 .as_ref()
257 .and_then(|id| uuid::Uuid::parse_str(id).ok()),
258 favorite: body.favorite,
259 reprompt: body.reprompt,
260 key: body.key.clone(),
261 notes: body.notes.clone(),
262 view_password: Some(true),
263 edit: Some(true),
264 organization_use_totp: Some(true),
265 revision_date: Some("2025-01-01T00:00:00Z".to_string()),
266 creation_date: Some("2025-01-01T00:00:00Z".to_string()),
267 deleted_date: None,
268 login: body.login,
269 card: body.card,
270 identity: body.identity,
271 secure_note: body.secure_note,
272 ssh_key: body.ssh_key,
273 bank_account: body.bank_account,
274 drivers_license: body.drivers_license,
275 passport: body.passport,
276 fields: body.fields,
277 password_history: body.password_history,
278 attachments: None,
279 permissions: None,
280 data: None,
281 archived_date: None,
282 })
283 })
284 .once();
285 });
286
287 let repository = MemoryRepository::<Cipher>::default();
288 let request = generate_test_cipher_create_request();
289
290 let result = create_cipher(
291 &store,
292 &api_client,
293 &repository,
294 TEST_USER_ID.parse().unwrap(),
295 convert_request_to_cipher_view(request),
296 )
297 .await
298 .unwrap();
299
300 assert_eq!(result.id, Some(cipher_id));
301 assert_eq!(result.name, "Test Login");
302 assert_eq!(
303 result.login,
304 Some(LoginView {
305 username: Some("[email protected]".to_string()),
306 password: Some("password123".to_string()),
307 password_revision_date: None,
308 uris: None,
309 totp: None,
310 autofill_on_page_load: None,
311 fido2_credentials: None,
312 })
313 );
314
315 let stored_cipher_view: CipherView = store
317 .decrypt(&repository.get(cipher_id).await.unwrap().unwrap())
318 .unwrap();
319 assert_eq!(stored_cipher_view.id, result.id);
320 assert_eq!(stored_cipher_view.name, result.name);
321 assert_eq!(stored_cipher_view.r#type, result.r#type);
322 assert!(stored_cipher_view.login.is_some());
323 assert_eq!(stored_cipher_view.favorite, result.favorite);
324 }
325
326 #[tokio::test]
327 async fn test_create_cipher_http_error() {
328 let store: KeyStore<KeySlotIds> = KeyStore::default();
329 {
330 let mut ctx = store.context_mut();
331 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
332 ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
333 .unwrap();
334 }
335
336 let api_client = ApiClient::new_mocked(move |mock| {
337 mock.ciphers_api
338 .expect_post()
339 .returning(move |_body| Err(std::io::Error::other("Simulated error").into()));
340 });
341
342 let repository = MemoryRepository::<Cipher>::default();
343
344 let request = generate_test_cipher_create_request();
345
346 let result = create_cipher(
347 &store,
348 &api_client,
349 &repository,
350 TEST_USER_ID.parse().unwrap(),
351 convert_request_to_cipher_view(request),
352 )
353 .await;
354
355 assert!(result.is_err());
356 assert!(matches!(result.unwrap_err(), CreateCipherError::Api(_)));
357 }
358
359 #[tokio::test]
360 async fn test_create_org_cipher() {
361 let api_client = ApiClient::new_mocked(move |mock| {
362 mock.ciphers_api
363 .expect_post_create()
364 .returning(move |body| {
365 let request_body = body.unwrap();
366
367 Ok(CipherResponseModel {
368 id: Some(TEST_CIPHER_ID.try_into().unwrap()),
369 organization_id: request_body
370 .cipher
371 .organization_id
372 .and_then(|id| id.parse().ok()),
373 name: Some(request_body.cipher.name.clone()),
374 r#type: request_body.cipher.r#type,
375 creation_date: Some(Utc::now().to_string()),
376 revision_date: Some(Utc::now().to_string()),
377 ..Default::default()
378 })
379 })
380 .once();
381 });
382
383 let store: KeyStore<KeySlotIds> = KeyStore::default();
384 {
385 let mut ctx = store.context_mut();
386 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
387 ctx.persist_symmetric_key(
388 local_key_id,
389 SymmetricKeySlotId::Organization(TEST_ORG_ID.parse().unwrap()),
390 )
391 .unwrap();
392 }
393 let repository = MemoryRepository::<Cipher>::default();
394 let request = CipherCreateRequest {
395 organization_id: Some(TEST_ORG_ID.parse().unwrap()),
396 collection_ids: vec![TEST_COLLECTION_ID.parse().unwrap()],
397 folder_id: None,
398 name: "Test Cipher".into(),
399 notes: None,
400 favorite: false,
401 reprompt: CipherRepromptType::None,
402 r#type: CipherViewType::Login(LoginView {
403 username: None,
404 password: None,
405 password_revision_date: None,
406 uris: None,
407 totp: None,
408 autofill_on_page_load: None,
409 fido2_credentials: None,
410 }),
411 fields: vec![],
412 archived_date: None,
413 };
414
415 let response = create_cipher(
416 &store,
417 &api_client,
418 &repository,
419 TEST_USER_ID.parse().unwrap(),
420 convert_request_to_cipher_view(request),
421 )
422 .await
423 .unwrap();
424
425 let cipher: Cipher = repository
426 .get(TEST_CIPHER_ID.parse().unwrap())
427 .await
428 .unwrap()
429 .unwrap();
430 let cipher_view: CipherView = store.decrypt(&cipher).unwrap();
431
432 assert_eq!(response.id, cipher_view.id);
433 assert_eq!(response.organization_id, cipher_view.organization_id);
434
435 assert_eq!(response.id, Some(TEST_CIPHER_ID.parse().unwrap()));
436 assert_eq!(response.organization_id, Some(TEST_ORG_ID.parse().unwrap()));
437 }
438}