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